Files
OmniRoute/open-sse/config/codexTurnState.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

145 lines
5.1 KiB
TypeScript

/**
* codexTurnState.ts — `x-codex-turn-state` relay bookkeeping and the
* cross-account echo guard.
*
* The upstream mints the opaque turn-state blob under the outbound identity
* (including the fingerprint-converged installation/session/thread ids), and
* the real Codex client echoes it back on later requests of the same turn —
* codex-rs captures it from the /responses SSE, the /responses/compact JSON,
* and the WS handshake (codex-api/src/sse/responses.rs, endpoint/compact.rs).
*
* Replaying a blob to the SAME account is self-consistent. Replaying it to a
* DIFFERENT account (failover rotated the connection while the client still
* echoes the old account's blob) is a contradiction only a proxy chain can
* produce — a real Codex client never emits it. The provenance table records
* which connection minted the blob a downstream session last received, and
* the outbound guard strips echoes known to come from another account.
*
* Mirrors sub2api v0.1.177 `openai_codex_turn_state.go` (commit 8219dcfc8).
* OmniRoute keys the table by the client's original session id only — the
* executor pipeline does not carry the API key id, and a real Codex session
* id is a random UUID, so accidental cross-key collisions are not a
* practical concern.
*/
const CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
/**
* How long a provenance record lives. The blob is echoed within one turn,
* but clients may hold it across a whole session; 2h covers the standard
* 5-hour quota window's early turns without letting the map grow stale
* entries for days.
*/
const CODEX_TURN_STATE_TTL_MS = 2 * 60 * 60 * 1000;
/** Opportunistic full sweep every N writes (the read side also lazily expires). */
const CODEX_TURN_STATE_SWEEP_EVERY_WRITES = 256;
type CodexTurnStateOrigin = {
accountKey: string;
expiresAt: number;
};
const turnStateOrigins = new Map<string, CodexTurnStateOrigin>();
let turnStateWrites = 0;
function normalizeAccountKey(accountKey: unknown): string | null {
if (typeof accountKey !== "string") return null;
const trimmed = accountKey.trim();
return trimmed || null;
}
/**
* Read the turn-state blob from a headers bag (Headers instance or a plain
* record with arbitrary casing). Returns null when absent/blank.
*/
export function readCodexTurnStateHeader(
headers: Headers | Record<string, unknown> | null | undefined
): string | null {
if (!headers) return null;
if (headers instanceof Headers) {
const value = headers.get(CODEX_TURN_STATE_HEADER);
return typeof value === "string" && value.trim() ? value.trim() : null;
}
if (typeof headers === "object") {
for (const [key, value] of Object.entries(headers)) {
if (
key.toLowerCase() === CODEX_TURN_STATE_HEADER &&
typeof value === "string" &&
value.trim()
) {
return value.trim();
}
}
}
return null;
}
function sweepExpiredTurnStateOrigins(now: number): void {
for (const [key, origin] of turnStateOrigins) {
if (origin.expiresAt <= now) {
turnStateOrigins.delete(key);
}
}
}
/**
* Record that `accountKey` minted the turn-state blob this downstream session
* just received. Must only be called at the response commit point — when the
* header is actually written to the client. Recording earlier (e.g. for an
* attempt later discarded by failover) would poison the table and make the
* guard strip the NEXT account's legitimate echo.
*/
export function noteCodexTurnStateProvenance(
clientSessionId: string | null | undefined,
accountKey: unknown,
nowMs?: number
): void {
const sessionId = typeof clientSessionId === "string" ? clientSessionId.trim() : "";
const account = normalizeAccountKey(accountKey);
if (!sessionId || !account) return;
const now = typeof nowMs === "number" ? nowMs : Date.now();
turnStateOrigins.set(sessionId, {
accountKey: account,
expiresAt: now + CODEX_TURN_STATE_TTL_MS,
});
turnStateWrites += 1;
if (turnStateWrites % CODEX_TURN_STATE_SWEEP_EVERY_WRITES === 0) {
sweepExpiredTurnStateOrigins(now);
}
}
/**
* Outbound guard: true when the echoed blob is KNOWN to have been minted by a
* different account and must be stripped before going upstream. Same-account
* or unknown provenance passes through unchanged — stripping only, never
* injection (clients that cannot echo are the Claude bridge's concern, not
* this module's).
*/
export function isCrossAccountCodexTurnState(
clientSessionId: string | null | undefined,
accountKey: unknown,
nowMs?: number
): boolean {
const sessionId = typeof clientSessionId === "string" ? clientSessionId.trim() : "";
const account = normalizeAccountKey(accountKey);
if (!sessionId || !account) return false;
const origin = turnStateOrigins.get(sessionId);
if (!origin) return false;
const now = typeof nowMs === "number" ? nowMs : Date.now();
if (origin.expiresAt <= now) {
turnStateOrigins.delete(sessionId);
return false;
}
return origin.accountKey !== account;
}
/** Test hook: forget all provenance records and reset the sweep counter. */
export function __resetCodexTurnStateOriginsForTesting(): void {
turnStateOrigins.clear();
turnStateWrites = 0;
}