-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(responses): recall last combo on compaction after a mid-session combo switch #3891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /** | ||
| * Session-scoped recall of the last successful combo selection (#3891). | ||
| * | ||
| * When Codex compacts a conversation that was switched to a different combo | ||
| * mid-session, it sends the *bare native model* of the new combo's first | ||
| * target (e.g. "gpt-5.6-terra") rather than the combo/<id> selector it | ||
| * uses for ordinary turns. Without recall, that bare model hits the router | ||
| * and fails with 404 ("requires the canonical openai provider") because no | ||
| * canonical route exists for it. | ||
| * | ||
| * This module remembers, per session lane, which combo last served a | ||
| * successful turn and what its concrete target model was. The compaction | ||
| * entry points then rewrite a bare model back to the remembered | ||
| * combo/<id> selector only when the bare model exactly matches the | ||
| * remembered combo target, so explicit provider/model selectors and | ||
| * unrelated models are never touched. | ||
| */ | ||
|
|
||
| interface ComboRecallEntry { | ||
| comboId: string; | ||
| targetModel: string; | ||
| at: number; | ||
| } | ||
|
|
||
| /** Bounded map: stale entries are dropped, oldest evicted at capacity. */ | ||
| const RECALL_CAPACITY = 256; | ||
| const RECALL_TTL_MS = 30 * 60 * 1000; | ||
|
|
||
| const recall = new Map<string, ComboRecallEntry>(); | ||
|
|
||
| export function rememberComboForLane( | ||
| lane: string | undefined, | ||
| comboId: string, | ||
| targetModel: string, | ||
| ): void { | ||
| if (!lane || !comboId || !targetModel) return; | ||
| // Delete-then-set keeps insertion order fresh for eviction. | ||
| recall.delete(lane); | ||
| recall.set(lane, { comboId, targetModel, at: Date.now() }); | ||
| while (recall.size > RECALL_CAPACITY) { | ||
| const oldest = recall.keys().next().value; | ||
| if (oldest === undefined) break; | ||
| recall.delete(oldest); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns the remembered combo id when the incoming bare model exactly | ||
| * matches the combo target that last succeeded on this lane. Returns | ||
| * undefined for explicit provider selectors, stale lanes, and | ||
| * non-matching models: those keep ordinary routing. | ||
| */ | ||
| export function recallComboForLane( | ||
| lane: string | undefined, | ||
| model: string, | ||
| ): string | undefined { | ||
| if (!lane || !model) return undefined; | ||
| const entry = recall.get(lane); | ||
| if (!entry) return undefined; | ||
| if (Date.now() - entry.at > RECALL_TTL_MS) { | ||
| recall.delete(lane); | ||
| return undefined; | ||
| } | ||
| if (entry.targetModel !== model) return undefined; | ||
| return entry.comboId; | ||
| } | ||
|
|
||
| /** Test-only: clear all recall state. */ | ||
| export function clearComboRecallForTests(): void { | ||
| recall.clear(); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -59,6 +59,10 @@ import { | |||||||||||||||||
| providerContinuationRouteScope, | ||||||||||||||||||
| sameProviderContinuationOwner, | ||||||||||||||||||
| } from "../../responses/provider-continuation"; | ||||||||||||||||||
| import { | ||||||||||||||||||
| rememberComboForLane, | ||||||||||||||||||
| recallComboForLane, | ||||||||||||||||||
| } from "./combo-session-recall"; | ||||||||||||||||||
| import { | ||||||||||||||||||
| comboRouteDecisionTrace, | ||||||||||||||||||
| NoEligiblePolicyCandidateError, | ||||||||||||||||||
|
|
@@ -2788,6 +2792,11 @@ export async function handleComboResponses( | |||||||||||||||||
| (logCtx.attempts ??= []).push(attempt); | ||||||||||||||||||
| attemptRetained = true; | ||||||||||||||||||
| noteComboSuccess(comboId, combo, pick.target, pick.writerGeneration); | ||||||||||||||||||
| rememberComboForLane( | ||||||||||||||||||
| sessionLaneIdFromRequest(req.headers), | ||||||||||||||||||
| comboId, | ||||||||||||||||||
| pick.target.model, | ||||||||||||||||||
| ); | ||||||||||||||||||
| Object.assign(logCtx, childLog, { | ||||||||||||||||||
| requestedModel, | ||||||||||||||||||
| model: requestedModel, | ||||||||||||||||||
|
|
@@ -3109,6 +3118,26 @@ async function handleResponsesInner( | |||||||||||||||||
| effort: comboEffortRow.effort, | ||||||||||||||||||
| }; | ||||||||||||||||||
| } | ||||||||||||||||||
| // A combo switch mid-session leaves Codex sending the bare native model of | ||||||||||||||||||
| // the new combo's target in the compaction request (#3886). Rewrite it to | ||||||||||||||||||
| // the remembered combo selector BEFORE comboIdFromRawBody reads model, so | ||||||||||||||||||
| // the combo dispatch and failover path engage. Only fires for compaction | ||||||||||||||||||
| // requests (compaction_trigger present in input) whose model is bare (no | ||||||||||||||||||
| // provider/ prefix) and exactly matches the combo target last served on | ||||||||||||||||||
| // this session lane. | ||||||||||||||||||
| if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) { | ||||||||||||||||||
| const rawModel = (body as { model?: unknown }).model; | ||||||||||||||||||
| const rawInput = (body as { input?: unknown }).input; | ||||||||||||||||||
| const isCompactionTrigger = Array.isArray(rawInput) | ||||||||||||||||||
| && rawInput.some((item: unknown) => | ||||||||||||||||||
| typeof item === "object" && item !== null && (item as { type?: string }).type === "compaction_trigger"); | ||||||||||||||||||
| if (typeof rawModel === "string" && !rawModel.includes("/") && isCompactionTrigger) { | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Exclude synthetic selectors from combo recall.
The v1 compact path excludes fast rows at Proposed fix- if (typeof rawModel === "string" && !rawModel.includes("/") && isCompactionTrigger) {
+ if (
+ typeof rawModel === "string"
+ && !rawModel.includes("/")
+ && !comboRows.fastRow
+ && !comboEffortRow
+ && isCompactionTrigger
+ ) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| const recalledComboId = recallComboForLane(sessionLaneIdFromRequest(req.headers), rawModel); | ||||||||||||||||||
| if (recalledComboId) { | ||||||||||||||||||
| (body as Record<string, unknown>).model = `combo/${recalledComboId}`; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null; | ||||||||||||||||||
| if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) { | ||||||||||||||||||
| options.onRequestBodyRead?.(); | ||||||||||||||||||
|
|
||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.