@juspay/neurolink 12.35.4 → 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.
@@ -322,7 +322,7 @@ async function showWelcomeScreen() {
322
322
  chalk.blue(" │"));
323
323
  logger.always(chalk.blue("│ │"));
324
324
  logger.always(chalk.blue("│ ") +
325
- chalk.gray("Universal interface for 8+ AI providers") +
325
+ chalk.gray("Unified interface for many AI providers") +
326
326
  chalk.blue(" │"));
327
327
  logger.always(chalk.blue("│ ") +
328
328
  chalk.gray("• Generate text, code, and creative content") +
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * NeuroLink AI Toolkit
3
3
  *
4
- * A unified AI provider interface with support for 14+ providers,
4
+ * A unified AI provider interface with support for many AI providers,
5
5
  * automatic fallback, streaming, MCP tool integration, HITL security,
6
6
  * Redis persistence, and enterprise-grade middleware.
7
7
  *
@@ -98,7 +98,7 @@ export declare const VERSION = "1.0.0";
98
98
  * Quick start factory function for creating AI provider instances.
99
99
  *
100
100
  * Creates a configured AI provider instance ready for immediate use.
101
- * Supports 14+ providers: OpenAI, Anthropic, Google AI Studio,
101
+ * Supports providers including OpenAI, Anthropic, Google AI Studio,
102
102
  * Google Vertex, AWS Bedrock, AWS SageMaker, Azure OpenAI, Hugging Face,
103
103
  * LiteLLM, Mistral, Ollama, OpenAI Compatible, OpenRouter, and more.
104
104
  *
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * NeuroLink AI Toolkit
3
3
  *
4
- * A unified AI provider interface with support for 14+ providers,
4
+ * A unified AI provider interface with support for many AI providers,
5
5
  * automatic fallback, streaming, MCP tool integration, HITL security,
6
6
  * Redis persistence, and enterprise-grade middleware.
7
7
  *
@@ -220,7 +220,7 @@ export const VERSION = "1.0.0";
220
220
  * Quick start factory function for creating AI provider instances.
221
221
  *
222
222
  * Creates a configured AI provider instance ready for immediate use.
223
- * Supports 14+ providers: OpenAI, Anthropic, Google AI Studio,
223
+ * Supports providers including OpenAI, Anthropic, Google AI Studio,
224
224
  * Google Vertex, AWS Bedrock, AWS SageMaker, Azure OpenAI, Hugging Face,
225
225
  * LiteLLM, Mistral, Ollama, OpenAI Compatible, OpenRouter, and more.
226
226
  *
@@ -692,7 +692,7 @@ export declare class NeuroLink {
692
692
  * Generate AI response with comprehensive feature support.
693
693
  *
694
694
  * Primary method for AI generation with support for all NeuroLink features:
695
- * - Multi-provider support (14+ providers)
695
+ * - Multi-provider support
696
696
  * - MCP tool integration
697
697
  * - Structured JSON output with Zod schemas
698
698
  * - Conversation memory (Redis or in-memory)
package/dist/neurolink.js CHANGED
@@ -194,7 +194,7 @@ const isNonRetryableForPool = sharedIsNonRetryableForPool;
194
194
  /**
195
195
  * NeuroLink - Universal AI Development Platform
196
196
  *
197
- * Main SDK class providing unified access to 14+ AI providers with enterprise features:
197
+ * Main SDK class providing unified access to many AI providers with enterprise features:
198
198
  * - Multi-provider support (OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, etc.)
199
199
  * - MCP (Model Context Protocol) tool integration — connect any MCP-compliant server
200
200
  * - Human-in-the-Loop (HITL) security workflows for regulated industries
@@ -3231,7 +3231,7 @@ Current user's request: ${currentInput}`;
3231
3231
  * Generate AI response with comprehensive feature support.
3232
3232
  *
3233
3233
  * Primary method for AI generation with support for all NeuroLink features:
3234
- * - Multi-provider support (14+ providers)
3234
+ * - Multi-provider support
3235
3235
  * - MCP tool integration
3236
3236
  * - Structured JSON output with Zod schemas
3237
3237
  * - Conversation memory (Redis or in-memory)
@@ -1,6 +1,6 @@
1
1
  import type { OpenAICompatCatalogEntry } from "../types/index.js";
2
2
  /**
3
- * Config-driven catalog of the 10 OpenAI-compatible providers whose only
3
+ * Config-driven catalog of the OpenAI-compatible providers whose only
4
4
  * differences from each other are expressible as named, closed catalog
5
5
  * quirks (messageContentFormat, timeoutErrorClass, responseFormatDowngrade)
6
6
  * rather than real code. Each entry fully replaces what used to be a
@@ -11,7 +11,7 @@ import type { OpenAICompatCatalogEntry } from "../types/index.js";
11
11
  *
12
12
  * The entries themselves are derived from the JSON catalog
13
13
  * (src/lib/providers/catalog/<id>.json) via buildCatalogEntries() — that
14
- * JSON is now the single source of truth for these 10 providers' identity,
14
+ * JSON is now the single source of truth for these providers' identity,
15
15
  * models, error rules and wire config. See
16
16
  * src/lib/providers/catalog/loader.ts for the derivation logic and
17
17
  * docs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md for the
@@ -1,6 +1,6 @@
1
1
  import { buildCatalogEntries } from "./catalog/loader.js";
2
2
  /**
3
- * Config-driven catalog of the 10 OpenAI-compatible providers whose only
3
+ * Config-driven catalog of the OpenAI-compatible providers whose only
4
4
  * differences from each other are expressible as named, closed catalog
5
5
  * quirks (messageContentFormat, timeoutErrorClass, responseFormatDowngrade)
6
6
  * rather than real code. Each entry fully replaces what used to be a
@@ -11,7 +11,7 @@ import { buildCatalogEntries } from "./catalog/loader.js";
11
11
  *
12
12
  * The entries themselves are derived from the JSON catalog
13
13
  * (src/lib/providers/catalog/<id>.json) via buildCatalogEntries() — that
14
- * JSON is now the single source of truth for these 10 providers' identity,
14
+ * JSON is now the single source of truth for these providers' identity,
15
15
  * models, error rules and wire config. See
16
16
  * src/lib/providers/catalog/loader.ts for the derivation logic and
17
17
  * docs/superpowers/plans/2026-08-28-provider-json-catalog-spec.md for the
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { LoggerProvider } from "@opentelemetry/sdk-logs";
1
+ import type { LoggerProvider as LoggerProviderType } from "@opentelemetry/sdk-logs";
2
2
  import type { ProxyBodyChunkEmitter, ProxyBodyDeliveryResult } from "../types/index.js";
3
3
  /** Explicit opt-in; configuration never silently falls back to file logging. */
4
4
  export declare function isProxyOtelOnly(): boolean;
@@ -9,7 +9,7 @@ export declare function isProxyOtelOnly(): boolean;
9
9
  */
10
10
  export declare function publishProxyOtelBody(captureId: string, body: string, emit: ProxyBodyChunkEmitter): Promise<ProxyBodyDeliveryResult>;
11
11
  /** Initialize a log-only provider in every proxy process, including the supervisor. */
12
- export declare function initializeProxyOtelLogs(role?: string): LoggerProvider | undefined;
12
+ export declare function initializeProxyOtelLogs(role?: string): LoggerProviderType | undefined;
13
13
  /** Structured evidence without final-request dashboard fields on auxiliary events. */
14
14
  export declare function emitProxyOtelEvent(kind: string, record: Record<string, unknown>): void;
15
15
  /** Capture application console diagnostics only inside proxy service processes. */