From 7ec8cb866ccbbc6ef787c333da49b600bbbfcbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E6=96=87=E9=94=8B0668000834?= Date: Wed, 18 Mar 2026 10:46:39 +0800 Subject: [PATCH] fix(provider): normalize openrouter model slugs in async spawn Fix OpenRouter model normalization for nested transport-prefixed model slugs. PicoClaw could forward raw values like openrouter/minimax/minimax-m2.5 directly to OpenRouter on async spawn/subagent paths, which causes 400 invalid model ID. This change normalizes nested OpenRouter-prefixed slugs to the provider-accepted / form before the API call, while preserving special cases like openrouter/auto and openrouter/free. - Strip leading openrouter/ segment when remaining value is a nested path - Preserve openrouter/auto and openrouter/free shorthand values - Add regression tests for nested OpenRouter model refs Fixes #1678 --- pkg/providers/openai_compat/provider.go | 3 + pkg/providers/openai_compat/provider_test.go | 71 +++++++++++++++++--- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index fb2abaa5c..cb43fa84a 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -182,6 +182,9 @@ func normalizeModel(model, apiBase string) string { } if strings.Contains(strings.ToLower(apiBase), "openrouter.ai") { + if strings.EqualFold(before, "openrouter") && strings.Contains(after, "/") { + return after + } return model } diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index ed9747f9d..820503796 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -561,17 +561,68 @@ func TestProviderChat_AcceptsNumericOptionTypes(t *testing.T) { } func TestNormalizeModel_UsesAPIBase(t *testing.T) { - if got := normalizeModel("deepseek/deepseek-chat", "https://api.deepseek.com/v1"); got != "deepseek-chat" { - t.Fatalf("normalizeModel(deepseek) = %q, want %q", got, "deepseek-chat") + tests := []struct { + name string + model string + apiBase string + want string + }{ + { + name: "strips deepseek transport prefix", + model: "deepseek/deepseek-chat", + apiBase: "https://api.deepseek.com/v1", + want: "deepseek-chat", + }, + { + name: "preserves openrouter auto shorthand", + model: "openrouter/auto", + apiBase: "https://openrouter.ai/api/v1", + want: "openrouter/auto", + }, + { + name: "preserves openrouter free shorthand", + model: "openrouter/free", + apiBase: "https://openrouter.ai/api/v1", + want: "openrouter/free", + }, + { + name: "strips openrouter transport prefix for nested vendor model", + model: "openrouter/openai/gpt-5.4", + apiBase: "https://openrouter.ai/api/v1", + want: "openai/gpt-5.4", + }, + { + name: "strips openrouter transport prefix for any nested provider path", + model: "openrouter/stepfun/step-3.5-flash:free", + apiBase: "https://openrouter.ai/api/v1", + want: "stepfun/step-3.5-flash:free", + }, + { + name: "strips openrouter transport prefix for other nested vendor model", + model: "openrouter/minimax/minimax-m2.5", + apiBase: "https://openrouter.ai/api/v1", + want: "minimax/minimax-m2.5", + }, + { + name: "strips vivgrid transport prefix", + model: "vivgrid/managed", + apiBase: "https://api.vivgrid.com/v1", + want: "managed", + }, + { + name: "strips vivgrid auto alias", + model: "vivgrid/auto", + apiBase: "https://api.vivgrid.com/v1", + want: "auto", + }, } - if got := normalizeModel("openrouter/auto", "https://openrouter.ai/api/v1"); got != "openrouter/auto" { - t.Fatalf("normalizeModel(openrouter) = %q, want %q", got, "openrouter/auto") - } - if got := normalizeModel("vivgrid/managed", "https://api.vivgrid.com/v1"); got != "managed" { - t.Fatalf("normalizeModel(vivgrid) = %q, want %q", got, "managed") - } - if got := normalizeModel("vivgrid/auto", "https://api.vivgrid.com/v1"); got != "auto" { - t.Fatalf("normalizeModel(vivgrid auto) = %q, want %q", got, "auto") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := normalizeModel(tt.model, tt.apiBase); got != tt.want { + t.Fatalf("normalizeModel(%q, %q) = %q, want %q", tt.model, tt.apiBase, got, tt.want) + } + }) } }