@juspay/neurolink 12.36.1 → 12.37.1

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.
@@ -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;
@@ -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;