@neutrome/open-ai-router 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.dev.vars.example +2 -0
  2. package/README.md +5 -1
  3. package/package.json +5 -4
  4. package/src/admission/access.ts +0 -1
  5. package/src/admission/types.ts +0 -1
  6. package/src/app/factory.test.ts +40 -34
  7. package/src/app/factory.ts +120 -173
  8. package/src/app/google-genai-route.ts +48 -0
  9. package/src/app/http-utils.ts +14 -0
  10. package/src/app/model-listing.ts +29 -0
  11. package/src/app/response-lifecycle.ts +66 -0
  12. package/src/app/types.ts +38 -0
  13. package/src/index.ts +11 -43
  14. package/src/otlp.ts +155 -0
  15. package/src/router/audit.ts +5 -12
  16. package/src/router/config.ts +4 -4
  17. package/src/router/error-codec.ts +170 -0
  18. package/src/router/errors.ts +38 -1
  19. package/src/router/execute.test.ts +177 -107
  20. package/src/router/execute.ts +68 -924
  21. package/src/router/execution-events.ts +39 -0
  22. package/src/router/execution-invocation.ts +144 -0
  23. package/src/router/execution-observation.ts +161 -0
  24. package/src/router/execution-resolution.ts +120 -0
  25. package/src/router/execution-runtime.test.ts +156 -83
  26. package/src/router/execution-runtime.ts +144 -542
  27. package/src/router/execution-transforms.ts +82 -0
  28. package/src/router/execution-types.ts +8 -19
  29. package/src/router/index.ts +6 -5
  30. package/src/router/mcp.ts +12 -6
  31. package/src/router/provider-invoker.ts +134 -0
  32. package/src/router/provider-stream.ts +192 -0
  33. package/src/router/provider-telemetry.ts +59 -0
  34. package/src/router/remote-mcp-execution.ts +61 -0
  35. package/src/router/request-program.ts +16 -0
  36. package/src/router/request-target.ts +56 -0
  37. package/src/router/resolve.test.ts +43 -10
  38. package/src/router/resolve.ts +50 -28
  39. package/src/router/response-delivery.ts +141 -0
  40. package/src/router/runtime.test.ts +56 -0
  41. package/src/router/runtime.ts +79 -9
  42. package/src/router/targets.ts +3 -1
  43. package/src/router/upstream-client.ts +137 -0
  44. package/src/telemetry.test.ts +78 -0
  45. package/src/telemetry.ts +316 -0
  46. package/src/trace-labels.ts +29 -0
  47. package/src/upstream-auth/env.ts +15 -3
  48. package/src/upstream-auth/proxy.ts +6 -1
  49. package/src/upstream-auth/registry.ts +3 -1
  50. package/src/util/glob.ts +1 -1
  51. package/src/util/model-syntax.ts +3 -3
  52. package/src/worker.ts +12 -6
  53. package/worker-configuration.d.ts +195 -164
  54. package/pnpm-workspace.yaml +0 -4
  55. package/src/admission/jwt.test.ts +0 -96
  56. package/src/admission/jwt.ts +0 -221
  57. package/src/app/handlers.ts +0 -198
  58. package/src/example.test.ts +0 -377
  59. package/src/example.ts +0 -73
  60. package/src/observer.test.ts +0 -54
  61. package/src/observer.ts +0 -315
  62. package/src/timing.ts +0 -36
@@ -18,7 +18,7 @@ describe("router execution", () => {
18
18
  config: {
19
19
  providers: {
20
20
  openrouter: {
21
- api_base_url: "https://openrouter.ai/api/v1",
21
+ apiBaseUrl: "https://openrouter.ai/api/v1",
22
22
  style: "chat-completions",
23
23
  exports: ["*"],
24
24
  },
@@ -35,7 +35,9 @@ describe("router execution", () => {
35
35
  fetchImpl: vi.fn(async (input, init) => {
36
36
  capturedUrl = String(input);
37
37
  capturedAuth = new Headers(init?.headers).get("authorization") ?? "";
38
- capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
38
+ capturedBody = JSON.parse(
39
+ decoder.decode(new Uint8Array(init?.body as ArrayBuffer)),
40
+ );
39
41
  return new Response(
40
42
  JSON.stringify({
41
43
  id: "resp_1",
@@ -78,7 +80,9 @@ describe("router execution", () => {
78
80
  model: "gpt-4o",
79
81
  messages: [{ role: "user", content: "hi" }],
80
82
  });
81
- expect(response.headers.get("x-openairouter-provider-id")).toBe("openrouter");
83
+ expect(response.headers.get("x-openairouter-provider-id")).toBe(
84
+ "openrouter",
85
+ );
82
86
  expect(response.headers.get("x-openairouter-model-id")).toBe("gpt-4o");
83
87
  expect(await response.json()).toEqual({
84
88
  object: "chat.completion",
@@ -98,11 +102,13 @@ describe("router execution", () => {
98
102
  const capturedBodies: Record<string, unknown>[] = [];
99
103
  const events: Array<{ kind: string; data?: Record<string, unknown> }> = [];
100
104
  const close = vi.fn(async () => {});
101
- const callTool = vi.fn(async (name: string, args: Record<string, unknown>) => {
102
- expect(name).toBe("lookup");
103
- expect(args).toEqual({ id: "42" });
104
- return { content: [{ type: "text", text: "customer is active" }] };
105
- });
105
+ const callTool = vi.fn(
106
+ async (name: string, args: Record<string, unknown>) => {
107
+ expect(name).toBe("lookup");
108
+ expect(args).toEqual({ id: "42" });
109
+ return { content: [{ type: "text", text: "customer is active" }] };
110
+ },
111
+ );
106
112
  const remoteMcpClientFactory: RemoteMcpClientFactory = vi.fn(async () => ({
107
113
  callTool,
108
114
  close,
@@ -112,7 +118,7 @@ describe("router execution", () => {
112
118
  config: {
113
119
  providers: {
114
120
  openrouter: {
115
- api_base_url: "https://openrouter.ai/api/v1",
121
+ apiBaseUrl: "https://openrouter.ai/api/v1",
116
122
  style: "chat-completions",
117
123
  exports: ["*"],
118
124
  },
@@ -122,15 +128,17 @@ describe("router execution", () => {
122
128
  type: "mcp_streamable_http",
123
129
  url: "https://mcp.example.test/mcp",
124
130
  headers: { authorization: "Bearer static" },
125
- tools: [{
126
- name: "lookup",
127
- description: "Look up a customer",
128
- inputSchema: {
129
- type: "object",
130
- properties: { id: { type: "string" } },
131
- required: ["id"],
131
+ tools: [
132
+ {
133
+ name: "lookup",
134
+ description: "Look up a customer",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: { id: { type: "string" } },
138
+ required: ["id"],
139
+ },
132
140
  },
133
- }],
141
+ ],
134
142
  },
135
143
  },
136
144
  modelNamespaces: {
@@ -147,37 +155,48 @@ describe("router execution", () => {
147
155
  },
148
156
  },
149
157
  fetchImpl: vi.fn(async (_input, init) => {
150
- const body = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
158
+ const body = JSON.parse(
159
+ decoder.decode(new Uint8Array(init?.body as ArrayBuffer)),
160
+ );
151
161
  capturedBodies.push(body);
152
162
  if (capturedBodies.length === 1) {
153
- expect(body.tools).toEqual([{
154
- type: "function",
155
- function: {
156
- name: "crm__lookup",
157
- description: "Look up a customer",
158
- parameters: {
159
- type: "object",
160
- properties: { id: { type: "string" } },
161
- required: ["id"],
163
+ expect(body.tools).toEqual([
164
+ {
165
+ type: "function",
166
+ function: {
167
+ name: "crm__lookup",
168
+ description: "Look up a customer",
169
+ parameters: {
170
+ type: "object",
171
+ properties: { id: { type: "string" } },
172
+ required: ["id"],
173
+ },
162
174
  },
163
175
  },
164
- }]);
176
+ ]);
165
177
  return new Response(
166
178
  JSON.stringify({
167
179
  id: "resp_1",
168
180
  model: "gpt-4o",
169
- choices: [{
170
- index: 0,
171
- message: {
172
- role: "assistant",
173
- tool_calls: [{
174
- id: "call_1",
175
- type: "function",
176
- function: { name: "crm__lookup", arguments: "{\"id\":\"42\"}" },
177
- }],
181
+ choices: [
182
+ {
183
+ index: 0,
184
+ message: {
185
+ role: "assistant",
186
+ tool_calls: [
187
+ {
188
+ id: "call_1",
189
+ type: "function",
190
+ function: {
191
+ name: "crm__lookup",
192
+ arguments: '{"id":"42"}',
193
+ },
194
+ },
195
+ ],
196
+ },
197
+ finish_reason: "tool_calls",
178
198
  },
179
- finish_reason: "tool_calls",
180
- }],
199
+ ],
181
200
  }),
182
201
  { headers: { "content-type": "application/json; charset=utf-8" } },
183
202
  );
@@ -191,11 +210,13 @@ describe("router execution", () => {
191
210
  JSON.stringify({
192
211
  id: "resp_2",
193
212
  model: "gpt-4o",
194
- choices: [{
195
- index: 0,
196
- message: { role: "assistant", content: "done" },
197
- finish_reason: "stop",
198
- }],
213
+ choices: [
214
+ {
215
+ index: 0,
216
+ message: { role: "assistant", content: "done" },
217
+ finish_reason: "stop",
218
+ },
219
+ ],
199
220
  }),
200
221
  { headers: { "content-type": "application/json; charset=utf-8" } },
201
222
  );
@@ -229,9 +250,17 @@ describe("router execution", () => {
229
250
  expect(callTool).toHaveBeenCalledOnce();
230
251
  expect(close).toHaveBeenCalledOnce();
231
252
  expect(capturedBodies).toHaveLength(2);
232
- expect(events.find((event) => event.kind === "remote_mcp.connect")?.data?.durationMs).toEqual(expect.any(Number));
233
- expect(events.find((event) => event.kind === "remote_mcp.tool_call")?.data?.durationMs).toEqual(expect.any(Number));
234
- expect(events.find((event) => event.kind === "tool.executed")?.data?.durationMs).toEqual(expect.any(Number));
253
+ expect(
254
+ events.find((event) => event.kind === "remote_mcp.connect")?.data
255
+ ?.durationMs,
256
+ ).toEqual(expect.any(Number));
257
+ expect(
258
+ events.find((event) => event.kind === "remote_mcp.tool_call")?.data
259
+ ?.durationMs,
260
+ ).toEqual(expect.any(Number));
261
+ expect(
262
+ events.find((event) => event.kind === "tool.executed")?.data?.durationMs,
263
+ ).toEqual(expect.any(Number));
235
264
  expect(await response.json()).toMatchObject({
236
265
  choices: [{ message: { content: "done" } }],
237
266
  });
@@ -245,7 +274,7 @@ describe("router execution", () => {
245
274
  config: {
246
275
  providers: {
247
276
  openai: {
248
- api_base_url: "https://api.openai.com/v1",
277
+ apiBaseUrl: "https://api.openai.com/v1",
249
278
  style: "responses",
250
279
  exports: ["*"],
251
280
  },
@@ -261,17 +290,21 @@ describe("router execution", () => {
261
290
  },
262
291
  fetchImpl: vi.fn(async (input, init) => {
263
292
  capturedUrl = String(input);
264
- capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
293
+ capturedBody = JSON.parse(
294
+ decoder.decode(new Uint8Array(init?.body as ArrayBuffer)),
295
+ );
265
296
  return new Response(
266
297
  JSON.stringify({
267
298
  id: "resp_1",
268
299
  model: "gpt-5",
269
- output: [{
270
- type: "message",
271
- role: "assistant",
272
- status: "completed",
273
- content: [{ type: "output_text", text: "hello" }],
274
- }],
300
+ output: [
301
+ {
302
+ type: "message",
303
+ role: "assistant",
304
+ status: "completed",
305
+ content: [{ type: "output_text", text: "hello" }],
306
+ },
307
+ ],
275
308
  }),
276
309
  { headers: { "content-type": "application/json; charset=utf-8" } },
277
310
  );
@@ -302,11 +335,13 @@ describe("router execution", () => {
302
335
  object: "chat.completion",
303
336
  id: "resp_1",
304
337
  model: "gpt-5",
305
- choices: [{
306
- index: 0,
307
- message: { role: "assistant", content: "hello" },
308
- finish_reason: "stop",
309
- }],
338
+ choices: [
339
+ {
340
+ index: 0,
341
+ message: { role: "assistant", content: "hello" },
342
+ finish_reason: "stop",
343
+ },
344
+ ],
310
345
  });
311
346
  });
312
347
 
@@ -318,7 +353,7 @@ describe("router execution", () => {
318
353
  config: {
319
354
  providers: {
320
355
  anthropic: {
321
- api_base_url: "https://api.anthropic.com",
356
+ apiBaseUrl: "https://api.anthropic.com",
322
357
  style: "anthropic-messages",
323
358
  exports: ["*"],
324
359
  },
@@ -327,14 +362,19 @@ describe("router execution", () => {
327
362
  default: {
328
363
  exports: ["*"],
329
364
  models: {
330
- "claude-sonnet-4": { provider: "anthropic", model: "claude-sonnet-4" },
365
+ "claude-sonnet-4": {
366
+ provider: "anthropic",
367
+ model: "claude-sonnet-4",
368
+ },
331
369
  },
332
370
  },
333
371
  },
334
372
  },
335
373
  fetchImpl: vi.fn(async (input, init) => {
336
374
  capturedUrl = String(input);
337
- capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
375
+ capturedBody = JSON.parse(
376
+ decoder.decode(new Uint8Array(init?.body as ArrayBuffer)),
377
+ );
338
378
  return new Response(
339
379
  JSON.stringify({
340
380
  id: "msg_1",
@@ -377,11 +417,13 @@ describe("router execution", () => {
377
417
  completion_tokens: 5,
378
418
  total_tokens: 15,
379
419
  },
380
- choices: [{
381
- index: 0,
382
- message: { role: "assistant", content: "hello" },
383
- finish_reason: "stop",
384
- }],
420
+ choices: [
421
+ {
422
+ index: 0,
423
+ message: { role: "assistant", content: "hello" },
424
+ finish_reason: "stop",
425
+ },
426
+ ],
385
427
  });
386
428
  });
387
429
 
@@ -393,7 +435,8 @@ describe("router execution", () => {
393
435
  config: {
394
436
  providers: {
395
437
  google: {
396
- api_base_url: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
438
+ apiBaseUrl:
439
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
397
440
  style: "google-genai",
398
441
  exports: ["*"],
399
442
  },
@@ -402,21 +445,28 @@ describe("router execution", () => {
402
445
  default: {
403
446
  exports: ["*"],
404
447
  models: {
405
- "gemini-2.5-flash": { provider: "google", model: "gemini-2.5-flash" },
448
+ "gemini-2.5-flash": {
449
+ provider: "google",
450
+ model: "gemini-2.5-flash",
451
+ },
406
452
  },
407
453
  },
408
454
  },
409
455
  },
410
456
  fetchImpl: vi.fn(async (input, init) => {
411
457
  capturedUrl = String(input);
412
- capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
458
+ capturedBody = JSON.parse(
459
+ decoder.decode(new Uint8Array(init?.body as ArrayBuffer)),
460
+ );
413
461
  return new Response(
414
462
  JSON.stringify({
415
463
  modelVersion: "gemini-2.5-flash",
416
- candidates: [{
417
- content: { parts: [{ text: "hello" }] },
418
- finishReason: "STOP",
419
- }],
464
+ candidates: [
465
+ {
466
+ content: { parts: [{ text: "hello" }] },
467
+ finishReason: "STOP",
468
+ },
469
+ ],
420
470
  }),
421
471
  { headers: { "content-type": "application/json; charset=utf-8" } },
422
472
  );
@@ -438,7 +488,9 @@ describe("router execution", () => {
438
488
 
439
489
  const response = await handleChatCompletions({ runtime, ctx });
440
490
  expect(response.status).toBe(200);
441
- expect(capturedUrl).toBe("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent");
491
+ expect(capturedUrl).toBe(
492
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
493
+ );
442
494
  expect(capturedBody).toEqual({
443
495
  model: "gemini-2.5-flash",
444
496
  contents: [{ role: "user", parts: [{ text: "hi" }] }],
@@ -446,11 +498,13 @@ describe("router execution", () => {
446
498
  expect(await response.json()).toEqual({
447
499
  object: "chat.completion",
448
500
  model: "gemini-2.5-flash",
449
- choices: [{
450
- index: 0,
451
- message: { role: "assistant", content: "hello" },
452
- finish_reason: "stop",
453
- }],
501
+ choices: [
502
+ {
503
+ index: 0,
504
+ message: { role: "assistant", content: "hello" },
505
+ finish_reason: "stop",
506
+ },
507
+ ],
454
508
  });
455
509
  });
456
510
 
@@ -521,7 +575,7 @@ describe("router execution", () => {
521
575
  config: {
522
576
  providers: {
523
577
  openrouter: {
524
- api_base_url: "https://openrouter.ai/api/v1",
578
+ apiBaseUrl: "https://openrouter.ai/api/v1",
525
579
  style: "chat-completions",
526
580
  exports: ["*"],
527
581
  },
@@ -543,7 +597,9 @@ describe("router execution", () => {
543
597
  `data: ${JSON.stringify({
544
598
  id: "chunk_1",
545
599
  model: "gpt-4o",
546
- choices: [{ index: 0, delta: { role: "assistant", content: "Hi" } }],
600
+ choices: [
601
+ { index: 0, delta: { role: "assistant", content: "Hi" } },
602
+ ],
547
603
  })}\n\n`,
548
604
  ),
549
605
  );
@@ -584,9 +640,11 @@ describe("router execution", () => {
584
640
  const response = await handleChatCompletions({ runtime, ctx });
585
641
  const text = await response.text();
586
642
 
587
- expect(response.headers.get("content-type")).toBe("text/event-stream; charset=utf-8");
588
- expect(text).toContain("\"content\":\"Hi\"");
589
- expect(text).toContain("\"finish_reason\":\"stop\"");
643
+ expect(response.headers.get("content-type")).toBe(
644
+ "text/event-stream; charset=utf-8",
645
+ );
646
+ expect(text).toContain('"content":"Hi"');
647
+ expect(text).toContain('"finish_reason":"stop"');
590
648
  expect(text).toContain("data: [DONE]");
591
649
  });
592
650
 
@@ -595,7 +653,7 @@ describe("router execution", () => {
595
653
  config: {
596
654
  providers: {
597
655
  openrouter: {
598
- api_base_url: "https://openrouter.ai/api/v1",
656
+ apiBaseUrl: "https://openrouter.ai/api/v1",
599
657
  style: "chat-completions",
600
658
  exports: ["*"],
601
659
  },
@@ -656,7 +714,7 @@ describe("router execution", () => {
656
714
  config: {
657
715
  providers: {
658
716
  anthropic: {
659
- api_base_url: "https://api.anthropic.com",
717
+ apiBaseUrl: "https://api.anthropic.com",
660
718
  style: "anthropic-messages",
661
719
  exports: ["*"],
662
720
  },
@@ -665,23 +723,30 @@ describe("router execution", () => {
665
723
  default: {
666
724
  exports: ["*"],
667
725
  models: {
668
- "claude-sonnet-4": { provider: "anthropic", model: "claude-sonnet-4" },
726
+ "claude-sonnet-4": {
727
+ provider: "anthropic",
728
+ model: "claude-sonnet-4",
729
+ },
669
730
  },
670
731
  },
671
732
  },
672
733
  },
673
- fetchImpl: vi.fn(async () =>
674
- new Response(
675
- JSON.stringify({
676
- type: "error",
677
- error: { type: "overloaded_error", message: "Provider is overloaded" },
678
- request_id: "req_upstream",
679
- }),
680
- {
681
- status: 529,
682
- headers: { "content-type": "application/json; charset=utf-8" },
683
- },
684
- ),
734
+ fetchImpl: vi.fn(
735
+ async () =>
736
+ new Response(
737
+ JSON.stringify({
738
+ type: "error",
739
+ error: {
740
+ type: "overloaded_error",
741
+ message: "Provider is overloaded",
742
+ },
743
+ request_id: "req_upstream",
744
+ }),
745
+ {
746
+ status: 529,
747
+ headers: { "content-type": "application/json; charset=utf-8" },
748
+ },
749
+ ),
685
750
  ),
686
751
  });
687
752
 
@@ -715,12 +780,17 @@ describe("router execution", () => {
715
780
  code: null,
716
781
  },
717
782
  });
718
- const observedError = events.find((event) =>
719
- event.kind === "provider.response" &&
720
- typeof event.data?.lilText === "string" &&
721
- event.data.lilText.includes("ERR_START"),
783
+ expect(events).toContainEqual(
784
+ expect.objectContaining({
785
+ kind: "provider.fetch_headers",
786
+ data: expect.objectContaining({ status: 529 }),
787
+ }),
788
+ );
789
+ expect(events).toContainEqual(
790
+ expect.objectContaining({
791
+ kind: "execution.failed",
792
+ data: expect.objectContaining({ message: "Provider is overloaded" }),
793
+ }),
722
794
  );
723
- expect(observedError?.data?.lilText).toContain('ERR_MESSAGE "Provider is overloaded"');
724
- expect(observedError?.data?.lilText).toContain('ERR_DATA "request_id"="req_upstream"');
725
795
  });
726
796
  });