@juspay/neurolink 12.36.0 → 12.37.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 +2 -2
- package/dist/agent/directTools.js +56 -22
- package/dist/browser/neurolink.min.js +374 -374
- package/dist/proxy/argumentParsing.d.ts +12 -0
- package/dist/proxy/argumentParsing.js +23 -0
- package/dist/proxy/codexOutboundFallback.d.ts +10 -6
- package/dist/proxy/codexOutboundFallback.js +163 -43
- package/dist/proxy/codexResponsesFormat.d.ts +125 -0
- package/dist/proxy/codexResponsesFormat.js +591 -0
- package/dist/proxy/codexToAnthropicFallback.d.ts +54 -0
- package/dist/proxy/codexToAnthropicFallback.js +408 -0
- package/dist/proxy/proxyTracer.d.ts +21 -0
- package/dist/proxy/proxyTracer.js +45 -0
- package/dist/proxy/streamLimits.d.ts +16 -0
- package/dist/proxy/streamLimits.js +16 -0
- package/dist/types/codex.d.ts +113 -1
- package/dist/types/file.d.ts +12 -0
- package/dist/types/proxy.d.ts +12 -1
- package/dist/utils/csvProcessor.d.ts +3 -3
- package/dist/utils/csvProcessor.js +53 -5
- package/docs-site/static/search-index.json +12 -12
- package/package.json +1 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives the Anthropic -> Codex response/stream codec against a real upstream
|
|
3
|
+
* `Response`. Mirrors `codexFallback.ts`, inverted: named after its own
|
|
4
|
+
* upstream, an Anthropic-shape engine, by the same convention `codexFallback.ts`
|
|
5
|
+
* is named after Codex.
|
|
6
|
+
*
|
|
7
|
+
* Scope: translation and the pre-commit/post-commit boundary only. Engine
|
|
8
|
+
* selection, OAuth/account selection, retry/cooldown policy and analytics
|
|
9
|
+
* recording are owned by sibling modules and are not this module's concern —
|
|
10
|
+
* it does not touch `src/lib/server/routes/codexProxyRoutes.ts`.
|
|
11
|
+
*
|
|
12
|
+
* SSE parsing note: this direction reads Anthropic-shaped SSE bytes, the same
|
|
13
|
+
* wire shape `preflightAnthropicStream` already buffers before this module
|
|
14
|
+
* ever sees the stream. Rather than duplicating `codexFallback.ts`'s
|
|
15
|
+
* carry/searchFrom scanner (built for the differently-shaped native Codex
|
|
16
|
+
* event vocabulary), this module reuses `extractSSEEvents` — the incremental
|
|
17
|
+
* Anthropic-SSE parser `preflightAnthropicStream` itself is built on — so the
|
|
18
|
+
* preflight-buffered chunks and the live remainder are parsed by the exact
|
|
19
|
+
* same code path. The carry/searchFrom scanner extraction into a shared
|
|
20
|
+
* `sseFrameScanner.ts` remains an explicitly-deferred follow-up either way.
|
|
21
|
+
*/
|
|
22
|
+
import { preflightAnthropicStream } from "./streamOutcome.js";
|
|
23
|
+
import { extractSSEEvents } from "./sseInterceptor.js";
|
|
24
|
+
import { CodexResponsesStreamSerializer, serializeCodexResponse, } from "./codexResponsesFormat.js";
|
|
25
|
+
import { CODEX_STREAM_MAX_TOOL_CALLS, CODEX_STREAM_SIZE_CEILING_BYTES, } from "./streamLimits.js";
|
|
26
|
+
/** Empty by default so an existing caller (no `toolKindByName` argument) keeps
|
|
27
|
+
* treating every tool_use block as a `function` call, unchanged. */
|
|
28
|
+
const NO_TOOL_KINDS = new Map();
|
|
29
|
+
/** A non-2xx reply (either path) or a non-JSON reply (non-streaming path) from
|
|
30
|
+
* the Anthropic-shape upstream, raised before any Codex frame is sent. */
|
|
31
|
+
export class AnthropicFallbackResponseError extends Error {
|
|
32
|
+
status;
|
|
33
|
+
responseBody;
|
|
34
|
+
constructor(status, responseBody) {
|
|
35
|
+
super(`Anthropic fallback request returned HTTP ${status}`);
|
|
36
|
+
this.name = "AnthropicFallbackResponseError";
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.responseBody = responseBody;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** A pre-commit or post-commit failure while translating the Anthropic SSE
|
|
42
|
+
* stream, or a well-formed non-streaming reply this module refuses to
|
|
43
|
+
* translate (tool calls over `streamLimits.ts`'s count or size ceiling). */
|
|
44
|
+
export class AnthropicFallbackStreamError extends Error {
|
|
45
|
+
status;
|
|
46
|
+
code;
|
|
47
|
+
constructor(status, message, code) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "AnthropicFallbackStreamError";
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function isTokenCount(value) {
|
|
58
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
59
|
+
}
|
|
60
|
+
function nonNegativeNumber(value) {
|
|
61
|
+
return isTokenCount(value) ? value : 0;
|
|
62
|
+
}
|
|
63
|
+
function optionalNumber(value) {
|
|
64
|
+
return isTokenCount(value) ? value : undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Caps how much unparsed Anthropic SSE text this module will buffer — now
|
|
67
|
+
* centralized in streamLimits.ts alongside the same ceiling `codexFallback.ts`
|
|
68
|
+
* applies on the reverse direction, instead of a second local constant. */
|
|
69
|
+
const MAX_BUFFER_CHARS = CODEX_STREAM_SIZE_CEILING_BYTES;
|
|
70
|
+
/**
|
|
71
|
+
* The streaming driver's per-turn tool-call ceilings. `open` and `addArgs`
|
|
72
|
+
* return the `emitFailure` arguments to end the turn with once a call would
|
|
73
|
+
* pass the count limit or its accumulated argument text the size limit.
|
|
74
|
+
*/
|
|
75
|
+
function createToolCallCeilings() {
|
|
76
|
+
let opened = 0;
|
|
77
|
+
let argsLength = 0;
|
|
78
|
+
return {
|
|
79
|
+
open() {
|
|
80
|
+
if (opened >= CODEX_STREAM_MAX_TOOL_CALLS) {
|
|
81
|
+
return [
|
|
82
|
+
502,
|
|
83
|
+
"Anthropic fallback stream exceeded the tool-call limit",
|
|
84
|
+
"too_many_tool_calls",
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
opened++;
|
|
88
|
+
argsLength = 0;
|
|
89
|
+
return undefined;
|
|
90
|
+
},
|
|
91
|
+
addArgs(fragment) {
|
|
92
|
+
argsLength += fragment.length;
|
|
93
|
+
return argsLength > CODEX_STREAM_SIZE_CEILING_BYTES
|
|
94
|
+
? [
|
|
95
|
+
502,
|
|
96
|
+
"Anthropic fallback tool arguments exceeded the size limit",
|
|
97
|
+
"tool_arguments_too_large",
|
|
98
|
+
]
|
|
99
|
+
: undefined;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Consume a fully-buffered, non-streaming Anthropic-shape response and
|
|
105
|
+
* translate it into one Codex Responses envelope.
|
|
106
|
+
*
|
|
107
|
+
* No `message_start`/`message_delta` split here: a single JSON object
|
|
108
|
+
* carries the whole turn's usage at once (confirmed against this repo's own
|
|
109
|
+
* `settleFromResponseUsage`, which reads a non-streaming Anthropic body the
|
|
110
|
+
* same way), so the translation delegates straight to `serializeCodexResponse`.
|
|
111
|
+
*/
|
|
112
|
+
export async function consumeAnthropicFallbackResponse(response, model, toolKindByName = NO_TOOL_KINDS) {
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new AnthropicFallbackResponseError(response.status, await response.text().catch(() => ""));
|
|
115
|
+
}
|
|
116
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
117
|
+
const text = await response.text().catch(() => "");
|
|
118
|
+
if (!contentType.toLowerCase().includes("application/json")) {
|
|
119
|
+
throw new AnthropicFallbackResponseError(response.status, text);
|
|
120
|
+
}
|
|
121
|
+
let parsed;
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(text);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throw new AnthropicFallbackResponseError(response.status, text);
|
|
127
|
+
}
|
|
128
|
+
if (!isRecord(parsed) ||
|
|
129
|
+
parsed.type !== "message" ||
|
|
130
|
+
!Array.isArray(parsed.content) ||
|
|
131
|
+
!isRecord(parsed.usage)) {
|
|
132
|
+
throw new AnthropicFallbackResponseError(response.status, text);
|
|
133
|
+
}
|
|
134
|
+
const toolUses = parsed.content.filter((block) => isRecord(block) && block.type === "tool_use");
|
|
135
|
+
if (toolUses.length > CODEX_STREAM_MAX_TOOL_CALLS) {
|
|
136
|
+
throw new AnthropicFallbackStreamError(502, "Anthropic fallback response exceeded the tool-call limit", "too_many_tool_calls");
|
|
137
|
+
}
|
|
138
|
+
if (toolUses.some((block) => JSON.stringify(block.input ?? {}).length >
|
|
139
|
+
CODEX_STREAM_SIZE_CEILING_BYTES)) {
|
|
140
|
+
throw new AnthropicFallbackStreamError(502, "Anthropic fallback tool arguments exceeded the size limit", "tool_arguments_too_large");
|
|
141
|
+
}
|
|
142
|
+
// The same number checks the streaming path applies to message_start, so a
|
|
143
|
+
// malformed count becomes 0 (or is omitted) rather than reaching the totals.
|
|
144
|
+
const usage = parsed.usage;
|
|
145
|
+
const cacheCreate = optionalNumber(usage.cache_creation_input_tokens);
|
|
146
|
+
const cacheRead = optionalNumber(usage.cache_read_input_tokens);
|
|
147
|
+
const result = {
|
|
148
|
+
...parsed,
|
|
149
|
+
usage: {
|
|
150
|
+
input_tokens: nonNegativeNumber(usage.input_tokens),
|
|
151
|
+
output_tokens: nonNegativeNumber(usage.output_tokens),
|
|
152
|
+
...(cacheCreate === undefined
|
|
153
|
+
? {}
|
|
154
|
+
: { cache_creation_input_tokens: cacheCreate }),
|
|
155
|
+
...(cacheRead === undefined
|
|
156
|
+
? {}
|
|
157
|
+
: { cache_read_input_tokens: cacheRead }),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
return serializeCodexResponse(result, model, toolKindByName);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Translate an Anthropic SSE stream into Codex-shape SSE frames as events
|
|
164
|
+
* arrive. The caller owns error framing downstream and must never retry
|
|
165
|
+
* after any frame has been emitted (mirrors `codexFallback.ts`'s own
|
|
166
|
+
* discipline, `:715-718`).
|
|
167
|
+
*/
|
|
168
|
+
export async function createAnthropicFallbackStream(response, model, toolKindByName = NO_TOOL_KINDS) {
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
throw new AnthropicFallbackResponseError(response.status, await response.text().catch(() => ""));
|
|
171
|
+
}
|
|
172
|
+
if (!response.body) {
|
|
173
|
+
throw new AnthropicFallbackStreamError(502, "Anthropic fallback returned an empty stream", "empty_response");
|
|
174
|
+
}
|
|
175
|
+
const reader = response.body.getReader();
|
|
176
|
+
let cancellation;
|
|
177
|
+
const cancel = (reason) => {
|
|
178
|
+
cancellation ??= reader
|
|
179
|
+
.cancel(reason)
|
|
180
|
+
.catch(() => undefined)
|
|
181
|
+
.finally(() => reader.releaseLock());
|
|
182
|
+
return cancellation;
|
|
183
|
+
};
|
|
184
|
+
const preflight = await preflightAnthropicStream(reader);
|
|
185
|
+
if (preflight.kind !== "ready") {
|
|
186
|
+
// Pre-commit: nothing Codex-shaped sent yet. Release the reader and its
|
|
187
|
+
// admission lease before throwing, so a caller that tries the next
|
|
188
|
+
// engine is not blocked by a lease this hop still held.
|
|
189
|
+
await cancel();
|
|
190
|
+
if (preflight.kind === "sse_error") {
|
|
191
|
+
throw new AnthropicFallbackStreamError(502, preflight.message, preflight.errorType);
|
|
192
|
+
}
|
|
193
|
+
if (preflight.kind === "transport_error") {
|
|
194
|
+
throw new AnthropicFallbackStreamError(502, "Anthropic fallback transport failed before any content", "transport_error");
|
|
195
|
+
}
|
|
196
|
+
throw new AnthropicFallbackStreamError(502, "Anthropic fallback stream ended before any content", "empty_response");
|
|
197
|
+
}
|
|
198
|
+
const serializer = new CodexResponsesStreamSerializer(model, toolKindByName);
|
|
199
|
+
async function* frames() {
|
|
200
|
+
yield* serializer.start();
|
|
201
|
+
const decoder = new TextDecoder();
|
|
202
|
+
let buffer = "";
|
|
203
|
+
let capturedInput = 0;
|
|
204
|
+
let capturedOutput = 0;
|
|
205
|
+
let capturedCacheCreate;
|
|
206
|
+
let capturedCacheRead;
|
|
207
|
+
let capturedStopReason = null;
|
|
208
|
+
// input_json_delta also streams for server_tool_use blocks, which have no
|
|
209
|
+
// Codex function_call to feed; only a tool_use block's fragments count.
|
|
210
|
+
let currentBlockType;
|
|
211
|
+
const ceilings = createToolCallCeilings();
|
|
212
|
+
/** One parsed Anthropic SSE event -> zero or more Codex frames, or the
|
|
213
|
+
* terminal envelope once `message_stop` (or a failure) is reached. */
|
|
214
|
+
function* processEvent(type, payload) {
|
|
215
|
+
switch (type) {
|
|
216
|
+
case "message_start": {
|
|
217
|
+
const message = payload.message;
|
|
218
|
+
const usage = isRecord(message) ? message.usage : undefined;
|
|
219
|
+
if (isRecord(usage)) {
|
|
220
|
+
capturedInput = nonNegativeNumber(usage.input_tokens);
|
|
221
|
+
capturedCacheCreate = optionalNumber(usage.cache_creation_input_tokens);
|
|
222
|
+
capturedCacheRead = optionalNumber(usage.cache_read_input_tokens);
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
case "content_block_start": {
|
|
227
|
+
const block = payload.content_block;
|
|
228
|
+
currentBlockType =
|
|
229
|
+
isRecord(block) && typeof block.type === "string"
|
|
230
|
+
? block.type
|
|
231
|
+
: undefined;
|
|
232
|
+
if (isRecord(block) && block.type === "tool_use") {
|
|
233
|
+
const overLimit = ceilings.open();
|
|
234
|
+
if (overLimit) {
|
|
235
|
+
return yield* serializer.emitFailure(...overLimit);
|
|
236
|
+
}
|
|
237
|
+
const id = typeof block.id === "string" ? block.id : "";
|
|
238
|
+
const name = typeof block.name === "string" ? block.name : "";
|
|
239
|
+
yield* serializer.openToolCall(id, name);
|
|
240
|
+
}
|
|
241
|
+
// "text" opens lazily on its first delta; "thinking" is dropped by
|
|
242
|
+
// design (ClaudeThinkingBlock carries no signature to preserve).
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
case "content_block_delta": {
|
|
246
|
+
const delta = payload.delta;
|
|
247
|
+
if (!isRecord(delta)) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
251
|
+
yield* serializer.pushDelta(delta.text);
|
|
252
|
+
}
|
|
253
|
+
else if (delta.type === "input_json_delta" &&
|
|
254
|
+
currentBlockType === "tool_use" &&
|
|
255
|
+
typeof delta.partial_json === "string") {
|
|
256
|
+
// Checked before the fragment is forwarded: the call closes
|
|
257
|
+
// incomplete, never completed with its arguments cut mid-object.
|
|
258
|
+
const overLimit = ceilings.addArgs(delta.partial_json);
|
|
259
|
+
if (overLimit) {
|
|
260
|
+
return yield* serializer.emitFailure(...overLimit);
|
|
261
|
+
}
|
|
262
|
+
yield* serializer.pushToolCallArgsDelta(delta.partial_json);
|
|
263
|
+
}
|
|
264
|
+
// thinking_delta: zero frames.
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
case "content_block_stop": {
|
|
268
|
+
currentBlockType = undefined;
|
|
269
|
+
try {
|
|
270
|
+
yield* serializer.closeToolCall();
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Malformed tool-call JSON: a named, handled case — never a
|
|
274
|
+
// fallthrough exception. No function_call_arguments.done was
|
|
275
|
+
// emitted, since closeToolCall throws before reaching it.
|
|
276
|
+
return yield* serializer.emitFailure(502, "Anthropic fallback tool arguments were not valid JSON", "invalid_tool_arguments");
|
|
277
|
+
}
|
|
278
|
+
yield* serializer.closeTextBlock();
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
case "message_delta": {
|
|
282
|
+
const delta = payload.delta;
|
|
283
|
+
if (isRecord(delta)) {
|
|
284
|
+
capturedStopReason =
|
|
285
|
+
typeof delta.stop_reason === "string" ? delta.stop_reason : null;
|
|
286
|
+
}
|
|
287
|
+
const usage = payload.usage;
|
|
288
|
+
// Only output_tokens is read here — never input/cache fields.
|
|
289
|
+
// message_delta carries only the running output total; reading
|
|
290
|
+
// cache fields from it would zero cache accounting on every
|
|
291
|
+
// response (that was the original design draft's Blocker).
|
|
292
|
+
if (isRecord(usage) && usage.output_tokens !== undefined) {
|
|
293
|
+
capturedOutput = nonNegativeNumber(usage.output_tokens);
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
case "message_stop":
|
|
298
|
+
return yield* serializer.finish(capturedStopReason, {
|
|
299
|
+
input_tokens: capturedInput,
|
|
300
|
+
output_tokens: capturedOutput,
|
|
301
|
+
...(capturedCacheCreate === undefined
|
|
302
|
+
? {}
|
|
303
|
+
: { cache_creation_input_tokens: capturedCacheCreate }),
|
|
304
|
+
...(capturedCacheRead === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: { cache_read_input_tokens: capturedCacheRead }),
|
|
307
|
+
});
|
|
308
|
+
case "error": {
|
|
309
|
+
// A mid-stream upstream error ends the turn: without this case it
|
|
310
|
+
// would fall to default and the client would wait on a stream that
|
|
311
|
+
// never terminates.
|
|
312
|
+
const error = payload.error;
|
|
313
|
+
const code = isRecord(error) && typeof error.type === "string"
|
|
314
|
+
? error.type
|
|
315
|
+
: "upstream_error";
|
|
316
|
+
const message = isRecord(error) && typeof error.message === "string"
|
|
317
|
+
? error.message
|
|
318
|
+
: "Anthropic fallback stream reported an error";
|
|
319
|
+
return yield* serializer.emitFailure(502, message, code);
|
|
320
|
+
}
|
|
321
|
+
// "ping" and any other event type this repo does not name are
|
|
322
|
+
// consumed and produce zero Codex-shape frames.
|
|
323
|
+
default:
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function* consumeEvents(events) {
|
|
328
|
+
for (const raw of events) {
|
|
329
|
+
let payload;
|
|
330
|
+
try {
|
|
331
|
+
payload = JSON.parse(raw.data);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// A single malformed Anthropic frame is skipped defensively rather
|
|
335
|
+
// than failing the whole turn over one unparsable event.
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!isRecord(payload)) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const type = typeof payload.type === "string" ? payload.type : raw.event;
|
|
342
|
+
if (!type) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
// After response.created has gone out, any serializer throw must still
|
|
346
|
+
// end the turn with response.failed; emitFailure closes an open item
|
|
347
|
+
// as incomplete without re-validating it.
|
|
348
|
+
let result;
|
|
349
|
+
try {
|
|
350
|
+
result = yield* processEvent(type, payload);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return yield* serializer.emitFailure(502, "Anthropic fallback stream could not be translated", "translation_error");
|
|
354
|
+
}
|
|
355
|
+
if (result) {
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
// Preflight's buffered chunks run THROUGH the interpreter first — never
|
|
363
|
+
// replayed raw, unlike the Claude-facing route where upstream shape
|
|
364
|
+
// already equals client shape.
|
|
365
|
+
for (const chunk of preflight.chunks) {
|
|
366
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
367
|
+
}
|
|
368
|
+
let parsed = extractSSEEvents(buffer);
|
|
369
|
+
buffer = parsed.remainder;
|
|
370
|
+
let result = yield* consumeEvents(parsed.events);
|
|
371
|
+
if (result) {
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
while (true) {
|
|
375
|
+
let chunk;
|
|
376
|
+
try {
|
|
377
|
+
chunk = await reader.read();
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Post-commit (response.created already flushed): a silent swap is
|
|
381
|
+
// impossible, so this closes the open item incomplete and emits a
|
|
382
|
+
// single response.failed rather than throwing.
|
|
383
|
+
return yield* serializer.emitFailure(502, "Anthropic fallback stream failed while reading", "upstream_read_error");
|
|
384
|
+
}
|
|
385
|
+
buffer += chunk.done
|
|
386
|
+
? decoder.decode()
|
|
387
|
+
: decoder.decode(chunk.value, { stream: true });
|
|
388
|
+
if (buffer.length > MAX_BUFFER_CHARS) {
|
|
389
|
+
return yield* serializer.emitFailure(502, "Anthropic fallback translation exceeded the stream limit", "stream_too_large");
|
|
390
|
+
}
|
|
391
|
+
parsed = extractSSEEvents(buffer);
|
|
392
|
+
buffer = parsed.remainder;
|
|
393
|
+
result = yield* consumeEvents(parsed.events);
|
|
394
|
+
if (result) {
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
if (chunk.done) {
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return yield* serializer.emitFailure(502, "Anthropic fallback stream ended before message_stop", "incomplete_stream");
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
await cancel();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return { frames: frames(), cancel };
|
|
408
|
+
}
|
|
@@ -127,6 +127,27 @@ export declare function recordFallbackAttempt(attrs: {
|
|
|
127
127
|
errorMessage?: string;
|
|
128
128
|
durationMs: number;
|
|
129
129
|
}): void;
|
|
130
|
+
/**
|
|
131
|
+
* Records one Codex-outbound tool-declaration schema degradation (a
|
|
132
|
+
* `$ref`/`$defs` flatten that hit a circular reference, a dropped `strict`
|
|
133
|
+
* flag, or a custom grammar tool wrapped into a one-string schema). `reason`
|
|
134
|
+
* is a short, stable label — never the schema or tool-name text itself, so a
|
|
135
|
+
* call site cannot accidentally leak request payload into a metric label.
|
|
136
|
+
*/
|
|
137
|
+
export declare function recordCodexOutboundSchemaDegraded(attrs: {
|
|
138
|
+
toolName: string;
|
|
139
|
+
reason: string;
|
|
140
|
+
}): void;
|
|
141
|
+
/**
|
|
142
|
+
* Records one Codex-outbound request field dropped during translation
|
|
143
|
+
* because this repo has no faithful Claude-request representation for it
|
|
144
|
+
* (e.g. a malformed `function_call.arguments` string wrapped instead of
|
|
145
|
+
* parsed). Mirrors `recordCodexOutboundSchemaDegraded`'s label discipline.
|
|
146
|
+
*/
|
|
147
|
+
export declare function recordCodexOutboundUnsupportedField(attrs: {
|
|
148
|
+
field: string;
|
|
149
|
+
reason: string;
|
|
150
|
+
}): void;
|
|
130
151
|
/** Reset leg-health state between tests. */
|
|
131
152
|
export declare const __fallbackLegHealthTestHooks: {
|
|
132
153
|
threshold: number;
|
|
@@ -102,6 +102,14 @@ function getProxyMetrics() {
|
|
|
102
102
|
description: "Total failed fallback provider responses",
|
|
103
103
|
unit: "{failure}",
|
|
104
104
|
}),
|
|
105
|
+
schemaDegradedTotal: meter.createCounter("proxy_codex_outbound_schema_degraded_total", {
|
|
106
|
+
description: "Total Codex-outbound tool declarations whose JSON Schema needed flattening or lost a feature on the way to a Claude tool",
|
|
107
|
+
unit: "{tool}",
|
|
108
|
+
}),
|
|
109
|
+
unsupportedFieldTotal: meter.createCounter("proxy_codex_outbound_unsupported_field_total", {
|
|
110
|
+
description: "Total Codex-outbound request fields this repo cannot faithfully represent in the Claude request, dropped rather than failing the translation",
|
|
111
|
+
unit: "{field}",
|
|
112
|
+
}),
|
|
105
113
|
};
|
|
106
114
|
_metrics = createdMetrics;
|
|
107
115
|
return createdMetrics;
|
|
@@ -865,6 +873,43 @@ export function recordFallbackAttempt(attrs) {
|
|
|
865
873
|
// metrics are best-effort
|
|
866
874
|
}
|
|
867
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Records one Codex-outbound tool-declaration schema degradation (a
|
|
878
|
+
* `$ref`/`$defs` flatten that hit a circular reference, a dropped `strict`
|
|
879
|
+
* flag, or a custom grammar tool wrapped into a one-string schema). `reason`
|
|
880
|
+
* is a short, stable label — never the schema or tool-name text itself, so a
|
|
881
|
+
* call site cannot accidentally leak request payload into a metric label.
|
|
882
|
+
*/
|
|
883
|
+
export function recordCodexOutboundSchemaDegraded(attrs) {
|
|
884
|
+
try {
|
|
885
|
+
const m = getProxyMetrics();
|
|
886
|
+
m.schemaDegradedTotal.add(1, {
|
|
887
|
+
toolName: attrs.toolName.slice(0, 100),
|
|
888
|
+
reason: attrs.reason.slice(0, 100),
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
// metrics are best-effort
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Records one Codex-outbound request field dropped during translation
|
|
897
|
+
* because this repo has no faithful Claude-request representation for it
|
|
898
|
+
* (e.g. a malformed `function_call.arguments` string wrapped instead of
|
|
899
|
+
* parsed). Mirrors `recordCodexOutboundSchemaDegraded`'s label discipline.
|
|
900
|
+
*/
|
|
901
|
+
export function recordCodexOutboundUnsupportedField(attrs) {
|
|
902
|
+
try {
|
|
903
|
+
const m = getProxyMetrics();
|
|
904
|
+
m.unsupportedFieldTotal.add(1, {
|
|
905
|
+
field: attrs.field.slice(0, 100),
|
|
906
|
+
reason: attrs.reason.slice(0, 100),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
// metrics are best-effort
|
|
911
|
+
}
|
|
912
|
+
}
|
|
868
913
|
/** Reset leg-health state between tests. */
|
|
869
914
|
export const __fallbackLegHealthTestHooks = {
|
|
870
915
|
threshold: DEAD_FALLBACK_LEG_THRESHOLD,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size/count ceilings for the Codex-outbound fallback, centralized so request
|
|
3
|
+
* ingestion (`codexOutboundFallback.ts`) and response translation
|
|
4
|
+
* (`codexToAnthropicFallback.ts`) share one source instead of re-declaring the
|
|
5
|
+
* numbers `codexFallback.ts` already applies on the reverse
|
|
6
|
+
* (Claude-into-Codex) leg (`:763`, `:784`).
|
|
7
|
+
*/
|
|
8
|
+
/** Caps how much unparsed upstream SSE text a translator buffers, and how
|
|
9
|
+
* long one tool call's argument, input or output text may be, mirroring
|
|
10
|
+
* codexFallback.ts's own ceiling for the reverse leg. */
|
|
11
|
+
export declare const CODEX_STREAM_SIZE_CEILING_BYTES: number;
|
|
12
|
+
/** Caps how many tool calls one turn may open or replay, mirroring
|
|
13
|
+
* codexFallback.ts's own ceiling for the reverse leg — a runaway or
|
|
14
|
+
* adversarial upstream cannot make a translator accumulate unbounded
|
|
15
|
+
* open-item state. */
|
|
16
|
+
export declare const CODEX_STREAM_MAX_TOOL_CALLS = 4096;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size/count ceilings for the Codex-outbound fallback, centralized so request
|
|
3
|
+
* ingestion (`codexOutboundFallback.ts`) and response translation
|
|
4
|
+
* (`codexToAnthropicFallback.ts`) share one source instead of re-declaring the
|
|
5
|
+
* numbers `codexFallback.ts` already applies on the reverse
|
|
6
|
+
* (Claude-into-Codex) leg (`:763`, `:784`).
|
|
7
|
+
*/
|
|
8
|
+
/** Caps how much unparsed upstream SSE text a translator buffers, and how
|
|
9
|
+
* long one tool call's argument, input or output text may be, mirroring
|
|
10
|
+
* codexFallback.ts's own ceiling for the reverse leg. */
|
|
11
|
+
export const CODEX_STREAM_SIZE_CEILING_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
/** Caps how many tool calls one turn may open or replay, mirroring
|
|
13
|
+
* codexFallback.ts's own ceiling for the reverse leg — a runaway or
|
|
14
|
+
* adversarial upstream cannot make a translator accumulate unbounded
|
|
15
|
+
* open-item state. */
|
|
16
|
+
export const CODEX_STREAM_MAX_TOOL_CALLS = 4096;
|
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, ClaudeContentBlock, InternalResult } from "./proxy.js";
|
|
14
|
+
import type { AccountCoolingReason, AccountQuota, ClaudeContentBlock, ClaudeTool, InternalResult } from "./proxy.js";
|
|
15
15
|
/** Token block inside `~/.codex/auth.json`. */
|
|
16
16
|
export type CodexAuthFileTokens = {
|
|
17
17
|
id_token?: string;
|
|
@@ -226,6 +226,26 @@ export type CodexNativeCustomToolDeclaration = {
|
|
|
226
226
|
};
|
|
227
227
|
/** A single tool declaration, discriminated by its own `type` field. */
|
|
228
228
|
export type CodexNativeToolDeclaration = CodexNativeFunctionToolDeclaration | CodexNativeCustomToolDeclaration;
|
|
229
|
+
/**
|
|
230
|
+
* A declared tool's own kind, independent of its namespace. Claude's
|
|
231
|
+
* `tool_use` block carries only `name` + `input`, with no such discriminator,
|
|
232
|
+
* so response serialization threads a name -> kind map (built while flattening
|
|
233
|
+
* `additional_tools` on the request side) to decide whether a tool_use's
|
|
234
|
+
* `input` is a real JSON-Schema-shaped object (`function`) or the single
|
|
235
|
+
* grammar-constrained string a custom tool was wrapped into (`custom`).
|
|
236
|
+
*/
|
|
237
|
+
export type CodexNativeToolKind = "function" | "custom";
|
|
238
|
+
/**
|
|
239
|
+
* One declared tool's mapping outcome inside `codexOutboundFallback.ts`:
|
|
240
|
+
* the translated Claude tool, its kind (for `toolKindByName`), and any
|
|
241
|
+
* degrade reasons collected while flattening its schema (fed to
|
|
242
|
+
* `recordCodexOutboundSchemaDegraded` in `proxyTracer.ts`).
|
|
243
|
+
*/
|
|
244
|
+
export type CodexOutboundToolMappingResult = {
|
|
245
|
+
tool: ClaudeTool;
|
|
246
|
+
kind: CodexNativeToolKind;
|
|
247
|
+
reasons: string[];
|
|
248
|
+
};
|
|
229
249
|
/** A named group of tool declarations inside one `additional_tools` item. */
|
|
230
250
|
export type CodexNativeToolNamespace = {
|
|
231
251
|
type: "namespace";
|
|
@@ -314,6 +334,8 @@ export type CodexNativeRequest = {
|
|
|
314
334
|
* A `parseCodexNativeRequest`/`translateCodexRequestToClaude` failure. Never thrown.
|
|
315
335
|
* `UNTRANSLATABLE_REQUEST` is a request Codex itself accepts but Anthropic would
|
|
316
336
|
* reject, as opposed to one that is invalid on the Codex wire.
|
|
337
|
+
* `REQUEST_TOO_LARGE` is a tool-call history over `streamLimits.ts`'s count or
|
|
338
|
+
* size ceiling; callers map it to HTTP 413 `request_too_large`.
|
|
317
339
|
*/
|
|
318
340
|
export type CodexTranslationError = {
|
|
319
341
|
code: "MALFORMED_REQUEST";
|
|
@@ -324,9 +346,99 @@ export type CodexTranslationError = {
|
|
|
324
346
|
} | {
|
|
325
347
|
code: "UNTRANSLATABLE_REQUEST";
|
|
326
348
|
message: string;
|
|
349
|
+
} | {
|
|
350
|
+
code: "REQUEST_TOO_LARGE";
|
|
351
|
+
message: string;
|
|
327
352
|
};
|
|
328
353
|
/** One coalesced run of same-role content while flattening a native Codex `input` array. */
|
|
329
354
|
export type CodexOutboundMessageGroup = {
|
|
330
355
|
role: "user" | "assistant";
|
|
331
356
|
blocks: ClaudeContentBlock[];
|
|
332
357
|
};
|
|
358
|
+
/** Lifecycle status of one item inside a Codex Responses `output[]` array. */
|
|
359
|
+
export type CodexResponseItemStatus = "in_progress" | "completed" | "incomplete";
|
|
360
|
+
/** Wire usage block inside a synthesized response.completed / non-stream response. */
|
|
361
|
+
export type CodexResponseUsage = {
|
|
362
|
+
input_tokens: number;
|
|
363
|
+
output_tokens: number;
|
|
364
|
+
total_tokens: number;
|
|
365
|
+
/** Omitted entirely when neither cache field was observed on the Anthropic side. */
|
|
366
|
+
input_tokens_details?: {
|
|
367
|
+
cached_tokens?: number;
|
|
368
|
+
cache_write_tokens?: number;
|
|
369
|
+
};
|
|
370
|
+
/** Always omitted — ClaudeUsage carries no reasoning-token count to source it from. */
|
|
371
|
+
output_tokens_details?: {
|
|
372
|
+
reasoning_tokens?: number;
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
/** One `output_text` content part inside a Codex `message` output item. */
|
|
376
|
+
export type CodexResponseOutputTextPart = {
|
|
377
|
+
type: "output_text";
|
|
378
|
+
text: string;
|
|
379
|
+
annotations: [];
|
|
380
|
+
};
|
|
381
|
+
/** A synthesized assistant-text item in a Codex `response.output[]` array. */
|
|
382
|
+
export type CodexResponseMessageItem = {
|
|
383
|
+
id: string;
|
|
384
|
+
type: "message";
|
|
385
|
+
role: "assistant";
|
|
386
|
+
status: CodexResponseItemStatus;
|
|
387
|
+
content: CodexResponseOutputTextPart[];
|
|
388
|
+
};
|
|
389
|
+
/** A synthesized tool-call item in a Codex `response.output[]` array. */
|
|
390
|
+
export type CodexResponseFunctionCallItem = {
|
|
391
|
+
id: string;
|
|
392
|
+
type: "function_call";
|
|
393
|
+
status: CodexResponseItemStatus;
|
|
394
|
+
call_id: string;
|
|
395
|
+
name: string;
|
|
396
|
+
/** Concatenation of raw partial_json fragments. Valid JSON when status is
|
|
397
|
+
* "completed"; may be a truncated, non-JSON fragment when status is
|
|
398
|
+
* "incomplete" (closed early by a mid-stream failure). */
|
|
399
|
+
arguments: string;
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* A synthesized call to a tool the request declared `custom` (grammar-
|
|
403
|
+
* constrained), in the item shape the real backend emits for one — the shape
|
|
404
|
+
* `codexUsage.ts` reads on the native route and a Codex CLI replays in its
|
|
405
|
+
* history. `input` is the bare grammar text, never JSON.
|
|
406
|
+
*/
|
|
407
|
+
export type CodexResponseCustomToolCallItem = {
|
|
408
|
+
id: string;
|
|
409
|
+
type: "custom_tool_call";
|
|
410
|
+
status: CodexResponseItemStatus;
|
|
411
|
+
call_id: string;
|
|
412
|
+
name: string;
|
|
413
|
+
/** Empty when status is "incomplete" and the wrapped input never parsed. */
|
|
414
|
+
input: string;
|
|
415
|
+
};
|
|
416
|
+
/** One item in a Codex `response.output[]` array, discriminated by `type`. */
|
|
417
|
+
export type CodexResponseItem = CodexResponseMessageItem | CodexResponseFunctionCallItem | CodexResponseCustomToolCallItem;
|
|
418
|
+
/** A complete Codex Responses envelope — the non-streaming body, and the shape
|
|
419
|
+
* carried inside a terminal `response.*` SSE event's `response` field. */
|
|
420
|
+
export type CodexResponseEnvelope = {
|
|
421
|
+
id: string;
|
|
422
|
+
object: "response";
|
|
423
|
+
created_at: number;
|
|
424
|
+
status: "completed" | "incomplete" | "failed";
|
|
425
|
+
model: string;
|
|
426
|
+
output: CodexResponseItem[];
|
|
427
|
+
usage?: CodexResponseUsage;
|
|
428
|
+
incomplete_details: {
|
|
429
|
+
reason: "max_output_tokens";
|
|
430
|
+
} | null;
|
|
431
|
+
error: {
|
|
432
|
+
code: string;
|
|
433
|
+
message: string;
|
|
434
|
+
} | null;
|
|
435
|
+
};
|
|
436
|
+
/** The Codex Responses SSE event-type vocabulary this codec emits. */
|
|
437
|
+
export type CodexResponseSSEEventType = "response.created" | "response.in_progress" | "response.output_item.added" | "response.content_part.added" | "response.output_text.delta" | "response.output_text.done" | "response.content_part.done" | "response.function_call_arguments.delta" | "response.function_call_arguments.done" | "response.custom_tool_call_input.delta" | "response.custom_tool_call_input.done" | "response.output_item.done" | "response.completed" | "response.incomplete" | "response.failed";
|
|
438
|
+
/** Incremental Codex-shape SSE frames and the terminal envelope they resolve to. */
|
|
439
|
+
export type CodexResponseStream = {
|
|
440
|
+
frames: AsyncGenerator<string, CodexResponseEnvelope>;
|
|
441
|
+
cancel: (reason?: unknown) => Promise<void>;
|
|
442
|
+
};
|
|
443
|
+
/** Which output item kind (if any) `CodexResponsesStreamSerializer` currently has open. */
|
|
444
|
+
export type CodexResponseOpenItemKind = "message" | "function_call" | "custom_tool_call" | null;
|