@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.
- package/CHANGELOG.md +3 -3
- package/README.md +10 -10
- package/dist/browser/neurolink.min.js +381 -381
- package/dist/cli/commands/setup.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/neurolink.d.ts +1 -1
- package/dist/neurolink.js +2 -2
- package/dist/providers/openaiCompatCatalog.d.ts +2 -2
- package/dist/providers/openaiCompatCatalog.js +2 -2
- package/dist/proxy/codexOutboundFallback.d.ts +35 -0
- package/dist/proxy/codexOutboundFallback.js +515 -0
- package/dist/proxy/otelLogSink.d.ts +2 -2
- package/dist/proxy/otelLogSink.js +44 -4
- package/dist/proxy/otlpLogTransport.js +25 -2
- package/dist/proxy/requestLogger.js +20 -1
- package/dist/routing/modelCatalog.d.ts +1 -1
- package/dist/routing/modelCatalog.js +1 -1
- package/dist/types/codex.d.ts +137 -1
- package/dist/utils/modelChoices.d.ts +5 -4
- package/dist/utils/modelChoices.js +11 -10
- package/dist/utils/providerHealth.d.ts +1 -1
- package/dist/utils/providerHealth.js +1 -1
- package/docs-site/static/search-index.json +151 -151
- package/package.json +1 -1
|
@@ -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("
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
*
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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):
|
|
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. */
|