@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,699 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { AnthropicBackend, CodexResponsesBackend, GoogleGenAiBackend } from "../provider-backends.js";
4
+ import { anthropicToChat } from "../adapters/anthropic.js";
5
+ import { attachReasoningSelection } from "../adapters/openai-chat-wire.js";
6
+ import { SseParseError } from "../sse/parse.js";
7
+ function sse(events, includeDone = false) {
8
+ const body = events
9
+ .map(({ event, data }) => `${event === undefined ? "" : `event: ${event}\n`}data: ${JSON.stringify(data)}\n\n`)
10
+ .join("") + (includeDone ? "data: [DONE]\n\n" : "");
11
+ return new Response(body, {
12
+ headers: { "content-type": "text/event-stream" }
13
+ });
14
+ }
15
+ test("Anthropic egress preserves tools and normalizes the response", async () => {
16
+ const original = globalThis.fetch;
17
+ let request;
18
+ globalThis.fetch = async (input, init) => {
19
+ request = new Request(input, init);
20
+ return Response.json({
21
+ id: "msg_1",
22
+ content: [
23
+ { type: "text", text: "working" },
24
+ { type: "tool_use", id: "tool_1", name: "read", input: { path: "a.ts" } }
25
+ ],
26
+ usage: { input_tokens: 4, output_tokens: 2 }
27
+ });
28
+ };
29
+ try {
30
+ const backend = new AnthropicBackend({
31
+ baseUrl: "https://api.anthropic.test/v1",
32
+ apiKey: "secret",
33
+ defaultModel: "claude-test"
34
+ });
35
+ const response = await backend.chat({
36
+ messages: [{ role: "user", content: "inspect" }],
37
+ tools: [
38
+ {
39
+ type: "function",
40
+ function: {
41
+ name: "read",
42
+ description: "read a file",
43
+ parameters: { type: "object" }
44
+ }
45
+ }
46
+ ]
47
+ });
48
+ assert.equal(request?.url, "https://api.anthropic.test/v1/messages");
49
+ assert.equal(request?.headers.get("x-api-key"), "secret");
50
+ const outbound = (await request?.json());
51
+ assert.equal(outbound.tools[0]?.name, "read");
52
+ const body = (await response.json());
53
+ assert.equal(body.choices[0]?.message.tool_calls[0]?.function.name, "read");
54
+ assert.equal(body.usage.input_tokens, 4);
55
+ }
56
+ finally {
57
+ globalThis.fetch = original;
58
+ }
59
+ });
60
+ test("Anthropic egress preserves native thinking controls, signed history, and buffered blocks", async () => {
61
+ const original = globalThis.fetch;
62
+ let request;
63
+ globalThis.fetch = async (input, init) => {
64
+ request = new Request(input, init);
65
+ return Response.json({
66
+ id: "msg_think",
67
+ content: [
68
+ {
69
+ type: "thinking",
70
+ thinking: "native thought",
71
+ signature: "sig-response"
72
+ },
73
+ { type: "redacted_thinking", data: "opaque-redaction" },
74
+ {
75
+ type: "tool_use",
76
+ id: "tool_2",
77
+ name: "read",
78
+ input: { path: "b.ts" }
79
+ }
80
+ ],
81
+ stop_reason: "tool_use",
82
+ usage: { input_tokens: 8, output_tokens: 5 }
83
+ });
84
+ };
85
+ try {
86
+ const backend = new AnthropicBackend({
87
+ baseUrl: "https://api.anthropic.test/v1",
88
+ apiKey: "secret",
89
+ defaultModel: "claude-test"
90
+ });
91
+ const chat = anthropicToChat({
92
+ model: "claude-test",
93
+ max_tokens: 4096,
94
+ thinking: {
95
+ type: "enabled",
96
+ budget_tokens: 2048,
97
+ display: "summarized"
98
+ },
99
+ output_config: { effort: "high" },
100
+ messages: [
101
+ { role: "user", content: "inspect" },
102
+ {
103
+ role: "assistant",
104
+ content: [
105
+ {
106
+ type: "thinking",
107
+ thinking: "prior thought",
108
+ signature: "sig-prior"
109
+ },
110
+ { type: "redacted_thinking", data: "prior-redaction" },
111
+ {
112
+ type: "tool_use",
113
+ id: "tool_1",
114
+ name: "read",
115
+ input: { path: "a.ts" }
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ role: "user",
121
+ content: [
122
+ {
123
+ type: "tool_result",
124
+ tool_use_id: "tool_1",
125
+ content: "source"
126
+ }
127
+ ]
128
+ }
129
+ ],
130
+ tools: [
131
+ {
132
+ name: "read",
133
+ input_schema: { type: "object" }
134
+ }
135
+ ],
136
+ tool_choice: { type: "tool", name: "read", disable_parallel_tool_use: true }
137
+ }, "claude-test");
138
+ const response = await backend.chat(chat);
139
+ const outbound = (await request?.json());
140
+ assert.equal(outbound.max_tokens, 4096);
141
+ assert.deepEqual(outbound.thinking, {
142
+ type: "enabled",
143
+ budget_tokens: 2048,
144
+ display: "summarized"
145
+ });
146
+ assert.deepEqual(outbound.output_config, { effort: "high" });
147
+ assert.deepEqual(outbound.tool_choice, {
148
+ type: "tool",
149
+ name: "read",
150
+ disable_parallel_tool_use: true
151
+ });
152
+ assert.deepEqual(outbound.messages[1]?.content.map((block) => block.type), ["thinking", "redacted_thinking", "tool_use"]);
153
+ assert.equal(outbound.messages[1]?.content[0]?.signature, "sig-prior");
154
+ const normalized = (await response.json());
155
+ assert.equal(normalized.choices[0]?.finish_reason, "tool_calls");
156
+ assert.equal(normalized.choices[0]?.message.content, null);
157
+ assert.equal(normalized.choices[0]?.message.reasoning, "native thought");
158
+ assert.deepEqual(normalized.choices[0]?.message.reasoning_details.map((detail) => detail.type), ["thinking", "redacted_thinking"]);
159
+ assert.equal(normalized.choices[0]?.message.reasoning_details[0]?.signature, "sig-response");
160
+ }
161
+ finally {
162
+ globalThis.fetch = original;
163
+ }
164
+ });
165
+ test("Anthropic egress preserves opaque effort and rejects impossible explicit budgets", async () => {
166
+ const requests = [];
167
+ const backend = new AnthropicBackend({
168
+ baseUrl: "https://api.anthropic.test/v1",
169
+ apiKey: "secret",
170
+ defaultModel: "claude-test",
171
+ transport: async (input, init) => {
172
+ requests.push(new Request(input, init));
173
+ return Response.json({
174
+ content: [{ type: "text", text: "ok" }],
175
+ stop_reason: "end_turn"
176
+ });
177
+ }
178
+ });
179
+ const valid = await backend.chat({
180
+ max_completion_tokens: 5000,
181
+ reasoning_effort: "ultra",
182
+ messages: [{ role: "user", content: "think" }]
183
+ });
184
+ assert.equal(valid.status, 200);
185
+ const outbound = (await requests[0]?.json());
186
+ assert.equal(outbound.max_tokens, 5000);
187
+ assert.deepEqual(outbound.thinking, { type: "adaptive" });
188
+ assert.deepEqual(outbound.output_config, { effort: "ultra" });
189
+ const invalidBody = {
190
+ max_completion_tokens: 1024,
191
+ messages: [{ role: "user", content: "think" }]
192
+ };
193
+ attachReasoningSelection(invalidBody, {
194
+ mode: "budget",
195
+ budgetTokens: 1024
196
+ });
197
+ const invalid = await backend.chat(invalidBody);
198
+ assert.equal(invalid.status, 400);
199
+ assert.equal(requests.length, 1, "invalid thinking must fail before transport");
200
+ assert.match(await invalid.text(), /less than max_tokens/);
201
+ });
202
+ test("Anthropic egress preserves native stop reasons and stop sequences", async () => {
203
+ const backend = new AnthropicBackend({
204
+ baseUrl: "https://api.anthropic.test/v1",
205
+ apiKey: "secret",
206
+ defaultModel: "claude-test",
207
+ transport: async () => Response.json({
208
+ content: [{ type: "text", text: "bounded" }],
209
+ stop_reason: "stop_sequence",
210
+ stop_sequence: "<END>"
211
+ })
212
+ });
213
+ const response = await backend.chat({
214
+ messages: [{ role: "user", content: "bounded answer" }]
215
+ });
216
+ const canonical = (await response.json());
217
+ assert.equal(canonical.choices[0]?.finish_reason, "stop");
218
+ assert.equal(canonical.choices[0]?.anthropic_stop_reason, "stop_sequence");
219
+ assert.equal(canonical.choices[0]?.anthropic_stop_sequence, "<END>");
220
+ });
221
+ test("Anthropic egress replays signed canonical reasoning_details from OpenAI clients", async () => {
222
+ let request;
223
+ const backend = new AnthropicBackend({
224
+ baseUrl: "https://api.anthropic.test/v1",
225
+ apiKey: "secret",
226
+ defaultModel: "claude-test",
227
+ transport: async (input, init) => {
228
+ request = new Request(input, init);
229
+ return Response.json({
230
+ content: [{ type: "text", text: "done" }],
231
+ stop_reason: "end_turn"
232
+ });
233
+ }
234
+ });
235
+ await backend.chat({
236
+ messages: [
237
+ {
238
+ role: "assistant",
239
+ content: null,
240
+ reasoning: "prior",
241
+ reasoning_details: [
242
+ {
243
+ type: "thinking",
244
+ index: 0,
245
+ thinking: "prior",
246
+ signature: "sig-canonical"
247
+ }
248
+ ],
249
+ tool_calls: [
250
+ {
251
+ id: "tool_1",
252
+ function: { name: "read", arguments: '{"path":"a.ts"}' }
253
+ }
254
+ ]
255
+ },
256
+ { role: "tool", tool_call_id: "tool_1", content: "source" }
257
+ ]
258
+ });
259
+ const outbound = (await request?.json());
260
+ assert.deepEqual(outbound.messages[0]?.content.map((block) => block.type), ["thinking", "tool_use"]);
261
+ assert.equal(outbound.messages[0]?.content[0]?.signature, "sig-canonical");
262
+ });
263
+ test("Google GenAI egress maps content, usage, and API-key auth", async () => {
264
+ const original = globalThis.fetch;
265
+ let request;
266
+ globalThis.fetch = async (input, init) => {
267
+ request = new Request(input, init);
268
+ return Response.json({
269
+ candidates: [{ content: { parts: [{ text: "answer" }] } }],
270
+ usageMetadata: {
271
+ promptTokenCount: 3,
272
+ candidatesTokenCount: 1,
273
+ totalTokenCount: 4
274
+ }
275
+ });
276
+ };
277
+ try {
278
+ const backend = new GoogleGenAiBackend({
279
+ baseUrl: "https://generativelanguage.test/v1beta",
280
+ apiKey: "google-secret",
281
+ defaultModel: "gemini-test"
282
+ });
283
+ const response = await backend.chat({
284
+ reasoning_effort: "deliberate",
285
+ messages: [{ role: "user", content: "hello" }]
286
+ });
287
+ assert.match(request?.url ?? "", /models\/gemini-test:generateContent$/);
288
+ assert.equal(request?.headers.get("x-goog-api-key"), "google-secret");
289
+ const outbound = (await request?.json());
290
+ assert.equal(outbound.generationConfig.thinkingConfig.thinkingLevel, "deliberate");
291
+ const body = (await response.json());
292
+ assert.equal(body.choices[0]?.message.content, "answer");
293
+ assert.equal(body.usage.total_tokens, 4);
294
+ }
295
+ finally {
296
+ globalThis.fetch = original;
297
+ }
298
+ });
299
+ test("Codex Responses egress preserves subscription auth and tool output", async () => {
300
+ const original = globalThis.fetch;
301
+ let request;
302
+ globalThis.fetch = async (input, init) => {
303
+ request = new Request(input, init);
304
+ return Response.json({
305
+ id: "resp_1",
306
+ output: [
307
+ {
308
+ type: "reasoning",
309
+ summary: [{ type: "summary_text", text: "considering the fix" }]
310
+ },
311
+ { type: "message", content: [{ type: "output_text", text: "done" }] },
312
+ {
313
+ type: "function_call",
314
+ call_id: "call_1",
315
+ name: "apply",
316
+ arguments: "{\"patch\":\"x\"}"
317
+ }
318
+ ],
319
+ usage: { input_tokens: 5, output_tokens: 2, total_tokens: 7 }
320
+ });
321
+ };
322
+ try {
323
+ const backend = new CodexResponsesBackend({
324
+ baseUrl: "https://chatgpt.test/backend-api/codex",
325
+ apiKey: "oauth",
326
+ accountId: "account",
327
+ defaultModel: "codex-test"
328
+ });
329
+ const response = await backend.chat({
330
+ reasoning_effort: "deep",
331
+ messages: [{ role: "user", content: "fix it" }],
332
+ tools: [{ type: "function", function: { name: "apply" } }]
333
+ });
334
+ assert.equal(request?.url, "https://chatgpt.test/backend-api/codex/responses");
335
+ assert.equal(request?.headers.get("authorization"), "Bearer oauth");
336
+ assert.equal(request?.headers.get("chatgpt-account-id"), "account");
337
+ const upstreamBody = (await request?.json());
338
+ assert.equal(upstreamBody?.store, false);
339
+ assert.deepEqual(upstreamBody?.reasoning, { effort: "deep" });
340
+ const body = (await response.json());
341
+ assert.equal(body.choices[0]?.message.content, "done");
342
+ assert.equal(body.choices[0]?.message.reasoning, "considering the fix");
343
+ assert.equal(body.choices[0]?.message.tool_calls.length, 1);
344
+ }
345
+ finally {
346
+ globalThis.fetch = original;
347
+ }
348
+ });
349
+ test("Codex subscription egress forces SSE and omits unsupported sampling", async () => {
350
+ const original = globalThis.fetch;
351
+ let request;
352
+ globalThis.fetch = async (input, init) => {
353
+ request = new Request(input, init);
354
+ return sse([
355
+ {
356
+ event: "response.completed",
357
+ data: {
358
+ response: {
359
+ output: [
360
+ {
361
+ type: "message",
362
+ content: [{ type: "output_text", text: "done" }]
363
+ }
364
+ ],
365
+ usage: { input_tokens: 3, output_tokens: 1, total_tokens: 4 }
366
+ }
367
+ }
368
+ }
369
+ ]);
370
+ };
371
+ try {
372
+ const backend = new CodexResponsesBackend({
373
+ baseUrl: "https://chatgpt.test/backend-api/codex",
374
+ apiKey: "oauth",
375
+ defaultModel: "codex-test",
376
+ forceStream: true,
377
+ omitSampling: true
378
+ });
379
+ const response = await backend.chat({
380
+ stream: false,
381
+ max_tokens: 16,
382
+ temperature: 0,
383
+ messages: [{ role: "user", content: "reply" }]
384
+ });
385
+ const outbound = (await request?.json());
386
+ assert.equal(outbound.stream, true);
387
+ assert.equal("max_output_tokens" in outbound, false);
388
+ assert.equal("temperature" in outbound, false);
389
+ const body = (await response.json());
390
+ assert.equal(body.choices[0]?.message.content, "done");
391
+ }
392
+ finally {
393
+ globalThis.fetch = original;
394
+ }
395
+ });
396
+ test("Anthropic streaming egress preserves tool calls and terminal usage", async () => {
397
+ const original = globalThis.fetch;
398
+ globalThis.fetch = async () => sse([
399
+ {
400
+ event: "message_start",
401
+ data: {
402
+ message: { usage: { input_tokens: 4 } }
403
+ }
404
+ },
405
+ {
406
+ event: "content_block_start",
407
+ data: {
408
+ index: 0,
409
+ content_block: { type: "tool_use", id: "tool_1", name: "read", input: {} }
410
+ }
411
+ },
412
+ {
413
+ event: "content_block_delta",
414
+ data: { index: 0, delta: { type: "input_json_delta", partial_json: "{\"path\":\"a.ts\"}" } }
415
+ },
416
+ {
417
+ event: "message_delta",
418
+ data: {
419
+ delta: { stop_reason: "tool_use" },
420
+ usage: { output_tokens: 2 }
421
+ }
422
+ }
423
+ ], true);
424
+ try {
425
+ const backend = new AnthropicBackend({
426
+ baseUrl: "https://api.anthropic.test/v1",
427
+ apiKey: "secret",
428
+ defaultModel: "claude-test"
429
+ });
430
+ const response = await backend.chat({
431
+ stream: true,
432
+ messages: [{ role: "user", content: "inspect" }],
433
+ tools: [{ type: "function", function: { name: "read", parameters: { type: "object" } } }]
434
+ });
435
+ const text = await response.text();
436
+ assert.match(text, /"name":"read"/);
437
+ assert.match(text, /\\"path\\":\\"a\.ts\\"/);
438
+ assert.match(text, /"finish_reason":"tool_calls"/);
439
+ assert.match(text, /"input_tokens":4/);
440
+ assert.match(text, /"output_tokens":2/);
441
+ assert.match(text, /data: \[DONE\]/);
442
+ assert.equal(text.match(/data: \[DONE\]/g)?.length, 1);
443
+ }
444
+ finally {
445
+ globalThis.fetch = original;
446
+ }
447
+ });
448
+ test("Anthropic streaming egress preserves thinking lifecycle, signatures, and redactions", async () => {
449
+ const original = globalThis.fetch;
450
+ globalThis.fetch = async () => sse([
451
+ {
452
+ event: "content_block_start",
453
+ data: {
454
+ index: 0,
455
+ content_block: { type: "thinking", thinking: "", signature: "" }
456
+ }
457
+ },
458
+ {
459
+ event: "content_block_delta",
460
+ data: {
461
+ index: 0,
462
+ delta: { type: "thinking_delta", thinking: "native thought" }
463
+ }
464
+ },
465
+ {
466
+ event: "content_block_delta",
467
+ data: {
468
+ index: 0,
469
+ delta: { type: "signature_delta", signature: "sig-stream" }
470
+ }
471
+ },
472
+ { event: "content_block_stop", data: { index: 0 } },
473
+ {
474
+ event: "content_block_start",
475
+ data: {
476
+ index: 1,
477
+ content_block: {
478
+ type: "redacted_thinking",
479
+ data: "opaque-stream"
480
+ }
481
+ }
482
+ },
483
+ { event: "content_block_stop", data: { index: 1 } },
484
+ {
485
+ event: "content_block_start",
486
+ data: { index: 2, content_block: { type: "text", text: "" } }
487
+ },
488
+ {
489
+ event: "content_block_delta",
490
+ data: { index: 2, delta: { type: "text_delta", text: "answer" } }
491
+ },
492
+ { event: "content_block_stop", data: { index: 2 } },
493
+ {
494
+ event: "message_delta",
495
+ data: {
496
+ delta: { stop_reason: "end_turn" },
497
+ usage: { input_tokens: 4, output_tokens: 5 }
498
+ }
499
+ }
500
+ ], true);
501
+ try {
502
+ const backend = new AnthropicBackend({
503
+ baseUrl: "https://api.anthropic.test/v1",
504
+ apiKey: "secret",
505
+ defaultModel: "claude-test"
506
+ });
507
+ const response = await backend.chat({
508
+ stream: true,
509
+ messages: [{ role: "user", content: "think" }]
510
+ });
511
+ const text = await response.text();
512
+ assert.match(text, /"reasoning":"native thought"/);
513
+ assert.match(text, /"phase":"start"/);
514
+ assert.match(text, /"phase":"signature","signature":"sig-stream"/);
515
+ assert.match(text, /"phase":"stop"/);
516
+ assert.match(text, /"type":"redacted_thinking"/);
517
+ assert.match(text, /"data":"opaque-stream"/);
518
+ assert.match(text, /"content":"answer"/);
519
+ assert.match(text, /"finish_reason":"stop"/);
520
+ }
521
+ finally {
522
+ globalThis.fetch = original;
523
+ }
524
+ });
525
+ test("Google streaming egress preserves function history, tools, and usage", async () => {
526
+ const original = globalThis.fetch;
527
+ let request;
528
+ globalThis.fetch = async (input, init) => {
529
+ request = new Request(input, init);
530
+ return sse([
531
+ {
532
+ data: {
533
+ candidates: [
534
+ {
535
+ content: {
536
+ parts: [{ functionCall: { name: "search", args: { query: "routekit" } } }]
537
+ },
538
+ finishReason: "STOP"
539
+ }
540
+ ],
541
+ usageMetadata: {
542
+ promptTokenCount: 5,
543
+ candidatesTokenCount: 1,
544
+ totalTokenCount: 6
545
+ }
546
+ }
547
+ }
548
+ ]);
549
+ };
550
+ try {
551
+ const backend = new GoogleGenAiBackend({
552
+ baseUrl: "https://generativelanguage.test/v1beta",
553
+ apiKey: "google-secret",
554
+ defaultModel: "gemini-test"
555
+ });
556
+ const response = await backend.chat({
557
+ stream: true,
558
+ messages: [
559
+ {
560
+ role: "assistant",
561
+ tool_calls: [
562
+ {
563
+ id: "call_1",
564
+ function: { name: "search", arguments: "{\"query\":\"first\"}" }
565
+ }
566
+ ]
567
+ },
568
+ { role: "tool", tool_call_id: "call_1", content: "first result" }
569
+ ],
570
+ tools: [
571
+ {
572
+ type: "function",
573
+ function: { name: "search", parameters: { type: "object" } }
574
+ }
575
+ ]
576
+ });
577
+ assert.match(request?.url ?? "", /models\/gemini-test:streamGenerateContent\?alt=sse$/);
578
+ const outbound = (await request?.json());
579
+ assert.equal(outbound.tools[0]?.functionDeclarations[0]?.name, "search");
580
+ assert.ok(outbound.contents.some((content) => content.parts.some((part) => "functionCall" in part)));
581
+ assert.ok(outbound.contents.some((content) => content.parts.some((part) => "functionResponse" in part)));
582
+ const text = await response.text();
583
+ assert.match(text, /"name":"search"/);
584
+ assert.match(text, /"finish_reason":"stop"/);
585
+ assert.match(text, /"total_tokens":6/);
586
+ }
587
+ finally {
588
+ globalThis.fetch = original;
589
+ }
590
+ });
591
+ test("Codex streaming egress preserves Responses tool history and deltas", async () => {
592
+ const original = globalThis.fetch;
593
+ let request;
594
+ globalThis.fetch = async (input, init) => {
595
+ request = new Request(input, init);
596
+ return sse([
597
+ {
598
+ event: "response.reasoning_summary_text.delta",
599
+ data: { delta: "considering the patch" }
600
+ },
601
+ {
602
+ event: "response.output_item.added",
603
+ data: {
604
+ output_index: 0,
605
+ item: {
606
+ type: "function_call",
607
+ id: "item_1",
608
+ call_id: "call_2",
609
+ name: "apply"
610
+ }
611
+ }
612
+ },
613
+ {
614
+ event: "response.function_call_arguments.delta",
615
+ data: { output_index: 0, delta: "{\"patch\":\"x\"}" }
616
+ },
617
+ {
618
+ event: "response.completed",
619
+ data: {
620
+ response: {
621
+ usage: { input_tokens: 7, output_tokens: 2, total_tokens: 9 }
622
+ }
623
+ }
624
+ }
625
+ ]);
626
+ };
627
+ try {
628
+ const backend = new CodexResponsesBackend({
629
+ baseUrl: "https://chatgpt.test/backend-api/codex",
630
+ apiKey: "oauth",
631
+ accountId: "account",
632
+ defaultModel: "codex-test"
633
+ });
634
+ const response = await backend.chat({
635
+ stream: true,
636
+ messages: [
637
+ {
638
+ role: "assistant",
639
+ tool_calls: [
640
+ {
641
+ id: "call_1",
642
+ function: { name: "read", arguments: "{\"path\":\"a.ts\"}" }
643
+ }
644
+ ]
645
+ },
646
+ { role: "tool", tool_call_id: "call_1", content: "source" }
647
+ ],
648
+ tools: [
649
+ {
650
+ type: "function",
651
+ function: { name: "apply", parameters: { type: "object" } }
652
+ }
653
+ ]
654
+ });
655
+ const outbound = (await request?.json());
656
+ assert.ok(outbound.input.some((item) => item.type === "function_call"));
657
+ assert.ok(outbound.input.some((item) => item.type === "function_call_output"));
658
+ assert.deepEqual(outbound.tools[0], {
659
+ type: "function",
660
+ name: "apply",
661
+ parameters: { type: "object" }
662
+ });
663
+ const text = await response.text();
664
+ assert.match(text, /"reasoning":"considering the patch"/);
665
+ assert.match(text, /"name":"apply"/);
666
+ assert.match(text, /\\"patch\\":\\"x\\"/);
667
+ assert.match(text, /"finish_reason":"tool_calls"/);
668
+ assert.match(text, /"total_tokens":9/);
669
+ }
670
+ finally {
671
+ globalThis.fetch = original;
672
+ }
673
+ });
674
+ test("provider streaming surfaces malformed and truncated SSE", async () => {
675
+ const original = globalThis.fetch;
676
+ const backend = new CodexResponsesBackend({
677
+ baseUrl: "https://chatgpt.test/backend-api/codex",
678
+ apiKey: "oauth",
679
+ defaultModel: "codex-test"
680
+ });
681
+ try {
682
+ for (const body of [
683
+ "event: response.output_text.delta\ndata: {malformed}\n\n",
684
+ 'event: response.output_text.delta\ndata: {"delta":"partial"}'
685
+ ]) {
686
+ globalThis.fetch = async () => new Response(body, {
687
+ headers: { "content-type": "text/event-stream" }
688
+ });
689
+ const response = await backend.chat({
690
+ stream: true,
691
+ messages: [{ role: "user", content: "hello" }]
692
+ });
693
+ await assert.rejects(response.text(), SseParseError);
694
+ }
695
+ }
696
+ finally {
697
+ globalThis.fetch = original;
698
+ }
699
+ });
@@ -0,0 +1 @@
1
+ export {};