UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

148 lines (147 loc) • 7.47 kB
/** * Codex-outbound fallback: cache-preservation guards and affinity-key * derivation for the Codex -> Anthropic/Vertex hop. * * Owns three things, all pure: the defensive shape check that guards where * `applyClaudeRequestCacheBreakpoints` marks breakpoint 1 (`system` must be * exactly the stable developer-block prefix), the role-alternation guard * that turns a translation defect into a loud, attributable error instead of * a silent Anthropic 400, and the session-affinity key a Codex-origin * request supplies so consecutive turns of one conversation route back to * the account already holding their cache. * * Both guards are scoped to the new Codex-fallback entry point only, called * immediately after `translateCodexRequestToClaude()` and before * `applyClaudeRequestCacheBreakpoints()`. They are not folded into the * shared breakpoint function itself, which the existing OpenAI bridge also * calls: adding a hard throw there would newly reject that bridge's * already-shipped, already-latent-buggy same-role-message traffic that has * shipped without incident. */ import { createHash } from "node:crypto"; /** * Thrown by `assertClaudeSystemPrefixShape` and `assertClaudeMessagesAlternate` * when the translated request does not have the shape breakpoint placement * (or Anthropic's role-alternation requirement) depends on. Never logged and * swallowed — a silent violation here defeats caching with no visible * symptom besides a slow-moving cache-hit-rate metric, or turns into an * undifferentiated Anthropic 400. */ export class ClaudeSystemPrefixShapeError extends Error { } /** * Enforces that `system` is exactly the stable developer-block prefix before * a caller marks breakpoint 1 on its last element. `translateCodexRequestToClaude` * is contracted to produce `system` containing only the developer-role-derived * blocks and nothing else — `environment_context` and any other per-turn * payload belong in `messages`. `system[system.length-1]` is only a safe * breakpoint-1 anchor while that invariant holds; trusting it silently would * be exactly the failure this guards against. */ export function assertClaudeSystemPrefixShape(system, expectedLength) { if (!Array.isArray(system)) { throw new ClaudeSystemPrefixShapeError(`expected an array-shaped system prefix, got ${typeof system}`); } if (system.length !== expectedLength) { throw new ClaudeSystemPrefixShapeError(`expected system.length === ${expectedLength} (fixed developer-block prefix), got ${system.length}`); } } /** * Throws, naming the offending index, when `messages` is not alternation-safe. * * Anthropic requires alternating `user`/`assistant` roles. A native Codex * request can carry two consecutive `user`-role items (an auto-injected * notice immediately followed by the literal user turn, or a synthetic * `environment_context` pair) — mapped 1:1 into `ClaudeMessage` entries that * would otherwise reach Anthropic as back-to-back `user` messages and get * rejected on every request that hits it. Merging same-role Codex items into * one Claude message with multiple content blocks is translation-correctness * work owned by `translateCodexRequestToClaude`, not this guard's job — this * only turns a missed merge into an attributable thrown error at the new * hop's boundary instead of an undifferentiated Anthropic 400. */ export function assertClaudeMessagesAlternate(messages) { for (let i = 1; i < messages.length; i++) { if (messages[i].role === messages[i - 1].role) { throw new ClaudeSystemPrefixShapeError(`messages[${i}] repeats role "${messages[i].role}" from messages[${i - 1}]` + " — Anthropic requires alternation; the Codex-request translator" + " must merge same-role turns into one message with multiple content blocks"); } } } /** * The leading, turn-stable bytes of a native Codex request: a bounded head of * the developer-role (instructions) text plus the declared tool names, mirroring * `codexCachePrefix` (`codexFallback.ts`) for the opposite direction. Descriptions * and schemas are deliberately left out — they sit behind the instructions in * the prefix, so two requests agreeing on this much already share a cacheable * head worth routing together. */ const PREFIX_DIGEST_HEAD_CHARS = 8192; function codexNativeInstructionsHead(native) { const developerText = native.input .filter((item) => item.type === "message" && item.role === "developer") .flatMap((item) => "content" in item && Array.isArray(item.content) ? item.content : []) .filter((part) => part.type === "input_text") .map((part) => part.text) .join("\n"); return developerText.slice(0, PREFIX_DIGEST_HEAD_CHARS); } function codexNativeToolNames(native) { const additionalTools = native.input .filter((item) => item.type === "additional_tools") .at(-1); if (!additionalTools) { return ""; } return additionalTools.tools .flatMap((namespace) => namespace.tools.map((tool) => tool.name)) .join("\u0000"); } /** * Hashed bounded-head-plus-tool-names digest of a native Codex request's * turn-stable prefix, used only as the last-resort affinity-key fallback * (`codex-prefix:`) when the wire carries none of `prompt_cache_key` / * `client_metadata.thread_id` / `client_metadata.session_id`. Hashed (rather * than returned raw, as `codexCachePrefix` does) because this value is used * directly as a routing/log key, not as sha256 input a caller hashes itself. */ function codexNativePrefixDigest(native) { const head = codexNativeInstructionsHead(native); const toolNames = codexNativeToolNames(native); if (!head && !toolNames) { return undefined; } return createHash("sha256").update(`${head}\u0001${toolNames}`).digest("hex"); } /** * Derives the session-affinity key a Codex-origin request supplies to the * Claude pipeline's existing session-affinity machinery, so consecutive * turns of one conversation route back to the account already holding their * cache. Preference order mirrors what a real Codex CLI capture carries: * `prompt_cache_key` (top-level, the client's own pre-computed key — already * exactly the signal being reconstructed) beats `client_metadata.thread_id` * beats `client_metadata.session_id`; a genuinely identity-free request * degrades to a prefix-derived key so at least concurrent turns of the same * agent config bucket together. * * The three-way prefix (`codex-thread:` / `codex-session:` / `codex-prefix:`) * keeps this namespace from colliding with a Claude-Code `metadata.user_id`- * derived UUID or with itself across cases. */ export function codexAnthropicAffinityKey(native) { const promptCacheKey = native.prompt_cache_key; if (typeof promptCacheKey === "string" && promptCacheKey.length > 0) { return `codex-thread:${promptCacheKey}`; } const threadId = native.client_metadata?.thread_id; if (typeof threadId === "string" && threadId.length > 0) { return `codex-thread:${threadId}`; } const sessionId = native.client_metadata?.session_id; if (typeof sessionId === "string" && sessionId.length > 0) { return `codex-session:${sessionId}`; } const prefix = codexNativePrefixDigest(native); return prefix ? `codex-prefix:${prefix}` : undefined; }