mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-19 13:42:09 +03:00
PR #10573 landed with two real defects surfaced by typecheck/tests on the combined release tip: - TS2322: allowedConnectionIds (string[]) was built from compatible.map(t => t.connectionId), whose type includes null. Filter nulls before assigning. - applyNativeCodexTurnPin never assigned the pinned connectionId onto a compatible candidate that didn't already carry it (e.g. an unresolved placeholder target with connectionId: null) — the pin was silently dropped instead of applied. Now resolves the pinned slot's connectionId explicitly (in original order, so allowedConnectionIds stays consistent regardless of pinned-first reordering) before building the returned target list. Confirmed via the existing focused suites: tests/unit/chatgpt-web-codex-turn-pin.test.ts and tests/unit/native-codex-turn-pin-10379.test.ts (14/14 pass), typecheck:core clean.
156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import type { ResolvedComboTarget } from "./types.ts";
|
|
|
|
type NativeTurnPin = {
|
|
comboName: string;
|
|
modelStr: string;
|
|
provider: string;
|
|
connectionId: string;
|
|
createdAt: number;
|
|
expiresAt: number;
|
|
};
|
|
|
|
const TTL_MS = 45 * 60_000;
|
|
const MAX_PINS = 1_000;
|
|
const pins = new Map<string, NativeTurnPin>();
|
|
|
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: undefined;
|
|
}
|
|
|
|
function turnMetadata(body: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
const metadata = record(body.client_metadata);
|
|
const raw = metadata?.["x-codex-turn-metadata"];
|
|
if (typeof raw === "string") {
|
|
try {
|
|
return record(JSON.parse(raw));
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
return record(raw);
|
|
}
|
|
|
|
export function nativeCodexTurnKey(
|
|
body: Record<string, unknown>,
|
|
comboName: string
|
|
): string | null {
|
|
const metadata = turnMetadata(body);
|
|
const threadId = typeof metadata?.thread_id === "string" ? metadata.thread_id : "";
|
|
const turnId = typeof metadata?.turn_id === "string" ? metadata.turn_id : "";
|
|
if (!threadId || !turnId) return null;
|
|
return createHash("sha256").update(JSON.stringify({ comboName, threadId, turnId })).digest("hex");
|
|
}
|
|
|
|
function prune(now = Date.now()): void {
|
|
for (const [key, pin] of pins) if (pin.expiresAt <= now) pins.delete(key);
|
|
while (pins.size > MAX_PINS) {
|
|
const oldest = pins.keys().next().value as string | undefined;
|
|
if (!oldest) break;
|
|
pins.delete(oldest);
|
|
}
|
|
}
|
|
|
|
export function getNativeCodexTurnPin(
|
|
body: Record<string, unknown>,
|
|
comboName: string
|
|
): NativeTurnPin | null {
|
|
prune();
|
|
const key = nativeCodexTurnKey(body, comboName);
|
|
return key ? (pins.get(key) ?? null) : null;
|
|
}
|
|
|
|
export function pinNativeCodexTurn(args: {
|
|
body: Record<string, unknown>;
|
|
comboName: string;
|
|
target: ResolvedComboTarget;
|
|
connectionId: string;
|
|
}): void {
|
|
const key = nativeCodexTurnKey(args.body, args.comboName);
|
|
if (!key || !args.connectionId) return;
|
|
const existing = pins.get(key);
|
|
if (
|
|
existing &&
|
|
(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,
|
|
modelStr: args.target.modelStr,
|
|
provider: args.target.provider,
|
|
connectionId: args.connectionId,
|
|
createdAt: existing?.createdAt ?? now,
|
|
expiresAt: now + TTL_MS,
|
|
});
|
|
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 compatible = targets.filter(
|
|
(candidate) => candidate.modelStr === pin.modelStr && candidate.provider === pin.provider
|
|
);
|
|
if (compatible.length === 0) return [];
|
|
|
|
let pinnedIndex = compatible.findIndex((t) => t.connectionId === pin.connectionId);
|
|
// No candidate already carries the pinned connectionId (e.g. the caller
|
|
// resolved the target before a connection was assigned) — assign the pin
|
|
// onto the first compatible candidate so dispatch targets it directly.
|
|
if (pinnedIndex < 0) pinnedIndex = 0;
|
|
|
|
// Resolve the pinned slot's connectionId in ORIGINAL order first, so
|
|
// allowedConnectionIds reflects the same set/order regardless of which
|
|
// candidate ends up first in the returned (pinned-first) array.
|
|
const resolved = compatible.map((t, i) =>
|
|
i === pinnedIndex ? { ...t, connectionId: pin.connectionId } : t
|
|
);
|
|
const allowedConnectionIds = resolved
|
|
.map((t) => t.connectionId)
|
|
.filter((id): id is string => id !== null);
|
|
|
|
// Pinned connection first, then same-provider/model siblings as fallback
|
|
const pinned = resolved[pinnedIndex];
|
|
const siblings = resolved.filter((_, i) => i !== pinnedIndex);
|
|
const ordered = [pinned, ...siblings];
|
|
|
|
return ordered.map((target) => ({
|
|
...target,
|
|
// Allow only connections for the pinned provider+model
|
|
allowedConnectionIds,
|
|
}));
|
|
}
|
|
|
|
export function revokeNativeCodexTurnPinsForConnection(connectionId: string): number {
|
|
let revoked = 0;
|
|
for (const [key, pin] of pins) {
|
|
if (pin.connectionId !== connectionId) continue;
|
|
pins.delete(key);
|
|
revoked += 1;
|
|
}
|
|
return revoked;
|
|
}
|
|
|
|
export function clearNativeCodexTurnPinsForTests(): void {
|
|
pins.clear();
|
|
}
|