@neutrome/open-ai-router 0.4.2 → 0.5.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 (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 +148 -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 +50 -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
@@ -1,377 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { appendAssistantMessage } from "@neutrome/lilsdk-ts/synthetic";
3
- import { streamTextResponse } from "@neutrome/lilsdk-ts/stream";
4
- import {
5
- getModel,
6
- } from "@neutrome/lil-engine";
7
- import { createApp } from "./example.ts";
8
-
9
- const decoder = new TextDecoder();
10
-
11
- describe("open-ai-router createApp", () => {
12
- it("lists configured executor aliases", async () => {
13
- const app = createApp({
14
- config: {
15
- providers: {
16
- openrouter: {
17
- api_base_url: "https://openrouter.ai/api/v1",
18
- style: "chat-completions",
19
- exports: ["*"],
20
- },
21
- },
22
- modelNamespaces: {
23
- semantyka: {
24
- exports: ["enei-*"],
25
- models: {
26
- "enei-1": { executorId: "enei-1" },
27
- },
28
- },
29
- },
30
- },
31
- });
32
-
33
- const response = await app.request("/v1/models");
34
-
35
- expect(response.status).toBe(200);
36
- expect(await response.json()).toEqual({
37
- object: "list",
38
- data: [
39
- { id: "semantyka/enei-1", object: "model", created: 0, owned_by: "semantyka" },
40
- ],
41
- });
42
- });
43
-
44
- it("serves provider-backed chat completions through the app surface", async () => {
45
- let capturedBody: Record<string, unknown> | null = null;
46
-
47
- const app = createApp({
48
- config: {
49
- upstreamAuth: {
50
- order: ["proxy"],
51
- proxy: {
52
- headers: ["authorization"],
53
- },
54
- },
55
- providers: {
56
- openrouter: {
57
- api_base_url: "https://openrouter.ai/api/v1",
58
- style: "chat-completions",
59
- exports: ["*"],
60
- },
61
- },
62
- modelNamespaces: {
63
- default: {
64
- exports: ["*"],
65
- models: {
66
- "gpt-4o": { provider: "openrouter", model: "gpt-4o" },
67
- },
68
- },
69
- },
70
- },
71
- fetchImpl: vi.fn(async (_input, init) => {
72
- capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
73
- return new Response(
74
- JSON.stringify({
75
- id: "resp_1",
76
- model: "gpt-4o",
77
- choices: [
78
- {
79
- index: 0,
80
- message: { role: "assistant", content: "hello" },
81
- finish_reason: "stop",
82
- },
83
- ],
84
- }),
85
- {
86
- headers: {
87
- "content-type": "application/json; charset=utf-8",
88
- },
89
- },
90
- );
91
- }),
92
- });
93
-
94
- const response = await app.request("/v1/chat/completions", {
95
- method: "POST",
96
- headers: {
97
- authorization: "Bearer test-key",
98
- "content-type": "application/json",
99
- },
100
- body: JSON.stringify({
101
- model: "gpt-4o",
102
- messages: [{ role: "user", content: "hi" }],
103
- }),
104
- });
105
-
106
- expect(response.status).toBe(200);
107
- expect(capturedBody).toEqual({
108
- model: "gpt-4o",
109
- messages: [{ role: "user", content: "hi" }],
110
- });
111
- expect(response.headers.get("x-openairouter-provider-id")).toBe("openrouter");
112
- expect(await response.json()).toEqual({
113
- object: "chat.completion",
114
- id: "resp_1",
115
- model: "gpt-4o",
116
- choices: [
117
- {
118
- index: 0,
119
- message: { role: "assistant", content: "hello" },
120
- finish_reason: "stop",
121
- },
122
- ],
123
- });
124
- });
125
-
126
- it("serves responses through chat-completions provider transport", async () => {
127
- let capturedBody: Record<string, unknown> | null = null;
128
-
129
- const app = createApp({
130
- config: {
131
- providers: {
132
- openrouter: {
133
- api_base_url: "https://openrouter.ai/api/v1",
134
- style: "chat-completions",
135
- exports: ["*"],
136
- },
137
- },
138
- modelNamespaces: {
139
- default: {
140
- exports: ["*"],
141
- models: {
142
- "gpt-4o": { provider: "openrouter", model: "gpt-4o" },
143
- },
144
- },
145
- },
146
- },
147
- fetchImpl: vi.fn(async (_input, init) => {
148
- capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
149
- return new Response(
150
- JSON.stringify({
151
- id: "resp_2",
152
- model: "gpt-4o",
153
- choices: [
154
- {
155
- index: 0,
156
- message: { role: "assistant", content: "hello" },
157
- finish_reason: "stop",
158
- },
159
- ],
160
- }),
161
- {
162
- headers: {
163
- "content-type": "application/json; charset=utf-8",
164
- },
165
- },
166
- );
167
- }),
168
- });
169
-
170
- const response = await app.request("/v1/responses", {
171
- method: "POST",
172
- headers: {
173
- "content-type": "application/json",
174
- },
175
- body: JSON.stringify({
176
- model: "gpt-4o",
177
- input: [{ role: "user", content: "hi" }],
178
- }),
179
- });
180
-
181
- expect(response.status).toBe(200);
182
- expect(capturedBody).toEqual({
183
- model: "gpt-4o",
184
- messages: [{ role: "user", content: "hi" }],
185
- });
186
- expect(await response.json()).toEqual({
187
- id: "resp_2",
188
- model: "gpt-4o",
189
- output: [
190
- {
191
- type: "message",
192
- role: "assistant",
193
- status: "completed",
194
- content: [{ type: "output_text", text: "hello" }],
195
- },
196
- ],
197
- });
198
- });
199
-
200
- it("serves anthropic messages through the app surface", async () => {
201
- let capturedBody: Record<string, unknown> | null = null;
202
-
203
- const app = createApp({
204
- config: {
205
- providers: {
206
- openrouter: {
207
- api_base_url: "https://openrouter.ai/api/v1",
208
- style: "chat-completions",
209
- exports: ["*"],
210
- },
211
- },
212
- modelNamespaces: {
213
- default: {
214
- exports: ["*"],
215
- models: {
216
- "gpt-4o": { provider: "openrouter", model: "gpt-4o" },
217
- },
218
- },
219
- },
220
- },
221
- fetchImpl: vi.fn(async (_input, init) => {
222
- capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
223
- return new Response(
224
- JSON.stringify({
225
- id: "resp_3",
226
- model: "gpt-4o",
227
- choices: [
228
- {
229
- index: 0,
230
- message: { role: "assistant", content: "hello" },
231
- finish_reason: "stop",
232
- },
233
- ],
234
- }),
235
- {
236
- headers: {
237
- "content-type": "application/json; charset=utf-8",
238
- },
239
- },
240
- );
241
- }),
242
- });
243
-
244
- const response = await app.request("/v1/messages", {
245
- method: "POST",
246
- headers: {
247
- "content-type": "application/json",
248
- },
249
- body: JSON.stringify({
250
- model: "gpt-4o",
251
- messages: [
252
- {
253
- role: "user",
254
- content: [{ type: "text", text: "hi" }],
255
- },
256
- ],
257
- }),
258
- });
259
-
260
- expect(response.status).toBe(200);
261
- expect(capturedBody).toEqual({
262
- model: "gpt-4o",
263
- messages: [{ role: "user", content: "hi" }],
264
- });
265
- expect(await response.json()).toEqual({
266
- id: "resp_3",
267
- model: "gpt-4o",
268
- role: "assistant",
269
- content: [{ type: "text", text: "hello" }],
270
- stop_reason: "end_turn",
271
- });
272
- });
273
-
274
- it("serves google generateContent routes and decodes namespaced model paths", async () => {
275
- let capturedModel: string | null = null;
276
- let executeCalled = false;
277
-
278
- const app = createApp({
279
- config: {
280
- providers: {},
281
- modelNamespaces: {
282
- support: {
283
- exports: ["*"],
284
- models: {
285
- "refund-router": { executorId: "support/refund-router" },
286
- },
287
- },
288
- },
289
- },
290
- executorImplementations: {
291
- "support/refund-router": {
292
- async execute(request) {
293
- executeCalled = true;
294
- capturedModel = getModel(request) ?? null;
295
- return appendAssistantMessage(request, "hello");
296
- },
297
- async *stream(request) {
298
- capturedModel = getModel(request) ?? null;
299
- yield* streamTextResponse("hello");
300
- },
301
- },
302
- },
303
- });
304
-
305
- const response = await app.request("/v1/models/support%2Frefund-router:generateContent", {
306
- method: "POST",
307
- headers: {
308
- "content-type": "application/json",
309
- },
310
- body: JSON.stringify({
311
- contents: [{ role: "user", parts: [{ text: "hi" }] }],
312
- }),
313
- });
314
-
315
- expect(response.status).toBe(200);
316
- expect(executeCalled).toBe(true);
317
- expect(capturedModel).toBe("support/refund-router");
318
- expect(await response.json()).toEqual({
319
- candidates: [
320
- {
321
- content: {
322
- role: "model",
323
- parts: [{ text: "hello" }],
324
- },
325
- },
326
- ],
327
- });
328
- });
329
-
330
- it("serves google streamGenerateContent routes as event streams", async () => {
331
- let capturedModel: string | null = null;
332
-
333
- const app = createApp({
334
- config: {
335
- providers: {},
336
- modelNamespaces: {
337
- support: {
338
- exports: ["*"],
339
- models: {
340
- "refund-router": { executorId: "support/refund-router" },
341
- },
342
- },
343
- },
344
- },
345
- executorImplementations: {
346
- "support/refund-router": {
347
- async execute(request) {
348
- capturedModel = getModel(request) ?? null;
349
- return appendAssistantMessage(request, "hello");
350
- },
351
- async *stream(request) {
352
- capturedModel = getModel(request) ?? null;
353
- yield* streamTextResponse("hello");
354
- },
355
- },
356
- },
357
- });
358
-
359
- const response = await app.request("/v1/models/support%2Frefund-router:streamGenerateContent", {
360
- method: "POST",
361
- headers: {
362
- "content-type": "application/json",
363
- },
364
- body: JSON.stringify({
365
- contents: [{ role: "user", parts: [{ text: "hi" }] }],
366
- }),
367
- });
368
-
369
- expect(response.status).toBe(200);
370
- expect(capturedModel).toBe("support/refund-router");
371
- expect(response.headers.get("content-type")).toContain("text/event-stream");
372
- const body = await response.text();
373
- expect(body).toContain("\"candidates\"");
374
- expect(body).toContain("\"text\":\"hello\"");
375
- expect(body).toContain("\"finishReason\":\"STOP\"");
376
- });
377
- });
package/src/example.ts DELETED
@@ -1,73 +0,0 @@
1
- import { Hono } from "hono";
2
- import { cors } from "./app/cors.ts";
3
- import {
4
- anthropicMessagesHandler,
5
- chatCompletionsHandler,
6
- googleGenAIHandler,
7
- listModelsHandler,
8
- responsesHandler,
9
- type ChatCompletionsHandlerOptions,
10
- } from "./app/handlers.ts";
11
- import { healthHandler } from "./app/health.ts";
12
- import { createRouterRuntime, type CreateRouterOptions } from "./router/index.ts";
13
-
14
- export type CreateAppOptions = CreateRouterOptions & ChatCompletionsHandlerOptions;
15
-
16
- export function createApp(options: CreateAppOptions) {
17
- const routerOptions = {
18
- config: options.config,
19
- } as CreateRouterOptions;
20
-
21
- if (options.fetchImpl) {
22
- routerOptions.fetchImpl = options.fetchImpl;
23
- }
24
- if (options.knownSuffixes) {
25
- routerOptions.knownSuffixes = options.knownSuffixes;
26
- }
27
- if (options.upstreamAuthDrivers) {
28
- routerOptions.upstreamAuthDrivers = options.upstreamAuthDrivers;
29
- }
30
-
31
- const runtime = createRouterRuntime(routerOptions);
32
- const app = new Hono();
33
-
34
- const handlerOptions = {} as ChatCompletionsHandlerOptions;
35
- if (options.executorImplementations) {
36
- handlerOptions.executorImplementations = options.executorImplementations;
37
- }
38
- if (options.transforms) {
39
- handlerOptions.transforms = options.transforms;
40
- }
41
- if (options.observe) {
42
- handlerOptions.observe = options.observe;
43
- }
44
- if (options.requestIdFactory) {
45
- handlerOptions.requestIdFactory = options.requestIdFactory;
46
- }
47
- if (options.executionIdFactory) {
48
- handlerOptions.executionIdFactory = options.executionIdFactory;
49
- }
50
- if (options.upstreamTimeoutMs !== undefined) {
51
- handlerOptions.upstreamTimeoutMs = options.upstreamTimeoutMs;
52
- }
53
-
54
- app.use("/v1/models", cors());
55
- app.get("/v1/models", listModelsHandler(runtime));
56
-
57
- app.use("/v1/chat/completions", cors());
58
- app.post("/v1/chat/completions", chatCompletionsHandler(runtime, handlerOptions));
59
-
60
- app.use("/v1/responses", cors());
61
- app.post("/v1/responses", responsesHandler(runtime, handlerOptions));
62
-
63
- app.use("/v1/messages", cors());
64
- app.post("/v1/messages", anthropicMessagesHandler(runtime, handlerOptions));
65
-
66
- app.use("/v1/models/*", cors());
67
- app.post("/v1/models/*", googleGenAIHandler(runtime, handlerOptions));
68
-
69
- app.get("/health", healthHandler());
70
- app.all("*", (c) => c.text("Not Found", 404));
71
-
72
- return app;
73
- }
@@ -1,54 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { createTraceCollector } from "./observer.ts";
3
-
4
- describe("createTraceCollector", () => {
5
- it("prefixes raw trace chunks with event comments", () => {
6
- const collector = createTraceCollector("00000000-0000-4000-8000-000000000001");
7
-
8
- collector.observe({
9
- kind: "request.received",
10
- requestId: "request-1",
11
- executionId: "request-1",
12
- timestamp: "2026-06-24T12:00:00.000Z",
13
- data: { lilText: "MSG_START" },
14
- });
15
- collector.observe({
16
- kind: "provider.request",
17
- requestId: "request-1",
18
- executionId: "provider-1",
19
- timestamp: "2026-06-24T12:00:01.000Z",
20
- data: { lilText: "SET_MODEL \"gpt-4o\"" },
21
- });
22
- collector.observe({
23
- kind: "provider.response_chunk",
24
- requestId: "request-1",
25
- executionId: "provider-1",
26
- timestamp: "2026-06-24T12:00:02.000Z",
27
- data: { lilText: "STREAM_DELTA \"Hi\"" },
28
- });
29
-
30
- expect(collector.getTrace().rawTrace).toBe(
31
- ';request:0\nMSG_START\n\n;provider-request:1\nSET_MODEL "gpt-4o"\n\n;provider-response-chunk:2\nSTREAM_DELTA "Hi"',
32
- );
33
- });
34
-
35
- it("appends structured timing comments with durationMs", () => {
36
- const collector = createTraceCollector("00000000-0000-4000-8000-000000000001");
37
-
38
- collector.observe({
39
- kind: "provider.finished",
40
- requestId: "request-1",
41
- executionId: "provider-1",
42
- timestamp: "2026-06-24T12:00:03.000Z",
43
- data: {
44
- startedAt: "2026-06-24T12:00:02.877Z",
45
- finishedAt: "2026-06-24T12:00:03.000Z",
46
- durationMs: 123,
47
- },
48
- });
49
-
50
- expect(collector.getTrace().rawTrace).toBe(
51
- ';timing {"kind":"provider","event":"provider.finished","requestId":"request-1","executionId":"provider-1","startedAt":"2026-06-24T12:00:02.877Z","finishedAt":"2026-06-24T12:00:03.000Z","durationMs":123}',
52
- );
53
- });
54
- });