diff --git a/internal/driver/openclaw/config.go b/internal/driver/openclaw/config.go index 389d74c..8faf918 100644 --- a/internal/driver/openclaw/config.go +++ b/internal/driver/openclaw/config.go @@ -56,7 +56,7 @@ func GenerateConfig(rc *driver.ResolvedClaw) ([]byte, error) { return nil, fmt.Errorf("config generation: cllama provider %q apiKey: %w", provider, err) } } - if err := setPath(config, basePath+".api", defaultModelAPIForProvider(provider)); err != nil { + if err := setPath(config, basePath+".api", cllamaModelAPIForProvider(provider)); err != nil { return nil, fmt.Errorf("config generation: cllama provider %q api: %w", provider, err) } modelDefs := make([]interface{}, 0, len(modelIDs)) @@ -459,6 +459,17 @@ func defaultModelAPIForProvider(provider string) string { } } +func cllamaModelAPIForProvider(provider string) string { + switch normalizeProviderID(provider) { + case "anthropic", "synthetic", "minimax-portal", "kimi-coding", "cloudflare-ai-gateway", "xiaomi": + return "anthropic-messages" + default: + // cllama exposes OpenAI-compatible routing for non-Anthropic providers, + // even when the upstream vendor has a native API surface. + return "openai-completions" + } +} + // setPath sets a nested value in a map using a dotted path. func setPath(obj map[string]interface{}, path string, value interface{}) error { return shared.SetPath(obj, path, value) diff --git a/internal/driver/openclaw/config_test.go b/internal/driver/openclaw/config_test.go index e1d649c..8e0f957 100644 --- a/internal/driver/openclaw/config_test.go +++ b/internal/driver/openclaw/config_test.go @@ -83,6 +83,9 @@ func TestGenerateConfigCllamaRewritesProviderBaseURL(t *testing.T) { if anthropic["baseUrl"] != "http://cllama:8080/v1" { t.Errorf("expected proxy baseUrl, got %v", anthropic["baseUrl"]) } + if anthropic["api"] != "anthropic-messages" { + t.Fatalf("expected anthropic provider behind cllama to use anthropic-messages, got %v", anthropic["api"]) + } modelEntries, ok := anthropic["models"].([]interface{}) if !ok || len(modelEntries) == 0 { t.Fatalf("expected models.providers.anthropic.models entries, got %T %v", anthropic["models"], anthropic["models"]) @@ -96,6 +99,70 @@ func TestGenerateConfigCllamaRewritesProviderBaseURL(t *testing.T) { } } +func TestGenerateConfigCllamaGoogleUsesOpenAICompletions(t *testing.T) { + rc := &driver.ResolvedClaw{ + Models: map[string]string{"primary": "google/gemini-3-flash-preview"}, + Cllama: []string{"passthrough"}, + CllamaToken: "weston:abc123hex", + } + data, err := GenerateConfig(rc) + if err != nil { + t.Fatal(err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatal(err) + } + modelsCfg, ok := config["models"].(map[string]interface{}) + if !ok { + t.Fatal("expected models config") + } + providers, ok := modelsCfg["providers"].(map[string]interface{}) + if !ok { + t.Fatal("expected models.providers config") + } + google, ok := providers["google"].(map[string]interface{}) + if !ok { + t.Fatal("expected models.providers.google config") + } + if google["baseUrl"] != "http://cllama:8080/v1" { + t.Fatalf("expected proxy baseUrl, got %v", google["baseUrl"]) + } + if google["apiKey"] != "weston:abc123hex" { + t.Fatalf("expected cllama bearer token, got %v", google["apiKey"]) + } + if google["api"] != "openai-completions" { + t.Fatalf("expected google provider behind cllama to use openai-completions, got %v", google["api"]) + } + modelEntries, ok := google["models"].([]interface{}) + if !ok || len(modelEntries) != 1 { + t.Fatalf("expected one google model entry, got %T %v", google["models"], google["models"]) + } + entry, ok := modelEntries[0].(map[string]interface{}) + if !ok { + t.Fatalf("expected google model entry object, got %T", modelEntries[0]) + } + if entry["id"] != "google/gemini-3-flash-preview" { + t.Fatalf("expected google model id to stay provider-prefixed for cllama, got %v", entry["id"]) + } +} + +func TestGenerateConfigDirectGoogleKeepsNativeAPI(t *testing.T) { + rc := &driver.ResolvedClaw{ + Models: map[string]string{"primary": "google/gemini-3-flash-preview"}, + } + data, err := GenerateConfig(rc) + if err != nil { + t.Fatal(err) + } + if got, ok := getPath(data, "models.providers.google.api"); ok { + t.Fatalf("expected no models.providers.google config without cllama, got %v", got) + } + if got, ok := getPath(data, "agents.defaults.model.primary"); !ok || got != "google/gemini-3-flash-preview" { + t.Fatalf("expected direct google model to remain on agents.defaults.model.primary, got %v (present=%v)", got, ok) + } +} + func TestGenerateConfigNoCllamaNoProviderRewrite(t *testing.T) { rc := &driver.ResolvedClaw{ Models: map[string]string{"primary": "anthropic/claude-sonnet-4"},