@velum-labs/routekit-gateway 0.9.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.
Files changed (99) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +28 -0
  3. package/dist/acp-agent.d.ts +38 -0
  4. package/dist/acp-agent.js +142 -0
  5. package/dist/acp-registry.d.ts +36 -0
  6. package/dist/acp-registry.js +85 -0
  7. package/dist/adapters/anthropic.d.ts +131 -0
  8. package/dist/adapters/anthropic.js +1195 -0
  9. package/dist/adapters/chat.d.ts +14 -0
  10. package/dist/adapters/chat.js +34 -0
  11. package/dist/adapters/cursor.d.ts +34 -0
  12. package/dist/adapters/cursor.js +305 -0
  13. package/dist/adapters/dropped.d.ts +10 -0
  14. package/dist/adapters/dropped.js +24 -0
  15. package/dist/adapters/openai-chat-wire.d.ts +93 -0
  16. package/dist/adapters/openai-chat-wire.js +143 -0
  17. package/dist/adapters/responses-stream.d.ts +7 -0
  18. package/dist/adapters/responses-stream.js +597 -0
  19. package/dist/adapters/responses.d.ts +174 -0
  20. package/dist/adapters/responses.js +778 -0
  21. package/dist/adapters/server-tool-loop.d.ts +94 -0
  22. package/dist/adapters/server-tool-loop.js +477 -0
  23. package/dist/adapters/upstream-error.d.ts +14 -0
  24. package/dist/adapters/upstream-error.js +25 -0
  25. package/dist/adapters/validate.d.ts +27 -0
  26. package/dist/adapters/validate.js +180 -0
  27. package/dist/adapters/web-search.d.ts +46 -0
  28. package/dist/adapters/web-search.js +151 -0
  29. package/dist/auth.d.ts +10 -0
  30. package/dist/auth.js +28 -0
  31. package/dist/backend.d.ts +151 -0
  32. package/dist/backend.js +143 -0
  33. package/dist/capacity-pool.d.ts +31 -0
  34. package/dist/capacity-pool.js +99 -0
  35. package/dist/cost.d.ts +49 -0
  36. package/dist/cost.js +112 -0
  37. package/dist/endpoint-health.d.ts +54 -0
  38. package/dist/endpoint-health.js +123 -0
  39. package/dist/index.d.ts +40 -0
  40. package/dist/index.js +23 -0
  41. package/dist/provenance.d.ts +31 -0
  42. package/dist/provenance.js +191 -0
  43. package/dist/provider-backends.d.ts +40 -0
  44. package/dist/provider-backends.js +1050 -0
  45. package/dist/provider-source.d.ts +40 -0
  46. package/dist/provider-source.js +293 -0
  47. package/dist/router.d.ts +168 -0
  48. package/dist/router.js +474 -0
  49. package/dist/server.d.ts +67 -0
  50. package/dist/server.js +930 -0
  51. package/dist/sse/chat-assembler.d.ts +45 -0
  52. package/dist/sse/chat-assembler.js +190 -0
  53. package/dist/sse/parse.d.ts +50 -0
  54. package/dist/sse/parse.js +149 -0
  55. package/dist/sse-wire.d.ts +10 -0
  56. package/dist/sse-wire.js +31 -0
  57. package/dist/switching-proxy.d.ts +15 -0
  58. package/dist/switching-proxy.js +232 -0
  59. package/dist/test/acp-agent.test.d.ts +1 -0
  60. package/dist/test/acp-agent.test.js +66 -0
  61. package/dist/test/acp-registry.test.d.ts +1 -0
  62. package/dist/test/acp-registry.test.js +70 -0
  63. package/dist/test/anthropic.test.d.ts +1 -0
  64. package/dist/test/anthropic.test.js +793 -0
  65. package/dist/test/auth.test.d.ts +1 -0
  66. package/dist/test/auth.test.js +25 -0
  67. package/dist/test/boundary.test.d.ts +1 -0
  68. package/dist/test/boundary.test.js +32 -0
  69. package/dist/test/chat.test.d.ts +1 -0
  70. package/dist/test/chat.test.js +418 -0
  71. package/dist/test/cost.test.d.ts +1 -0
  72. package/dist/test/cost.test.js +60 -0
  73. package/dist/test/cursor.test.d.ts +1 -0
  74. package/dist/test/cursor.test.js +100 -0
  75. package/dist/test/drain.test.d.ts +1 -0
  76. package/dist/test/drain.test.js +116 -0
  77. package/dist/test/dropped.test.d.ts +1 -0
  78. package/dist/test/dropped.test.js +80 -0
  79. package/dist/test/endpoint-health.test.d.ts +1 -0
  80. package/dist/test/endpoint-health.test.js +73 -0
  81. package/dist/test/provenance.test.d.ts +1 -0
  82. package/dist/test/provenance.test.js +176 -0
  83. package/dist/test/provider-backends.test.d.ts +1 -0
  84. package/dist/test/provider-backends.test.js +699 -0
  85. package/dist/test/responses.test.d.ts +1 -0
  86. package/dist/test/responses.test.js +813 -0
  87. package/dist/test/routed-backend.test.d.ts +1 -0
  88. package/dist/test/routed-backend.test.js +39 -0
  89. package/dist/test/router.test.d.ts +1 -0
  90. package/dist/test/router.test.js +297 -0
  91. package/dist/test/server-resilience.test.d.ts +1 -0
  92. package/dist/test/server-resilience.test.js +169 -0
  93. package/dist/test/sse-codec.test.d.ts +1 -0
  94. package/dist/test/sse-codec.test.js +186 -0
  95. package/dist/test/web-search-loop.test.d.ts +1 -0
  96. package/dist/test/web-search-loop.test.js +469 -0
  97. package/dist/test/wire-validation.test.d.ts +1 -0
  98. package/dist/test/wire-validation.test.js +140 -0
  99. package/package.json +48 -0
@@ -0,0 +1,1050 @@
1
+ import { randomId } from "@velum-labs/routekit-runtime";
2
+ import { joinPath } from "./backend.js";
3
+ import { SseDecoder, SseParseError } from "./sse/parse.js";
4
+ import { ANTHROPIC_MESSAGE_CONTENT, ANTHROPIC_REQUEST_METADATA, anthropicReasoningDetailsOf, reasoningSelectionOf } from "./adapters/openai-chat-wire.js";
5
+ class HttpProviderBackend {
6
+ defaultModel;
7
+ baseUrl;
8
+ apiKey;
9
+ extraHeaders;
10
+ transport;
11
+ constructor(options) {
12
+ this.baseUrl = options.baseUrl;
13
+ this.apiKey = options.apiKey;
14
+ this.defaultModel = options.defaultModel;
15
+ this.extraHeaders = options.headers ?? {};
16
+ this.transport =
17
+ options.transport ?? (async (url, init) => await fetch(url, init));
18
+ }
19
+ listModelIds() {
20
+ return this.defaultModel === undefined ? [] : [this.defaultModel];
21
+ }
22
+ servesModel(model) {
23
+ return this.defaultModel === undefined || model === this.defaultModel;
24
+ }
25
+ models() {
26
+ const data = this.listModelIds().map((id) => ({ id, object: "model", owned_by: "provider" }));
27
+ return Promise.resolve(new Response(JSON.stringify({ object: "list", data }), {
28
+ headers: { "content-type": "application/json" }
29
+ }));
30
+ }
31
+ embeddings() {
32
+ return Promise.resolve(new Response(JSON.stringify({ error: { message: "embeddings are not supported" } }), {
33
+ status: 501,
34
+ headers: { "content-type": "application/json" }
35
+ }));
36
+ }
37
+ }
38
+ function bodyRecord(body) {
39
+ return typeof body === "object" && body !== null && !Array.isArray(body)
40
+ ? body
41
+ : {};
42
+ }
43
+ function textContent(content) {
44
+ if (typeof content === "string")
45
+ return content;
46
+ if (!Array.isArray(content))
47
+ return "";
48
+ return content
49
+ .flatMap((part) => typeof part === "object" &&
50
+ part !== null &&
51
+ "text" in part &&
52
+ typeof part.text === "string"
53
+ ? [part.text]
54
+ : [])
55
+ .join("");
56
+ }
57
+ function jsonResponse(value, status = 200, headers) {
58
+ return new Response(JSON.stringify(value), {
59
+ status,
60
+ headers: {
61
+ "content-type": "application/json",
62
+ ...(headers?.get("x-request-id") !== null
63
+ ? { "x-request-id": headers?.get("x-request-id") ?? "" }
64
+ : {})
65
+ }
66
+ });
67
+ }
68
+ function copyFailure(response, text) {
69
+ return new Response(text, {
70
+ status: response.status,
71
+ statusText: response.statusText,
72
+ headers: response.headers
73
+ });
74
+ }
75
+ function chatCompletion(model, message, usage, finishReason = "stop", choiceMetadata = {}) {
76
+ return {
77
+ id: randomId(18, "chatcmpl_"),
78
+ object: "chat.completion",
79
+ created: Math.floor(Date.now() / 1000),
80
+ model,
81
+ choices: [{ index: 0, message, finish_reason: finishReason, ...choiceMetadata }],
82
+ ...(usage !== undefined ? { usage } : {})
83
+ };
84
+ }
85
+ function normalizedOpenAiUsage(usage) {
86
+ if (typeof usage !== "object" || usage === null || Array.isArray(usage))
87
+ return usage;
88
+ const value = usage;
89
+ const promptTokens = value.prompt_tokens ?? value.input_tokens;
90
+ const completionTokens = value.completion_tokens ?? value.output_tokens;
91
+ const totalTokens = value.total_tokens ??
92
+ (typeof promptTokens === "number" && typeof completionTokens === "number"
93
+ ? promptTokens + completionTokens
94
+ : undefined);
95
+ return {
96
+ ...value,
97
+ ...(promptTokens !== undefined ? { prompt_tokens: promptTokens } : {}),
98
+ ...(completionTokens !== undefined ? { completion_tokens: completionTokens } : {}),
99
+ ...(totalTokens !== undefined ? { total_tokens: totalTokens } : {})
100
+ };
101
+ }
102
+ function mapSse(response, mapper) {
103
+ if (response.body === null)
104
+ return response;
105
+ const decoder = new SseDecoder();
106
+ const encoder = new TextEncoder();
107
+ const mapEvents = (events, controller) => {
108
+ for (const event of events) {
109
+ const raw = event.data.trim();
110
+ if (raw.length === 0 || raw === "[DONE]")
111
+ continue;
112
+ let data;
113
+ try {
114
+ data = JSON.parse(raw);
115
+ }
116
+ catch {
117
+ throw new SseParseError("provider SSE event contained malformed JSON", raw.slice(0, 200));
118
+ }
119
+ for (const mapped of mapper(event.event ?? "message", data)) {
120
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(mapped)}\n\n`));
121
+ }
122
+ }
123
+ };
124
+ const transformed = response.body.pipeThrough(new TransformStream({
125
+ transform(chunk, controller) {
126
+ mapEvents(decoder.feed(chunk), controller);
127
+ },
128
+ flush(controller) {
129
+ mapEvents(decoder.flush(), controller);
130
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
131
+ }
132
+ }));
133
+ return new Response(transformed, {
134
+ status: response.status,
135
+ headers: { "content-type": "text/event-stream; charset=utf-8" }
136
+ });
137
+ }
138
+ function anthropicNativeContent(message) {
139
+ const content = message[ANTHROPIC_MESSAGE_CONTENT];
140
+ if (Array.isArray(content))
141
+ return content;
142
+ const details = anthropicReasoningDetailsOf(message.reasoning_details, "message")
143
+ .filter((detail) => detail.type === "redacted_thinking" ||
144
+ (detail.type === "thinking" &&
145
+ typeof detail.signature === "string" &&
146
+ detail.signature.length > 0))
147
+ .sort((a, b) => a.index - b.index);
148
+ if (details.length === 0)
149
+ return undefined;
150
+ const native = details.map((detail) => detail.type === "redacted_thinking"
151
+ ? { type: "redacted_thinking", data: detail.data }
152
+ : {
153
+ type: "thinking",
154
+ thinking: detail.thinking ?? "",
155
+ signature: detail.signature ?? ""
156
+ });
157
+ const text = textContent(message.content);
158
+ if (text.length > 0)
159
+ native.push({ type: "text", text });
160
+ for (const call of message.tool_calls ?? []) {
161
+ let input = {};
162
+ try {
163
+ input = JSON.parse(call.function?.arguments ?? "{}");
164
+ }
165
+ catch {
166
+ input = { raw: call.function?.arguments ?? "" };
167
+ }
168
+ native.push({
169
+ type: "tool_use",
170
+ id: call.id ?? randomId(12, "toolu_"),
171
+ name: call.function?.name ?? "tool",
172
+ input
173
+ });
174
+ }
175
+ return native;
176
+ }
177
+ function anthropicMetadata(body) {
178
+ return body[ANTHROPIC_REQUEST_METADATA];
179
+ }
180
+ function anthropicToolChoice(choice, parallelToolCalls) {
181
+ const disableParallel = parallelToolCalls === false ? { disable_parallel_tool_use: true } : {};
182
+ if (choice === "auto")
183
+ return { type: "auto", ...disableParallel };
184
+ if (choice === "required")
185
+ return { type: "any", ...disableParallel };
186
+ if (choice === "none")
187
+ return { type: "none", ...disableParallel };
188
+ if (typeof choice !== "object" || choice === null || Array.isArray(choice)) {
189
+ return parallelToolCalls === false ? { type: "auto", ...disableParallel } : undefined;
190
+ }
191
+ const fn = choice.function;
192
+ return typeof fn?.name === "string"
193
+ ? { type: "tool", name: fn.name, ...disableParallel }
194
+ : undefined;
195
+ }
196
+ function anthropicMessages(body, model) {
197
+ const system = (body.messages ?? [])
198
+ .filter((message) => message.role === "system")
199
+ .map((message) => textContent(message.content))
200
+ .join("\n\n");
201
+ const messages = (body.messages ?? []).flatMap((message) => {
202
+ if (message.role === "system")
203
+ return [];
204
+ const nativeContent = anthropicNativeContent(message);
205
+ if (message.role === "assistant" && nativeContent !== undefined) {
206
+ return [{ role: "assistant", content: nativeContent }];
207
+ }
208
+ if (message.role === "tool") {
209
+ return [
210
+ {
211
+ role: "user",
212
+ content: [
213
+ {
214
+ type: "tool_result",
215
+ tool_use_id: message.tool_call_id ?? "",
216
+ content: textContent(message.content)
217
+ }
218
+ ]
219
+ }
220
+ ];
221
+ }
222
+ const content = [];
223
+ const text = textContent(message.content);
224
+ if (text.length > 0)
225
+ content.push({ type: "text", text });
226
+ for (const call of message.tool_calls ?? []) {
227
+ let input = {};
228
+ try {
229
+ input = JSON.parse(call.function?.arguments ?? "{}");
230
+ }
231
+ catch {
232
+ input = { raw: call.function?.arguments ?? "" };
233
+ }
234
+ content.push({
235
+ type: "tool_use",
236
+ id: call.id ?? randomId(12, "toolu_"),
237
+ name: call.function?.name ?? "tool",
238
+ input
239
+ });
240
+ }
241
+ return [{ role: message.role === "assistant" ? "assistant" : "user", content }];
242
+ });
243
+ const maxTokens = body.max_completion_tokens ?? body.max_tokens ?? 4096;
244
+ const metadata = anthropicMetadata(body);
245
+ const selection = reasoningSelectionOf(body);
246
+ const translatedThinking = selection.mode === "budget"
247
+ ? { type: "enabled", budget_tokens: selection.budgetTokens }
248
+ : selection.mode === "adaptive" || selection.mode === "effort"
249
+ ? { type: "adaptive" }
250
+ : selection.mode === "disabled"
251
+ ? { type: "disabled" }
252
+ : undefined;
253
+ const translatedOutput = selection.mode === "effort" ? { effort: selection.effort } : undefined;
254
+ const thinking = metadata?.thinking ?? translatedThinking;
255
+ const toolChoice = anthropicToolChoice(body.tool_choice, body.parallel_tool_calls);
256
+ return {
257
+ model,
258
+ messages,
259
+ max_tokens: maxTokens,
260
+ stream: body.stream === true,
261
+ ...(system.length > 0 ? { system } : {}),
262
+ ...(body.temperature !== undefined ? { temperature: body.temperature } : {}),
263
+ ...(body.top_p !== undefined ? { top_p: body.top_p } : {}),
264
+ ...(body.top_k !== undefined ? { top_k: body.top_k } : {}),
265
+ ...(thinking !== undefined ? { thinking } : {}),
266
+ ...(metadata?.output_config != null
267
+ ? { output_config: metadata.output_config }
268
+ : translatedOutput !== undefined
269
+ ? { output_config: translatedOutput }
270
+ : {}),
271
+ ...(toolChoice !== undefined ? { tool_choice: toolChoice } : {}),
272
+ ...(body.tools !== undefined
273
+ ? {
274
+ tools: body.tools.flatMap((tool) => tool.function === undefined
275
+ ? []
276
+ : [
277
+ {
278
+ name: tool.function.name,
279
+ description: tool.function.description,
280
+ input_schema: tool.function.parameters ?? { type: "object" }
281
+ }
282
+ ])
283
+ }
284
+ : {})
285
+ };
286
+ }
287
+ function anthropicThinkingValidationError(body) {
288
+ const maxTokens = body.max_completion_tokens ?? body.max_tokens ?? 4096;
289
+ const exact = anthropicMetadata(body)?.thinking;
290
+ const selection = reasoningSelectionOf(body);
291
+ const budget = exact?.type === "enabled"
292
+ ? exact.budget_tokens
293
+ : selection.mode === "budget"
294
+ ? selection.budgetTokens
295
+ : undefined;
296
+ if (budget !== undefined) {
297
+ if (!Number.isInteger(budget) || budget < 1_024 || budget >= maxTokens) {
298
+ return `thinking.budget_tokens must be an integer >= 1024 and less than max_tokens (${maxTokens})`;
299
+ }
300
+ }
301
+ return undefined;
302
+ }
303
+ function openAiFinishReasonFromAnthropic(stopReason) {
304
+ if (stopReason === "tool_use")
305
+ return "tool_calls";
306
+ if (stopReason === "max_tokens" || stopReason === "model_context_window_exceeded") {
307
+ return "length";
308
+ }
309
+ if (stopReason === "refusal")
310
+ return "content_filter";
311
+ return "stop";
312
+ }
313
+ export class AnthropicBackend extends HttpProviderBackend {
314
+ chat(body, signal, options) {
315
+ return this.#chat(bodyRecord(body), signal, options);
316
+ }
317
+ async #chat(body, signal, options) {
318
+ const model = body.model ?? this.defaultModel ?? "";
319
+ const thinkingError = anthropicThinkingValidationError(body);
320
+ if (thinkingError !== undefined) {
321
+ return jsonResponse({ error: { type: "invalid_request_error", message: thinkingError } }, 400);
322
+ }
323
+ const response = await this.transport(joinPath(this.baseUrl, "/messages"), {
324
+ method: "POST",
325
+ headers: {
326
+ "content-type": "application/json",
327
+ "x-api-key": this.apiKey,
328
+ "anthropic-version": "2023-06-01",
329
+ ...this.extraHeaders
330
+ },
331
+ body: JSON.stringify(anthropicMessages(body, model)),
332
+ ...(signal !== undefined ? { signal } : {})
333
+ }, options);
334
+ if (!response.ok)
335
+ return copyFailure(response, await response.text());
336
+ if (body.stream === true) {
337
+ const blockTypes = new Map();
338
+ return mapSse(response, (event, data) => {
339
+ const item = data;
340
+ const delta = item.delta;
341
+ if (event === "message_start") {
342
+ const message = item.message;
343
+ return message?.usage === undefined
344
+ ? []
345
+ : [
346
+ {
347
+ id: randomId(18, "chatcmpl_"),
348
+ object: "chat.completion.chunk",
349
+ model,
350
+ choices: [],
351
+ usage: normalizedOpenAiUsage(message.usage)
352
+ }
353
+ ];
354
+ }
355
+ if (event === "content_block_start") {
356
+ const block = item.content_block;
357
+ const sourceIndex = typeof item.index === "number" ? item.index : 0;
358
+ if (typeof block?.type === "string")
359
+ blockTypes.set(sourceIndex, block.type);
360
+ if (block?.type === "tool_use") {
361
+ return [
362
+ {
363
+ id: randomId(18, "chatcmpl_"),
364
+ object: "chat.completion.chunk",
365
+ model,
366
+ choices: [
367
+ {
368
+ index: 0,
369
+ delta: {
370
+ tool_calls: [
371
+ {
372
+ index: item.index ?? 0,
373
+ id: block.id,
374
+ type: "function",
375
+ function: { name: block.name, arguments: "" }
376
+ }
377
+ ]
378
+ },
379
+ finish_reason: null
380
+ }
381
+ ]
382
+ }
383
+ ];
384
+ }
385
+ if (block?.type === "thinking") {
386
+ return [
387
+ {
388
+ id: randomId(18, "chatcmpl_"),
389
+ object: "chat.completion.chunk",
390
+ model,
391
+ choices: [
392
+ {
393
+ index: 0,
394
+ delta: {
395
+ reasoning_details: [
396
+ {
397
+ type: "thinking",
398
+ index: sourceIndex,
399
+ phase: "start",
400
+ signature: typeof block.signature === "string" ? block.signature : ""
401
+ }
402
+ ]
403
+ },
404
+ finish_reason: null
405
+ }
406
+ ]
407
+ }
408
+ ];
409
+ }
410
+ if (block?.type === "redacted_thinking" &&
411
+ typeof block.data === "string") {
412
+ return [
413
+ {
414
+ id: randomId(18, "chatcmpl_"),
415
+ object: "chat.completion.chunk",
416
+ model,
417
+ choices: [
418
+ {
419
+ index: 0,
420
+ delta: {
421
+ reasoning_details: [
422
+ {
423
+ type: "redacted_thinking",
424
+ index: sourceIndex,
425
+ phase: "block",
426
+ data: block.data
427
+ }
428
+ ]
429
+ },
430
+ finish_reason: null
431
+ }
432
+ ]
433
+ }
434
+ ];
435
+ }
436
+ }
437
+ if (event === "content_block_delta") {
438
+ const sourceIndex = typeof item.index === "number" ? item.index : 0;
439
+ const content = delta?.type === "text_delta" ? delta.text : undefined;
440
+ const toolArguments = delta?.type === "input_json_delta" ? delta.partial_json : undefined;
441
+ const reasoning = delta?.type === "thinking_delta" ? delta.thinking : undefined;
442
+ const signature = delta?.type === "signature_delta" ? delta.signature : undefined;
443
+ return [
444
+ {
445
+ id: randomId(18, "chatcmpl_"),
446
+ object: "chat.completion.chunk",
447
+ model,
448
+ choices: [
449
+ {
450
+ index: 0,
451
+ delta: toolArguments !== undefined
452
+ ? {
453
+ tool_calls: [
454
+ {
455
+ index: item.index ?? 0,
456
+ function: { arguments: toolArguments }
457
+ }
458
+ ]
459
+ }
460
+ : reasoning !== undefined
461
+ ? {
462
+ reasoning,
463
+ reasoning_details: [
464
+ {
465
+ type: "thinking",
466
+ index: sourceIndex,
467
+ phase: "delta",
468
+ thinking: reasoning
469
+ }
470
+ ]
471
+ }
472
+ : signature !== undefined
473
+ ? {
474
+ reasoning_details: [
475
+ {
476
+ type: "thinking",
477
+ index: sourceIndex,
478
+ phase: "signature",
479
+ signature
480
+ }
481
+ ]
482
+ }
483
+ : { content },
484
+ finish_reason: null
485
+ }
486
+ ]
487
+ }
488
+ ];
489
+ }
490
+ if (event === "content_block_stop") {
491
+ const sourceIndex = typeof item.index === "number" ? item.index : 0;
492
+ if (blockTypes.get(sourceIndex) === "thinking") {
493
+ return [
494
+ {
495
+ id: randomId(18, "chatcmpl_"),
496
+ object: "chat.completion.chunk",
497
+ model,
498
+ choices: [
499
+ {
500
+ index: 0,
501
+ delta: {
502
+ reasoning_details: [
503
+ {
504
+ type: "thinking",
505
+ index: sourceIndex,
506
+ phase: "stop"
507
+ }
508
+ ]
509
+ },
510
+ finish_reason: null
511
+ }
512
+ ]
513
+ }
514
+ ];
515
+ }
516
+ return [];
517
+ }
518
+ if (event === "message_delta") {
519
+ const stopReason = delta?.stop_reason;
520
+ return [
521
+ {
522
+ id: randomId(18, "chatcmpl_"),
523
+ object: "chat.completion.chunk",
524
+ model,
525
+ choices: [
526
+ {
527
+ index: 0,
528
+ delta: {},
529
+ finish_reason: openAiFinishReasonFromAnthropic(stopReason),
530
+ ...(typeof stopReason === "string"
531
+ ? { anthropic_stop_reason: stopReason }
532
+ : {}),
533
+ ...(typeof delta?.stop_sequence === "string"
534
+ ? { anthropic_stop_sequence: delta.stop_sequence }
535
+ : {})
536
+ }
537
+ ],
538
+ ...(item.usage !== undefined
539
+ ? { usage: normalizedOpenAiUsage(item.usage) }
540
+ : {})
541
+ }
542
+ ];
543
+ }
544
+ return [];
545
+ });
546
+ }
547
+ const payload = (await response.json());
548
+ const content = (payload.content ?? [])
549
+ .filter((block) => block.type === "text")
550
+ .map((block) => String(block.text ?? ""))
551
+ .join("");
552
+ const reasoning = (payload.content ?? [])
553
+ .filter((block) => block.type === "thinking")
554
+ .map((block) => String(block.thinking ?? ""))
555
+ .join("");
556
+ const reasoningDetails = (payload.content ?? []).flatMap((block, index) => {
557
+ if (block.type === "thinking") {
558
+ return [
559
+ {
560
+ type: "thinking",
561
+ index,
562
+ thinking: String(block.thinking ?? ""),
563
+ signature: typeof block.signature === "string" ? block.signature : ""
564
+ }
565
+ ];
566
+ }
567
+ if (block.type === "redacted_thinking" && typeof block.data === "string") {
568
+ return [{ type: "redacted_thinking", index, data: block.data }];
569
+ }
570
+ return [];
571
+ });
572
+ const toolCalls = (payload.content ?? []).flatMap((block, index) => block.type === "tool_use"
573
+ ? [
574
+ {
575
+ id: block.id,
576
+ type: "function",
577
+ function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) },
578
+ index
579
+ }
580
+ ]
581
+ : []);
582
+ return jsonResponse(chatCompletion(model, {
583
+ role: "assistant",
584
+ content: content.length > 0 ? content : null,
585
+ ...(reasoning.length > 0 ? { reasoning } : {}),
586
+ ...(reasoningDetails.length > 0
587
+ ? { reasoning_details: reasoningDetails }
588
+ : {}),
589
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {})
590
+ }, normalizedOpenAiUsage(payload.usage), openAiFinishReasonFromAnthropic(payload.stop_reason), {
591
+ ...(typeof payload.stop_reason === "string"
592
+ ? { anthropic_stop_reason: payload.stop_reason }
593
+ : {}),
594
+ ...(typeof payload.stop_sequence === "string"
595
+ ? { anthropic_stop_sequence: payload.stop_sequence }
596
+ : {})
597
+ }), 200, response.headers);
598
+ }
599
+ }
600
+ function googleRequest(body) {
601
+ const systemText = (body.messages ?? [])
602
+ .filter((message) => message.role === "system")
603
+ .map((message) => textContent(message.content))
604
+ .join("\n\n");
605
+ const toolNames = new Map();
606
+ for (const message of body.messages ?? []) {
607
+ for (const call of message.tool_calls ?? []) {
608
+ if (call.id !== undefined && call.function?.name !== undefined) {
609
+ toolNames.set(call.id, call.function.name);
610
+ }
611
+ }
612
+ }
613
+ const reasoning = reasoningSelectionOf(body);
614
+ const thinkingConfig = reasoning.mode === "effort"
615
+ ? { thinkingLevel: reasoning.effort }
616
+ : reasoning.mode === "budget"
617
+ ? { thinkingBudget: reasoning.budgetTokens }
618
+ : reasoning.mode === "adaptive"
619
+ ? { thinkingBudget: -1 }
620
+ : reasoning.mode === "disabled"
621
+ ? { thinkingBudget: 0 }
622
+ : undefined;
623
+ return {
624
+ contents: (body.messages ?? []).flatMap((message) => {
625
+ if (message.role === "system")
626
+ return [];
627
+ if (message.role === "tool") {
628
+ return [
629
+ {
630
+ role: "user",
631
+ parts: [
632
+ {
633
+ functionResponse: {
634
+ name: toolNames.get(message.tool_call_id ?? "") ?? "tool",
635
+ response: { output: textContent(message.content) }
636
+ }
637
+ }
638
+ ]
639
+ }
640
+ ];
641
+ }
642
+ const parts = [];
643
+ const text = textContent(message.content);
644
+ if (text.length > 0)
645
+ parts.push({ text });
646
+ for (const call of message.tool_calls ?? []) {
647
+ let args = {};
648
+ try {
649
+ args = JSON.parse(call.function?.arguments ?? "{}");
650
+ }
651
+ catch {
652
+ args = { raw: call.function?.arguments ?? "" };
653
+ }
654
+ parts.push({
655
+ functionCall: {
656
+ name: call.function?.name ?? "tool",
657
+ args
658
+ }
659
+ });
660
+ }
661
+ return [{ role: message.role === "assistant" ? "model" : "user", parts }];
662
+ }),
663
+ ...(systemText.length > 0
664
+ ? { systemInstruction: { role: "system", parts: [{ text: systemText }] } }
665
+ : {}),
666
+ generationConfig: {
667
+ ...(body.max_tokens !== undefined ? { maxOutputTokens: body.max_tokens } : {}),
668
+ ...(body.temperature !== undefined ? { temperature: body.temperature } : {}),
669
+ ...(thinkingConfig !== undefined ? { thinkingConfig } : {})
670
+ },
671
+ ...(body.tools !== undefined
672
+ ? {
673
+ tools: [
674
+ {
675
+ functionDeclarations: body.tools.flatMap((tool) => tool.function === undefined
676
+ ? []
677
+ : [
678
+ {
679
+ name: tool.function.name,
680
+ description: tool.function.description,
681
+ parameters: tool.function.parameters
682
+ }
683
+ ])
684
+ }
685
+ ]
686
+ }
687
+ : {})
688
+ };
689
+ }
690
+ function googleMessage(payload) {
691
+ const candidates = payload.candidates;
692
+ const content = candidates?.[0]?.content;
693
+ const parts = content?.parts;
694
+ const text = (parts ?? []).map((part) => (typeof part.text === "string" ? part.text : "")).join("");
695
+ const toolCalls = (parts ?? []).flatMap((part, index) => {
696
+ const call = part.functionCall;
697
+ return call === undefined
698
+ ? []
699
+ : [
700
+ {
701
+ id: randomId(12, "call_"),
702
+ type: "function",
703
+ index,
704
+ function: { name: call.name, arguments: JSON.stringify(call.args ?? {}) }
705
+ }
706
+ ];
707
+ });
708
+ return {
709
+ role: "assistant",
710
+ content: text,
711
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {})
712
+ };
713
+ }
714
+ export class GoogleGenAiBackend extends HttpProviderBackend {
715
+ chat(body, signal, options) {
716
+ return this.#chat(bodyRecord(body), signal, options);
717
+ }
718
+ async #chat(body, signal, options) {
719
+ const model = body.model ?? this.defaultModel ?? "";
720
+ const method = body.stream === true ? "streamGenerateContent" : "generateContent";
721
+ const response = await this.transport(`${joinPath(this.baseUrl, `/models/${encodeURIComponent(model)}:${method}`)}${body.stream === true ? "?alt=sse" : ""}`, {
722
+ method: "POST",
723
+ headers: {
724
+ "content-type": "application/json",
725
+ "x-goog-api-key": this.apiKey,
726
+ ...this.extraHeaders
727
+ },
728
+ body: JSON.stringify(googleRequest(body)),
729
+ ...(signal !== undefined ? { signal } : {})
730
+ }, options);
731
+ if (!response.ok)
732
+ return copyFailure(response, await response.text());
733
+ if (body.stream === true) {
734
+ return mapSse(response, (_event, data) => {
735
+ const payload = data;
736
+ const candidates = payload.candidates;
737
+ const finishReason = candidates?.[0]?.finishReason;
738
+ const usage = payload.usageMetadata;
739
+ return [
740
+ {
741
+ id: randomId(18, "chatcmpl_"),
742
+ object: "chat.completion.chunk",
743
+ model,
744
+ choices: [
745
+ {
746
+ index: 0,
747
+ delta: googleMessage(payload),
748
+ finish_reason: finishReason === undefined
749
+ ? null
750
+ : finishReason === "MAX_TOKENS"
751
+ ? "length"
752
+ : "stop"
753
+ }
754
+ ],
755
+ ...(usage !== undefined
756
+ ? {
757
+ usage: {
758
+ prompt_tokens: usage.promptTokenCount,
759
+ completion_tokens: usage.candidatesTokenCount,
760
+ total_tokens: usage.totalTokenCount
761
+ }
762
+ }
763
+ : {})
764
+ }
765
+ ];
766
+ });
767
+ }
768
+ const payload = (await response.json());
769
+ const usage = payload.usageMetadata;
770
+ return jsonResponse(chatCompletion(model, googleMessage(payload), {
771
+ prompt_tokens: usage?.promptTokenCount,
772
+ completion_tokens: usage?.candidatesTokenCount,
773
+ total_tokens: usage?.totalTokenCount
774
+ }));
775
+ }
776
+ }
777
+ function responsesRequest(body, model, options) {
778
+ const reasoning = reasoningSelectionOf(body);
779
+ const input = (body.messages ?? []).flatMap((message) => {
780
+ if (message.role === "tool") {
781
+ return [
782
+ {
783
+ type: "function_call_output",
784
+ call_id: message.tool_call_id ?? "",
785
+ output: textContent(message.content)
786
+ }
787
+ ];
788
+ }
789
+ const items = [];
790
+ const text = textContent(message.content);
791
+ if (text.length > 0) {
792
+ items.push({
793
+ role: message.role,
794
+ content: [
795
+ {
796
+ type: message.role === "assistant" ? "output_text" : "input_text",
797
+ text
798
+ }
799
+ ]
800
+ });
801
+ }
802
+ for (const call of message.tool_calls ?? []) {
803
+ items.push({
804
+ type: "function_call",
805
+ call_id: call.id ?? randomId(12, "call_"),
806
+ name: call.function?.name ?? "tool",
807
+ arguments: call.function?.arguments ?? "{}"
808
+ });
809
+ }
810
+ return items;
811
+ });
812
+ return {
813
+ model,
814
+ input,
815
+ stream: options.forceStream || body.stream === true,
816
+ store: false,
817
+ ...(reasoning.mode === "effort"
818
+ ? { reasoning: { effort: reasoning.effort } }
819
+ : {}),
820
+ ...(!options.omitSampling && body.max_tokens !== undefined
821
+ ? { max_output_tokens: body.max_tokens }
822
+ : {}),
823
+ ...(!options.omitSampling && body.temperature !== undefined
824
+ ? { temperature: body.temperature }
825
+ : {}),
826
+ ...(body.tool_choice !== undefined ? { tool_choice: body.tool_choice } : {}),
827
+ ...(body.tools !== undefined
828
+ ? {
829
+ tools: body.tools.flatMap((tool) => tool.function === undefined
830
+ ? []
831
+ : [
832
+ {
833
+ type: "function",
834
+ name: tool.function.name,
835
+ description: tool.function.description,
836
+ parameters: tool.function.parameters ?? { type: "object" }
837
+ }
838
+ ])
839
+ }
840
+ : {})
841
+ };
842
+ }
843
+ function responsesOutput(payload) {
844
+ const output = payload.output;
845
+ const reasoning = (output ?? [])
846
+ .filter((item) => item.type === "reasoning")
847
+ .flatMap((item) => {
848
+ const summary = item.summary;
849
+ return (summary ?? []).flatMap((part) => typeof part.text === "string" ? [part.text] : []);
850
+ })
851
+ .join("");
852
+ const text = (output ?? []).flatMap((item) => {
853
+ const content = item.content;
854
+ return (content ?? []).flatMap((part) => typeof part.text === "string" ? [part.text] : []);
855
+ }).join("");
856
+ const toolCalls = (output ?? []).flatMap((item, index) => item.type === "function_call"
857
+ ? [
858
+ {
859
+ id: item.call_id ?? item.id,
860
+ type: "function",
861
+ index,
862
+ function: { name: item.name, arguments: item.arguments ?? "{}" }
863
+ }
864
+ ]
865
+ : []);
866
+ return {
867
+ role: "assistant",
868
+ content: text,
869
+ ...(reasoning.length > 0 ? { reasoning } : {}),
870
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {})
871
+ };
872
+ }
873
+ export class CodexResponsesBackend extends HttpProviderBackend {
874
+ #accountId;
875
+ #forceStream;
876
+ #omitSampling;
877
+ constructor(options) {
878
+ super(options);
879
+ this.#accountId = options.accountId;
880
+ this.#forceStream = options.forceStream ?? false;
881
+ this.#omitSampling = options.omitSampling ?? false;
882
+ }
883
+ chat(body, signal, options) {
884
+ return this.#chat(bodyRecord(body), signal, options);
885
+ }
886
+ async #chat(body, signal, options) {
887
+ const model = body.model ?? this.defaultModel ?? "";
888
+ const reasoning = reasoningSelectionOf(body);
889
+ if (reasoning.mode === "budget" || reasoning.mode === "adaptive") {
890
+ return jsonResponse({
891
+ error: {
892
+ type: "invalid_request_error",
893
+ message: `Codex Responses cannot represent reasoning mode "${reasoning.mode}"`
894
+ }
895
+ }, 400);
896
+ }
897
+ const response = await this.transport(joinPath(this.baseUrl, "/responses"), {
898
+ method: "POST",
899
+ headers: {
900
+ "content-type": "application/json",
901
+ authorization: `Bearer ${this.apiKey}`,
902
+ ...(this.#accountId !== undefined ? { "chatgpt-account-id": this.#accountId } : {}),
903
+ ...this.extraHeaders
904
+ },
905
+ body: JSON.stringify(responsesRequest(body, model, {
906
+ forceStream: this.#forceStream,
907
+ omitSampling: this.#omitSampling
908
+ })),
909
+ ...(signal !== undefined ? { signal } : {})
910
+ }, options);
911
+ if (!response.ok)
912
+ return copyFailure(response, await response.text());
913
+ if (body.stream === true) {
914
+ let hasToolCalls = false;
915
+ return mapSse(response, (event, data) => {
916
+ const item = data;
917
+ if (event === "response.reasoning_summary_text.delta" ||
918
+ event === "response.reasoning_text.delta") {
919
+ return [
920
+ {
921
+ id: randomId(18, "chatcmpl_"),
922
+ object: "chat.completion.chunk",
923
+ model,
924
+ choices: [
925
+ {
926
+ index: 0,
927
+ delta: { reasoning: item.delta },
928
+ finish_reason: null
929
+ }
930
+ ]
931
+ }
932
+ ];
933
+ }
934
+ if (event === "response.output_text.delta") {
935
+ return [
936
+ {
937
+ id: randomId(18, "chatcmpl_"),
938
+ object: "chat.completion.chunk",
939
+ model,
940
+ choices: [{ index: 0, delta: { content: item.delta }, finish_reason: null }]
941
+ }
942
+ ];
943
+ }
944
+ if (event === "response.function_call_arguments.delta") {
945
+ return [
946
+ {
947
+ id: randomId(18, "chatcmpl_"),
948
+ object: "chat.completion.chunk",
949
+ model,
950
+ choices: [
951
+ {
952
+ index: 0,
953
+ delta: {
954
+ tool_calls: [
955
+ {
956
+ index: item.output_index ?? 0,
957
+ function: { arguments: item.delta }
958
+ }
959
+ ]
960
+ },
961
+ finish_reason: null
962
+ }
963
+ ]
964
+ }
965
+ ];
966
+ }
967
+ if (event === "response.output_item.added") {
968
+ const output = item.item;
969
+ if (output?.type !== "function_call")
970
+ return [];
971
+ hasToolCalls = true;
972
+ return [
973
+ {
974
+ id: randomId(18, "chatcmpl_"),
975
+ object: "chat.completion.chunk",
976
+ model,
977
+ choices: [
978
+ {
979
+ index: 0,
980
+ delta: {
981
+ tool_calls: [
982
+ {
983
+ index: item.output_index ?? 0,
984
+ id: output.call_id ?? output.id,
985
+ type: "function",
986
+ function: { name: output.name, arguments: "" }
987
+ }
988
+ ]
989
+ },
990
+ finish_reason: null
991
+ }
992
+ ]
993
+ }
994
+ ];
995
+ }
996
+ if (event === "response.completed") {
997
+ const completed = item.response;
998
+ return [
999
+ {
1000
+ id: randomId(18, "chatcmpl_"),
1001
+ object: "chat.completion.chunk",
1002
+ model,
1003
+ choices: [
1004
+ {
1005
+ index: 0,
1006
+ delta: {},
1007
+ finish_reason: hasToolCalls ? "tool_calls" : "stop"
1008
+ }
1009
+ ],
1010
+ ...(completed?.usage !== undefined
1011
+ ? { usage: normalizedOpenAiUsage(completed.usage) }
1012
+ : {})
1013
+ }
1014
+ ];
1015
+ }
1016
+ return [];
1017
+ });
1018
+ }
1019
+ if (this.#forceStream) {
1020
+ const decoder = new SseDecoder();
1021
+ const events = [
1022
+ ...decoder.feed(await response.text()),
1023
+ ...decoder.flush()
1024
+ ];
1025
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1026
+ const event = events[index];
1027
+ if (event?.event !== "response.completed")
1028
+ continue;
1029
+ let completed;
1030
+ try {
1031
+ completed = JSON.parse(event.data);
1032
+ }
1033
+ catch {
1034
+ throw new SseParseError("provider SSE event contained malformed JSON", event.data.slice(0, 200));
1035
+ }
1036
+ if (typeof completed === "object" &&
1037
+ completed !== null &&
1038
+ typeof completed.response === "object" &&
1039
+ completed.response !== null) {
1040
+ const payload = completed
1041
+ .response;
1042
+ return jsonResponse(chatCompletion(model, responsesOutput(payload), normalizedOpenAiUsage(payload.usage)));
1043
+ }
1044
+ }
1045
+ throw new SseParseError("provider SSE stream ended without response.completed");
1046
+ }
1047
+ const payload = (await response.json());
1048
+ return jsonResponse(chatCompletion(model, responsesOutput(payload), normalizedOpenAiUsage(payload.usage)));
1049
+ }
1050
+ }