From df2e44037ee95a13460486191d0479806a2b65ec Mon Sep 17 00:00:00 2001 From: Circumsized <109891092+Circumsized@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:17:22 +0800 Subject: [PATCH 1/3] fix(model-routing): normalize display-name prefixed models for routing --- internal/accounts/pool.go | 23 ++++++++++++++++++++++- internal/accounts/pool_test.go | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/accounts/pool.go b/internal/accounts/pool.go index 050db84..84ba264 100644 --- a/internal/accounts/pool.go +++ b/internal/accounts/pool.go @@ -160,8 +160,29 @@ func CanonicalModelID(model string) string { return key } +// NormalizeModelName converts display-name formats sent by external clients +// into the canonical model ID used for routing. It currently strips a leading +// "Provider: " segment (single-word provider, no spaces) before lowercasing +// and folding separators so names like "DeepSeek: DeepSeek V4.1 Flash" become +// "deepseek-v4.1-flash". +func NormalizeModelName(model string) string { + model = strings.TrimSpace(model) + if model == "" { + return model + } + parts := strings.SplitN(model, ":", 2) + if len(parts) == 2 { + provider := strings.TrimSpace(parts[0]) + displayName := strings.TrimSpace(parts[1]) + if !strings.Contains(provider, " ") { + model = displayName + } + } + return CanonicalModelID(model) +} + func routeModel(model string) string { - id := CanonicalModelID(model) + id := NormalizeModelName(model) if id == "" || id == "auto" { return "" } diff --git a/internal/accounts/pool_test.go b/internal/accounts/pool_test.go index a6210a0..921b994 100644 --- a/internal/accounts/pool_test.go +++ b/internal/accounts/pool_test.go @@ -569,3 +569,17 @@ func TestQuotaCooledEmptyCatalogSurfacesQuotaHint(t *testing.T) { t.Fatal("quota-cooled empty catalog must not count as a live candidate") } } + +func TestNormalizeModelNameStripsProviderPrefix(t *testing.T) { + for input, want := range map[string]string{ + "DeepSeek: DeepSeek V4.1 Flash": "deepseek-v4.1-flash", + "DeepSeek_V4.1_Flash": "deepseek-v4.1-flash", + "workbuddy/deepseek-v4.1-flash": "deepseek-v4.1-flash", + "MiniMax-M3": "minimax-m3", + "Qwen3.7-Plus": "qwen3.7-plus", + } { + if got := NormalizeModelName(input); got != want { + t.Fatalf("NormalizeModelName(%q) = %q, want %q", input, got, want) + } + } +} From c6b25e6f7b4bdd6d7b09802f05a1cb7a980a7073 Mon Sep 17 00:00:00 2001 From: Circumsized <109891092+Circumsized@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:26:12 +0800 Subject: [PATCH 2/3] fix(logging): classify context canceled as canceled instead of unavailable --- internal/accounts/classify.go | 1 + internal/api/chat.go | 2 +- internal/executor/chat.go | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/accounts/classify.go b/internal/accounts/classify.go index 30c957c..ac45c06 100644 --- a/internal/accounts/classify.go +++ b/internal/accounts/classify.go @@ -16,6 +16,7 @@ const ( KindUnavailable = "unavailable" KindInvalidRequest = "invalid_request" KindModelNotAvailable = "model_not_available" + KindCanceled = "canceled" ) const maxRetryAfter = 10 * time.Minute diff --git a/internal/api/chat.go b/internal/api/chat.go index 052d4f1..bcf1e15 100644 --- a/internal/api/chat.go +++ b/internal/api/chat.go @@ -912,7 +912,7 @@ func classifyAPIError(err error) accounts.Classified { } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return accounts.Classified{ - Kind: accounts.KindUnavailable, Status: 499, Failover: false, + Kind: accounts.KindCanceled, Status: 499, Failover: false, Code: "request_canceled", Message: err.Error(), } } diff --git a/internal/executor/chat.go b/internal/executor/chat.go index 7f47666..59c25e1 100644 --- a/internal/executor/chat.go +++ b/internal/executor/chat.go @@ -965,6 +965,12 @@ func (e ChatExecutor) classifyInProcessError(err error) accounts.Classified { } var providerErr *providers.Error if !errors.As(err, &providerErr) || providerErr == nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return accounts.Classified{ + Kind: accounts.KindCanceled, Status: 499, Failover: false, + Code: "request_canceled", Message: err.Error(), + } + } return accounts.Classify(0, err.Error(), "", accounts.KindUnavailable, "") } message := strings.TrimSpace(providerErr.Message) From 293ac2fb7c8ba3e0d4f1c2c08f11993db1c59499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E5=8F=8A?= <522caiji@gmail.com> Date: Tue, 15 Sep 2026 15:27:03 +0800 Subject: [PATCH 3/3] fix(model-routing): keep provider slash prefixes in normalize tests NormalizeModelName only strips "Provider: display" segments. Slash-prefixed IDs like workbuddy/deepseek-v4.1-flash stay intact for routing. Also assert canceled credential errors classify as KindCanceled. --- internal/accounts/pool_test.go | 2 +- internal/api/auth_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/accounts/pool_test.go b/internal/accounts/pool_test.go index 921b994..0b938ec 100644 --- a/internal/accounts/pool_test.go +++ b/internal/accounts/pool_test.go @@ -574,7 +574,7 @@ func TestNormalizeModelNameStripsProviderPrefix(t *testing.T) { for input, want := range map[string]string{ "DeepSeek: DeepSeek V4.1 Flash": "deepseek-v4.1-flash", "DeepSeek_V4.1_Flash": "deepseek-v4.1-flash", - "workbuddy/deepseek-v4.1-flash": "deepseek-v4.1-flash", + "workbuddy/deepseek-v4.1-flash": "workbuddy/deepseek-v4.1-flash", "MiniMax-M3": "minimax-m3", "Qwen3.7-Plus": "qwen3.7-plus", } { diff --git a/internal/api/auth_test.go b/internal/api/auth_test.go index 10e65fa..43ebe0e 100644 --- a/internal/api/auth_test.go +++ b/internal/api/auth_test.go @@ -25,7 +25,7 @@ func TestClassifyCanceledErrorDoesNotBecomeAuth(t *testing.T) { if classified.Kind == accounts.KindAuth { t.Fatalf("canceled credential error classified as auth: %+v", classified) } - if classified.Failover || classified.Cooldown != 0 || classified.Code != "request_canceled" { + if classified.Kind != accounts.KindCanceled || classified.Failover || classified.Cooldown != 0 || classified.Code != "request_canceled" { t.Fatalf("canceled error classification = %+v", classified) } }