mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
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 <houminxi@gmail.com> * fix(test): replace as any with properly typed ResolvedComboTarget literal Addresses ESLint no-explicit-any error in tests/. Signed-off-by: Minxi Hou <houminxi@gmail.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
149
tests/unit/native-codex-turn-pin-10379.test.ts
Normal file
149
tests/unit/native-codex-turn-pin-10379.test.ts
Normal file
@@ -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/
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user