@juspay/neurolink 12.40.3 → 12.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +447 -443
- package/dist/cli/commands/proxy.js +5 -1
- package/dist/proxy/accountLedger.js +3 -0
- package/dist/proxy/codexOutboundCache.d.ts +52 -0
- package/dist/proxy/codexOutboundCache.js +147 -0
- package/dist/proxy/codexOutboundFallback.d.ts +27 -2
- package/dist/proxy/codexOutboundFallback.js +147 -11
- package/dist/proxy/codexResponsesFormat.js +7 -1
- package/dist/proxy/codexToAnthropicFallback.js +6 -0
- package/dist/proxy/proxyAnalysis.js +4 -0
- package/dist/proxy/proxyConfig.js +120 -0
- package/dist/proxy/proxyTokenUsage.d.ts +15 -0
- package/dist/proxy/proxyTokenUsage.js +33 -0
- package/dist/proxy/proxyTracer.d.ts +16 -1
- package/dist/proxy/proxyTracer.js +26 -5
- package/dist/proxy/requestLogger.js +9 -0
- package/dist/proxy/runtimeConfig.js +20 -0
- package/dist/proxy/sseInterceptor.js +9 -0
- package/dist/proxy/vertexAnthropicFallback.js +9 -0
- package/dist/server/routes/claudeProxyRoutes.js +65 -1
- package/dist/server/routes/codexProxyRoutes.d.ts +39 -3
- package/dist/server/routes/codexProxyRoutes.js +792 -8
- package/dist/server/routes/index.d.ts +8 -0
- package/dist/server/routes/index.js +9 -1
- package/dist/types/analytics.d.ts +25 -0
- package/dist/types/codex.d.ts +45 -1
- package/dist/types/proxy.d.ts +40 -2
- package/dist/types/proxyClient.d.ts +2 -0
- package/dist/types/server.d.ts +15 -0
- package/dist/types/subscription.d.ts +22 -0
- package/dist/utils/anthropicCacheBreakpoints.d.ts +2 -2
- package/dist/utils/anthropicCacheBreakpoints.js +14 -3
- package/dist/utils/pricing.js +101 -24
- package/docs-site/static/search-index.json +17 -6
- package/package.json +1 -1
|
@@ -1650,7 +1650,11 @@ export async function createProxyStartApp(params) {
|
|
|
1650
1650
|
// Keep bridge capabilities, accounting and cancellation in this worker
|
|
1651
1651
|
// across supervisor generation switches; never re-enter the public listener.
|
|
1652
1652
|
(request) => app.fetch(request));
|
|
1653
|
-
const codexRouteGroup = createCodexProxyRoutes(""
|
|
1653
|
+
const codexRouteGroup = createCodexProxyRoutes("", runtimeConfigProvider, params.port,
|
|
1654
|
+
// Same in-process loopback discipline as the OpenAI->Anthropic bridge:
|
|
1655
|
+
// stay in this worker across supervisor generation switches, never
|
|
1656
|
+
// re-enter the public listener.
|
|
1657
|
+
(request) => app.fetch(request));
|
|
1654
1658
|
const geminiRouteGroup = createGeminiProxyRoutes(params.modelRouter, "", params.port, runtimeConfigProvider);
|
|
1655
1659
|
const allProxyRoutes = [
|
|
1656
1660
|
...routeGroup.routes,
|
|
@@ -166,6 +166,7 @@ async function advanceCursor(fileName, cursor) {
|
|
|
166
166
|
outputTokens: finiteNumber(record.outputTokens),
|
|
167
167
|
cacheReadTokens: finiteNumber(record.cacheReadTokens),
|
|
168
168
|
cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
|
|
169
|
+
cacheCreation1hTokens: finiteNumber(record.cacheCreation1hTokens),
|
|
169
170
|
clientApp: resolveClientApp(record),
|
|
170
171
|
};
|
|
171
172
|
// A later record for the same request enriches the earlier one — it must
|
|
@@ -198,6 +199,7 @@ async function advanceCursor(fileName, cursor) {
|
|
|
198
199
|
outputTokens: Math.max(previous.outputTokens, next.outputTokens),
|
|
199
200
|
cacheReadTokens: Math.max(previous.cacheReadTokens, next.cacheReadTokens),
|
|
200
201
|
cacheCreationTokens: Math.max(previous.cacheCreationTokens, next.cacheCreationTokens),
|
|
202
|
+
cacheCreation1hTokens: Math.max(previous.cacheCreation1hTokens, next.cacheCreation1hTokens),
|
|
201
203
|
// Likewise keep a real model name over a placeholder.
|
|
202
204
|
model: next.model && next.model !== "-" ? next.model : previous.model,
|
|
203
205
|
}
|
|
@@ -339,6 +341,7 @@ export async function readAccountUsage(date = currentUsageDate()) {
|
|
|
339
341
|
total: entry.inputTokens + entry.outputTokens,
|
|
340
342
|
cacheReadTokens: entry.cacheReadTokens,
|
|
341
343
|
cacheCreationTokens: entry.cacheCreationTokens,
|
|
344
|
+
cacheCreation1hTokens: entry.cacheCreation1hTokens,
|
|
342
345
|
});
|
|
343
346
|
row.costUsd += cost;
|
|
344
347
|
client.costUsd += cost;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ClaudeMessage, ClaudeTextBlock, CodexAnthropicAffinityKey, CodexNativeRequest } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by `assertClaudeSystemPrefixShape` and `assertClaudeMessagesAlternate`
|
|
4
|
+
* when the translated request does not have the shape breakpoint placement
|
|
5
|
+
* (or Anthropic's role-alternation requirement) depends on. Never logged and
|
|
6
|
+
* swallowed — a silent violation here defeats caching with no visible
|
|
7
|
+
* symptom besides a slow-moving cache-hit-rate metric, or turns into an
|
|
8
|
+
* undifferentiated Anthropic 400.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ClaudeSystemPrefixShapeError extends Error {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Enforces that `system` is exactly the stable developer-block prefix before
|
|
14
|
+
* a caller marks breakpoint 1 on its last element. `translateCodexRequestToClaude`
|
|
15
|
+
* is contracted to produce `system` containing only the developer-role-derived
|
|
16
|
+
* blocks and nothing else — `environment_context` and any other per-turn
|
|
17
|
+
* payload belong in `messages`. `system[system.length-1]` is only a safe
|
|
18
|
+
* breakpoint-1 anchor while that invariant holds; trusting it silently would
|
|
19
|
+
* be exactly the failure this guards against.
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertClaudeSystemPrefixShape(system: ClaudeTextBlock[] | string | undefined, expectedLength: number): void;
|
|
22
|
+
/**
|
|
23
|
+
* Throws, naming the offending index, when `messages` is not alternation-safe.
|
|
24
|
+
*
|
|
25
|
+
* Anthropic requires alternating `user`/`assistant` roles. A native Codex
|
|
26
|
+
* request can carry two consecutive `user`-role items (an auto-injected
|
|
27
|
+
* notice immediately followed by the literal user turn, or a synthetic
|
|
28
|
+
* `environment_context` pair) — mapped 1:1 into `ClaudeMessage` entries that
|
|
29
|
+
* would otherwise reach Anthropic as back-to-back `user` messages and get
|
|
30
|
+
* rejected on every request that hits it. Merging same-role Codex items into
|
|
31
|
+
* one Claude message with multiple content blocks is translation-correctness
|
|
32
|
+
* work owned by `translateCodexRequestToClaude`, not this guard's job — this
|
|
33
|
+
* only turns a missed merge into an attributable thrown error at the new
|
|
34
|
+
* hop's boundary instead of an undifferentiated Anthropic 400.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertClaudeMessagesAlternate(messages: ReadonlyArray<ClaudeMessage>): void;
|
|
37
|
+
/**
|
|
38
|
+
* Derives the session-affinity key a Codex-origin request supplies to the
|
|
39
|
+
* Claude pipeline's existing session-affinity machinery, so consecutive
|
|
40
|
+
* turns of one conversation route back to the account already holding their
|
|
41
|
+
* cache. Preference order mirrors what a real Codex CLI capture carries:
|
|
42
|
+
* `prompt_cache_key` (top-level, the client's own pre-computed key — already
|
|
43
|
+
* exactly the signal being reconstructed) beats `client_metadata.thread_id`
|
|
44
|
+
* beats `client_metadata.session_id`; a genuinely identity-free request
|
|
45
|
+
* degrades to a prefix-derived key so at least concurrent turns of the same
|
|
46
|
+
* agent config bucket together.
|
|
47
|
+
*
|
|
48
|
+
* The three-way prefix (`codex-thread:` / `codex-session:` / `codex-prefix:`)
|
|
49
|
+
* keeps this namespace from colliding with a Claude-Code `metadata.user_id`-
|
|
50
|
+
* derived UUID or with itself across cases.
|
|
51
|
+
*/
|
|
52
|
+
export declare function codexAnthropicAffinityKey(native: CodexNativeRequest): CodexAnthropicAffinityKey | undefined;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex-outbound fallback: cache-preservation guards and affinity-key
|
|
3
|
+
* derivation for the Codex -> Anthropic/Vertex hop.
|
|
4
|
+
*
|
|
5
|
+
* Owns three things, all pure: the defensive shape check that guards where
|
|
6
|
+
* `applyClaudeRequestCacheBreakpoints` marks breakpoint 1 (`system` must be
|
|
7
|
+
* exactly the stable developer-block prefix), the role-alternation guard
|
|
8
|
+
* that turns a translation defect into a loud, attributable error instead of
|
|
9
|
+
* a silent Anthropic 400, and the session-affinity key a Codex-origin
|
|
10
|
+
* request supplies so consecutive turns of one conversation route back to
|
|
11
|
+
* the account already holding their cache.
|
|
12
|
+
*
|
|
13
|
+
* Both guards are scoped to the new Codex-fallback entry point only, called
|
|
14
|
+
* immediately after `translateCodexRequestToClaude()` and before
|
|
15
|
+
* `applyClaudeRequestCacheBreakpoints()`. They are not folded into the
|
|
16
|
+
* shared breakpoint function itself, which the existing OpenAI bridge also
|
|
17
|
+
* calls: adding a hard throw there would newly reject that bridge's
|
|
18
|
+
* already-shipped, already-latent-buggy same-role-message traffic that has
|
|
19
|
+
* shipped without incident.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
/**
|
|
23
|
+
* Thrown by `assertClaudeSystemPrefixShape` and `assertClaudeMessagesAlternate`
|
|
24
|
+
* when the translated request does not have the shape breakpoint placement
|
|
25
|
+
* (or Anthropic's role-alternation requirement) depends on. Never logged and
|
|
26
|
+
* swallowed — a silent violation here defeats caching with no visible
|
|
27
|
+
* symptom besides a slow-moving cache-hit-rate metric, or turns into an
|
|
28
|
+
* undifferentiated Anthropic 400.
|
|
29
|
+
*/
|
|
30
|
+
export class ClaudeSystemPrefixShapeError extends Error {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Enforces that `system` is exactly the stable developer-block prefix before
|
|
34
|
+
* a caller marks breakpoint 1 on its last element. `translateCodexRequestToClaude`
|
|
35
|
+
* is contracted to produce `system` containing only the developer-role-derived
|
|
36
|
+
* blocks and nothing else — `environment_context` and any other per-turn
|
|
37
|
+
* payload belong in `messages`. `system[system.length-1]` is only a safe
|
|
38
|
+
* breakpoint-1 anchor while that invariant holds; trusting it silently would
|
|
39
|
+
* be exactly the failure this guards against.
|
|
40
|
+
*/
|
|
41
|
+
export function assertClaudeSystemPrefixShape(system, expectedLength) {
|
|
42
|
+
if (!Array.isArray(system)) {
|
|
43
|
+
throw new ClaudeSystemPrefixShapeError(`expected an array-shaped system prefix, got ${typeof system}`);
|
|
44
|
+
}
|
|
45
|
+
if (system.length !== expectedLength) {
|
|
46
|
+
throw new ClaudeSystemPrefixShapeError(`expected system.length === ${expectedLength} (fixed developer-block prefix), got ${system.length}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Throws, naming the offending index, when `messages` is not alternation-safe.
|
|
51
|
+
*
|
|
52
|
+
* Anthropic requires alternating `user`/`assistant` roles. A native Codex
|
|
53
|
+
* request can carry two consecutive `user`-role items (an auto-injected
|
|
54
|
+
* notice immediately followed by the literal user turn, or a synthetic
|
|
55
|
+
* `environment_context` pair) — mapped 1:1 into `ClaudeMessage` entries that
|
|
56
|
+
* would otherwise reach Anthropic as back-to-back `user` messages and get
|
|
57
|
+
* rejected on every request that hits it. Merging same-role Codex items into
|
|
58
|
+
* one Claude message with multiple content blocks is translation-correctness
|
|
59
|
+
* work owned by `translateCodexRequestToClaude`, not this guard's job — this
|
|
60
|
+
* only turns a missed merge into an attributable thrown error at the new
|
|
61
|
+
* hop's boundary instead of an undifferentiated Anthropic 400.
|
|
62
|
+
*/
|
|
63
|
+
export function assertClaudeMessagesAlternate(messages) {
|
|
64
|
+
for (let i = 1; i < messages.length; i++) {
|
|
65
|
+
if (messages[i].role === messages[i - 1].role) {
|
|
66
|
+
throw new ClaudeSystemPrefixShapeError(`messages[${i}] repeats role "${messages[i].role}" from messages[${i - 1}]` +
|
|
67
|
+
" — Anthropic requires alternation; the Codex-request translator" +
|
|
68
|
+
" must merge same-role turns into one message with multiple content blocks");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The leading, turn-stable bytes of a native Codex request: a bounded head of
|
|
74
|
+
* the developer-role (instructions) text plus the declared tool names, mirroring
|
|
75
|
+
* `codexCachePrefix` (`codexFallback.ts`) for the opposite direction. Descriptions
|
|
76
|
+
* and schemas are deliberately left out — they sit behind the instructions in
|
|
77
|
+
* the prefix, so two requests agreeing on this much already share a cacheable
|
|
78
|
+
* head worth routing together.
|
|
79
|
+
*/
|
|
80
|
+
const PREFIX_DIGEST_HEAD_CHARS = 8192;
|
|
81
|
+
function codexNativeInstructionsHead(native) {
|
|
82
|
+
const developerText = native.input
|
|
83
|
+
.filter((item) => item.type === "message" && item.role === "developer")
|
|
84
|
+
.flatMap((item) => "content" in item && Array.isArray(item.content) ? item.content : [])
|
|
85
|
+
.filter((part) => part.type === "input_text")
|
|
86
|
+
.map((part) => part.text)
|
|
87
|
+
.join("\n");
|
|
88
|
+
return developerText.slice(0, PREFIX_DIGEST_HEAD_CHARS);
|
|
89
|
+
}
|
|
90
|
+
function codexNativeToolNames(native) {
|
|
91
|
+
const additionalTools = native.input
|
|
92
|
+
.filter((item) => item.type === "additional_tools")
|
|
93
|
+
.at(-1);
|
|
94
|
+
if (!additionalTools) {
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
return additionalTools.tools
|
|
98
|
+
.flatMap((namespace) => namespace.tools.map((tool) => tool.name))
|
|
99
|
+
.join("\u0000");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Hashed bounded-head-plus-tool-names digest of a native Codex request's
|
|
103
|
+
* turn-stable prefix, used only as the last-resort affinity-key fallback
|
|
104
|
+
* (`codex-prefix:`) when the wire carries none of `prompt_cache_key` /
|
|
105
|
+
* `client_metadata.thread_id` / `client_metadata.session_id`. Hashed (rather
|
|
106
|
+
* than returned raw, as `codexCachePrefix` does) because this value is used
|
|
107
|
+
* directly as a routing/log key, not as sha256 input a caller hashes itself.
|
|
108
|
+
*/
|
|
109
|
+
function codexNativePrefixDigest(native) {
|
|
110
|
+
const head = codexNativeInstructionsHead(native);
|
|
111
|
+
const toolNames = codexNativeToolNames(native);
|
|
112
|
+
if (!head && !toolNames) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return createHash("sha256").update(`${head}\u0001${toolNames}`).digest("hex");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Derives the session-affinity key a Codex-origin request supplies to the
|
|
119
|
+
* Claude pipeline's existing session-affinity machinery, so consecutive
|
|
120
|
+
* turns of one conversation route back to the account already holding their
|
|
121
|
+
* cache. Preference order mirrors what a real Codex CLI capture carries:
|
|
122
|
+
* `prompt_cache_key` (top-level, the client's own pre-computed key — already
|
|
123
|
+
* exactly the signal being reconstructed) beats `client_metadata.thread_id`
|
|
124
|
+
* beats `client_metadata.session_id`; a genuinely identity-free request
|
|
125
|
+
* degrades to a prefix-derived key so at least concurrent turns of the same
|
|
126
|
+
* agent config bucket together.
|
|
127
|
+
*
|
|
128
|
+
* The three-way prefix (`codex-thread:` / `codex-session:` / `codex-prefix:`)
|
|
129
|
+
* keeps this namespace from colliding with a Claude-Code `metadata.user_id`-
|
|
130
|
+
* derived UUID or with itself across cases.
|
|
131
|
+
*/
|
|
132
|
+
export function codexAnthropicAffinityKey(native) {
|
|
133
|
+
const promptCacheKey = native.prompt_cache_key;
|
|
134
|
+
if (typeof promptCacheKey === "string" && promptCacheKey.length > 0) {
|
|
135
|
+
return `codex-thread:${promptCacheKey}`;
|
|
136
|
+
}
|
|
137
|
+
const threadId = native.client_metadata?.thread_id;
|
|
138
|
+
if (typeof threadId === "string" && threadId.length > 0) {
|
|
139
|
+
return `codex-thread:${threadId}`;
|
|
140
|
+
}
|
|
141
|
+
const sessionId = native.client_metadata?.session_id;
|
|
142
|
+
if (typeof sessionId === "string" && sessionId.length > 0) {
|
|
143
|
+
return `codex-session:${sessionId}`;
|
|
144
|
+
}
|
|
145
|
+
const prefix = codexNativePrefixDigest(native);
|
|
146
|
+
return prefix ? `codex-prefix:${prefix}` : undefined;
|
|
147
|
+
}
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* `tool_use` block, no kind discriminator) knows which declared tools were
|
|
18
18
|
* `custom` rather than `function`.
|
|
19
19
|
*/
|
|
20
|
-
import type { ClaudeRequest } from "../types/index.js";
|
|
21
|
-
import type { CodexNativeRequest, CodexNativeToolKind, CodexTranslationError } from "../types/index.js";
|
|
20
|
+
import type { ClaudeRequest, ClaudeTextBlock } from "../types/index.js";
|
|
21
|
+
import type { CodexNativeInputItem, CodexNativeRequest, CodexNativeToolKind, CodexOutboundFailureInput, CodexOutboundFallbackDecision, CodexTranslationError } from "../types/index.js";
|
|
22
22
|
export declare function parseCodexNativeRequest(raw: unknown): {
|
|
23
23
|
ok: true;
|
|
24
24
|
value: CodexNativeRequest;
|
|
@@ -26,6 +26,7 @@ export declare function parseCodexNativeRequest(raw: unknown): {
|
|
|
26
26
|
ok: false;
|
|
27
27
|
error: CodexTranslationError;
|
|
28
28
|
};
|
|
29
|
+
export declare function buildSystemBlocksFromDeveloperMessages(items: readonly CodexNativeInputItem[]): ClaudeTextBlock[];
|
|
29
30
|
export declare function translateCodexRequestToClaude(request: CodexNativeRequest, target: {
|
|
30
31
|
provider: "anthropic" | "vertex";
|
|
31
32
|
model: string;
|
|
@@ -37,3 +38,27 @@ export declare function translateCodexRequestToClaude(request: CodexNativeReques
|
|
|
37
38
|
ok: false;
|
|
38
39
|
error: CodexTranslationError;
|
|
39
40
|
};
|
|
41
|
+
/** Caps how many times one request may cross between engines (Codex->Anthropic
|
|
42
|
+
* or Anthropic->Codex) in a single turn. A crossing is an engine change, never
|
|
43
|
+
* a same-engine account rotation, so this bounds `Codex -> Anthropic ->
|
|
44
|
+
* Codex -> ...` cycles without limiting ordinary in-pool retries. */
|
|
45
|
+
export declare const MAX_ENGINE_CROSSINGS = 1;
|
|
46
|
+
/** Read a transport error code the same way `codexProxyRoutes.ts`'s own
|
|
47
|
+
* (unexported) `getCodexTransportErrorCode` does: a direct `.code`, falling
|
|
48
|
+
* back to `.cause.code`. Kept here, independently, as a pure utility the
|
|
49
|
+
* trigger-policy classifier can be exercised against without importing a
|
|
50
|
+
* route-handler module. */
|
|
51
|
+
export declare function parseCodexErrorCode(error: unknown): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Classify one `codexProxyRoutes.ts` `dispatch()` failure into an
|
|
54
|
+
* outbound-fallback eligibility decision. A total function over
|
|
55
|
+
* `CodexOutboundFailureClass` (exhaustive switch, no `default:`): a missing
|
|
56
|
+
* case fails to compile under `noImplicitReturns`, never falls through to a
|
|
57
|
+
* silent `undefined`.
|
|
58
|
+
*
|
|
59
|
+
* `context_preflight` / `token_budget` errors are never modeled here: both
|
|
60
|
+
* are self-contained terminal returns inside `dispatch()`'s own outer `try`,
|
|
61
|
+
* before any of the four insertion points this classifier serves, so they
|
|
62
|
+
* never reach it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function classifyCodexOutboundFailure(input: CodexOutboundFailureInput): CodexOutboundFallbackDecision;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolveClaudeMaxTokens } from "../utils/tokenLimits.js";
|
|
2
2
|
import { inlineJsonSchema } from "../utils/schemaConversion.js";
|
|
3
|
+
import { modelSupportsForcedToolChoice, claudeDisabledThinkingReplacement, } from "../models/modelRegistry.js";
|
|
3
4
|
import { parseToolArguments } from "./argumentParsing.js";
|
|
5
|
+
import { classifyProxyFailureCode } from "./proxyFailureDetails.js";
|
|
4
6
|
import { recordCodexOutboundSchemaDegraded, recordCodexOutboundUnsupportedField, } from "./proxyTracer.js";
|
|
5
7
|
import { CODEX_STREAM_MAX_TOOL_CALLS, CODEX_STREAM_SIZE_CEILING_BYTES, } from "./streamLimits.js";
|
|
6
8
|
// ---------------------------------------------------------------------------
|
|
@@ -132,21 +134,35 @@ export function parseCodexNativeRequest(raw) {
|
|
|
132
134
|
// output to attach to, regardless of whether the source request bothered to
|
|
133
135
|
// say "auto" out loud. `parallel_tool_calls: false` is inert on `"none"` —
|
|
134
136
|
// there is nothing running in parallel to disable.
|
|
135
|
-
|
|
137
|
+
//
|
|
138
|
+
// `targetModel` gates a second degradation: some Claude families (Sonnet
|
|
139
|
+
// 5.5, Opus 5.5, Fable/Mythos 5.1 — see modelSupportsForcedToolChoice) answer
|
|
140
|
+
// a forced tool_choice (`any`/named `tool`) with an HTTP 400. For those
|
|
141
|
+
// targets only, a forced choice is downgraded to `{type:"auto"}` (keeping
|
|
142
|
+
// `disable_parallel_tool_use` when the source asked for it) instead of going
|
|
143
|
+
// out as a request the target is guaranteed to reject. Every other target
|
|
144
|
+
// model is unaffected — this is a per-request, per-target decision, not a
|
|
145
|
+
// global behavior change.
|
|
146
|
+
function mapCodexToolChoiceToClaude(choice, parallelToolCalls, targetModel) {
|
|
136
147
|
const disable = parallelToolCalls === false;
|
|
137
148
|
if (choice === undefined || choice === "auto") {
|
|
138
149
|
return disable
|
|
139
150
|
? { type: "auto", disable_parallel_tool_use: true }
|
|
140
151
|
: { type: "auto" };
|
|
141
152
|
}
|
|
153
|
+
if (choice === "none") {
|
|
154
|
+
return { type: "none" };
|
|
155
|
+
}
|
|
156
|
+
if (!modelSupportsForcedToolChoice(targetModel)) {
|
|
157
|
+
return disable
|
|
158
|
+
? { type: "auto", disable_parallel_tool_use: true }
|
|
159
|
+
: { type: "auto" };
|
|
160
|
+
}
|
|
142
161
|
if (choice === "required") {
|
|
143
162
|
return disable
|
|
144
163
|
? { type: "any", disable_parallel_tool_use: true }
|
|
145
164
|
: { type: "any" };
|
|
146
165
|
}
|
|
147
|
-
if (choice === "none") {
|
|
148
|
-
return { type: "none" };
|
|
149
|
-
}
|
|
150
166
|
return disable
|
|
151
167
|
? { type: "tool", name: choice.name, disable_parallel_tool_use: true }
|
|
152
168
|
: { type: "tool", name: choice.name };
|
|
@@ -206,8 +222,10 @@ function mapCodexFunctionToolToClaude(tool) {
|
|
|
206
222
|
}
|
|
207
223
|
function mapCodexCustomToolToClaude(tool) {
|
|
208
224
|
// Lossy, explicitly flagged: no JSON Schema exists for a Lark/regex grammar.
|
|
209
|
-
// The model is no longer grammar-constrained after this mapping
|
|
210
|
-
//
|
|
225
|
+
// The model is no longer grammar-constrained after this mapping. SIGNED OFF
|
|
226
|
+
// 2026-09-28 (design doc §10, ruling 7): grammar/custom tools wrapped as a
|
|
227
|
+
// single `{input: string}` parameter, with `custom_tool_call` on the way
|
|
228
|
+
// back, is the shipped behavior — not a placeholder awaiting approval.
|
|
211
229
|
return {
|
|
212
230
|
tool: {
|
|
213
231
|
name: tool.name,
|
|
@@ -262,11 +280,23 @@ const REASONING_BUDGET = {
|
|
|
262
280
|
xhigh: 24576,
|
|
263
281
|
max: 32768,
|
|
264
282
|
};
|
|
265
|
-
|
|
283
|
+
// Codex "no reasoning" never goes out as an explicit `thinking:{type:"disabled"}`
|
|
284
|
+
// — some Claude 5.5/5.1 families reject that shape with a 400 (probed on
|
|
285
|
+
// Vertex, 2026-09-29) because they think adaptively and always on. What
|
|
286
|
+
// "thinking off" means for those targets is decided by
|
|
287
|
+
// claudeDisabledThinkingReplacement: `between_tools` (Sonnet 5.5's lowest
|
|
288
|
+
// setting) when available, otherwise `omit` (nothing sent — the model cannot
|
|
289
|
+
// be turned off, and the field is dropped rather than asking for a rejected
|
|
290
|
+
// state). For every target outside that family the function returns "keep",
|
|
291
|
+
// which here means the historical, unaffected behavior: omit the field.
|
|
292
|
+
function mapCodexReasoningToThinking(reasoning, resolvedMaxTokens, targetModel) {
|
|
266
293
|
if (!reasoning ||
|
|
267
294
|
reasoning.effort === "none" ||
|
|
268
295
|
reasoning.effort === "minimal") {
|
|
269
|
-
|
|
296
|
+
const replacement = claudeDisabledThinkingReplacement(targetModel, reasoning?.effort);
|
|
297
|
+
return replacement === "between_tools"
|
|
298
|
+
? { type: "between_tools" }
|
|
299
|
+
: undefined;
|
|
270
300
|
}
|
|
271
301
|
const table = REASONING_BUDGET[reasoning.effort];
|
|
272
302
|
// Explicit floor: makes the ">= 1024" invariant correct by construction
|
|
@@ -395,7 +425,12 @@ function coalesceCodexInputToClaudeMessages(items) {
|
|
|
395
425
|
: g.blocks,
|
|
396
426
|
}));
|
|
397
427
|
}
|
|
398
|
-
|
|
428
|
+
// Exported so callers (codexProxyRoutes.ts's cache-preservation wiring) can
|
|
429
|
+
// independently recompute the expected system-prefix length from the same
|
|
430
|
+
// native request `translateCodexRequestToClaude` translated, for
|
|
431
|
+
// `assertClaudeSystemPrefixShape`'s guard against a future translation
|
|
432
|
+
// regression that leaks extra content into `system`.
|
|
433
|
+
export function buildSystemBlocksFromDeveloperMessages(items) {
|
|
399
434
|
return items
|
|
400
435
|
.filter((i) => i.type === "message" && i.role === "developer")
|
|
401
436
|
.flatMap((i) => i.content.map(toClaudeContentPart))
|
|
@@ -554,7 +589,7 @@ export function translateCodexRequestToClaude(request, target) {
|
|
|
554
589
|
.filter((item) => item.type === "additional_tools")
|
|
555
590
|
.at(-1);
|
|
556
591
|
const { tools, toolKindByName, reasons: schemaReasons, } = buildClaudeTools(additionalTools);
|
|
557
|
-
const toolChoice = mapCodexToolChoiceToClaude(request.tool_choice, request.parallel_tool_calls);
|
|
592
|
+
const toolChoice = mapCodexToolChoiceToClaude(request.tool_choice, request.parallel_tool_calls, target.model);
|
|
558
593
|
const forcesTool = toolChoice.type === "any" || toolChoice.type === "tool";
|
|
559
594
|
if (forcesTool && !tools) {
|
|
560
595
|
return {
|
|
@@ -601,7 +636,7 @@ export function translateCodexRequestToClaude(request, target) {
|
|
|
601
636
|
// its reasoning is OpenAI ciphertext.
|
|
602
637
|
const thinking = forcesTool || lastAssistantMessageUsesTool(messages)
|
|
603
638
|
? undefined
|
|
604
|
-
: mapCodexReasoningToThinking(request.reasoning, maxTokens);
|
|
639
|
+
: mapCodexReasoningToThinking(request.reasoning, maxTokens, target.model);
|
|
605
640
|
const claudeRequest = {
|
|
606
641
|
model: target.model,
|
|
607
642
|
messages,
|
|
@@ -633,3 +668,104 @@ export function translateCodexRequestToClaude(request, target) {
|
|
|
633
668
|
}
|
|
634
669
|
return { ok: true, value: claudeRequest, toolKindByName };
|
|
635
670
|
}
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
672
|
+
// §1 Trigger policy (stage-c-trigger.md). Pure classification of a native-
|
|
673
|
+
// Codex-route failure into an outbound-fallback eligibility decision. Owns no
|
|
674
|
+
// dispatch, no HTTP, no account state — `codexProxyRoutes.ts`'s four insertion
|
|
675
|
+
// points call this and still apply their own config/loop-prevention/depth
|
|
676
|
+
// gates on top of a `true` verdict.
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
/** Caps how many times one request may cross between engines (Codex->Anthropic
|
|
679
|
+
* or Anthropic->Codex) in a single turn. A crossing is an engine change, never
|
|
680
|
+
* a same-engine account rotation, so this bounds `Codex -> Anthropic ->
|
|
681
|
+
* Codex -> ...` cycles without limiting ordinary in-pool retries. */
|
|
682
|
+
export const MAX_ENGINE_CROSSINGS = 1;
|
|
683
|
+
/** Read a transport error code the same way `codexProxyRoutes.ts`'s own
|
|
684
|
+
* (unexported) `getCodexTransportErrorCode` does: a direct `.code`, falling
|
|
685
|
+
* back to `.cause.code`. Kept here, independently, as a pure utility the
|
|
686
|
+
* trigger-policy classifier can be exercised against without importing a
|
|
687
|
+
* route-handler module. */
|
|
688
|
+
export function parseCodexErrorCode(error) {
|
|
689
|
+
if (!error || typeof error !== "object") {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
const directCode = error.code;
|
|
693
|
+
if (typeof directCode === "string") {
|
|
694
|
+
return directCode;
|
|
695
|
+
}
|
|
696
|
+
const cause = error.cause;
|
|
697
|
+
if (!cause || typeof cause !== "object") {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
const causeCode = cause.code;
|
|
701
|
+
return typeof causeCode === "string" ? causeCode : undefined;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Classify one `codexProxyRoutes.ts` `dispatch()` failure into an
|
|
705
|
+
* outbound-fallback eligibility decision. A total function over
|
|
706
|
+
* `CodexOutboundFailureClass` (exhaustive switch, no `default:`): a missing
|
|
707
|
+
* case fails to compile under `noImplicitReturns`, never falls through to a
|
|
708
|
+
* silent `undefined`.
|
|
709
|
+
*
|
|
710
|
+
* `context_preflight` / `token_budget` errors are never modeled here: both
|
|
711
|
+
* are self-contained terminal returns inside `dispatch()`'s own outer `try`,
|
|
712
|
+
* before any of the four insertion points this classifier serves, so they
|
|
713
|
+
* never reach it.
|
|
714
|
+
*/
|
|
715
|
+
export function classifyCodexOutboundFailure(input) {
|
|
716
|
+
switch (input.failureClass) {
|
|
717
|
+
case "no_accounts":
|
|
718
|
+
return {
|
|
719
|
+
eligible: true,
|
|
720
|
+
reason: "no Codex accounts are configured for this request",
|
|
721
|
+
};
|
|
722
|
+
case "pool_exhausted":
|
|
723
|
+
return {
|
|
724
|
+
eligible: true,
|
|
725
|
+
reason: "every configured Codex account is cooling down or otherwise unhealthy",
|
|
726
|
+
};
|
|
727
|
+
case "loop_fallthrough": {
|
|
728
|
+
// A bare 401/403 is a credential failure on Codex's own OAuth pool,
|
|
729
|
+
// which says nothing about the fallback target; only the error code can
|
|
730
|
+
// tell a content-policy 403 from an auth one.
|
|
731
|
+
if (classifyProxyFailureCode(input.lastErrorCode).retryable === false) {
|
|
732
|
+
return {
|
|
733
|
+
eligible: false,
|
|
734
|
+
reason: `the last exhausted account attempt ended in ${input.lastErrorCode ?? "unknown"}, a rejection of the request itself rather than a Codex pool failure`,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
const status = input.lastStatus;
|
|
738
|
+
if (status !== undefined &&
|
|
739
|
+
status >= 400 &&
|
|
740
|
+
status < 500 &&
|
|
741
|
+
status !== 401 &&
|
|
742
|
+
status !== 403 &&
|
|
743
|
+
status !== 429) {
|
|
744
|
+
return {
|
|
745
|
+
eligible: false,
|
|
746
|
+
reason: `the last exhausted account attempt ended in a ${status}, a rejection of the request itself rather than a Codex pool failure`,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
eligible: true,
|
|
751
|
+
reason: "the account-retry loop exhausted every eligible Codex account without a terminal response",
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
case "non_retryable_transport": {
|
|
755
|
+
// Reuses the same classifier the Claude engine's own failure handling
|
|
756
|
+
// relies on, as loop_fallthrough does: a content-policy or
|
|
757
|
+
// invalid-request code is not a pool problem, and not something a
|
|
758
|
+
// different upstream is likely to answer differently.
|
|
759
|
+
if (classifyProxyFailureCode(input.transportErrorCode).retryable === false) {
|
|
760
|
+
return {
|
|
761
|
+
eligible: false,
|
|
762
|
+
reason: `transport error ${input.transportErrorCode ?? "unknown"} rejects the request itself, not a Codex pool failure`,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
eligible: true,
|
|
767
|
+
reason: `non-retryable transport error ${input.transportErrorCode ?? "unknown"} exhausted this Codex account attempt`,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
@@ -83,14 +83,20 @@ function mapClaudeStopReasonToCodex(stopReason) {
|
|
|
83
83
|
export function synthesizeCodexUsage(usage) {
|
|
84
84
|
const cacheRead = usage.cache_read_input_tokens;
|
|
85
85
|
const cacheCreation = usage.cache_creation_input_tokens;
|
|
86
|
+
const cacheCreation1h = usage.cacheCreation1hTokens;
|
|
86
87
|
const inputTokens = usage.input_tokens + (cacheRead ?? 0) + (cacheCreation ?? 0);
|
|
87
|
-
const details = cacheRead === undefined &&
|
|
88
|
+
const details = cacheRead === undefined &&
|
|
89
|
+
cacheCreation === undefined &&
|
|
90
|
+
cacheCreation1h === undefined
|
|
88
91
|
? undefined
|
|
89
92
|
: {
|
|
90
93
|
...(cacheRead === undefined ? {} : { cached_tokens: cacheRead }),
|
|
91
94
|
...(cacheCreation === undefined
|
|
92
95
|
? {}
|
|
93
96
|
: { cache_write_tokens: cacheCreation }),
|
|
97
|
+
...(cacheCreation1h === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { cache_write_1h_tokens: cacheCreation1h }),
|
|
94
100
|
};
|
|
95
101
|
return {
|
|
96
102
|
input_tokens: inputTokens,
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { preflightAnthropicStream } from "./streamOutcome.js";
|
|
23
23
|
import { extractSSEEvents } from "./sseInterceptor.js";
|
|
24
|
+
import { readCacheCreation1hTokens, oneHourCacheWriteFields, } from "./proxyTokenUsage.js";
|
|
24
25
|
import { CodexResponsesStreamSerializer, serializeCodexResponse, } from "./codexResponsesFormat.js";
|
|
25
26
|
import { CODEX_STREAM_MAX_TOOL_CALLS, CODEX_STREAM_SIZE_CEILING_BYTES, } from "./streamLimits.js";
|
|
26
27
|
/** Empty by default so an existing caller (no `toolKindByName` argument) keeps
|
|
@@ -144,6 +145,7 @@ export async function consumeAnthropicFallbackResponse(response, model, toolKind
|
|
|
144
145
|
const usage = parsed.usage;
|
|
145
146
|
const cacheCreate = optionalNumber(usage.cache_creation_input_tokens);
|
|
146
147
|
const cacheRead = optionalNumber(usage.cache_read_input_tokens);
|
|
148
|
+
const cacheCreate1h = readCacheCreation1hTokens(usage);
|
|
147
149
|
const result = {
|
|
148
150
|
...parsed,
|
|
149
151
|
usage: {
|
|
@@ -155,6 +157,7 @@ export async function consumeAnthropicFallbackResponse(response, model, toolKind
|
|
|
155
157
|
...(cacheRead === undefined
|
|
156
158
|
? {}
|
|
157
159
|
: { cache_read_input_tokens: cacheRead }),
|
|
160
|
+
...oneHourCacheWriteFields(cacheCreate1h),
|
|
158
161
|
},
|
|
159
162
|
};
|
|
160
163
|
return serializeCodexResponse(result, model, toolKindByName);
|
|
@@ -204,6 +207,7 @@ export async function createAnthropicFallbackStream(response, model, toolKindByN
|
|
|
204
207
|
let capturedOutput = 0;
|
|
205
208
|
let capturedCacheCreate;
|
|
206
209
|
let capturedCacheRead;
|
|
210
|
+
let capturedCacheCreate1h;
|
|
207
211
|
let capturedStopReason = null;
|
|
208
212
|
// input_json_delta also streams for server_tool_use blocks, which have no
|
|
209
213
|
// Codex function_call to feed; only a tool_use block's fragments count.
|
|
@@ -220,6 +224,7 @@ export async function createAnthropicFallbackStream(response, model, toolKindByN
|
|
|
220
224
|
capturedInput = nonNegativeNumber(usage.input_tokens);
|
|
221
225
|
capturedCacheCreate = optionalNumber(usage.cache_creation_input_tokens);
|
|
222
226
|
capturedCacheRead = optionalNumber(usage.cache_read_input_tokens);
|
|
227
|
+
capturedCacheCreate1h = readCacheCreation1hTokens(usage);
|
|
223
228
|
}
|
|
224
229
|
return undefined;
|
|
225
230
|
}
|
|
@@ -304,6 +309,7 @@ export async function createAnthropicFallbackStream(response, model, toolKindByN
|
|
|
304
309
|
...(capturedCacheRead === undefined
|
|
305
310
|
? {}
|
|
306
311
|
: { cache_read_input_tokens: capturedCacheRead }),
|
|
312
|
+
...oneHourCacheWriteFields(capturedCacheCreate1h),
|
|
307
313
|
});
|
|
308
314
|
case "error": {
|
|
309
315
|
// A mid-stream upstream error ends the turn: without this case it
|
|
@@ -512,6 +512,9 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
|
|
|
512
512
|
outputTokens: request.outputTokens ?? 0,
|
|
513
513
|
cacheCreationTokens: request.cacheCreationTokens ?? 0,
|
|
514
514
|
cacheReadTokens: request.cacheReadTokens ?? 0,
|
|
515
|
+
...(request.cacheCreation1hTokens !== null
|
|
516
|
+
? { cacheCreation1hTokens: request.cacheCreation1hTokens }
|
|
517
|
+
: {}),
|
|
515
518
|
});
|
|
516
519
|
requestsWithUsage += 1;
|
|
517
520
|
inputTokens += billingUsage.input;
|
|
@@ -1088,6 +1091,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
1088
1091
|
outputTokens: finiteNumber(record.outputTokens),
|
|
1089
1092
|
cacheReadTokens: finiteNumber(record.cacheReadTokens),
|
|
1090
1093
|
cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
|
|
1094
|
+
cacheCreation1hTokens: finiteNumber(record.cacheCreation1hTokens),
|
|
1091
1095
|
// Read as a boolean only. An absent flag means observed, which keeps
|
|
1092
1096
|
// every record written before the flag existed counting as it did.
|
|
1093
1097
|
// The merge below drops undefined from the later record, so a
|