Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
目的
未开启“高级调度”(
openai_advanced_scheduler_enabled,默认关闭)时,让选号决策如实反映粘性命中,并让带previous_response_id的请求回到持有该响应的账号——与高级调度的行为对齐。解决的问题
selectAccountWithSchedulerOnce在调度器为 nil(即未开高级调度)时走的是旧选号路径,这条路径有两处缺口:1. 决策标签恒为
load_balance。 进入旧路径时decision.Layer被固定成load_balance,返回前不回填SelectedAccountID/SelectedAccountType,StickySessionHit恒为 false——即使账号其实来自会话粘性命中。后果是openai.account_schedule_decision、schedule_layer这些日志字段在默认配置下不可信,排查“会话为什么换了账号”时看不出粘性是否生效。2.
previous_response_id不参与选号。 旧路径从不按previous_response_id查持有响应的账号,StickyPreviousHit恒为 false。而 WS 入口(openai_gateway_handler.go的“切组/会话失配防护”)见到previousResponseID != "" && !StickyPreviousHit && previousResponseCanMove就会剥掉首包里的previous_response_id。于是在默认配置下,只要客户端在选号时带了previous_response_id(HTTP 请求,或 WS 连接的首帧):previous_response_id总会被剥掉、退化为用 input 重建上下文,即使会话粘性恰好选回了原账号也一样;function_call_output的工具续链无法重建、previous_response_id原样保留,按该处注释的说法,原样转发到别的账号会触发上游会话链鉴权失败。这一条是对照高级调度读代码发现的缺口。我们自己的 Codex 流量在选号时不带
previous_response_id(8 天约 2.3 万次选号里出现 0 次),走不到这条路径;受影响的是用previous_response_id串联多轮的 Responses API 客户端。因为没有线上流量数据,这一条改用专门的端到端探针在真实上游上验证,见“效果”。效果
线上数据:决策标签
我们自建实例未开高级调度(
openai_advanced_scheduler_enabled=false),运行日志为 debug 级。按部署本修复的时刻把openai.account_schedule_decision日志切成两段统计:layer=load_balancelayer=session_hash(sticky_session_hit=true)改前另有 2 条
guardian_parent。WS 入口的openai.websocket_account_selected同样:改前 5,290 条schedule_layer全是load_balance,改后 4,110 条里 2,387 条(58.1%)是session_hash。也就是说,改前日志显示“粘性从不命中”,而实际上八成以上的选号来自会话粘性。改后窗口内实例还带有我们 fork 的其它调度改动,比例本身不宜直接归因;能归因到本 PR 的是“粘性命中从完全不可见变为如实记录”。
真实上游:按响应路由
环境:未开高级调度,账号为 ctx_pool 模式,请求
store=false。每条 WS 连接使用不同的session_id,排除靠会话粘性碰巧回到原账号的可能。okschedule_layer=load_balance,账号 A,上游连接 Cprevious_response_id和一句“暗号是什么”,不带任何历史schedule_layer=previous_response_id,仍是账号 A;ingress_ws_continuation_probe的preferred_conn_id指向 C 并复用了它previous_response_idunknownschedule_layer=load_balance改前的行为没有在旧二进制上实跑。按代码,改前连接 2 首帧的
previous_response_id会被剥掉,请求内容就等同于上面的对照行。行为对照
layer=load_balance,sticky_session_hit=false,未回填选中账号layer=session_hash,sticky_session_hit=true,回填选中账号previous_response_id,持有账号可用sticky_previous_hit=false,WS 首包被剥previous_response_idlayer=previous_response_id,sticky_previous_hit=true改动
AccountSelectionResult增加未导出字段stickySessionHit,旧路径两个分支(未开批量负载、负载感知的粘性分支)命中粘性时置位;新增applyLegacySelectionDecision在返回前回填选中账号,粘性命中时标session_hash层与StickySessionHit。selectAccountForModelWithExclusions保持原签名,内部委托给多返回一个“是否粘性命中”的selectAccountForModelWithExclusionsStickyHit。selectLegacyAccountByPreviousResponse,旧路径选号前先用selectAccountByPreviousResponseIDForCapability找持有该响应的账号,校验直接复用高级调度器 previous_response 层的isAccountRequestCompatibleReason(分组、隐私、运行期封禁、代理隔离、上游模型渠道限制、传输与能力),另在路由前做请求模型的渠道定价限制检查。不兼容就释放槽位、回退到原有选号;命中后标previous_response_id层与StickyPreviousHit,并顺带写会话粘性绑定。仅对 OpenAI 平台生效。开启高级调度的部署不受影响:两处改动都在
scheduler == nil分支内。测试
新增
openai_legacy_scheduler_decision_test.go:TestLegacySchedulerDecision_StickySessionLayer:批量负载开/关两种配置下,粘性命中标session_hash、未命中保持load_balance,两种情况都回填选中账号。TestLegacySchedulerDecision_PreviousResponseRouting:路由到持有账号;持有账号被排除时回退;传输不兼容、上游模型受渠道限制、代理被隔离时释放槽位并回退;请求模型受渠道限制时在路由前失败;全部代理被隔离时放行到持有账号(与高级调度的 fail-open 一致)。用例在开头复位包级的高级调度开关缓存(5 秒 TTL),避免同包其它用例写入的开关串扰。
./internal/service全量-tags=unit:除TestOllamaProbeCallback_StaleLongDoesNotOverrideNewShort外全部通过。该用例是上游既有用例,本机 Windows 上因时钟步进固定失败,本 PR 未触及相关限流代码。golangci-lint run ./internal/service/:0 issues。代价与保留项
store=false时响应状态保存在上游那条 WS 连接里,而 ctx_pool 的上游连接是共享的。探针第一次运行时,我们在连接 1 与连接 2 之间插了另一个会话的请求,它复用了同一条上游连接并开了新链;随后带previous_response_id的请求虽然被正确路由到同一账号、同一条上游连接,上游仍返回previous_response_not_found。这是 ctx_pool 共享连接的固有性质,高级调度走的是同一套逻辑,不是本 PR 引入的。本 PR 只保证“路由到持有账号并保留previous_response_id”,不保证上游一定还认得它。previous_response_id的请求在旧路径上多一次响应→账号的缓存查询与一次兼容校验;未带时直接跳过,无额外开销。selectAccountByPreviousResponseIDForCapability的现有行为:返回该账号的等待计划(粘性等待的超时与人数上限),不改等待/排队语义。