From fb89fafc3a28dcfb8ee41719e49c34d8b8f78b89 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Tue, 18 Aug 2026 21:58:22 +0800 Subject: [PATCH] fix(backend,combo,cursor): header budget, Codex failover, kv_after_text (#10573) * fix(combo): allow fill-first failover across Codex OAuth connections applyNativeCodexTurnPin previously narrowed the target pool to the single pinned connection, making same-provider failover impossible when the pinned connection was rejected by pre-dispatch checks. Return all compatible connections (same provider + model) with the pinned connection first, so the combo engine can fall over to siblings. Also allow pinNativeCodexTurn to update connectionId for failover recovery while still rejecting provider/model changes. Fixes #10379 Signed-off-by: Minxi Hou * fix(test): replace as any with properly typed ResolvedComboTarget literal Addresses ESLint no-explicit-any error in tests/. Signed-off-by: Minxi Hou --------- Signed-off-by: Minxi Hou --- open-sse/services/combo/nativeCodexTurnPin.ts | 40 +++-- .../unit/native-codex-turn-pin-10379.test.ts | 149 ++++++++++++++++++ 2 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 tests/unit/native-codex-turn-pin-10379.test.ts diff --git a/open-sse/services/combo/nativeCodexTurnPin.ts b/open-sse/services/combo/nativeCodexTurnPin.ts index 5ffa6bcf70..9f9ab96d6d 100644 --- a/open-sse/services/combo/nativeCodexTurnPin.ts +++ b/open-sse/services/combo/nativeCodexTurnPin.ts @@ -74,12 +74,12 @@ export function pinNativeCodexTurn(args: { const existing = pins.get(key); if ( existing && - (existing.modelStr !== args.target.modelStr || - existing.provider !== args.target.provider || - existing.connectionId !== args.connectionId) + (existing.modelStr !== args.target.modelStr || existing.provider !== args.target.provider) ) { throw new Error("Native Codex turn target changed after output was emitted"); } + // ConnectionId changes are allowed (failover to sibling connection) + // as long as provider + model stay the same. const now = Date.now(); pins.set(key, { comboName: args.comboName, @@ -92,21 +92,37 @@ export function pinNativeCodexTurn(args: { prune(now); } +/** + * Apply a native Codex turn pin to the target list. + * + * Returns all compatible targets (same provider + model) with the pinned + * connection preferred first. This allows fill-first failover: if the + * pinned connection is rejected by a pre-dispatch gate, the combo engine + * tries the next compatible connection instead of returning 503. + * + * Provider + model remain locked for the turn — only the connection + * can fall over. + */ export function applyNativeCodexTurnPin( targets: ResolvedComboTarget[], pin: NativeTurnPin ): ResolvedComboTarget[] { - const target = targets.find( + const compatible = targets.filter( (candidate) => candidate.modelStr === pin.modelStr && candidate.provider === pin.provider ); - if (!target) return []; - return [ - { - ...target, - connectionId: pin.connectionId, - allowedConnectionIds: [pin.connectionId], - }, - ]; + if (compatible.length === 0) return []; + + const pinned = compatible.find((t) => t.connectionId === pin.connectionId); + const siblings = compatible.filter((t) => t.connectionId !== pin.connectionId); + + // Pinned connection first, then same-provider/model siblings as fallback + const ordered = pinned ? [pinned, ...siblings] : compatible; + + return ordered.map((target) => ({ + ...target, + // Allow only connections for the pinned provider+model + allowedConnectionIds: compatible.map((t) => t.connectionId), + })); } export function revokeNativeCodexTurnPinsForConnection(connectionId: string): number { diff --git a/tests/unit/native-codex-turn-pin-10379.test.ts b/tests/unit/native-codex-turn-pin-10379.test.ts new file mode 100644 index 0000000000..54a58bdb4d --- /dev/null +++ b/tests/unit/native-codex-turn-pin-10379.test.ts @@ -0,0 +1,149 @@ +// #10379: Native Codex turn pin must allow fill-first failover across +// compatible connections for the same provider + model. +import { test } from "node:test"; +import assert from "node:assert/strict"; + +const { + applyNativeCodexTurnPin, + pinNativeCodexTurn, + getNativeCodexTurnPin, + clearNativeCodexTurnPinsForTests, +} = await import("../../open-sse/services/combo/nativeCodexTurnPin.ts"); + +const BODY = { + client_metadata: { + "x-codex-turn-metadata": JSON.stringify({ thread_id: "t1", turn_id: "turn1" }), + }, +}; + +function makeTarget(connectionId: string, model = "gpt-5.6-sol", provider = "codex") { + return { + kind: "model" as const, + stepId: `step-${connectionId}`, + executionKey: `ek-${connectionId}`, + modelStr: model, + provider, + providerId: null, + connectionId, + weight: 1, + label: null, + }; +} + +test("pinned connection is preferred but siblings are included as fallback", () => { + clearNativeCodexTurnPinsForTests(); + pinNativeCodexTurn({ + body: BODY, + comboName: "test-combo", + target: makeTarget("conn-1"), + connectionId: "conn-1", + }); + + const pin = getNativeCodexTurnPin(BODY, "test-combo")!; + const targets = [makeTarget("conn-1"), makeTarget("conn-2"), makeTarget("conn-3")]; + const result = applyNativeCodexTurnPin(targets, pin); + + assert.equal(result.length, 3, "all compatible connections should be returned"); + assert.equal(result[0].connectionId, "conn-1", "pinned connection should be first"); + assert.deepEqual(result[0].allowedConnectionIds, ["conn-1", "conn-2", "conn-3"]); +}); + +test("fallback connections share the same allowedConnectionIds", () => { + clearNativeCodexTurnPinsForTests(); + pinNativeCodexTurn({ + body: BODY, + comboName: "test-combo", + target: makeTarget("conn-2"), + connectionId: "conn-2", + }); + + const pin = getNativeCodexTurnPin(BODY, "test-combo")!; + const targets = [makeTarget("conn-1"), makeTarget("conn-2"), makeTarget("conn-3")]; + const result = applyNativeCodexTurnPin(targets, pin); + + assert.equal(result[0].connectionId, "conn-2"); + assert.equal(result[1].connectionId, "conn-1"); + assert.equal(result[2].connectionId, "conn-3"); + for (const t of result) { + assert.deepEqual(t.allowedConnectionIds, ["conn-1", "conn-2", "conn-3"]); + } +}); + +test("incompatible targets (different provider/model) are excluded", () => { + clearNativeCodexTurnPinsForTests(); + pinNativeCodexTurn({ + body: BODY, + comboName: "test-combo", + target: makeTarget("conn-1"), + connectionId: "conn-1", + }); + + const pin = getNativeCodexTurnPin(BODY, "test-combo")!; + const targets = [ + makeTarget("conn-1"), + makeTarget("conn-2"), + makeTarget("conn-other", "different-model", "other-provider"), + ]; + const result = applyNativeCodexTurnPin(targets, pin); + + assert.equal(result.length, 2, "incompatible target should be excluded"); + assert.deepEqual(result[0].allowedConnectionIds, ["conn-1", "conn-2"]); +}); + +test("empty result when no compatible targets exist", () => { + clearNativeCodexTurnPinsForTests(); + pinNativeCodexTurn({ + body: BODY, + comboName: "test-combo", + target: makeTarget("conn-1"), + connectionId: "conn-1", + }); + + const pin = getNativeCodexTurnPin(BODY, "test-combo")!; + const targets = [makeTarget("conn-x", "other-model", "other-provider")]; + const result = applyNativeCodexTurnPin(targets, pin); + + assert.equal(result.length, 0); +}); + +test("pinNativeCodexTurn allows connectionId change for same provider+model", () => { + clearNativeCodexTurnPinsForTests(); + pinNativeCodexTurn({ + body: BODY, + comboName: "test-combo", + target: makeTarget("conn-1"), + connectionId: "conn-1", + }); + + // Should NOT throw when only connectionId changes + pinNativeCodexTurn({ + body: BODY, + comboName: "test-combo", + target: makeTarget("conn-2"), + connectionId: "conn-2", + }); + + const pin = getNativeCodexTurnPin(BODY, "test-combo")!; + assert.equal(pin.connectionId, "conn-2", "pin should update to new connection"); +}); + +test("pinNativeCodexTurn rejects provider/model change", () => { + clearNativeCodexTurnPinsForTests(); + pinNativeCodexTurn({ + body: BODY, + comboName: "test-combo", + target: makeTarget("conn-1"), + connectionId: "conn-1", + }); + + assert.throws( + () => + pinNativeCodexTurn({ + body: BODY, + comboName: "test-combo", + target: makeTarget("conn-1", "different-model", "codex"), + connectionId: "conn-1", + }), + /Native Codex turn target changed/ + ); +});