@juspay/neurolink 12.36.1 → 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 +3 -3
- package/dist/browser/neurolink.min.js +1 -1
- 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/proxy.d.ts +12 -1
- package/docs-site/static/search-index.json +7 -7
- package/package.json +1 -1
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-format translation: Anthropic `ClaudeResponse` shape -> the Codex
|
|
3
|
+
* Responses shape a native Codex client expects.
|
|
4
|
+
*
|
|
5
|
+
* Pure wire-format only — no account selection, retry policy, or transport.
|
|
6
|
+
* Mirrors `claudeFormat.ts`'s role for the opposite direction: that module
|
|
7
|
+
* serializes NeuroLink's internal result into Claude SSE, this one serializes
|
|
8
|
+
* an already-Claude-shaped result into Codex SSE (or one JSON envelope).
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { formatSSE } from "./claudeFormat.js";
|
|
12
|
+
/** Empty by default so every existing caller (no `toolKindByName` argument)
|
|
13
|
+
* keeps treating every tool_use block as a `function` call, unchanged. */
|
|
14
|
+
const NO_TOOL_KINDS = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* A `custom` tool was wrapped, on the request side, into a single-field
|
|
17
|
+
* `{ input: "..." }` schema (§3.4 of `codexOutboundFallback.ts`), so the
|
|
18
|
+
* client's grammar text is that one field. Undefined when the record does not
|
|
19
|
+
* carry it as a string.
|
|
20
|
+
*/
|
|
21
|
+
function unwrapCustomToolInput(value) {
|
|
22
|
+
return typeof value === "object" &&
|
|
23
|
+
value !== null &&
|
|
24
|
+
!Array.isArray(value) &&
|
|
25
|
+
"input" in value &&
|
|
26
|
+
typeof value.input === "string"
|
|
27
|
+
? value.input
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
/** The accumulated `input_json_delta` text of a custom call, unwrapped, or
|
|
31
|
+
* undefined when it is not (yet) a complete `{ input: "..." }` object. */
|
|
32
|
+
function unwrapCustomToolInputJson(text) {
|
|
33
|
+
try {
|
|
34
|
+
return unwrapCustomToolInput(JSON.parse(text));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Same idiom as `generateToolUseId` (`claudeFormat.ts`), Codex's own prefix. */
|
|
41
|
+
export function generateCodexResponseId() {
|
|
42
|
+
return `resp_${randomBytes(24).toString("base64url")}`;
|
|
43
|
+
}
|
|
44
|
+
const CODEX_ITEM_ID_PREFIX = {
|
|
45
|
+
message: "msg_",
|
|
46
|
+
function_call: "fc_",
|
|
47
|
+
custom_tool_call: "ctc_",
|
|
48
|
+
};
|
|
49
|
+
/** Item ids are prefixed by kind so a client can tell message/tool items apart at a glance. */
|
|
50
|
+
export function generateCodexItemId(kind) {
|
|
51
|
+
return `${CODEX_ITEM_ID_PREFIX[kind]}${randomBytes(18).toString("base64url").slice(0, 24)}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reuse the Anthropic `tool_use.id` (`toolu_...`) verbatim as the Codex `call_id`.
|
|
55
|
+
* No id-mapping table exists anywhere in this codebase's translation modules, and
|
|
56
|
+
* nothing on the way in validates `call_id` format — see the design doc's §5 for
|
|
57
|
+
* the full reasoning and its one unresolved edge (a real, non-fallback Codex
|
|
58
|
+
* backend rejecting a non-`call_`-prefixed id on a later turn).
|
|
59
|
+
*/
|
|
60
|
+
export function codexCallIdFromToolUseId(toolUseId) {
|
|
61
|
+
return toolUseId;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `ClaudeResponse.stop_reason` is `string | null` with no enum in this repo
|
|
65
|
+
* (`proxy.ts`), so any value this repo cannot name — including a real
|
|
66
|
+
* Anthropic "refusal" or "pause_turn" — falls into "completed" rather than
|
|
67
|
+
* surfacing as unusual. Deliberate simplification, stated explicitly.
|
|
68
|
+
*/
|
|
69
|
+
function mapClaudeStopReasonToCodex(stopReason) {
|
|
70
|
+
if (stopReason === "max_tokens") {
|
|
71
|
+
return {
|
|
72
|
+
status: "incomplete",
|
|
73
|
+
incomplete_details: { reason: "max_output_tokens" },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { status: "completed", incomplete_details: null };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Absent cache fields are omitted rather than zeroed (never conflate "not
|
|
80
|
+
* observed" with "observed as zero" — the field's own doc comment on
|
|
81
|
+
* `CodexResponseUsage` states this contract).
|
|
82
|
+
*/
|
|
83
|
+
export function synthesizeCodexUsage(usage) {
|
|
84
|
+
const cacheRead = usage.cache_read_input_tokens;
|
|
85
|
+
const cacheCreation = usage.cache_creation_input_tokens;
|
|
86
|
+
const inputTokens = usage.input_tokens + (cacheRead ?? 0) + (cacheCreation ?? 0);
|
|
87
|
+
const details = cacheRead === undefined && cacheCreation === undefined
|
|
88
|
+
? undefined
|
|
89
|
+
: {
|
|
90
|
+
...(cacheRead === undefined ? {} : { cached_tokens: cacheRead }),
|
|
91
|
+
...(cacheCreation === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: { cache_write_tokens: cacheCreation }),
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
input_tokens: inputTokens,
|
|
97
|
+
output_tokens: usage.output_tokens,
|
|
98
|
+
total_tokens: inputTokens + usage.output_tokens,
|
|
99
|
+
...(details ? { input_tokens_details: details } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Shared by the streaming serializer's non-failure item close and the
|
|
104
|
+
* non-streaming path (`serializeCodexResponse`), so item shape is defined
|
|
105
|
+
* once. Always produces a "completed" item — the streaming failure path
|
|
106
|
+
* (an item closed "incomplete" mid-turn) builds its item directly, since it
|
|
107
|
+
* must preserve the raw accumulated text/argument bytes rather than a value
|
|
108
|
+
* reconstructed through this function.
|
|
109
|
+
*/
|
|
110
|
+
export function buildCodexResponseItem(block, itemId, toolKindByName = NO_TOOL_KINDS) {
|
|
111
|
+
if (block.type === "text") {
|
|
112
|
+
// The streaming path opens a message item only on a block's first delta,
|
|
113
|
+
// so an empty text block produces no item there; match it here.
|
|
114
|
+
if (block.text === "") {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: itemId,
|
|
119
|
+
type: "message",
|
|
120
|
+
role: "assistant",
|
|
121
|
+
status: "completed",
|
|
122
|
+
content: [{ type: "output_text", text: block.text, annotations: [] }],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (block.type === "tool_use" &&
|
|
126
|
+
toolKindByName.get(block.name) === "custom") {
|
|
127
|
+
return {
|
|
128
|
+
id: itemId,
|
|
129
|
+
type: "custom_tool_call",
|
|
130
|
+
status: "completed",
|
|
131
|
+
call_id: codexCallIdFromToolUseId(block.id),
|
|
132
|
+
name: block.name,
|
|
133
|
+
input: unwrapCustomToolInput(block.input) ?? JSON.stringify(block.input),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (block.type === "tool_use") {
|
|
137
|
+
return {
|
|
138
|
+
id: itemId,
|
|
139
|
+
type: "function_call",
|
|
140
|
+
status: "completed",
|
|
141
|
+
call_id: codexCallIdFromToolUseId(block.id),
|
|
142
|
+
name: block.name,
|
|
143
|
+
arguments: JSON.stringify(block.input),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// thinking / image / tool_result — not emitted; see the design doc's §5.
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
/** Non-streaming path: `§7` of the design — the document root IS a `CodexResponseEnvelope`. */
|
|
150
|
+
export function serializeCodexResponse(result, requestModel, toolKindByName = NO_TOOL_KINDS) {
|
|
151
|
+
const items = [];
|
|
152
|
+
for (const block of result.content) {
|
|
153
|
+
const item = buildCodexResponseItem(block, generateCodexItemId(block.type !== "tool_use"
|
|
154
|
+
? "message"
|
|
155
|
+
: toolKindByName.get(block.name) === "custom"
|
|
156
|
+
? "custom_tool_call"
|
|
157
|
+
: "function_call"), toolKindByName);
|
|
158
|
+
if (item) {
|
|
159
|
+
items.push(item);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const { status, incomplete_details } = mapClaudeStopReasonToCodex(result.stop_reason);
|
|
163
|
+
return {
|
|
164
|
+
id: generateCodexResponseId(),
|
|
165
|
+
object: "response",
|
|
166
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
167
|
+
status,
|
|
168
|
+
model: requestModel,
|
|
169
|
+
output: items,
|
|
170
|
+
usage: synthesizeCodexUsage(result.usage),
|
|
171
|
+
incomplete_details,
|
|
172
|
+
error: null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Push-based streaming serializer. A genuinely incremental tool-call-argument
|
|
177
|
+
* path (`openToolCall`/`pushToolCallArgsDelta`/`closeToolCall`) forwards each
|
|
178
|
+
* raw `input_json_delta` chunk as it arrives rather than buffering the whole
|
|
179
|
+
* call before emitting anything — `partial_json` is not individually valid
|
|
180
|
+
* JSON, so a serializer that validated per-chunk could not do this.
|
|
181
|
+
*/
|
|
182
|
+
export class CodexResponsesStreamSerializer {
|
|
183
|
+
model;
|
|
184
|
+
toolKindByName;
|
|
185
|
+
state = "idle";
|
|
186
|
+
responseId = generateCodexResponseId();
|
|
187
|
+
outputIndex = 0;
|
|
188
|
+
openKind = null;
|
|
189
|
+
openItemId = "";
|
|
190
|
+
textAccum = "";
|
|
191
|
+
argsAccum = "";
|
|
192
|
+
toolCallId = "";
|
|
193
|
+
toolName = "";
|
|
194
|
+
closedItems = [];
|
|
195
|
+
terminalEnvelope = null;
|
|
196
|
+
constructor(model, toolKindByName = NO_TOOL_KINDS) {
|
|
197
|
+
this.model = model;
|
|
198
|
+
this.toolKindByName = toolKindByName;
|
|
199
|
+
}
|
|
200
|
+
*start() {
|
|
201
|
+
this.assertNotTerminal();
|
|
202
|
+
this.state = "streaming";
|
|
203
|
+
const response = {
|
|
204
|
+
id: this.responseId,
|
|
205
|
+
object: "response",
|
|
206
|
+
status: "in_progress",
|
|
207
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
208
|
+
model: this.model,
|
|
209
|
+
output: [],
|
|
210
|
+
usage: null,
|
|
211
|
+
incomplete_details: null,
|
|
212
|
+
error: null,
|
|
213
|
+
};
|
|
214
|
+
yield formatSSE("response.created", { type: "response.created", response });
|
|
215
|
+
// Whether the real CLI tolerates a stream without in_progress was never
|
|
216
|
+
// bisected (test/fixtures/sse-bisection-findings.md), so the order the
|
|
217
|
+
// real backend uses is kept: created, then in_progress, then items.
|
|
218
|
+
yield formatSSE("response.in_progress", {
|
|
219
|
+
type: "response.in_progress",
|
|
220
|
+
response,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
*pushDelta(text) {
|
|
224
|
+
this.assertNotTerminal();
|
|
225
|
+
if (this.openKind !== "message") {
|
|
226
|
+
yield* this.closeOpenItem(false);
|
|
227
|
+
yield* this.openMessage();
|
|
228
|
+
}
|
|
229
|
+
this.textAccum += text;
|
|
230
|
+
yield formatSSE("response.output_text.delta", {
|
|
231
|
+
type: "response.output_text.delta",
|
|
232
|
+
output_index: this.outputIndex,
|
|
233
|
+
item_id: this.openItemId,
|
|
234
|
+
content_index: 0,
|
|
235
|
+
delta: text,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Opens the tool-call item header at content_block_start, before any argument text
|
|
239
|
+
* is known — mirrors what content_block_start already gives us (id, name). A tool
|
|
240
|
+
* the request declared `custom` opens as a `custom_tool_call` item. */
|
|
241
|
+
*openToolCall(toolUseId, name) {
|
|
242
|
+
this.assertNotTerminal();
|
|
243
|
+
yield* this.closeOpenItem(false);
|
|
244
|
+
const kind = this.toolKindByName.get(name) === "custom"
|
|
245
|
+
? "custom_tool_call"
|
|
246
|
+
: "function_call";
|
|
247
|
+
this.openKind = kind;
|
|
248
|
+
this.openItemId = generateCodexItemId(kind);
|
|
249
|
+
this.toolCallId = codexCallIdFromToolUseId(toolUseId);
|
|
250
|
+
this.toolName = name;
|
|
251
|
+
this.argsAccum = "";
|
|
252
|
+
const item = kind === "custom_tool_call"
|
|
253
|
+
? {
|
|
254
|
+
id: this.openItemId,
|
|
255
|
+
type: kind,
|
|
256
|
+
status: "in_progress",
|
|
257
|
+
call_id: this.toolCallId,
|
|
258
|
+
name: this.toolName,
|
|
259
|
+
input: "",
|
|
260
|
+
}
|
|
261
|
+
: {
|
|
262
|
+
id: this.openItemId,
|
|
263
|
+
type: kind,
|
|
264
|
+
status: "in_progress",
|
|
265
|
+
call_id: this.toolCallId,
|
|
266
|
+
name: this.toolName,
|
|
267
|
+
arguments: "",
|
|
268
|
+
};
|
|
269
|
+
yield formatSSE("response.output_item.added", {
|
|
270
|
+
type: "response.output_item.added",
|
|
271
|
+
output_index: this.outputIndex,
|
|
272
|
+
item,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/** Forwards one raw partial_json fragment as-is — no JSON.parse here, because a lone
|
|
276
|
+
* fragment is not required to be valid JSON on its own. A custom call's fragments
|
|
277
|
+
* are the `{"input":"..."}` wrapper, not client text, so they are only buffered;
|
|
278
|
+
* `closeToolCall` emits the unwrapped input once the whole wrapper is known. */
|
|
279
|
+
*pushToolCallArgsDelta(rawChunk) {
|
|
280
|
+
this.assertNotTerminal();
|
|
281
|
+
if (this.openKind !== "function_call" &&
|
|
282
|
+
this.openKind !== "custom_tool_call") {
|
|
283
|
+
throw new Error("pushToolCallArgsDelta with no open tool-call item");
|
|
284
|
+
}
|
|
285
|
+
this.argsAccum += rawChunk;
|
|
286
|
+
if (this.openKind === "custom_tool_call") {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
yield formatSSE("response.function_call_arguments.delta", {
|
|
290
|
+
type: "response.function_call_arguments.delta",
|
|
291
|
+
output_index: this.outputIndex,
|
|
292
|
+
item_id: this.openItemId,
|
|
293
|
+
delta: rawChunk,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Public close for a tool-use block at `content_block_stop`. A no-op when no
|
|
298
|
+
* tool-call item is open, so a driver can call it unconditionally.
|
|
299
|
+
* Validates the accumulated arguments once, here — the one place validity
|
|
300
|
+
* is required. Throws on malformed JSON; the caller must catch and route to
|
|
301
|
+
* `emitFailure` (never swallowed, never retried after this point).
|
|
302
|
+
*/
|
|
303
|
+
*closeToolCall() {
|
|
304
|
+
this.assertNotTerminal();
|
|
305
|
+
if (this.openKind === "custom_tool_call") {
|
|
306
|
+
yield* this.closeCustomToolCall();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (this.openKind !== "function_call") {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// A zero-argument call arrives with no input_json_delta at all. The value
|
|
313
|
+
// that was validated is the value emitted, so it becomes "{}" everywhere,
|
|
314
|
+
// and one delta carries it so the client sees the same shape as any call.
|
|
315
|
+
const args = this.argsAccum === "" ? "{}" : this.argsAccum;
|
|
316
|
+
JSON.parse(args); // throws on malformed JSON
|
|
317
|
+
if (this.argsAccum === "") {
|
|
318
|
+
yield formatSSE("response.function_call_arguments.delta", {
|
|
319
|
+
type: "response.function_call_arguments.delta",
|
|
320
|
+
output_index: this.outputIndex,
|
|
321
|
+
item_id: this.openItemId,
|
|
322
|
+
delta: args,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
yield formatSSE("response.function_call_arguments.done", {
|
|
326
|
+
type: "response.function_call_arguments.done",
|
|
327
|
+
output_index: this.outputIndex,
|
|
328
|
+
item_id: this.openItemId,
|
|
329
|
+
arguments: args,
|
|
330
|
+
});
|
|
331
|
+
const item = {
|
|
332
|
+
id: this.openItemId,
|
|
333
|
+
type: "function_call",
|
|
334
|
+
status: "completed",
|
|
335
|
+
call_id: this.toolCallId,
|
|
336
|
+
name: this.toolName,
|
|
337
|
+
arguments: args,
|
|
338
|
+
};
|
|
339
|
+
yield formatSSE("response.output_item.done", {
|
|
340
|
+
type: "response.output_item.done",
|
|
341
|
+
output_index: this.outputIndex,
|
|
342
|
+
item,
|
|
343
|
+
});
|
|
344
|
+
this.closedItems.push(item);
|
|
345
|
+
this.openKind = null;
|
|
346
|
+
this.outputIndex++;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Public close for a text block at `content_block_stop`, mirroring
|
|
350
|
+
* `closeToolCall`'s role for the message item. A no-op when no message item
|
|
351
|
+
* is open, so a driver can call it unconditionally alongside `closeToolCall`
|
|
352
|
+
* without first inspecting which kind is currently open.
|
|
353
|
+
*/
|
|
354
|
+
*closeTextBlock() {
|
|
355
|
+
this.assertNotTerminal();
|
|
356
|
+
if (this.openKind !== "message") {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
yield* this.closeMessageItem(false);
|
|
360
|
+
}
|
|
361
|
+
*finish(stopReason, usage) {
|
|
362
|
+
this.assertNotTerminal();
|
|
363
|
+
yield* this.closeOpenItem(false);
|
|
364
|
+
const { status, incomplete_details } = mapClaudeStopReasonToCodex(stopReason);
|
|
365
|
+
const envelope = {
|
|
366
|
+
id: this.responseId,
|
|
367
|
+
object: "response",
|
|
368
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
369
|
+
status,
|
|
370
|
+
model: this.model,
|
|
371
|
+
output: this.closedItems,
|
|
372
|
+
usage: synthesizeCodexUsage(usage),
|
|
373
|
+
incomplete_details,
|
|
374
|
+
error: null,
|
|
375
|
+
};
|
|
376
|
+
this.state = "done";
|
|
377
|
+
this.terminalEnvelope = envelope;
|
|
378
|
+
yield formatSSE(status === "incomplete" ? "response.incomplete" : "response.completed", {
|
|
379
|
+
type: status === "incomplete"
|
|
380
|
+
? "response.incomplete"
|
|
381
|
+
: "response.completed",
|
|
382
|
+
response: envelope,
|
|
383
|
+
});
|
|
384
|
+
return envelope;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Post-commit terminal path. Closes any open item as "incomplete" — now
|
|
388
|
+
* representable via `CodexResponseItemStatus` — then emits a single
|
|
389
|
+
* `response.failed`. Idempotent: a second call after this one already ran
|
|
390
|
+
* returns the same envelope without emitting further frames, so a driver
|
|
391
|
+
* that reaches it defensively from more than one place never double-emits.
|
|
392
|
+
*/
|
|
393
|
+
// `status` is accepted (not folded into the wire envelope, which has no
|
|
394
|
+
// HTTP-status field) so a caller's classification — e.g. 502 upstream vs.
|
|
395
|
+
// 400 malformed-arguments — is available to whatever logs or maps this
|
|
396
|
+
// failure, mirroring `CodexFallbackStreamError`'s own `status` field on
|
|
397
|
+
// the reverse direction.
|
|
398
|
+
*emitFailure(_status, message, code) {
|
|
399
|
+
if (this.terminalEnvelope) {
|
|
400
|
+
return this.terminalEnvelope;
|
|
401
|
+
}
|
|
402
|
+
yield* this.closeOpenItem(true);
|
|
403
|
+
this.state = "error";
|
|
404
|
+
const envelope = {
|
|
405
|
+
id: this.responseId,
|
|
406
|
+
object: "response",
|
|
407
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
408
|
+
status: "failed",
|
|
409
|
+
model: this.model,
|
|
410
|
+
output: this.closedItems,
|
|
411
|
+
incomplete_details: null,
|
|
412
|
+
error: { code: code ?? "upstream_error", message },
|
|
413
|
+
};
|
|
414
|
+
this.terminalEnvelope = envelope;
|
|
415
|
+
yield formatSSE("response.failed", {
|
|
416
|
+
type: "response.failed",
|
|
417
|
+
response: envelope,
|
|
418
|
+
});
|
|
419
|
+
return envelope;
|
|
420
|
+
}
|
|
421
|
+
assertNotTerminal() {
|
|
422
|
+
if (this.state === "done" || this.state === "error") {
|
|
423
|
+
throw new Error("CodexResponsesStreamSerializer: push after terminal event");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* The custom-kind close: nothing was streamed live, so one input delta
|
|
428
|
+
* carries the whole unwrapped input, then its done event. Lenient where the
|
|
429
|
+
* function path throws: the wrapper is Claude's own output for the
|
|
430
|
+
* single-field schema `mapCodexCustomToolToClaude` declared, so a parse
|
|
431
|
+
* failure is an internal bug, and the raw accumulated text is emitted rather
|
|
432
|
+
* than failing the turn over it.
|
|
433
|
+
*/
|
|
434
|
+
*closeCustomToolCall() {
|
|
435
|
+
const input = unwrapCustomToolInputJson(this.argsAccum) ?? this.argsAccum;
|
|
436
|
+
yield formatSSE("response.custom_tool_call_input.delta", {
|
|
437
|
+
type: "response.custom_tool_call_input.delta",
|
|
438
|
+
output_index: this.outputIndex,
|
|
439
|
+
item_id: this.openItemId,
|
|
440
|
+
delta: input,
|
|
441
|
+
});
|
|
442
|
+
yield formatSSE("response.custom_tool_call_input.done", {
|
|
443
|
+
type: "response.custom_tool_call_input.done",
|
|
444
|
+
output_index: this.outputIndex,
|
|
445
|
+
item_id: this.openItemId,
|
|
446
|
+
input,
|
|
447
|
+
});
|
|
448
|
+
const item = {
|
|
449
|
+
id: this.openItemId,
|
|
450
|
+
type: "custom_tool_call",
|
|
451
|
+
status: "completed",
|
|
452
|
+
call_id: this.toolCallId,
|
|
453
|
+
name: this.toolName,
|
|
454
|
+
input,
|
|
455
|
+
};
|
|
456
|
+
yield formatSSE("response.output_item.done", {
|
|
457
|
+
type: "response.output_item.done",
|
|
458
|
+
output_index: this.outputIndex,
|
|
459
|
+
item,
|
|
460
|
+
});
|
|
461
|
+
this.closedItems.push(item);
|
|
462
|
+
this.openKind = null;
|
|
463
|
+
this.outputIndex++;
|
|
464
|
+
}
|
|
465
|
+
*openMessage() {
|
|
466
|
+
this.openKind = "message";
|
|
467
|
+
this.openItemId = generateCodexItemId("message");
|
|
468
|
+
this.textAccum = "";
|
|
469
|
+
yield formatSSE("response.output_item.added", {
|
|
470
|
+
type: "response.output_item.added",
|
|
471
|
+
output_index: this.outputIndex,
|
|
472
|
+
item: {
|
|
473
|
+
id: this.openItemId,
|
|
474
|
+
type: "message",
|
|
475
|
+
role: "assistant",
|
|
476
|
+
status: "in_progress",
|
|
477
|
+
content: [],
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
yield formatSSE("response.content_part.added", {
|
|
481
|
+
type: "response.content_part.added",
|
|
482
|
+
output_index: this.outputIndex,
|
|
483
|
+
item_id: this.openItemId,
|
|
484
|
+
content_index: 0,
|
|
485
|
+
part: { type: "output_text", text: "", annotations: [] },
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
/** Closes whichever item is currently open, as "incomplete" when `failed`. */
|
|
489
|
+
*closeOpenItem(failed) {
|
|
490
|
+
if (this.openKind === "function_call") {
|
|
491
|
+
if (failed) {
|
|
492
|
+
yield* this.closeFailedFunctionCall();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
yield* this.closeToolCall();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (this.openKind === "custom_tool_call") {
|
|
499
|
+
if (failed) {
|
|
500
|
+
yield* this.closeFailedCustomToolCall();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
yield* this.closeToolCall();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (this.openKind === "message") {
|
|
507
|
+
yield* this.closeMessageItem(failed);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The failure-close counterpart to `closeToolCall`. Arguments are carried
|
|
512
|
+
* verbatim from `argsAccum` and never re-validated — `JSON.parse` is not
|
|
513
|
+
* called on the failure path, and no `function_call_arguments.done` is
|
|
514
|
+
* emitted, since the arguments never reached a validated, complete state.
|
|
515
|
+
*/
|
|
516
|
+
*closeFailedFunctionCall() {
|
|
517
|
+
const item = {
|
|
518
|
+
id: this.openItemId,
|
|
519
|
+
type: "function_call",
|
|
520
|
+
status: "incomplete",
|
|
521
|
+
call_id: this.toolCallId,
|
|
522
|
+
name: this.toolName,
|
|
523
|
+
arguments: this.argsAccum,
|
|
524
|
+
};
|
|
525
|
+
yield formatSSE("response.output_item.done", {
|
|
526
|
+
type: "response.output_item.done",
|
|
527
|
+
output_index: this.outputIndex,
|
|
528
|
+
item,
|
|
529
|
+
});
|
|
530
|
+
this.closedItems.push(item);
|
|
531
|
+
this.openKind = null;
|
|
532
|
+
this.outputIndex++;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The failure close for a custom call. Its accumulated bytes are Anthropic's
|
|
536
|
+
* JSON wrapper, not the client's grammar text, so `input` is the unwrapped
|
|
537
|
+
* string when the wrapper already parses and "" otherwise — an incomplete
|
|
538
|
+
* item is never executed. No input done event, as on the function path.
|
|
539
|
+
*/
|
|
540
|
+
*closeFailedCustomToolCall() {
|
|
541
|
+
const item = {
|
|
542
|
+
id: this.openItemId,
|
|
543
|
+
type: "custom_tool_call",
|
|
544
|
+
status: "incomplete",
|
|
545
|
+
call_id: this.toolCallId,
|
|
546
|
+
name: this.toolName,
|
|
547
|
+
input: unwrapCustomToolInputJson(this.argsAccum) ?? "",
|
|
548
|
+
};
|
|
549
|
+
yield formatSSE("response.output_item.done", {
|
|
550
|
+
type: "response.output_item.done",
|
|
551
|
+
output_index: this.outputIndex,
|
|
552
|
+
item,
|
|
553
|
+
});
|
|
554
|
+
this.closedItems.push(item);
|
|
555
|
+
this.openKind = null;
|
|
556
|
+
this.outputIndex++;
|
|
557
|
+
}
|
|
558
|
+
*closeMessageItem(failed) {
|
|
559
|
+
const text = this.textAccum;
|
|
560
|
+
yield formatSSE("response.output_text.done", {
|
|
561
|
+
type: "response.output_text.done",
|
|
562
|
+
output_index: this.outputIndex,
|
|
563
|
+
item_id: this.openItemId,
|
|
564
|
+
content_index: 0,
|
|
565
|
+
text,
|
|
566
|
+
});
|
|
567
|
+
yield formatSSE("response.content_part.done", {
|
|
568
|
+
type: "response.content_part.done",
|
|
569
|
+
output_index: this.outputIndex,
|
|
570
|
+
item_id: this.openItemId,
|
|
571
|
+
content_index: 0,
|
|
572
|
+
part: { type: "output_text", text, annotations: [] },
|
|
573
|
+
});
|
|
574
|
+
const item = {
|
|
575
|
+
id: this.openItemId,
|
|
576
|
+
type: "message",
|
|
577
|
+
role: "assistant",
|
|
578
|
+
status: failed ? "incomplete" : "completed",
|
|
579
|
+
content: [{ type: "output_text", text, annotations: [] }],
|
|
580
|
+
};
|
|
581
|
+
yield formatSSE("response.output_item.done", {
|
|
582
|
+
type: "response.output_item.done",
|
|
583
|
+
output_index: this.outputIndex,
|
|
584
|
+
item,
|
|
585
|
+
});
|
|
586
|
+
this.closedItems.push(item);
|
|
587
|
+
this.openKind = null;
|
|
588
|
+
this.textAccum = "";
|
|
589
|
+
this.outputIndex++;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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 type { CodexNativeToolKind, CodexResponseEnvelope, CodexResponseStream } from "../types/index.js";
|
|
23
|
+
/** A non-2xx reply (either path) or a non-JSON reply (non-streaming path) from
|
|
24
|
+
* the Anthropic-shape upstream, raised before any Codex frame is sent. */
|
|
25
|
+
export declare class AnthropicFallbackResponseError extends Error {
|
|
26
|
+
readonly status: number;
|
|
27
|
+
readonly responseBody: string;
|
|
28
|
+
constructor(status: number, responseBody: string);
|
|
29
|
+
}
|
|
30
|
+
/** A pre-commit or post-commit failure while translating the Anthropic SSE
|
|
31
|
+
* stream, or a well-formed non-streaming reply this module refuses to
|
|
32
|
+
* translate (tool calls over `streamLimits.ts`'s count or size ceiling). */
|
|
33
|
+
export declare class AnthropicFallbackStreamError extends Error {
|
|
34
|
+
readonly status: number;
|
|
35
|
+
readonly code: string;
|
|
36
|
+
constructor(status: number, message: string, code: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Consume a fully-buffered, non-streaming Anthropic-shape response and
|
|
40
|
+
* translate it into one Codex Responses envelope.
|
|
41
|
+
*
|
|
42
|
+
* No `message_start`/`message_delta` split here: a single JSON object
|
|
43
|
+
* carries the whole turn's usage at once (confirmed against this repo's own
|
|
44
|
+
* `settleFromResponseUsage`, which reads a non-streaming Anthropic body the
|
|
45
|
+
* same way), so the translation delegates straight to `serializeCodexResponse`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function consumeAnthropicFallbackResponse(response: Response, model: string, toolKindByName?: ReadonlyMap<string, CodexNativeToolKind>): Promise<CodexResponseEnvelope>;
|
|
48
|
+
/**
|
|
49
|
+
* Translate an Anthropic SSE stream into Codex-shape SSE frames as events
|
|
50
|
+
* arrive. The caller owns error framing downstream and must never retry
|
|
51
|
+
* after any frame has been emitted (mirrors `codexFallback.ts`'s own
|
|
52
|
+
* discipline, `:715-718`).
|
|
53
|
+
*/
|
|
54
|
+
export declare function createAnthropicFallbackStream(response: Response, model: string, toolKindByName?: ReadonlyMap<string, CodexNativeToolKind>): Promise<CodexResponseStream>;
|