mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 20:02:45 +03:00
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.
145 lines
5.1 KiB
TypeScript
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;
|
|
}
|