From e93e6368fc87cb38ab4f8cab38f7b3b6b44306f1 Mon Sep 17 00:00:00 2001 From: HuntercodeT <61233559+HuntercodeT@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:13:11 +0800 Subject: [PATCH] fix(scheduler): keep OpenAI passthrough flag in the scheduler snapshot projection filterSchedulerExtra is an allowlist and never carried openai_passthrough / openai_oauth_passthrough, while credentials.model_mapping is preserved in full. Candidate filtering (ListSchedulableAccounts) reads that trimmed projection (sched:meta:), so there a passthrough account looks exactly like a whitelist-mode one: Account.IsModelSupported falls back to the model_mapping whitelist and the account is dropped as model_not_supported. Forwarding, which hydrates the full account from sched:acc:, still honors passthrough - hence "testing the account directly succeeds, but the gateway reports no available accounts supporting model X". The practical impact is that a stale leftover whitelist silently shrinks the candidate pool of accounts that were explicitly configured to let the upstream decide. When no account in the group lists the requested model, the group is fully dark for it even though every account can serve it. This is not a regression of #4936: that fix corrected the predicate (Account.IsModelSupported short-circuits passthrough before the mapping check), this one restores the input the predicate is fed. The projection allowlist predates #4936 by three and a half months and has never carried the flag, so the fix simply never took effect on the snapshot path. Add both keys to the allowlist, plus a regression test that round-trips the projection through JSON (the real sched:meta write/read path) and asserts the model gate still lets a non-whitelisted model through for a passthrough account. Claude-Session: https://claude.ai/code/session_01LwvyJVV2sdqiu41Z9JDNUq --- .../internal/repository/scheduler_cache.go | 7 ++++ .../repository/scheduler_cache_unit_test.go | 42 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/backend/internal/repository/scheduler_cache.go b/backend/internal/repository/scheduler_cache.go index b53c51d43f..e5ff4a9d9f 100644 --- a/backend/internal/repository/scheduler_cache.go +++ b/backend/internal/repository/scheduler_cache.go @@ -1000,6 +1000,13 @@ func filterSchedulerExtra(extra map[string]any) map[string]any { "openai_ws_force_http", "openai_responses_mode", "openai_responses_supported", + // 透传开关必须进投影:候选过滤(ListSchedulableAccounts)读的是本投影, + // 而 Account.IsModelSupported 靠 extra 上的这两个键短路 model_mapping 白名单。 + // 裁掉它们,透传账号在选号阶段会退回按(常为过期的)白名单判定并被误判为 + // model_not_supported —— 转发阶段却仍按透传工作,表现为"单独测账号能通、 + // 走网关报 no available accounts"。 + "openai_passthrough", + "openai_oauth_passthrough", "codex_fingerprint_mode", "codex_fingerprint_seed", "codex_5h_used_percent", diff --git a/backend/internal/repository/scheduler_cache_unit_test.go b/backend/internal/repository/scheduler_cache_unit_test.go index 354cf6d8aa..f151e33cd8 100644 --- a/backend/internal/repository/scheduler_cache_unit_test.go +++ b/backend/internal/repository/scheduler_cache_unit_test.go @@ -1079,3 +1079,45 @@ func schedulerCacheBenchmarkAccounts(size int) []service.Account { } return accounts } + +// 调度投影必须保留 OpenAI 透传开关。 +// +// 候选过滤走 ListSchedulableAccounts,读的是 buildSchedulerMetadataAccount 产出的精简投影; +// Account.IsModelSupported 又靠 extra 上的透传开关短路 model_mapping 白名单(#4936)。 +// 一旦投影把开关裁掉、却保留了白名单,透传账号在选号阶段就会退回白名单判定并被误判成 +// model_not_supported,而转发阶段(读完整账号)仍按透传工作 —— 表现为"单独测这个账号能通、 +// 走网关却报 no available accounts"。#4936 修的是判定逻辑,这里守的是喂给判定的输入。 +func TestBuildSchedulerMetadataAccount_KeepsOpenAIPassthroughForModelGate(t *testing.T) { + for _, key := range []string{"openai_passthrough", "openai_oauth_passthrough"} { + t.Run(key, func(t *testing.T) { + account := service.Account{ + ID: 383, + Platform: service.PlatformOpenAI, + Type: service.AccountTypeOAuth, + Credentials: map[string]any{ + // 账号从白名单模式切到透传后常见的残留映射,未列出请求的模型。 + "model_mapping": map[string]any{"gpt-5.5": "gpt-5.5"}, + "access_token": "drop-me", + }, + Extra: map[string]any{key: true}, + } + require.True(t, account.IsModelSupported("gpt-5.6-sol"), + "前置条件:透传账号本应放行白名单外的模型") + + meta := buildSchedulerMetadataAccount(account) + + // 走一遍真实的序列化/反序列化路径(写入 sched:meta 再由 decodeCachedAccount 读回)。 + payload, err := json.Marshal(meta) + require.NoError(t, err) + var restored service.Account + require.NoError(t, json.Unmarshal(payload, &restored)) + + require.Equal(t, true, restored.Extra[key]) + require.True(t, restored.IsOpenAIPassthroughEnabled()) + require.True(t, restored.IsModelSupported("gpt-5.6-sol"), + "投影裁掉透传开关会让透传账号在候选过滤阶段被误判为 model_not_supported") + // 白名单本身仍需保留:非透传账号依赖它做模型门。 + require.Equal(t, map[string]any{"gpt-5.5": "gpt-5.5"}, restored.Credentials["model_mapping"]) + }) + } +}