mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 14:16:08 +00:00
feat: update MiniMax and Z.AI provider defaults
Refresh MiniMax to MiniMax-M3, update Z.AI defaults to glm-5.2, and add focused provider catalog/runtime coverage.
This commit is contained in:
1 parent
2a66401a28
commit
b5d5fce6e4
10 files changed
+480
-38
No files matched your search
+27
-14
@@ -76,8 +76,11 @@ func registerProviders(registry *providers.Registry, cfg *config.Config, modelRe
|
||||
}
|
||||
|
||||
if cfg.Providers.MiniMax.APIKey != "" {
|
||||
registry.Register(providers.NewOpenAIProvider("minimax", cfg.Providers.MiniMax.APIKey, "https://api.minimax.io/v1", "MiniMax-M2.5").
|
||||
WithChatPath("/text/chatcompletion_v2"))
|
||||
base := cfg.Providers.MiniMax.APIBase
|
||||
if base == "" {
|
||||
base = store.MiniMaxDefaultAPIBase
|
||||
}
|
||||
registry.Register(providers.NewOpenAIProvider("minimax", cfg.Providers.MiniMax.APIKey, base, store.MiniMaxDefaultModel))
|
||||
slog.Info("registered provider", "name", "minimax")
|
||||
}
|
||||
|
||||
@@ -109,18 +112,18 @@ func registerProviders(registry *providers.Registry, cfg *config.Config, modelRe
|
||||
if cfg.Providers.Zai.APIKey != "" {
|
||||
base := cfg.Providers.Zai.APIBase
|
||||
if base == "" {
|
||||
base = "https://api.z.ai/api/paas/v4"
|
||||
base = store.ZaiDefaultAPIBase
|
||||
}
|
||||
registry.Register(providers.NewOpenAIProvider("zai", cfg.Providers.Zai.APIKey, base, "glm-5"))
|
||||
registry.Register(providers.NewOpenAIProvider("zai", cfg.Providers.Zai.APIKey, base, store.ZaiDefaultModel))
|
||||
slog.Info("registered provider", "name", "zai")
|
||||
}
|
||||
|
||||
if cfg.Providers.ZaiCoding.APIKey != "" {
|
||||
base := cfg.Providers.ZaiCoding.APIBase
|
||||
if base == "" {
|
||||
base = "https://api.z.ai/api/coding/paas/v4"
|
||||
base = store.ZaiCodingDefaultAPIBase
|
||||
}
|
||||
registry.Register(providers.NewOpenAIProvider("zai-coding", cfg.Providers.ZaiCoding.APIKey, base, "glm-5"))
|
||||
registry.Register(providers.NewOpenAIProvider("zai-coding", cfg.Providers.ZaiCoding.APIKey, base, store.ZaiDefaultModel))
|
||||
slog.Info("registered provider", "name", "zai-coding")
|
||||
}
|
||||
|
||||
@@ -362,15 +365,15 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
|
||||
case store.ProviderZai:
|
||||
base := p.APIBase
|
||||
if base == "" {
|
||||
base = "https://api.z.ai/api/paas/v4"
|
||||
base = store.ZaiDefaultAPIBase
|
||||
}
|
||||
registry.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, "glm-5"))
|
||||
registry.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.ZaiDefaultModel))
|
||||
case store.ProviderZaiCoding:
|
||||
base := p.APIBase
|
||||
if base == "" {
|
||||
base = "https://api.z.ai/api/coding/paas/v4"
|
||||
base = store.ZaiCodingDefaultAPIBase
|
||||
}
|
||||
registry.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, "glm-5"))
|
||||
registry.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.ZaiDefaultModel))
|
||||
case store.ProviderOllamaCloud:
|
||||
base := p.APIBase
|
||||
if base == "" {
|
||||
@@ -413,11 +416,9 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
|
||||
})
|
||||
registry.RegisterForTenant(p.TenantID, prov)
|
||||
default:
|
||||
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, p.APIBase, "")
|
||||
base, model := openAIProviderDefaults(p.ProviderType, p.APIBase)
|
||||
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, base, model)
|
||||
prov.WithProviderType(p.ProviderType)
|
||||
if p.ProviderType == store.ProviderMiniMax {
|
||||
prov.WithChatPath("/text/chatcompletion_v2")
|
||||
}
|
||||
if p.ProviderType == store.ProviderOpenRouter {
|
||||
prov.WithSiteInfo("https://goclaw.sh", "GoClaw")
|
||||
}
|
||||
@@ -427,6 +428,18 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
|
||||
}
|
||||
}
|
||||
|
||||
func openAIProviderDefaults(providerType, apiBase string) (string, string) {
|
||||
switch providerType {
|
||||
case store.ProviderMiniMax:
|
||||
if apiBase == "" {
|
||||
apiBase = store.MiniMaxDefaultAPIBase
|
||||
}
|
||||
return apiBase, store.MiniMaxDefaultModel
|
||||
default:
|
||||
return apiBase, ""
|
||||
}
|
||||
}
|
||||
|
||||
func registerClaudeCLIFromConfig(registry *providers.Registry, cfg *config.Config) {
|
||||
if cfg == nil || cfg.Providers.ClaudeCLI.CLIPath == "" {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
func TestRegisterProvidersUsesCurrentMiniMaxAndZaiDefaults(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
cfg.Providers.MiniMax.APIKey = "minimax-token"
|
||||
cfg.Providers.Zai.APIKey = "zai-token"
|
||||
cfg.Providers.ZaiCoding.APIKey = "zai-coding-token"
|
||||
|
||||
registry := providers.NewRegistry(nil)
|
||||
registerProviders(registry, cfg, providers.NewInMemoryRegistry())
|
||||
|
||||
assertProviderDefault(t, registry, providers.MasterTenantID, "minimax", "MiniMax-M3", "https://api.minimax.io/v1")
|
||||
assertProviderDefault(t, registry, providers.MasterTenantID, "zai", "glm-5.2", "https://api.z.ai/api/paas/v4")
|
||||
assertProviderDefault(t, registry, providers.MasterTenantID, "zai-coding", "glm-5.2", "https://api.z.ai/api/coding/paas/v4")
|
||||
}
|
||||
|
||||
func TestRegisterProvidersMiniMaxUsesOpenAIChatCompletionsPath(t *testing.T) {
|
||||
var capturedPath string
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedPath = r.URL.Path
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"choices": []map[string]any{
|
||||
{
|
||||
"message": map[string]string{"content": "ok"},
|
||||
"finish_reason": "stop",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
|
||||
cfg := &config.Config{}
|
||||
cfg.Providers.MiniMax.APIKey = "minimax-token"
|
||||
cfg.Providers.MiniMax.APIBase = upstream.URL
|
||||
|
||||
registry := providers.NewRegistry(nil)
|
||||
registerProviders(registry, cfg, providers.NewInMemoryRegistry())
|
||||
|
||||
runtimeProvider, err := registry.GetForTenant(providers.MasterTenantID, "minimax")
|
||||
if err != nil {
|
||||
t.Fatalf("GetForTenant() error = %v", err)
|
||||
}
|
||||
_, err = runtimeProvider.Chat(context.Background(), providers.ChatRequest{
|
||||
Messages: []providers.Message{{Role: "user", Content: "hi"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error = %v", err)
|
||||
}
|
||||
if capturedPath != "/chat/completions" {
|
||||
t.Fatalf("captured path = %q, want /chat/completions", capturedPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterProvidersFromDBUsesCurrentMiniMaxAndZaiDefaults(t *testing.T) {
|
||||
tenantID := uuid.New()
|
||||
providerStore := gatewayProvidersStoreStub{
|
||||
providers: []store.LLMProviderData{
|
||||
{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
TenantID: tenantID,
|
||||
Name: "db-minimax",
|
||||
ProviderType: store.ProviderMiniMax,
|
||||
APIKey: "minimax-token",
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
TenantID: tenantID,
|
||||
Name: "db-zai",
|
||||
ProviderType: store.ProviderZai,
|
||||
APIKey: "zai-token",
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
TenantID: tenantID,
|
||||
Name: "db-zai-coding",
|
||||
ProviderType: store.ProviderZaiCoding,
|
||||
APIKey: "zai-coding-token",
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
registry := providers.NewRegistry(nil)
|
||||
registerProvidersFromDB(registry, providerStore, nil, "", "", nil, &config.Config{}, providers.NewInMemoryRegistry())
|
||||
|
||||
assertProviderDefault(t, registry, tenantID, "db-minimax", "MiniMax-M3", "https://api.minimax.io/v1")
|
||||
assertProviderDefault(t, registry, tenantID, "db-zai", "glm-5.2", "https://api.z.ai/api/paas/v4")
|
||||
assertProviderDefault(t, registry, tenantID, "db-zai-coding", "glm-5.2", "https://api.z.ai/api/coding/paas/v4")
|
||||
}
|
||||
|
||||
func assertProviderDefault(t *testing.T, registry *providers.Registry, tenantID uuid.UUID, name, wantModel, wantBase string) {
|
||||
t.Helper()
|
||||
runtimeProvider, err := registry.GetForTenant(tenantID, name)
|
||||
if err != nil {
|
||||
t.Fatalf("GetForTenant(%q) error = %v", name, err)
|
||||
}
|
||||
if got := runtimeProvider.DefaultModel(); got != wantModel {
|
||||
t.Fatalf("%s DefaultModel() = %q, want %q", name, got, wantModel)
|
||||
}
|
||||
openai, ok := runtimeProvider.(*providers.OpenAIProvider)
|
||||
if !ok {
|
||||
t.Fatalf("%s runtime provider = %T, want *providers.OpenAIProvider", name, runtimeProvider)
|
||||
}
|
||||
if got := openai.APIBase(); got != wantBase {
|
||||
t.Fatalf("%s APIBase() = %q, want %q", name, got, wantBase)
|
||||
}
|
||||
}
|
||||
|
||||
type gatewayProvidersStoreStub struct {
|
||||
providers []store.LLMProviderData
|
||||
}
|
||||
|
||||
func (s gatewayProvidersStoreStub) CreateProvider(context.Context, *store.LLMProviderData) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (s gatewayProvidersStoreStub) GetProvider(context.Context, uuid.UUID) (*store.LLMProviderData, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (s gatewayProvidersStoreStub) GetProviderByName(context.Context, string) (*store.LLMProviderData, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (s gatewayProvidersStoreStub) ListProviders(context.Context) ([]store.LLMProviderData, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (s gatewayProvidersStoreStub) ListAllProviders(context.Context) ([]store.LLMProviderData, error) {
|
||||
return s.providers, nil
|
||||
}
|
||||
|
||||
func (s gatewayProvidersStoreStub) UpdateProvider(context.Context, uuid.UUID, map[string]any) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (s gatewayProvidersStoreStub) DeleteProvider(context.Context, uuid.UUID) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
@@ -101,13 +101,13 @@ Supported price units: input, output, cache read, cache write, reasoning, reques
|
||||
| gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | `gemini-2.0-flash` | Skips empty content fields |
|
||||
| mistral | `https://api.mistral.ai/v1` | `mistral-large-latest` | |
|
||||
| xai | `https://api.x.ai/v1` | `grok-3-mini` | |
|
||||
| minimax | `https://api.minimax.io/v1` | `MiniMax-M2.5` | Uses custom chat path |
|
||||
| minimax | `https://api.minimax.io/v1` | `MiniMax-M3` | Uses OpenAI-compatible chat completions; MiniMax also exposes an Anthropic-compatible API, but GoClaw keeps the current OpenAI-compatible path |
|
||||
| cohere | `https://api.cohere.ai/compatibility/v1` | `command-a` | |
|
||||
| perplexity | `https://api.perplexity.ai` | `sonar-pro` | |
|
||||
| ollama | `http://localhost:11434/v1` | `llama3.3` | Local/configurable |
|
||||
| bailian | `https://coding-intl.dashscope.aliyuncs.com/v1` | `qwen3.5-plus` | Alibaba Coding API |
|
||||
| zai | `https://api.z.ai/api/paas/v4` | `glm-5` | |
|
||||
| zai-coding | `https://api.z.ai/api/coding/paas/v4` | `glm-5` | |
|
||||
| zai | `https://api.z.ai/api/paas/v4` | `glm-5.2` | 1M context, 128K max output |
|
||||
| zai-coding | `https://api.z.ai/api/coding/paas/v4` | `glm-5.2` | 1M context, 128K max output |
|
||||
| byteplus | `https://ark.ap-southeast.bytepluses.com/api/v3` | `seed-2-0-lite-260228` | Seed 2.0 models |
|
||||
| byteplus_coding | `https://ark.ap-southeast.bytepluses.com/api/coding/v3` | `seed-2-0-lite-260228` | Seed 2.0 Coding Plan |
|
||||
|
||||
|
||||
@@ -97,16 +97,18 @@ func (h *ProvidersHandler) handleListProviderModels(w http.ResponseWriter, r *ht
|
||||
var models []ModelInfo
|
||||
|
||||
switch p.ProviderType {
|
||||
case "anthropic_native":
|
||||
case store.ProviderAnthropicNative:
|
||||
models, err = fetchAnthropicModels(ctx, p.APIKey, h.resolveAPIBase(p))
|
||||
case "gemini_native":
|
||||
case store.ProviderGeminiNative:
|
||||
models, err = fetchGeminiModels(ctx, p.APIKey)
|
||||
case "bailian":
|
||||
case store.ProviderBailian:
|
||||
models = bailianModels()
|
||||
case "dashscope":
|
||||
case store.ProviderDashScope:
|
||||
models = dashScopeModels()
|
||||
case "minimax_native":
|
||||
case store.ProviderMiniMax:
|
||||
models = minimaxModels()
|
||||
case store.ProviderZai, store.ProviderZaiCoding:
|
||||
models = zaiModels()
|
||||
default:
|
||||
// All other types use OpenAI-compatible /models endpoint
|
||||
apiBase := openAIModelsAPIBase(p.ProviderType, h.resolveAPIBase(p))
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package http
|
||||
|
||||
import "github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// bailianModels returns a hardcoded list of models available on the
|
||||
// Bailian Coding platform (coding-intl.dashscope.aliyuncs.com).
|
||||
@@ -26,10 +29,16 @@ func bailianModels() []ModelInfo {
|
||||
func minimaxModels() []ModelInfo {
|
||||
return []ModelInfo{
|
||||
// Chat / text
|
||||
{ID: store.MiniMaxDefaultModel, Name: "MiniMax M3"},
|
||||
{ID: "MiniMax-Text-01", Name: "MiniMax Text 01"},
|
||||
{ID: "MiniMax-M1", Name: "MiniMax M1"},
|
||||
{ID: "MiniMax-M2.7", Name: "MiniMax M2.7"},
|
||||
{ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 Highspeed"},
|
||||
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
||||
{ID: "MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 Highspeed"},
|
||||
{ID: "MiniMax-M2.1", Name: "MiniMax M2.1"},
|
||||
{ID: "MiniMax-M2.1-highspeed", Name: "MiniMax M2.1 Highspeed"},
|
||||
{ID: "MiniMax-M2", Name: "MiniMax M2"},
|
||||
// Image generation
|
||||
{ID: "image-01", Name: "Image 01"},
|
||||
// Video generation
|
||||
@@ -45,6 +54,28 @@ func minimaxModels() []ModelInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// zaiModels returns a hardcoded list of Z.AI GLM models.
|
||||
// Z.AI supports OpenAI-compatible chat completions, but /models availability is
|
||||
// not required for GoClaw's model picker.
|
||||
func zaiModels() []ModelInfo {
|
||||
return []ModelInfo{
|
||||
{ID: store.ZaiDefaultModel, Name: "GLM 5.2"},
|
||||
{ID: "glm-5.1", Name: "GLM 5.1"},
|
||||
{ID: "glm-5-turbo", Name: "GLM 5 Turbo"},
|
||||
{ID: "glm-5", Name: "GLM 5"},
|
||||
{ID: "glm-4.7", Name: "GLM 4.7"},
|
||||
{ID: "glm-4.7-flash", Name: "GLM 4.7 Flash"},
|
||||
{ID: "glm-4.7-flashx", Name: "GLM 4.7 FlashX"},
|
||||
{ID: "glm-4.6", Name: "GLM 4.6"},
|
||||
{ID: "glm-4.5", Name: "GLM 4.5"},
|
||||
{ID: "glm-4.5-air", Name: "GLM 4.5 Air"},
|
||||
{ID: "glm-4.5-x", Name: "GLM 4.5 X"},
|
||||
{ID: "glm-4.5-airx", Name: "GLM 4.5 AirX"},
|
||||
{ID: "glm-4.5-flash", Name: "GLM 4.5 Flash"},
|
||||
{ID: "glm-4-32b-0414-128k", Name: "GLM 4 32B 0414 128K"},
|
||||
}
|
||||
}
|
||||
|
||||
// dashScopeModels returns a hardcoded list of DashScope (Qwen) models.
|
||||
// DashScope does not expose a standard /v1/models endpoint.
|
||||
func dashScopeModels() []ModelInfo {
|
||||
|
||||
@@ -52,6 +52,22 @@ func ollamaModelsRequest(t *testing.T, mux *http.ServeMux, providerID uuid.UUID,
|
||||
return result, w.Code
|
||||
}
|
||||
|
||||
func providerModelsRequest(t *testing.T, mux *http.ServeMux, providerID uuid.UUID, token string) ProviderModelsResponse {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/providers/"+providerID.String()+"/models", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d, want %d, body=%s", w.Code, http.StatusOK, w.Body.String())
|
||||
}
|
||||
var result ProviderModelsResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
||||
t.Fatalf("Decode() error = %v", err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestProvidersHandlerListProviderModelsChatGPTOAuthIncludesReasoningMetadata(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
providerStore := newMockProviderStore()
|
||||
@@ -164,6 +180,84 @@ func TestProvidersHandlerListProviderModelsBailianIncludesQwen37Plus(t *testing.
|
||||
t.Fatalf("qwen3.7-plus not found in Bailian model list: %#v", result.Models)
|
||||
}
|
||||
|
||||
func TestProvidersHandlerListProviderModelsMiniMaxIncludesM3AndLegacyModels(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
providerStore := newMockProviderStore()
|
||||
provider := &store.LLMProviderData{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
Name: "minimax",
|
||||
ProviderType: store.ProviderMiniMax,
|
||||
APIKey: "token",
|
||||
Enabled: true,
|
||||
}
|
||||
if err := providerStore.CreateProvider(t.Context(), provider); err != nil {
|
||||
t.Fatalf("CreateProvider() error = %v", err)
|
||||
}
|
||||
|
||||
handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterRoutes(mux)
|
||||
|
||||
result := providerModelsRequest(t, mux, provider.ID, token)
|
||||
assertModelIDsInOrder(t, result.Models, []string{
|
||||
"MiniMax-M3",
|
||||
"MiniMax-M2.7",
|
||||
"MiniMax-M2.5",
|
||||
"MiniMax-M2.1",
|
||||
"MiniMax-M2",
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvidersHandlerListProviderModelsZaiUsesLocalCatalog(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
|
||||
for _, providerType := range []string{store.ProviderZai, store.ProviderZaiCoding} {
|
||||
t.Run(providerType, func(t *testing.T) {
|
||||
providerStore := newMockProviderStore()
|
||||
provider := &store.LLMProviderData{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
Name: providerType,
|
||||
ProviderType: providerType,
|
||||
APIKey: "token",
|
||||
Enabled: true,
|
||||
}
|
||||
if err := providerStore.CreateProvider(t.Context(), provider); err != nil {
|
||||
t.Fatalf("CreateProvider() error = %v", err)
|
||||
}
|
||||
|
||||
handler := NewProvidersHandler(providerStore, newMockSecretsStore(), nil, "")
|
||||
mux := http.NewServeMux()
|
||||
handler.RegisterRoutes(mux)
|
||||
|
||||
result := providerModelsRequest(t, mux, provider.ID, token)
|
||||
assertModelIDsInOrder(t, result.Models, []string{
|
||||
"glm-5.2",
|
||||
"glm-5.1",
|
||||
"glm-5",
|
||||
"glm-4.7",
|
||||
"glm-4.5",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertModelIDsInOrder(t *testing.T, models []ModelInfo, want []string) {
|
||||
t.Helper()
|
||||
position := 0
|
||||
for _, model := range models {
|
||||
if position < len(want) && model.ID == want[position] {
|
||||
position++
|
||||
}
|
||||
}
|
||||
if position != len(want) {
|
||||
got := make([]string, 0, len(models))
|
||||
for _, model := range models {
|
||||
got = append(got, model.ID)
|
||||
}
|
||||
t.Fatalf("model IDs = %#v, want sequence %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersHandlerListProviderModelsOpenAICompatAnnotatesKnownModels(t *testing.T) {
|
||||
token := setupProvidersAdminToken(t)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -309,6 +309,18 @@ func (h *ProvidersHandler) registerInMemory(p *store.LLMProviderData) providerRu
|
||||
}
|
||||
h.providerReg.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, "qwen3.5-plus").
|
||||
WithProviderType(p.ProviderType))
|
||||
case store.ProviderZai:
|
||||
base := apiBase
|
||||
if base == "" {
|
||||
base = store.ZaiDefaultAPIBase
|
||||
}
|
||||
h.providerReg.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.ZaiDefaultModel))
|
||||
case store.ProviderZaiCoding:
|
||||
base := apiBase
|
||||
if base == "" {
|
||||
base = store.ZaiCodingDefaultAPIBase
|
||||
}
|
||||
h.providerReg.RegisterForTenant(p.TenantID, providers.NewOpenAIProvider(p.Name, p.APIKey, base, store.ZaiDefaultModel))
|
||||
case store.ProviderNovita:
|
||||
base := apiBase
|
||||
if base == "" {
|
||||
@@ -328,15 +340,25 @@ func (h *ProvidersHandler) registerInMemory(p *store.LLMProviderData) providerRu
|
||||
})
|
||||
h.providerReg.RegisterForTenant(p.TenantID, prov)
|
||||
default:
|
||||
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, apiBase, "")
|
||||
if p.ProviderType == store.ProviderMiniMax {
|
||||
prov.WithChatPath("/text/chatcompletion_v2")
|
||||
}
|
||||
base, model := openAIProviderDefaults(p.ProviderType, apiBase)
|
||||
prov := providers.NewOpenAIProvider(p.Name, p.APIKey, base, model)
|
||||
h.providerReg.RegisterForTenant(p.TenantID, prov)
|
||||
}
|
||||
return providerRuntimeRegistered
|
||||
}
|
||||
|
||||
func openAIProviderDefaults(providerType, apiBase string) (string, string) {
|
||||
switch providerType {
|
||||
case store.ProviderMiniMax:
|
||||
if apiBase == "" {
|
||||
apiBase = store.MiniMaxDefaultAPIBase
|
||||
}
|
||||
return apiBase, store.MiniMaxDefaultModel
|
||||
default:
|
||||
return apiBase, ""
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeOllamaAPIBase ensures Ollama and OllamaCloud api_base values include the
|
||||
// /v1 suffix required for OpenAI-compatible endpoints. Normalizing at write time means
|
||||
// resolveAPIBase() always returns a ready-to-use base URL.
|
||||
|
||||
@@ -95,6 +95,121 @@ func TestProvidersHandlerRegisterInMemoryUsesDBNameForAnthropic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersHandlerRegisterInMemoryMiniMaxUsesM3Default(t *testing.T) {
|
||||
providerReg := providers.NewRegistry(nil)
|
||||
handler := NewProvidersHandler(newMockProviderStore(), newMockSecretsStore(), providerReg, "")
|
||||
|
||||
provider := &store.LLMProviderData{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
TenantID: uuid.New(),
|
||||
Name: "minimax",
|
||||
ProviderType: store.ProviderMiniMax,
|
||||
APIKey: "token",
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
handler.registerInMemory(provider)
|
||||
|
||||
runtimeProvider, err := providerReg.GetForTenant(provider.TenantID, provider.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("GetForTenant() error = %v", err)
|
||||
}
|
||||
if got := runtimeProvider.DefaultModel(); got != "MiniMax-M3" {
|
||||
t.Fatalf("DefaultModel() = %q, want MiniMax-M3", got)
|
||||
}
|
||||
openai, ok := runtimeProvider.(*providers.OpenAIProvider)
|
||||
if !ok {
|
||||
t.Fatalf("runtime provider = %T, want *providers.OpenAIProvider", runtimeProvider)
|
||||
}
|
||||
if got := openai.APIBase(); got != "https://api.minimax.io/v1" {
|
||||
t.Fatalf("APIBase() = %q, want MiniMax default base", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersHandlerRegisterInMemoryMiniMaxUsesOpenAIChatCompletionsPath(t *testing.T) {
|
||||
var capturedPath string
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedPath = r.URL.Path
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"choices": []map[string]any{
|
||||
{
|
||||
"message": map[string]string{"content": "ok"},
|
||||
"finish_reason": "stop",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
|
||||
providerReg := providers.NewRegistry(nil)
|
||||
handler := NewProvidersHandler(newMockProviderStore(), newMockSecretsStore(), providerReg, "")
|
||||
provider := &store.LLMProviderData{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
TenantID: uuid.New(),
|
||||
Name: "minimax",
|
||||
ProviderType: store.ProviderMiniMax,
|
||||
APIBase: upstream.URL,
|
||||
APIKey: "token",
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
handler.registerInMemory(provider)
|
||||
|
||||
runtimeProvider, err := providerReg.GetForTenant(provider.TenantID, provider.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("GetForTenant() error = %v", err)
|
||||
}
|
||||
_, err = runtimeProvider.Chat(context.Background(), providers.ChatRequest{
|
||||
Messages: []providers.Message{{Role: "user", Content: "hi"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error = %v", err)
|
||||
}
|
||||
if capturedPath != "/chat/completions" {
|
||||
t.Fatalf("captured path = %q, want /chat/completions", capturedPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersHandlerRegisterInMemoryZaiUsesGLM52Default(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
providerType string
|
||||
wantBase string
|
||||
}{
|
||||
{providerType: store.ProviderZai, wantBase: "https://api.z.ai/api/paas/v4"},
|
||||
{providerType: store.ProviderZaiCoding, wantBase: "https://api.z.ai/api/coding/paas/v4"},
|
||||
} {
|
||||
t.Run(tt.providerType, func(t *testing.T) {
|
||||
providerReg := providers.NewRegistry(nil)
|
||||
handler := NewProvidersHandler(newMockProviderStore(), newMockSecretsStore(), providerReg, "")
|
||||
provider := &store.LLMProviderData{
|
||||
BaseModel: store.BaseModel{ID: uuid.New()},
|
||||
TenantID: uuid.New(),
|
||||
Name: tt.providerType,
|
||||
ProviderType: tt.providerType,
|
||||
APIKey: "token",
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
handler.registerInMemory(provider)
|
||||
|
||||
runtimeProvider, err := providerReg.GetForTenant(provider.TenantID, provider.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("GetForTenant() error = %v", err)
|
||||
}
|
||||
if got := runtimeProvider.DefaultModel(); got != "glm-5.2" {
|
||||
t.Fatalf("DefaultModel() = %q, want glm-5.2", got)
|
||||
}
|
||||
openai, ok := runtimeProvider.(*providers.OpenAIProvider)
|
||||
if !ok {
|
||||
t.Fatalf("runtime provider = %T, want *providers.OpenAIProvider", runtimeProvider)
|
||||
}
|
||||
if got := openai.APIBase(); got != tt.wantBase {
|
||||
t.Fatalf("APIBase() = %q, want %q", got, tt.wantBase)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProvidersHandlerRegisterInMemoryUsesDBNameForClaudeCLI mirrors the Anthropic guard for Claude CLI.
|
||||
// Custom-named CLI providers must be registered under their DB name to be locatable via verify.
|
||||
func TestProvidersHandlerRegisterInMemoryUsesDBNameForClaudeCLI(t *testing.T) {
|
||||
|
||||
@@ -14,15 +14,15 @@ type OpenAIProvider struct {
|
||||
chatPath string // defaults to "/chat/completions"
|
||||
authPrefix string // auth header prefix, defaults to "Bearer " if empty
|
||||
defaultModel string
|
||||
providerType string // DB provider_type (e.g. "gemini_native", "openai", "minimax_native")
|
||||
siteURL string // optional site URL for provider identification (e.g. OpenRouter HTTP-Referer)
|
||||
siteTitle string // optional site title for provider identification (e.g. OpenRouter X-Title)
|
||||
providerType string // DB provider_type (e.g. "gemini_native", "openai", "minimax_native")
|
||||
siteURL string // optional site URL for provider identification (e.g. OpenRouter HTTP-Referer)
|
||||
siteTitle string // optional site title for provider identification (e.g. OpenRouter X-Title)
|
||||
extraHeaders map[string]string // static headers set on every outgoing request (e.g. fixed User-Agent for kimi_coding)
|
||||
client *http.Client
|
||||
retryConfig RetryConfig
|
||||
middlewares RequestMiddleware // composed middleware chain (nil = no-op)
|
||||
registry ModelRegistry // model resolution registry (nil = skip)
|
||||
noAuthHeader bool // when true, doRequest() skips setting Authorization (e.g. Vertex OAuth transport injects its own)
|
||||
registry ModelRegistry // model resolution registry (nil = skip)
|
||||
noAuthHeader bool // when true, doRequest() skips setting Authorization (e.g. Vertex OAuth transport injects its own)
|
||||
}
|
||||
|
||||
func NewOpenAIProvider(name, apiKey, apiBase, defaultModel string) *OpenAIProvider {
|
||||
@@ -43,7 +43,7 @@ func NewOpenAIProvider(name, apiKey, apiBase, defaultModel string) *OpenAIProvid
|
||||
}
|
||||
}
|
||||
|
||||
// WithChatPath returns a copy with a custom chat completions path (e.g. "/text/chatcompletion_v2" for MiniMax native API).
|
||||
// WithChatPath returns a copy with a custom chat completions path.
|
||||
func (p *OpenAIProvider) WithChatPath(path string) *OpenAIProvider {
|
||||
p.chatPath = path
|
||||
return p
|
||||
|
||||
@@ -27,15 +27,24 @@ const (
|
||||
ProviderYesScale = "yescale"
|
||||
ProviderZai = "zai"
|
||||
ProviderZaiCoding = "zai_coding"
|
||||
ProviderOllama = "ollama" // local or self-hosted Ollama (no API key)
|
||||
ProviderOllamaCloud = "ollama_cloud" // Ollama Cloud (Bearer token required)
|
||||
ProviderACP = "acp" // ACP (Agent Client Protocol) agent subprocess
|
||||
ProviderOllama = "ollama" // local or self-hosted Ollama (no API key)
|
||||
ProviderOllamaCloud = "ollama_cloud" // Ollama Cloud (Bearer token required)
|
||||
ProviderACP = "acp" // ACP (Agent Client Protocol) agent subprocess
|
||||
ProviderNovita = "novita" // Novita AI (OpenAI-compatible endpoint)
|
||||
ProviderBytePlus = "byteplus" // BytePlus ModelArk (Seed 2.0 models)
|
||||
ProviderBytePlusCoding = "byteplus_coding" // BytePlus ModelArk Coding Plan
|
||||
ProviderVertex = "vertex" // Google Cloud Vertex AI (OAuth2 service account + ADC)
|
||||
ProviderKimiCoding = "kimi_coding" // Moonshot Kimi Coding (OpenAI-compat, requires fixed User-Agent)
|
||||
|
||||
// MiniMax defaults.
|
||||
MiniMaxDefaultAPIBase = "https://api.minimax.io/v1"
|
||||
MiniMaxDefaultModel = "MiniMax-M3"
|
||||
|
||||
// Z.AI defaults.
|
||||
ZaiDefaultAPIBase = "https://api.z.ai/api/paas/v4"
|
||||
ZaiCodingDefaultAPIBase = "https://api.z.ai/api/coding/paas/v4"
|
||||
ZaiDefaultModel = "glm-5.2"
|
||||
|
||||
// Novita AI defaults.
|
||||
NovitaDefaultAPIBase = "https://api.novita.ai/openai"
|
||||
NovitaDefaultModel = "moonshotai/kimi-k2.5"
|
||||
@@ -48,8 +57,8 @@ const (
|
||||
// Kimi Coding defaults. The upstream requires a fixed User-Agent on every
|
||||
// request — handled by the runtime in cmd/gateway_providers.go via
|
||||
// OpenAIProvider.WithExtraHeaders.
|
||||
KimiCodingDefaultAPIBase = "https://api.kimi.com/coding/v1"
|
||||
KimiCodingDefaultModel = "kimi-k2-turbo-preview"
|
||||
KimiCodingDefaultAPIBase = "https://api.kimi.com/coding/v1"
|
||||
KimiCodingDefaultModel = "kimi-k2-turbo-preview"
|
||||
KimiCodingRequiredUserAgent = "claude-code/0.1.0"
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user