@juspay/neurolink 12.35.5 → 12.36.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
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
## [12.
|
|
1
|
+
## [12.36.0](https://github.com/juspay/neurolink/compare/v12.35.5...v12.36.0) (2026-09-28)
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### Features
|
|
4
4
|
|
|
5
|
-
- **(proxy):**
|
|
5
|
+
- **(proxy):** translate native Codex requests to Anthropic Messages shape ([70c11ea](https://github.com/juspay/neurolink/commit/70c11eaec4f2c792b401d1f1d70d6b24cfe57610))
|
|
6
6
|
|
|
7
7
|
## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
|
|
8
8
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex-outbound fallback: request translation, Codex Responses -> ClaudeRequest.
|
|
3
|
+
*
|
|
4
|
+
* Named to mirror the existing asymmetry: `codexFallback.ts` is the Claude
|
|
5
|
+
* engine's fallback _into_ Codex; this file is the Codex engine's fallback
|
|
6
|
+
* _out_ to the Anthropic OAuth pool or Vertex-Claude. Owns one thing:
|
|
7
|
+
* untrusted inbound Codex Responses JSON -> a typed `ClaudeRequest`. Does not
|
|
8
|
+
* own target/model selection, response/stream translation back to Codex wire
|
|
9
|
+
* format, tool execution, or dispatch/HTTP.
|
|
10
|
+
*
|
|
11
|
+
* `parallel_tool_calls` is parsed and validated but not yet consumed here —
|
|
12
|
+
* `mapCodexToolChoiceToClaude` stays the base bijection this PR ships.
|
|
13
|
+
* Threading it into `tool_choice.disable_parallel_tool_use` (and the matching
|
|
14
|
+
* `ClaudeRequest.tool_choice` type change) is the tool-call-fidelity build-order
|
|
15
|
+
* PR's explicit modification to this same function, not a gap in this one.
|
|
16
|
+
*/
|
|
17
|
+
import type { ClaudeRequest } from "../types/index.js";
|
|
18
|
+
import type { CodexNativeRequest, CodexTranslationError } from "../types/index.js";
|
|
19
|
+
export declare function parseCodexNativeRequest(raw: unknown): {
|
|
20
|
+
ok: true;
|
|
21
|
+
value: CodexNativeRequest;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
error: CodexTranslationError;
|
|
25
|
+
};
|
|
26
|
+
export declare function translateCodexRequestToClaude(request: CodexNativeRequest, target: {
|
|
27
|
+
provider: "anthropic" | "vertex";
|
|
28
|
+
model: string;
|
|
29
|
+
}): {
|
|
30
|
+
ok: true;
|
|
31
|
+
value: ClaudeRequest;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
error: CodexTranslationError;
|
|
35
|
+
};
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { resolveClaudeMaxTokens } from "../utils/tokenLimits.js";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// §3.1 parseCodexNativeRequest — the untrusted-JSON -> typed boundary.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function isCodexContentPart(value) {
|
|
9
|
+
if (!isRecord(value)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (value.type === "input_text" || value.type === "output_text") {
|
|
13
|
+
return typeof value.text === "string";
|
|
14
|
+
}
|
|
15
|
+
if (value.type === "input_image") {
|
|
16
|
+
return typeof value.image_url === "string";
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function isCodexNativeToolDeclaration(value) {
|
|
21
|
+
if (!isRecord(value) || typeof value.name !== "string") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (value.type === "function") {
|
|
25
|
+
return (typeof value.strict === "boolean" &&
|
|
26
|
+
isRecord(value.parameters) &&
|
|
27
|
+
(value.description === undefined || typeof value.description === "string"));
|
|
28
|
+
}
|
|
29
|
+
if (value.type === "custom") {
|
|
30
|
+
return (isRecord(value.format) &&
|
|
31
|
+
value.format.type === "grammar" &&
|
|
32
|
+
(value.format.syntax === "lark" || value.format.syntax === "regex") &&
|
|
33
|
+
typeof value.format.definition === "string" &&
|
|
34
|
+
(value.description === undefined || typeof value.description === "string"));
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function isCodexNativeToolNamespace(value) {
|
|
39
|
+
return (isRecord(value) &&
|
|
40
|
+
value.type === "namespace" &&
|
|
41
|
+
typeof value.name === "string" &&
|
|
42
|
+
typeof value.description === "string" &&
|
|
43
|
+
Array.isArray(value.tools) &&
|
|
44
|
+
value.tools.every(isCodexNativeToolDeclaration));
|
|
45
|
+
}
|
|
46
|
+
function isCodexNativeInputItem(value) {
|
|
47
|
+
if (!isRecord(value)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
switch (value.type) {
|
|
51
|
+
case "message":
|
|
52
|
+
return ((value.role === "user" ||
|
|
53
|
+
value.role === "assistant" ||
|
|
54
|
+
value.role === "developer") &&
|
|
55
|
+
Array.isArray(value.content) &&
|
|
56
|
+
value.content.every(isCodexContentPart));
|
|
57
|
+
case "function_call":
|
|
58
|
+
return (typeof value.call_id === "string" &&
|
|
59
|
+
typeof value.name === "string" &&
|
|
60
|
+
typeof value.arguments === "string");
|
|
61
|
+
case "function_call_output":
|
|
62
|
+
case "custom_tool_call_output":
|
|
63
|
+
return (typeof value.call_id === "string" && typeof value.output === "string");
|
|
64
|
+
case "custom_tool_call":
|
|
65
|
+
return (typeof value.call_id === "string" &&
|
|
66
|
+
typeof value.name === "string" &&
|
|
67
|
+
typeof value.input === "string");
|
|
68
|
+
case "reasoning":
|
|
69
|
+
return true;
|
|
70
|
+
case "additional_tools":
|
|
71
|
+
return (value.role === "developer" &&
|
|
72
|
+
Array.isArray(value.tools) &&
|
|
73
|
+
value.tools.every(isCodexNativeToolNamespace));
|
|
74
|
+
default:
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function isCodexNativeToolChoice(value) {
|
|
79
|
+
if (value === "auto" || value === "required" || value === "none") {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return (isRecord(value) &&
|
|
83
|
+
value.type === "function" &&
|
|
84
|
+
typeof value.name === "string");
|
|
85
|
+
}
|
|
86
|
+
const CODEX_REASONING_EFFORTS = [
|
|
87
|
+
"none",
|
|
88
|
+
"minimal",
|
|
89
|
+
"low",
|
|
90
|
+
"medium",
|
|
91
|
+
"high",
|
|
92
|
+
"xhigh",
|
|
93
|
+
"max",
|
|
94
|
+
];
|
|
95
|
+
function isCodexNativeReasoning(value) {
|
|
96
|
+
return (isRecord(value) &&
|
|
97
|
+
CODEX_REASONING_EFFORTS.includes(value.effort) &&
|
|
98
|
+
(value.context === undefined || typeof value.context === "string"));
|
|
99
|
+
}
|
|
100
|
+
export function parseCodexNativeRequest(raw) {
|
|
101
|
+
if (!isRecord(raw) ||
|
|
102
|
+
typeof raw.model !== "string" ||
|
|
103
|
+
typeof raw.stream !== "boolean" ||
|
|
104
|
+
typeof raw.store !== "boolean" ||
|
|
105
|
+
!Array.isArray(raw.input) ||
|
|
106
|
+
!raw.input.every(isCodexNativeInputItem) ||
|
|
107
|
+
(raw.tool_choice !== undefined &&
|
|
108
|
+
!isCodexNativeToolChoice(raw.tool_choice)) ||
|
|
109
|
+
(raw.reasoning !== undefined && !isCodexNativeReasoning(raw.reasoning)) ||
|
|
110
|
+
(raw.parallel_tool_calls !== undefined &&
|
|
111
|
+
typeof raw.parallel_tool_calls !== "boolean")) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: {
|
|
115
|
+
code: "MALFORMED_REQUEST",
|
|
116
|
+
message: "inbound body does not match CodexNativeRequest",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, value: raw };
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// §3.3 tool_choice — exact inversion of codexFallback.ts's convertClaudeRequestToCodex.
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
function mapCodexToolChoiceToClaude(choice) {
|
|
126
|
+
if (choice === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
if (choice === "required") {
|
|
130
|
+
return { type: "any" };
|
|
131
|
+
}
|
|
132
|
+
if (choice === "auto") {
|
|
133
|
+
return { type: "auto" };
|
|
134
|
+
}
|
|
135
|
+
if (choice === "none") {
|
|
136
|
+
return { type: "none" };
|
|
137
|
+
}
|
|
138
|
+
return { type: "tool", name: choice.name };
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// §3.4 Tools: flattened across namespaces, split by each declaration's own type.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
function mapCodexFunctionToolToClaude(tool) {
|
|
144
|
+
return {
|
|
145
|
+
name: tool.name,
|
|
146
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
147
|
+
input_schema: tool.parameters,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function mapCodexCustomToolToClaude(tool) {
|
|
151
|
+
// Lossy, explicitly flagged: no JSON Schema exists for a Lark/regex grammar.
|
|
152
|
+
// The model is no longer grammar-constrained after this mapping — needs
|
|
153
|
+
// product sign-off before shipping (§10 of the design).
|
|
154
|
+
return {
|
|
155
|
+
name: tool.name,
|
|
156
|
+
description: [
|
|
157
|
+
tool.description,
|
|
158
|
+
`Grammar (${tool.format.syntax}):\n${tool.format.definition}`,
|
|
159
|
+
]
|
|
160
|
+
.filter((s) => Boolean(s))
|
|
161
|
+
.join("\n\n"),
|
|
162
|
+
input_schema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
input: {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: "Raw command text, constrained by the grammar in this tool's description.",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
required: ["input"],
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function mapCodexToolDeclarationToClaude(tool) {
|
|
175
|
+
// Discriminate on the declaration's own `type`, never on which namespace it
|
|
176
|
+
// came from — the real capture disproves the namespace-implies-kind
|
|
177
|
+
// assumption (`exec` is `type:"custom"` inside a `type:"function"`-heavy
|
|
178
|
+
// namespace).
|
|
179
|
+
return tool.type === "custom"
|
|
180
|
+
? mapCodexCustomToolToClaude(tool)
|
|
181
|
+
: mapCodexFunctionToolToClaude(tool);
|
|
182
|
+
}
|
|
183
|
+
function buildClaudeTools(additionalTools) {
|
|
184
|
+
if (!additionalTools) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const tools = additionalTools.tools.flatMap((namespace) => namespace.tools.map(mapCodexToolDeclarationToClaude));
|
|
188
|
+
return tools.length > 0 ? tools : undefined;
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// §3.5 Reasoning: reasoning.effort -> thinking.
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
const REASONING_BUDGET = {
|
|
194
|
+
low: 4096,
|
|
195
|
+
medium: 8192,
|
|
196
|
+
high: 16384,
|
|
197
|
+
xhigh: 24576,
|
|
198
|
+
max: 32768,
|
|
199
|
+
};
|
|
200
|
+
function mapCodexReasoningToThinking(reasoning, resolvedMaxTokens) {
|
|
201
|
+
if (!reasoning ||
|
|
202
|
+
reasoning.effort === "none" ||
|
|
203
|
+
reasoning.effort === "minimal") {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const table = REASONING_BUDGET[reasoning.effort];
|
|
207
|
+
// Explicit floor: makes the ">= 1024" invariant correct by construction
|
|
208
|
+
// rather than an accident of today's model-table minimums.
|
|
209
|
+
const budget_tokens = Math.max(1024, Math.min(table, resolvedMaxTokens - 1024));
|
|
210
|
+
return { type: "enabled", budget_tokens };
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// §3.6 Non-JSON function_call.arguments — never throws.
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
function parseInboundToolArguments(raw) {
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(raw);
|
|
218
|
+
if (isRecord(parsed)) {
|
|
219
|
+
return parsed;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// fall through — not JSON at all (e.g. exec's raw shell command)
|
|
224
|
+
}
|
|
225
|
+
return { input: raw };
|
|
226
|
+
}
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// §3.7 Content parts, with the image data-URI fix.
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
const DATA_URI_RE = /^data:([^;,]+);base64,(.+)$/s;
|
|
231
|
+
// The four image types the Messages API accepts (mirrors
|
|
232
|
+
// SUPPORTED_IMAGE_MEDIA_TYPES in providers/anthropicImageBlocks.ts).
|
|
233
|
+
const ANTHROPIC_IMAGE_MEDIA_TYPES = new Set([
|
|
234
|
+
"image/jpeg",
|
|
235
|
+
"image/png",
|
|
236
|
+
"image/gif",
|
|
237
|
+
"image/webp",
|
|
238
|
+
]);
|
|
239
|
+
const ANTHROPIC_TOOL_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
240
|
+
function normalizeImageMediaType(mediaType) {
|
|
241
|
+
const lower = mediaType.toLowerCase();
|
|
242
|
+
return lower === "image/jpg" ? "image/jpeg" : lower;
|
|
243
|
+
}
|
|
244
|
+
function toClaudeContentPart(part) {
|
|
245
|
+
if (part.type === "input_image") {
|
|
246
|
+
const match = DATA_URI_RE.exec(part.image_url);
|
|
247
|
+
if (match) {
|
|
248
|
+
return {
|
|
249
|
+
type: "image",
|
|
250
|
+
source: {
|
|
251
|
+
type: "base64",
|
|
252
|
+
media_type: normalizeImageMediaType(match[1]),
|
|
253
|
+
data: match[2],
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return { type: "image", source: { type: "url", url: part.image_url } };
|
|
258
|
+
}
|
|
259
|
+
return { type: "text", text: part.text };
|
|
260
|
+
}
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// §4 Role-alternation coalescing.
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
function coalesceCodexInputToClaudeMessages(items) {
|
|
265
|
+
const groups = [];
|
|
266
|
+
let current;
|
|
267
|
+
const open = (role) => {
|
|
268
|
+
if (current?.role === role) {
|
|
269
|
+
return current;
|
|
270
|
+
}
|
|
271
|
+
if (current) {
|
|
272
|
+
groups.push(current);
|
|
273
|
+
}
|
|
274
|
+
current = { role, blocks: [] };
|
|
275
|
+
return current;
|
|
276
|
+
};
|
|
277
|
+
for (const item of items) {
|
|
278
|
+
// additional_tools is handled out-of-band (§3.4). A reasoning item carries
|
|
279
|
+
// OpenAI-encrypted chain of thought that only the OpenAI backend can read;
|
|
280
|
+
// Anthropic has no way to consume it, so it is dropped by rule, not by
|
|
281
|
+
// accident (unknown item types still fail parsing, per A.1).
|
|
282
|
+
if (item.type === "additional_tools" || item.type === "reasoning") {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (item.type === "message") {
|
|
286
|
+
// Developer messages become system blocks (below). An empty message
|
|
287
|
+
// adds nothing, and on its own would become a content-less turn.
|
|
288
|
+
if (item.role === "developer" || item.content.length === 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
open(item.role).blocks.push(...item.content.map(toClaudeContentPart));
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (item.type === "function_call" || item.type === "custom_tool_call") {
|
|
295
|
+
open("assistant").blocks.push({
|
|
296
|
+
type: "tool_use",
|
|
297
|
+
id: item.call_id,
|
|
298
|
+
name: item.name,
|
|
299
|
+
// A custom tool is declared with a single `input` string (§3.4), so
|
|
300
|
+
// its raw call text maps onto that field unchanged.
|
|
301
|
+
input: item.type === "function_call"
|
|
302
|
+
? parseInboundToolArguments(item.arguments)
|
|
303
|
+
: { input: item.input },
|
|
304
|
+
});
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
open("user").blocks.push({
|
|
308
|
+
type: "tool_result",
|
|
309
|
+
tool_use_id: item.call_id,
|
|
310
|
+
content: item.output,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (current) {
|
|
314
|
+
groups.push(current);
|
|
315
|
+
}
|
|
316
|
+
// In a user turn the Messages API wants tool_result blocks before any other
|
|
317
|
+
// content, so a user message that arrives between a call and its output is
|
|
318
|
+
// moved after the result rather than sent in an order the API rejects.
|
|
319
|
+
return groups.map((g) => ({
|
|
320
|
+
role: g.role,
|
|
321
|
+
content: g.role === "user"
|
|
322
|
+
? [
|
|
323
|
+
...g.blocks.filter((block) => block.type === "tool_result"),
|
|
324
|
+
...g.blocks.filter((block) => block.type !== "tool_result"),
|
|
325
|
+
]
|
|
326
|
+
: g.blocks,
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
function buildSystemBlocksFromDeveloperMessages(items) {
|
|
330
|
+
return items
|
|
331
|
+
.filter((i) => i.type === "message" && i.role === "developer")
|
|
332
|
+
.flatMap((i) => i.content.map(toClaudeContentPart))
|
|
333
|
+
.filter((b) => b.type === "text");
|
|
334
|
+
}
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// §5 Resumed-session history contract — fail loud, never guess.
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Detects only a missing tools preamble. A client-side prune that drops turns
|
|
339
|
+
// from the middle while resending the preamble passes this check; nothing in
|
|
340
|
+
// the wire format marks such a gap (spec Q6, still open).
|
|
341
|
+
function hasFreshSessionPreamble(items) {
|
|
342
|
+
return items[0]?.type === "additional_tools";
|
|
343
|
+
}
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Inputs Codex accepts but the Messages API rejects — fail loud, never guess.
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
function describeUntranslatableItem(item, provider) {
|
|
348
|
+
if (item.type === "additional_tools") {
|
|
349
|
+
const badTool = item.tools
|
|
350
|
+
.flatMap((namespace) => namespace.tools)
|
|
351
|
+
.find((tool) => !ANTHROPIC_TOOL_NAME_RE.test(tool.name));
|
|
352
|
+
return badTool
|
|
353
|
+
? `tool name ${JSON.stringify(badTool.name)} does not match the Messages API's ${String(ANTHROPIC_TOOL_NAME_RE)}`
|
|
354
|
+
: undefined;
|
|
355
|
+
}
|
|
356
|
+
if (item.type !== "message") {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
return item.content
|
|
360
|
+
.map((part) => {
|
|
361
|
+
if (part.type !== "input_image") {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
if (item.role === "developer") {
|
|
365
|
+
return "a developer message carries an image, which a text-only system prompt cannot hold";
|
|
366
|
+
}
|
|
367
|
+
if (!part.image_url.startsWith("data:")) {
|
|
368
|
+
// Claude on Vertex takes only inline (base64) image sources; the
|
|
369
|
+
// Vertex client in providers/googleVertex fetches URLs for the same reason.
|
|
370
|
+
return provider === "vertex"
|
|
371
|
+
? "Claude on Vertex accepts only base64 image sources, not an image URL"
|
|
372
|
+
: undefined;
|
|
373
|
+
}
|
|
374
|
+
const match = DATA_URI_RE.exec(part.image_url);
|
|
375
|
+
if (!match) {
|
|
376
|
+
return "an image data URI is not base64-encoded";
|
|
377
|
+
}
|
|
378
|
+
const mediaType = normalizeImageMediaType(match[1]);
|
|
379
|
+
return ANTHROPIC_IMAGE_MEDIA_TYPES.has(mediaType)
|
|
380
|
+
? undefined
|
|
381
|
+
: `image media type ${mediaType} is not one the Messages API accepts (jpeg, png, gif, webp)`;
|
|
382
|
+
})
|
|
383
|
+
.find((reason) => reason !== undefined);
|
|
384
|
+
}
|
|
385
|
+
function toolUseIds(message) {
|
|
386
|
+
return message?.role === "assistant" && Array.isArray(message.content)
|
|
387
|
+
? message.content.flatMap((block) => block.type === "tool_use" ? [block.id] : [])
|
|
388
|
+
: [];
|
|
389
|
+
}
|
|
390
|
+
function toolResultIds(message) {
|
|
391
|
+
return message?.role === "user" && Array.isArray(message.content)
|
|
392
|
+
? message.content.flatMap((block) => block.type === "tool_result" ? [block.tool_use_id] : [])
|
|
393
|
+
: [];
|
|
394
|
+
}
|
|
395
|
+
// The Messages API requires every tool call to be answered in the very next
|
|
396
|
+
// user turn, and every tool result to answer a call in the turn before it.
|
|
397
|
+
// History pruned on the client side can break either; this catches it here
|
|
398
|
+
// instead of as an upstream 400.
|
|
399
|
+
function describeToolPairingGap(messages) {
|
|
400
|
+
return messages
|
|
401
|
+
.map((message, index) => {
|
|
402
|
+
const answered = toolResultIds(messages[index + 1]);
|
|
403
|
+
const unanswered = toolUseIds(message).find((id) => !answered.includes(id));
|
|
404
|
+
if (unanswered) {
|
|
405
|
+
return `tool call ${unanswered} has no result in the next user turn`;
|
|
406
|
+
}
|
|
407
|
+
const called = toolUseIds(messages[index - 1]);
|
|
408
|
+
const orphaned = toolResultIds(message).find((id) => !called.includes(id));
|
|
409
|
+
return orphaned
|
|
410
|
+
? `tool result for ${orphaned} answers no call in the preceding assistant turn`
|
|
411
|
+
: undefined;
|
|
412
|
+
})
|
|
413
|
+
.find((reason) => reason !== undefined);
|
|
414
|
+
}
|
|
415
|
+
function lastAssistantMessageUsesTool(messages) {
|
|
416
|
+
const lastAssistant = [...messages]
|
|
417
|
+
.reverse()
|
|
418
|
+
.find((message) => message.role === "assistant");
|
|
419
|
+
return (Array.isArray(lastAssistant?.content) &&
|
|
420
|
+
lastAssistant.content.some((block) => block.type === "tool_use"));
|
|
421
|
+
}
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// §3.2 translateCodexRequestToClaude — the full field-by-field mapping.
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
export function translateCodexRequestToClaude(request, target) {
|
|
426
|
+
// Read from client_metadata, not top-level session_id/thread_id: the real
|
|
427
|
+
// capture carries neither at top level (only nested in client_metadata),
|
|
428
|
+
// which is why CodexNativeRequest has no top-level fields for them.
|
|
429
|
+
if ((request.client_metadata?.session_id !== undefined ||
|
|
430
|
+
request.client_metadata?.thread_id !== undefined) &&
|
|
431
|
+
!hasFreshSessionPreamble(request.input)) {
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
error: {
|
|
435
|
+
code: "SUSPECTED_PARTIAL_HISTORY",
|
|
436
|
+
message: "a request with session metadata does not open with the additional_tools preamble; refusing to translate what may be an incremental history as if it were complete",
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const untranslatable = request.input
|
|
441
|
+
.map((item) => describeUntranslatableItem(item, target.provider))
|
|
442
|
+
.find((reason) => reason !== undefined);
|
|
443
|
+
if (untranslatable) {
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
error: { code: "UNTRANSLATABLE_REQUEST", message: untranslatable },
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
// The spec's cardinality rule: when more than one additional_tools item
|
|
450
|
+
// appears, the last one wins.
|
|
451
|
+
const additionalTools = request.input
|
|
452
|
+
.filter((item) => item.type === "additional_tools")
|
|
453
|
+
.at(-1);
|
|
454
|
+
const tools = buildClaudeTools(additionalTools);
|
|
455
|
+
const toolChoice = mapCodexToolChoiceToClaude(request.tool_choice);
|
|
456
|
+
const forcesTool = toolChoice?.type === "any" || toolChoice?.type === "tool";
|
|
457
|
+
if (forcesTool && !tools) {
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
error: {
|
|
461
|
+
code: "MALFORMED_REQUEST",
|
|
462
|
+
message: "tool_choice forces a tool call but the request declares no tools",
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (toolChoice?.type === "tool" &&
|
|
467
|
+
!tools?.some((tool) => tool.name === toolChoice.name)) {
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
error: {
|
|
471
|
+
code: "MALFORMED_REQUEST",
|
|
472
|
+
message: `tool_choice names ${JSON.stringify(toolChoice.name)}, which the request does not declare`,
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const messages = coalesceCodexInputToClaudeMessages(request.input);
|
|
477
|
+
if (messages[0]?.role !== "user") {
|
|
478
|
+
return {
|
|
479
|
+
ok: false,
|
|
480
|
+
error: {
|
|
481
|
+
code: "UNTRANSLATABLE_REQUEST",
|
|
482
|
+
message: "the translated history does not open with a user turn, which the Messages API requires",
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const pairingGap = describeToolPairingGap(messages);
|
|
487
|
+
if (pairingGap) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
error: { code: "UNTRANSLATABLE_REQUEST", message: pairingGap },
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const maxTokens = resolveClaudeMaxTokens(target.model, undefined);
|
|
494
|
+
const systemBlocks = buildSystemBlocksFromDeveloperMessages(request.input);
|
|
495
|
+
// Thinking is left off in two cases the Messages API would reject: a forced
|
|
496
|
+
// tool_choice (the same rule oauthFetch.ts applies), and a history whose
|
|
497
|
+
// final assistant turn calls a tool, which with thinking on must open with a
|
|
498
|
+
// thinking block. Codex history carries none that can be translated, since
|
|
499
|
+
// its reasoning is OpenAI ciphertext.
|
|
500
|
+
const thinking = forcesTool || lastAssistantMessageUsesTool(messages)
|
|
501
|
+
? undefined
|
|
502
|
+
: mapCodexReasoningToThinking(request.reasoning, maxTokens);
|
|
503
|
+
const claudeRequest = {
|
|
504
|
+
model: target.model,
|
|
505
|
+
messages,
|
|
506
|
+
max_tokens: maxTokens,
|
|
507
|
+
...(systemBlocks.length > 0 ? { system: systemBlocks } : {}),
|
|
508
|
+
...(tools ? { tools } : {}),
|
|
509
|
+
// The Messages API accepts tool_choice only alongside tools.
|
|
510
|
+
...(toolChoice && tools ? { tool_choice: toolChoice } : {}),
|
|
511
|
+
...(thinking ? { thinking } : {}),
|
|
512
|
+
stream: request.stream ?? true,
|
|
513
|
+
};
|
|
514
|
+
return { ok: true, value: claudeRequest };
|
|
515
|
+
}
|
package/dist/types/codex.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* stored through the same AccountQuota shape as Anthropic — its primary window
|
|
12
12
|
* maps onto the session fields and its secondary window onto the weekly fields.
|
|
13
13
|
*/
|
|
14
|
-
import type { AccountCoolingReason, AccountQuota, InternalResult } from "./proxy.js";
|
|
14
|
+
import type { AccountCoolingReason, AccountQuota, ClaudeContentBlock, InternalResult } from "./proxy.js";
|
|
15
15
|
/** Token block inside `~/.codex/auth.json`. */
|
|
16
16
|
export type CodexAuthFileTokens = {
|
|
17
17
|
id_token?: string;
|
|
@@ -194,3 +194,139 @@ export type CodexFallbackStream = {
|
|
|
194
194
|
frames: AsyncGenerator<string, CodexFallbackResult>;
|
|
195
195
|
cancel: (reason?: unknown) => Promise<void>;
|
|
196
196
|
};
|
|
197
|
+
/** Role a native Codex `input` message item can carry. */
|
|
198
|
+
export type CodexNativeRole = "developer" | "user" | "assistant";
|
|
199
|
+
/** A plain message item in a native Codex request's `input` array. */
|
|
200
|
+
export type CodexNativeMessageInputItem = {
|
|
201
|
+
type: "message";
|
|
202
|
+
id?: string;
|
|
203
|
+
role: CodexNativeRole;
|
|
204
|
+
content: CodexContentPart[];
|
|
205
|
+
};
|
|
206
|
+
/** A tool declaration backed by a JSON Schema, dispatched as a function call. */
|
|
207
|
+
export type CodexNativeFunctionToolDeclaration = {
|
|
208
|
+
type: "function";
|
|
209
|
+
name: string;
|
|
210
|
+
description?: string;
|
|
211
|
+
strict: boolean;
|
|
212
|
+
parameters: Record<string, unknown>;
|
|
213
|
+
};
|
|
214
|
+
/** Grammar constraining a custom tool's argument text. */
|
|
215
|
+
export type CodexNativeCustomToolFormat = {
|
|
216
|
+
type: "grammar";
|
|
217
|
+
syntax: "lark" | "regex";
|
|
218
|
+
definition: string;
|
|
219
|
+
};
|
|
220
|
+
/** A tool declaration whose arguments are grammar-constrained free text. */
|
|
221
|
+
export type CodexNativeCustomToolDeclaration = {
|
|
222
|
+
type: "custom";
|
|
223
|
+
name: string;
|
|
224
|
+
description?: string;
|
|
225
|
+
format: CodexNativeCustomToolFormat;
|
|
226
|
+
};
|
|
227
|
+
/** A single tool declaration, discriminated by its own `type` field. */
|
|
228
|
+
export type CodexNativeToolDeclaration = CodexNativeFunctionToolDeclaration | CodexNativeCustomToolDeclaration;
|
|
229
|
+
/** A named group of tool declarations inside one `additional_tools` item. */
|
|
230
|
+
export type CodexNativeToolNamespace = {
|
|
231
|
+
type: "namespace";
|
|
232
|
+
name: string;
|
|
233
|
+
description: string;
|
|
234
|
+
tools: CodexNativeToolDeclaration[];
|
|
235
|
+
};
|
|
236
|
+
/** The `input` item carrying every tool declaration for a native Codex request. */
|
|
237
|
+
export type CodexNativeAdditionalToolsInputItem = {
|
|
238
|
+
type: "additional_tools";
|
|
239
|
+
id?: string;
|
|
240
|
+
role: "developer";
|
|
241
|
+
tools: CodexNativeToolNamespace[];
|
|
242
|
+
};
|
|
243
|
+
/** A model-emitted tool call replayed in a native Codex request's history. */
|
|
244
|
+
export type CodexNativeFunctionCallInputItem = {
|
|
245
|
+
type: "function_call";
|
|
246
|
+
call_id: string;
|
|
247
|
+
name: string;
|
|
248
|
+
arguments: string;
|
|
249
|
+
};
|
|
250
|
+
/** A client-supplied tool result replayed in a native Codex request's history. */
|
|
251
|
+
export type CodexNativeFunctionCallOutputInputItem = {
|
|
252
|
+
type: "function_call_output";
|
|
253
|
+
call_id: string;
|
|
254
|
+
output: string;
|
|
255
|
+
};
|
|
256
|
+
/** A call to a grammar-constrained (`type:"custom"`) tool, replayed in history. */
|
|
257
|
+
export type CodexNativeCustomToolCallInputItem = {
|
|
258
|
+
type: "custom_tool_call";
|
|
259
|
+
id?: string;
|
|
260
|
+
call_id: string;
|
|
261
|
+
name: string;
|
|
262
|
+
input: string;
|
|
263
|
+
};
|
|
264
|
+
/** A client-supplied result for a `custom_tool_call`, replayed in history. */
|
|
265
|
+
export type CodexNativeCustomToolCallOutputInputItem = {
|
|
266
|
+
type: "custom_tool_call_output";
|
|
267
|
+
call_id: string;
|
|
268
|
+
output: string;
|
|
269
|
+
};
|
|
270
|
+
/**
|
|
271
|
+
* A reasoning item the client echoes back with `store:false`. Its content is
|
|
272
|
+
* opaque (`encrypted_content` is readable only by the OpenAI backend), so its
|
|
273
|
+
* shape is deliberately loose beyond the discriminator.
|
|
274
|
+
*/
|
|
275
|
+
export type CodexNativeReasoningInputItem = {
|
|
276
|
+
type: "reasoning";
|
|
277
|
+
id?: string;
|
|
278
|
+
encrypted_content?: string | null;
|
|
279
|
+
summary?: unknown[];
|
|
280
|
+
};
|
|
281
|
+
/** A single item in a native Codex request's `input` array. */
|
|
282
|
+
export type CodexNativeInputItem = CodexNativeMessageInputItem | CodexNativeAdditionalToolsInputItem | CodexNativeFunctionCallInputItem | CodexNativeFunctionCallOutputInputItem | CodexNativeCustomToolCallInputItem | CodexNativeCustomToolCallOutputInputItem | CodexNativeReasoningInputItem;
|
|
283
|
+
/** How a native Codex request constrains which tool the model may call. */
|
|
284
|
+
export type CodexNativeToolChoice = "auto" | "required" | "none" | {
|
|
285
|
+
type: "function";
|
|
286
|
+
name: string;
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* A native Codex Responses request, as sent by the real Codex CLI.
|
|
290
|
+
*
|
|
291
|
+
* No top-level `session_id`/`thread_id` — real traffic carries neither at
|
|
292
|
+
* top level, only inside `client_metadata`, which the existing
|
|
293
|
+
* `Record<string, unknown>` field already covers.
|
|
294
|
+
*/
|
|
295
|
+
export type CodexNativeRequest = {
|
|
296
|
+
model: string;
|
|
297
|
+
input: CodexNativeInputItem[];
|
|
298
|
+
stream: boolean;
|
|
299
|
+
store: boolean;
|
|
300
|
+
tool_choice?: CodexNativeToolChoice;
|
|
301
|
+
parallel_tool_calls?: boolean;
|
|
302
|
+
reasoning?: {
|
|
303
|
+
effort: CodexReasoningEffort;
|
|
304
|
+
context?: string;
|
|
305
|
+
};
|
|
306
|
+
include?: string[];
|
|
307
|
+
text?: {
|
|
308
|
+
verbosity?: string;
|
|
309
|
+
};
|
|
310
|
+
prompt_cache_key?: string;
|
|
311
|
+
client_metadata?: Record<string, unknown>;
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* A `parseCodexNativeRequest`/`translateCodexRequestToClaude` failure. Never thrown.
|
|
315
|
+
* `UNTRANSLATABLE_REQUEST` is a request Codex itself accepts but Anthropic would
|
|
316
|
+
* reject, as opposed to one that is invalid on the Codex wire.
|
|
317
|
+
*/
|
|
318
|
+
export type CodexTranslationError = {
|
|
319
|
+
code: "MALFORMED_REQUEST";
|
|
320
|
+
message: string;
|
|
321
|
+
} | {
|
|
322
|
+
code: "SUSPECTED_PARTIAL_HISTORY";
|
|
323
|
+
message: string;
|
|
324
|
+
} | {
|
|
325
|
+
code: "UNTRANSLATABLE_REQUEST";
|
|
326
|
+
message: string;
|
|
327
|
+
};
|
|
328
|
+
/** One coalesced run of same-role content while flattening a native Codex `input` array. */
|
|
329
|
+
export type CodexOutboundMessageGroup = {
|
|
330
|
+
role: "user" | "assistant";
|
|
331
|
+
blocks: ClaudeContentBlock[];
|
|
332
|
+
};
|
|
@@ -10772,12 +10772,12 @@
|
|
|
10772
10772
|
{"objectID":"25e1ac265c89596a09bce35c4ba7b8d82ccf92f016708d1f6f4826b290af00ae","title":"Delivery","url":"/docs/superpowers/specs/2026-09-23-proxy-account-routing-policies-design#delivery","content":"juspay/neurolink merges one commit per PR, so two PRs:\nRefactor, no behaviour change: extract the comparator into accountRanking.ts and count in-flight on the unlimited lease. Proven by the replay and the existing route tests.\nPolicies: headroom-first, prefer-primary, session affinity, spill, config, telemetry, /status, and the documentation correction below.","hierarchy":{"lvl0":"Superpowers","lvl1":"Proxy Account Routing Policies Design","lvl2":"Delivery","lvl3":""}},
|
|
10773
10773
|
{"objectID":"06eb4e8ea79a4077f7cb445c1ec17985516046ab28d49d3c87b85ade0155f2c1","title":"Documentation","url":"/docs/superpowers/specs/2026-09-23-proxy-account-routing-policies-design#documentation","content":"docs/features/claude-proxy-config-reference.md gains the five keys. The primary-account text and source comments that still say the primary is \"tried first under fill-first\" are corrected: with quota routing the primary is the final tiebreaker unless prefer-primary is set.","hierarchy":{"lvl0":"Superpowers","lvl1":"Proxy Account Routing Policies Design","lvl2":"Documentation","lvl3":""}},
|
|
10774
10774
|
{"objectID":"1cf022d6f43b961b6fd1c13d8a5677077f9109584a3a5e67d837c4d5f6e613da","title":"Out of scope","url":"/docs/superpowers/specs/2026-09-23-proxy-account-routing-policies-design#out-of-scope","content":"Per-model policies, weighted scoring, persisting affinity across restarts, an account reserve, and changes to cooldown classification, in-stream rate-limit handling or state persistence. Those last three are separate designs.","hierarchy":{"lvl0":"Superpowers","lvl1":"Proxy Account Routing Policies Design","lvl2":"Out of scope","lvl3":""}},
|
|
10775
|
-
{"objectID":"f0109d9239beed7372578f5d0ad41b745b321c526407db0d3bbb32adced24e2e","title":"Codex Outbound Fallback — Design","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design","content":"Codex Outbound Fallback — Design\n\nStatus: design complete, implementation not started. Produced by 22 parallel agents\n(6 grounding, 8 design, 8 adversarial review) plus 8 resolution passes; every design\nclaim below was checked against the code by a reviewer whose brief was to refute it.\n\nGoal\n\nA native Codex client request that its own account pool cannot serve currently fails to the\nclient. This design gives the Codex engine an outbound fallback chain to the Anthropic OAuth\npool and to Vertex, so the request is served instead — with prompt caching preserved across\nthe hop, tool calls translated faithfully in both directions, and no client-visible protocol\nanomaly.\n\nEvidence\nsrc/lib/server/routes/codexProxyRoutes.ts has no outbound fallback. Its only fallback\n references are a string-default helper (line 138) and code recognising when the Codex engine\n is itself invoked as an inner Anthropic fallback (lines 666, 817, CODEX_FALLBACK_METADATA_KEY\n at 109). On pool exhaustion it returns a terminal error: buildCodexErrorResponse\n (1458-1462), or buildCodexQuotaExhaustedResponse (909-914) when every account is already\n cooling.\nThree directions exist and work: Claude Code → Anthropic pool; Claude/Anthropic → Codex\n (executeClaudeCodexFallback, claudeProxyRoutes.ts:5499, called at 6131, in-process — no\n HTTP hop); Claude/Anthropic → Vertex (vertexAnthropicFallback.ts:611, gated on\n isVertexAnthropicModel, 576).\nThree do not exist: Vertex → Anthropic, native Codex → Anthropic, native Codex →\n Vertex. PR #1782 fixed translation defects in the existing Claude → Vertex direction; it\n did not add a reverse path.\nCodex prompt caching is live but partial. proxy_tokens_cache_read by model:\n gpt-5.6-sol 566,657,536; gpt-6-astra 3,285,888; gpt-5.6-terra 490,752;\n gpt-5.6-luna 5,888. proxy_tokens_cache_creation is 0 for every Codex series, which is\n correct — OpenAI caching is automatic and has no explicit write.\nThe caching asymmetry is the central design constraint. Anthropic caching is explicit:\n up to 4 cache_control breakpoints over a tools → system → messages prefix, write at\n 1.25x base input (5-minute TTL) or 2x (1-hour), read at 0.1x, and each account holds its\n own cache. A Codex request translated to Anthropic arrives with no breakpoints at all,\n so it would be cached not at all unless the translator inserts them deliberately.\nSince Claude-on-Vertex speaks the Anthropic Messages wire format, one Codex→Anthropic\n translator covers both targets. There is no smaller Vertex-only variant of this work.\n\nBuild order\n\nDependency-ordered; each step is one reviewable PR under the one-commit-per-PR policy. Default\noff at every step, so behaviour is byte-for-byte unchanged until the final enable.\n\n| # | PR | Depends on | Gate |\n| --- | ------------------------------------------------------------------- | ---------- | -------------------------------------------- |\n| 1 | IR types + codec contract (src/lib/types/proxyIR.ts) | — | typecheck; exhaustiveness is a compile error |\n| 2 | Request codec Codex → Anthropic (codexOutboundFallback.ts) | 1 | offline shape suite |\n| 3 | Response/stream codec Anthropic → Codex | 1 | offline event-order suite |\n| 4 | Tool fidelity + id correlation | 2, 3 | offline round-trip suite |\n| 5 | Cache breakpoints + prefix byte-stability (codexOutboundCache.ts) | 2 | byte-stability assertion across two turns |\n| 6 | Trigger policy, commitment rule, loop prevention, accounting | 2-4 | totality + commitment suites |\n| 7 | Config keys, kill switch, metrics | 6 | config validation suite |\n| 8 | Enable + live model matrix | all | live pass criteria |\n\nOpen questions gating the build\n\nEvery one needs the same thing: real captured native Codex traffic. The repo has 4 synthetic\nfixtures, one real single-turn capture (~/.neurolink/reference/codex-cli-wire-sample.json,\ncaptured 2026-09-21), a real usage-frame capture (test/fixtures/codex-response-usage.sse,\ncaptured 2026-08-21), and one real controlled bisection experiment against the actual\ncodex_exec/0.155.1 binary (scripts/codex-replay-listener.ts, test/fixtures/sse-bisection-findings.md,\nrun 2026-09-27). Two questions are now settled; the rest are listed here because they can still\ninvalidate a whole section, so they should be settled before the PRs that depend on them are\nwritten.\n\n| Question | Blocks | Settled by
|
|
10775
|
+
{"objectID":"f0109d9239beed7372578f5d0ad41b745b321c526407db0d3bbb32adced24e2e","title":"Codex Outbound Fallback — Design","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design","content":"Codex Outbound Fallback — Design\n\nStatus: design complete, implementation not started. Produced by 22 parallel agents\n(6 grounding, 8 design, 8 adversarial review) plus 8 resolution passes; every design\nclaim below was checked against the code by a reviewer whose brief was to refute it.\n\nGoal\n\nA native Codex client request that its own account pool cannot serve currently fails to the\nclient. This design gives the Codex engine an outbound fallback chain to the Anthropic OAuth\npool and to Vertex, so the request is served instead — with prompt caching preserved across\nthe hop, tool calls translated faithfully in both directions, and no client-visible protocol\nanomaly.\n\nEvidence\nsrc/lib/server/routes/codexProxyRoutes.ts has no outbound fallback. Its only fallback\n references are a string-default helper (line 138) and code recognising when the Codex engine\n is itself invoked as an inner Anthropic fallback (lines 666, 817, CODEX_FALLBACK_METADATA_KEY\n at 109). On pool exhaustion it returns a terminal error: buildCodexErrorResponse\n (1458-1462), or buildCodexQuotaExhaustedResponse (909-914) when every account is already\n cooling.\nThree directions exist and work: Claude Code → Anthropic pool; Claude/Anthropic → Codex\n (executeClaudeCodexFallback, claudeProxyRoutes.ts:5499, called at 6131, in-process — no\n HTTP hop); Claude/Anthropic → Vertex (vertexAnthropicFallback.ts:611, gated on\n isVertexAnthropicModel, 576).\nThree do not exist: Vertex → Anthropic, native Codex → Anthropic, native Codex →\n Vertex. PR #1782 fixed translation defects in the existing Claude → Vertex direction; it\n did not add a reverse path.\nCodex prompt caching is live but partial. proxy_tokens_cache_read by model:\n gpt-5.6-sol 566,657,536; gpt-6-astra 3,285,888; gpt-5.6-terra 490,752;\n gpt-5.6-luna 5,888. proxy_tokens_cache_creation is 0 for every Codex series, which is\n correct — OpenAI caching is automatic and has no explicit write.\nThe caching asymmetry is the central design constraint. Anthropic caching is explicit:\n up to 4 cache_control breakpoints over a tools → system → messages prefix, write at\n 1.25x base input (5-minute TTL) or 2x (1-hour), read at 0.1x, and each account holds its\n own cache. A Codex request translated to Anthropic arrives with no breakpoints at all,\n so it would be cached not at all unless the translator inserts them deliberately.\nSince Claude-on-Vertex speaks the Anthropic Messages wire format, one Codex→Anthropic\n translator covers both targets. There is no smaller Vertex-only variant of this work.\n\nBuild order\n\nDependency-ordered; each step is one reviewable PR under the one-commit-per-PR policy. Default\noff at every step, so behaviour is byte-for-byte unchanged until the final enable.\n\n| # | PR | Depends on | Gate |\n| --- | ------------------------------------------------------------------- | ---------- | -------------------------------------------- |\n| 1 | IR types + codec contract (src/lib/types/proxyIR.ts) | — | typecheck; exhaustiveness is a compile error |\n| 2 | Request codec Codex → Anthropic (codexOutboundFallback.ts) | 1 | offline shape suite |\n| 3 | Response/stream codec Anthropic → Codex | 1 | offline event-order suite |\n| 4 | Tool fidelity + id correlation | 2, 3 | offline round-trip suite |\n| 5 | Cache breakpoints + prefix byte-stability (codexOutboundCache.ts) | 2 | byte-stability assertion across two turns |\n| 6 | Trigger policy, commitment rule, loop prevention, accounting | 2-4 | totality + commitment suites |\n| 7 | Config keys, kill switch, metrics | 6 | config validation suite |\n| 8 | Enable + live model matrix | all | live pass criteria |\n\nOpen questions gating the build\n\nEvery one needs the same thing: real captured native Codex traffic. The repo has 4 synthetic\nfixtures, one real single-turn capture (~/.neurolink/reference/codex-cli-wire-sample.json,\ncaptured 2026-09-21), a real usage-frame capture (test/fixtures/codex-response-usage.sse,\ncaptured 2026-08-21), and one real controlled bisection experiment against the actual\ncodex_exec/0.155.1 binary (scripts/codex-replay-listener.ts, test/fixtures/sse-bisection-findings.md,\nrun 2026-09-27). Two questions are now settled; the rest are listed here because they can still\ninvalidate a whole section, so they should be settled before the PRs that depend on them are\nwritten.\n\n| Question | Blocks | Settled by ","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"","lvl3":""}},
|
|
10776
10776
|
{"objectID":"6034ef59cb5b3d2520fa92f2f181aca534a58aa28338f3411aa9d28d998e487e","title":"Codex Outbound Fallback — Design","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#codex-outbound-fallback-design","content":"Status: design complete, implementation not started. Produced by 22 parallel agents\n(6 grounding, 8 design, 8 adversarial review) plus 8 resolution passes; every design\nclaim below was checked against the code by a reviewer whose brief was to refute it.","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"Codex Outbound Fallback — Design","lvl3":""}},
|
|
10777
10777
|
{"objectID":"580c5a06163d1e603bb34f5f3b51c78e4efa36e12426cf22b63706680af0ea56","title":"Goal","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#goal","content":"A native Codex client request that its own account pool cannot serve currently fails to the\nclient. This design gives the Codex engine an outbound fallback chain to the Anthropic OAuth\npool and to Vertex, so the request is served instead — with prompt caching preserved across\nthe hop, tool calls translated faithfully in both directions, and no client-visible protocol\nanomaly.","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"Goal","lvl3":""}},
|
|
10778
10778
|
{"objectID":"ed180fb4ca61ad0027c4359503ac6ea54b391281020d32b2a9b498cd6c83abeb","title":"Evidence","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#evidence","content":"src/lib/server/routes/codexProxyRoutes.ts has no outbound fallback. Its only fallback\n references are a string-default helper (line 138) and code recognising when the Codex engine\n is itself invoked as an inner Anthropic fallback (lines 666, 817, CODEX_FALLBACK_METADATA_KEY\n at 109). On pool exhaustion it returns a terminal error: buildCodexErrorResponse\n (1458-1462), or buildCodexQuotaExhaustedResponse (909-914) when every account is already\n cooling.\nThree directions exist and work: Claude Code → Anthropic pool; Claude/Anthropic → Codex\n (executeClaudeCodexFallback, claudeProxyRoutes.ts:5499, called at 6131, in-process — no\n HTTP hop); Claude/Anthropic → Vertex (vertexAnthropicFallback.ts:611, gated on\n isVertexAnthropicModel, 576).\nThree do not exist: Vertex → Anthropic, native Codex → Anthropic, native Codex →\n Vertex. PR #1782 fixed translation defects in the existing Claude → Vertex direction; it\n did not add a reverse path.\nCodex prompt caching is live but partial. proxy_tokens_cache_read by model:\n gpt-5.6-sol 566,657,536; gpt-6-astra 3,285,888; gpt-5.6-terra 490,752;\n gpt-5.6-luna 5,888. proxy_tokens_cache_creation is 0 for every Codex series, which is\n correct — OpenAI caching is automatic and has no explicit write.\nThe caching asymmetry is the central design constraint. Anthropic caching is explicit:\n up to 4 cache_control breakpoints over a tools → system → messages prefix, write at\n 1.25x base input (5-minute TTL) or 2x (1-hour), read at 0.1x, and each account holds its\n own cache. A Codex request translated to Anthropic arrives with no breakpoints at all,\n so it would be cached not at all unless the translator inserts them deliberately.\nSince Claude-on-Vertex speaks the Anthropic Messages wire format, one Codex→Anthropic\n translator covers both targets. There is no smaller Vertex-only variant of this work.","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"Evidence","lvl3":""}},
|
|
10779
10779
|
{"objectID":"a5aa19ce68fa1175c4bade50ae303cc0a954bf1128588e294af993c049e2f466","title":"Build order","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#build-order","content":"Dependency-ordered; each step is one reviewable PR under the one-commit-per-PR policy. Default\noff at every step, so behaviour is byte-for-byte unchanged until the final enable.\n\n| # | PR | Depends on | Gate |\n| --- | ------------------------------------------------------------------- | ---------- | -------------------------------------------- |\n| 1 | IR types + codec contract (src/lib/types/proxyIR.ts) | — | typecheck; exhaustiveness is a compile error |\n| 2 | Request codec Codex → Anthropic (codexOutboundFallback.ts) | 1 | offline shape suite |\n| 3 | Response/stream codec Anthropic → Codex | 1 | offline event-order suite |\n| 4 | Tool fidelity + id correlation | 2, 3 | offline round-trip suite |\n| 5 | Cache breakpoints + prefix byte-stability (codexOutboundCache.ts) | 2 | byte-stability assertion across two turns |\n| 6 | Trigger policy, commitment rule, loop prevention, accounting | 2-4 | totality + commitment suites |\n| 7 | Config keys, kill switch, metrics | 6 | config validation suite |\n| 8 | Enable + live model matrix | all | live pass criteria |","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"Build order","lvl3":""}},
|
|
10780
|
-
{"objectID":"db4fcb5c145b85c8e756be096fdda4d88c5f6fc576ec62653a8545735ff1a653","title":"Open questions gating the build","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#open-questions-gating-the-build","content":"Every one needs the same thing: real captured native Codex traffic. The repo has 4 synthetic\nfixtures, one real single-turn capture (~/.neurolink/reference/codex-cli-wire-sample.json,\ncaptured 2026-09-21), a real usage-frame capture (test/fixtures/codex-response-usage.sse,\ncaptured 2026-08-21), and one real controlled bisection experiment against the actual\ncodex_exec/0.155.1 binary (scripts/codex-replay-listener.ts, test/fixtures/sse-bisection-findings.md,\nrun 2026-09-27). Two questions are now settled; the rest are listed here because they can still\ninvalidate a whole section, so they should be settled before the PRs that depend on them are\nwritten.\n\n| Question | Blocks | Settled by
|
|
10780
|
+
{"objectID":"db4fcb5c145b85c8e756be096fdda4d88c5f6fc576ec62653a8545735ff1a653","title":"Open questions gating the build","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#open-questions-gating-the-build","content":"Every one needs the same thing: real captured native Codex traffic. The repo has 4 synthetic\nfixtures, one real single-turn capture (~/.neurolink/reference/codex-cli-wire-sample.json,\ncaptured 2026-09-21), a real usage-frame capture (test/fixtures/codex-response-usage.sse,\ncaptured 2026-08-21), and one real controlled bisection experiment against the actual\ncodex_exec/0.155.1 binary (scripts/codex-replay-listener.ts, test/fixtures/sse-bisection-findings.md,\nrun 2026-09-27). Two questions are now settled; the rest are listed here because they can still\ninvalidate a whole section, so they should be settled before the PRs that depend on them are\nwritten.\n\n| Question | Blocks | Settled by |\n| ---------------------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Does the real Codex CLI accept a call_id it did not mint (a toolu_-shaped id)? | PR 4 | SETTLED (2026-09-27) — yes, verbatim. codex-replay-listener.ts --script tool-call against real codex_exec/0.155.1: the CLI's follow-up function_call_output carried call_id: \"toolu_replay0000000000000000\" unmodified (own id field is CLI-minted fco_-","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"Open questions gating the build","lvl3":""}},
|
|
10781
10781
|
{"objectID":"a85c34c0265b0c27d696611075427124ef72062ef3e1248bf75ddc2f859229d0","title":"Contents","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#contents","content":"Translation layer: canonical IR and codec contract\nRequest translation: Codex Responses → Anthropic Messages\nResponse translation: Anthropic → Codex wire format\nTool-call fidelity in both directions\nPreserving prompt caching across the hop\nTrigger policy, commitment safety, loop prevention, accounting\nConfiguration surface and staged rollout\nValidation plan","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"Contents","lvl3":""}},
|
|
10782
10782
|
{"objectID":"62546125e30c54bebd2104e396943d1fb60478f7d22873e75792c81c408d2791","title":"Consolidated unresolved items","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#consolidated-unresolved-items","content":"ir-and-types — whether native Codex ever sends top-level reasoning/tools on a genuine inbound request — the one check that would settle it is a live-captured native Codex CLI request with non-default reasoning effort set.\nir-and-types — whether Anthropic's live API ever rejects consecutive same-role messages in some edge case — the one check that would settle it is a probe request against the real API with two consecutive user messages.\nir-and-types — the storage/lifetime contract for a resumed session's id map spanning multiple proxy requests — the one check that would settle it is reading how originSessionId/originThreadId are persisted by the session-affinity\nir-and-types — above.\nrequest-translation — in §10. It converts an unknown into a loud, caller-visible failure instead of a silent one, which is what the \"nothing may break silently\" requirement demands even without live traffic.\nrequest-translation — whether additional_tools/developer items can appear anywhere other than before the first user item in real traffic — the one check that would settle it: capture one real Codex CLI multi-turn session with a mid-conv\nrequest-translation — whether a resumed-session request's input is genuinely full-history or incremental — the one check that would settle it: capture one real resumed-session Codex Responses request (the referenced ~/.neurolink/reference/\n- **request-translation** — whether toolchoice or reasoning ever appear on genuine inbound requests at all (absent from all 4 fixtures) — the one check that would settle it: grep captured production Codex traffic logs (once available) for eith\n- **request-translation** — whether more than one additionaltools item can appear in one request — the one check that would settle it: same real-traffic capture as (1).\n- **response-translation** — a genuine live-captured native Codex Responses SSE transcript (text, tool-call, incomplete/error cases) — the check that would settle it is running a real Codex client through this","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"Consolidated unresolved items","lvl3":""}},
|
|
10783
10783
|
{"objectID":"b890fc666ae87c04302cb9122b852c6e394f3a6eb3ad2c2a3b90aed60a49e057","title":"Decision (kept)","url":"/docs/superpowers/specs/2026-09-27-codex-outbound-fallback-design#decision-kept","content":"Single canonical IR, not ClaudeRequest-as-hub, not pairwise codecs. Verified: openaiFormat.ts/geminiFormat.ts already use ClaudeRequest as an implicit hub only because it is also the literal Anthropic wire type; codexFallback.ts already needs a role cast ((message.role as string) !== \"system\", line 154) to special-case Claude Code's inline-system convention inside a converter meant to be neutral. A new file, src/lib/types/proxyIR.ts, holds a real wire-neutral IR. ClaudeRequest/ClaudeResponse (proxy.ts:154, :187) stay untouched — the Anthropic hot path (claudeProxyRoutes.ts) never changes.\n\nVertex needs no separate codec. Confirmed: executeVertexAnthropicFallback (vertexAnthropicFallback.ts:605-665) forwards the upstream body/stream untouched with a code comment that it is \"already Anthropic-shaped\"; buildVertexAnthropicPayload (line 399) only strips UNSUPPORTED_FIELDS = [\"model\", \"context_management\"] (line 42). Vertex reuses anthropicMessagesIRCodec.buildRequest's output as-is.","hierarchy":{"lvl0":"Superpowers","lvl1":"Codex Outbound Fallback — Design","lvl2":"Decision (kept)","lvl3":""}},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.36.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "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/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|