Skip to content

Commit 4fc2985

Browse files
authored
Merge pull request #129 from mostlydev/issue-127-openclaw-google-cllama-api
fix(openclaw): use proxy API for google models behind cllama
2 parents 4fbcfcb + 6599062 commit 4fc2985

File tree

2 files changed

+79
-1
lines changed

2 files changed

+79
-1
lines changed

internal/driver/openclaw/config.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func GenerateConfig(rc *driver.ResolvedClaw) ([]byte, error) {
5656
return nil, fmt.Errorf("config generation: cllama provider %q apiKey: %w", provider, err)
5757
}
5858
}
59-
if err := setPath(config, basePath+".api", defaultModelAPIForProvider(provider)); err != nil {
59+
if err := setPath(config, basePath+".api", cllamaModelAPIForProvider(provider)); err != nil {
6060
return nil, fmt.Errorf("config generation: cllama provider %q api: %w", provider, err)
6161
}
6262
modelDefs := make([]interface{}, 0, len(modelIDs))
@@ -459,6 +459,17 @@ func defaultModelAPIForProvider(provider string) string {
459459
}
460460
}
461461

462+
func cllamaModelAPIForProvider(provider string) string {
463+
switch normalizeProviderID(provider) {
464+
case "anthropic", "synthetic", "minimax-portal", "kimi-coding", "cloudflare-ai-gateway", "xiaomi":
465+
return "anthropic-messages"
466+
default:
467+
// cllama exposes OpenAI-compatible routing for non-Anthropic providers,
468+
// even when the upstream vendor has a native API surface.
469+
return "openai-completions"
470+
}
471+
}
472+
462473
// setPath sets a nested value in a map using a dotted path.
463474
func setPath(obj map[string]interface{}, path string, value interface{}) error {
464475
return shared.SetPath(obj, path, value)

internal/driver/openclaw/config_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ func TestGenerateConfigCllamaRewritesProviderBaseURL(t *testing.T) {
8383
if anthropic["baseUrl"] != "http://cllama:8080/v1" {
8484
t.Errorf("expected proxy baseUrl, got %v", anthropic["baseUrl"])
8585
}
86+
if anthropic["api"] != "anthropic-messages" {
87+
t.Fatalf("expected anthropic provider behind cllama to use anthropic-messages, got %v", anthropic["api"])
88+
}
8689
modelEntries, ok := anthropic["models"].([]interface{})
8790
if !ok || len(modelEntries) == 0 {
8891
t.Fatalf("expected models.providers.anthropic.models entries, got %T %v", anthropic["models"], anthropic["models"])
@@ -96,6 +99,70 @@ func TestGenerateConfigCllamaRewritesProviderBaseURL(t *testing.T) {
9699
}
97100
}
98101

102+
func TestGenerateConfigCllamaGoogleUsesOpenAICompletions(t *testing.T) {
103+
rc := &driver.ResolvedClaw{
104+
Models: map[string]string{"primary": "google/gemini-3-flash-preview"},
105+
Cllama: []string{"passthrough"},
106+
CllamaToken: "weston:abc123hex",
107+
}
108+
data, err := GenerateConfig(rc)
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
var config map[string]interface{}
113+
if err := json.Unmarshal(data, &config); err != nil {
114+
t.Fatal(err)
115+
}
116+
modelsCfg, ok := config["models"].(map[string]interface{})
117+
if !ok {
118+
t.Fatal("expected models config")
119+
}
120+
providers, ok := modelsCfg["providers"].(map[string]interface{})
121+
if !ok {
122+
t.Fatal("expected models.providers config")
123+
}
124+
google, ok := providers["google"].(map[string]interface{})
125+
if !ok {
126+
t.Fatal("expected models.providers.google config")
127+
}
128+
if google["baseUrl"] != "http://cllama:8080/v1" {
129+
t.Fatalf("expected proxy baseUrl, got %v", google["baseUrl"])
130+
}
131+
if google["apiKey"] != "weston:abc123hex" {
132+
t.Fatalf("expected cllama bearer token, got %v", google["apiKey"])
133+
}
134+
if google["api"] != "openai-completions" {
135+
t.Fatalf("expected google provider behind cllama to use openai-completions, got %v", google["api"])
136+
}
137+
modelEntries, ok := google["models"].([]interface{})
138+
if !ok || len(modelEntries) != 1 {
139+
t.Fatalf("expected one google model entry, got %T %v", google["models"], google["models"])
140+
}
141+
entry, ok := modelEntries[0].(map[string]interface{})
142+
if !ok {
143+
t.Fatalf("expected google model entry object, got %T", modelEntries[0])
144+
}
145+
if entry["id"] != "google/gemini-3-flash-preview" {
146+
t.Fatalf("expected google model id to stay provider-prefixed for cllama, got %v", entry["id"])
147+
}
148+
}
149+
150+
func TestGenerateConfigDirectGoogleKeepsNativeAPI(t *testing.T) {
151+
rc := &driver.ResolvedClaw{
152+
Models: map[string]string{"primary": "google/gemini-3-flash-preview"},
153+
}
154+
data, err := GenerateConfig(rc)
155+
if err != nil {
156+
t.Fatal(err)
157+
}
158+
if got, ok := getPath(data, "models.providers.google.api"); ok {
159+
t.Fatalf("expected no models.providers.google config without cllama, got %v", got)
160+
}
161+
if got, ok := getPath(data, "agents.defaults.model.primary"); !ok || got != "google/gemini-3-flash-preview" {
162+
t.Fatalf("expected direct google model to remain on agents.defaults.model.primary, got %v (present=%v)", got, ok)
163+
}
164+
}
165+
99166
func TestGenerateConfigNoCllamaNoProviderRewrite(t *testing.T) {
100167
rc := &driver.ResolvedClaw{
101168
Models: map[string]string{"primary": "anthropic/claude-sonnet-4"},

0 commit comments

Comments
 (0)