@neutrome/open-ai-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ export { createApp } from "./example.ts";
2
+ export type { CreateAppOptions } from "./example.ts";
3
+
4
+ export {
5
+ createFetchProviderInvoker,
6
+ createRouterExecutionRuntime,
7
+ createRouterRuntime,
8
+ handleChatCompletions,
9
+ handleResponses,
10
+ listConfiguredModels,
11
+ ProviderHttpError,
12
+ resolveInvocationTarget,
13
+ resolveInvocationTargets,
14
+ resolveRequestTarget,
15
+ RouterError,
16
+ } from "./router/index.ts";
17
+ export type {
18
+ AuthConfig,
19
+ CreateRouterOptions,
20
+ ExecutorNamespaceConfig,
21
+ ExecutorRouteConfig,
22
+ ExecutorRouteRuntime,
23
+ ExecutorRuntime,
24
+ HandleChatCompletionsOptions,
25
+ HandleResponsesOptions,
26
+ ProviderApiStyle,
27
+ ProviderApiStyle as ProviderStyle,
28
+ ProviderRuntime,
29
+ ProviderTargetConfig,
30
+ ResolvedTarget,
31
+ RouterConfig,
32
+ RouterExecutionOptions,
33
+ RouterRuntime,
34
+ UsageReport,
35
+ } from "./router/index.ts";
36
+
37
+ export type {
38
+ AuthDriver,
39
+ AuthResult,
40
+ RequestContext,
41
+ TargetAuthContext,
42
+ } from "./auth/types.ts";
43
+ export { buildAuthChain } from "./auth/registry.ts";
44
+ export { envAuthDriver } from "./auth/env.ts";
45
+ export { proxyAuthDriver } from "./auth/proxy.ts";
46
+
47
+ export { cors } from "./app/cors.ts";
48
+ export { healthHandler } from "./app/health.ts";
49
+ export { chatCompletionsHandler, listModelsHandler, responsesHandler } from "./app/handlers.ts";
50
+ export type { ChatCompletionsHandlerOptions, ResponsesHandlerOptions } from "./app/handlers.ts";
@@ -0,0 +1,41 @@
1
+ import type { ProviderStyle } from "@neutrome/lil-engine";
2
+ import type { AuthConfigShape } from "../auth/config.ts";
3
+
4
+ export type ProviderApiStyle = ProviderStyle;
5
+
6
+ export type AuthConfig = AuthConfigShape;
7
+
8
+ export type ProviderTargetConfig = {
9
+ api_base_url: string;
10
+ style: ProviderApiStyle;
11
+ exports?: string[] | null;
12
+ endpoint_path?: string;
13
+ allow_anonymous?: boolean;
14
+ no_prefix?: boolean;
15
+ headers?: Record<string, string>;
16
+ catalog?: string[];
17
+ };
18
+
19
+ export type ExecutorRouteConfig =
20
+ | {
21
+ executor: string;
22
+ alias?: string;
23
+ transforms?: string[];
24
+ }
25
+ | {
26
+ provider: string;
27
+ model: string;
28
+ transforms?: string[];
29
+ };
30
+
31
+ export type ExecutorNamespaceConfig = {
32
+ exports?: string[] | null;
33
+ models: Record<string, ExecutorRouteConfig>;
34
+ };
35
+
36
+ export type RouterConfig = {
37
+ trace?: boolean;
38
+ auth?: AuthConfig;
39
+ providers: Record<string, ProviderTargetConfig>;
40
+ executors: Record<string, ExecutorNamespaceConfig>;
41
+ };
@@ -0,0 +1,478 @@
1
+ import { appendAssistantMessage } from "@neutrome/lil-engine";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import type { RequestContext } from "../auth/types.ts";
4
+ import { handleChatCompletions } from "./execute.ts";
5
+ import { createRouterRuntime } from "./runtime.ts";
6
+
7
+ const encoder = new TextEncoder();
8
+ const decoder = new TextDecoder();
9
+
10
+ describe("router execution", () => {
11
+ it("routes chat completions requests through provider transport", async () => {
12
+ let capturedUrl = "";
13
+ let capturedBody: Record<string, unknown> | null = null;
14
+ let capturedAuth = "";
15
+
16
+ const runtime = createRouterRuntime({
17
+ config: {
18
+ providers: {
19
+ openrouter: {
20
+ api_base_url: "https://openrouter.ai/api/v1",
21
+ style: "chat-completions",
22
+ allow_anonymous: true,
23
+ exports: ["gpt-4o"],
24
+ no_prefix: true,
25
+ },
26
+ },
27
+ executors: {},
28
+ },
29
+ fetchImpl: vi.fn(async (input, init) => {
30
+ capturedUrl = String(input);
31
+ capturedAuth = new Headers(init?.headers).get("authorization") ?? "";
32
+ capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
33
+ return new Response(
34
+ JSON.stringify({
35
+ id: "resp_1",
36
+ model: "gpt-4o",
37
+ choices: [
38
+ {
39
+ index: 0,
40
+ message: { role: "assistant", content: "hello" },
41
+ finish_reason: "stop",
42
+ },
43
+ ],
44
+ }),
45
+ { headers: { "content-type": "application/json; charset=utf-8" } },
46
+ );
47
+ }),
48
+ });
49
+
50
+ const ctx: RequestContext = {
51
+ request: new Request("http://local/v1/chat/completions", {
52
+ method: "POST",
53
+ headers: {
54
+ authorization: "Bearer incoming",
55
+ "content-type": "application/json",
56
+ },
57
+ body: JSON.stringify({
58
+ model: "gpt-4o",
59
+ messages: [{ role: "user", content: "hi" }],
60
+ }),
61
+ }),
62
+ incomingAuth: new Map(),
63
+ env: {},
64
+ };
65
+
66
+ const response = await handleChatCompletions({ runtime, ctx });
67
+
68
+ expect(response.status).toBe(200);
69
+ expect(capturedUrl).toBe("https://openrouter.ai/api/v1/chat/completions");
70
+ expect(capturedAuth).toBe("Bearer incoming");
71
+ expect(capturedBody).toEqual({
72
+ model: "gpt-4o",
73
+ messages: [{ role: "user", content: "hi" }],
74
+ });
75
+ expect(response.headers.get("x-openairouter-provider-id")).toBe("openrouter");
76
+ expect(response.headers.get("x-openairouter-model-id")).toBe("gpt-4o");
77
+ expect(await response.json()).toEqual({
78
+ object: "chat.completion",
79
+ id: "resp_1",
80
+ model: "gpt-4o",
81
+ choices: [
82
+ {
83
+ index: 0,
84
+ message: { role: "assistant", content: "hello" },
85
+ finish_reason: "stop",
86
+ },
87
+ ],
88
+ });
89
+ });
90
+
91
+ it("routes responses-style providers through provider transport", async () => {
92
+ let capturedUrl = "";
93
+ let capturedBody: Record<string, unknown> | null = null;
94
+
95
+ const runtime = createRouterRuntime({
96
+ config: {
97
+ providers: {
98
+ openai: {
99
+ api_base_url: "https://api.openai.com/v1",
100
+ style: "responses",
101
+ allow_anonymous: true,
102
+ exports: ["gpt-5"],
103
+ no_prefix: true,
104
+ },
105
+ },
106
+ executors: {},
107
+ },
108
+ fetchImpl: vi.fn(async (input, init) => {
109
+ capturedUrl = String(input);
110
+ capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
111
+ return new Response(
112
+ JSON.stringify({
113
+ id: "resp_1",
114
+ model: "gpt-5",
115
+ output: [{
116
+ type: "message",
117
+ role: "assistant",
118
+ status: "completed",
119
+ content: [{ type: "output_text", text: "hello" }],
120
+ }],
121
+ }),
122
+ { headers: { "content-type": "application/json; charset=utf-8" } },
123
+ );
124
+ }),
125
+ });
126
+
127
+ const ctx: RequestContext = {
128
+ request: new Request("http://local/v1/chat/completions", {
129
+ method: "POST",
130
+ headers: { "content-type": "application/json" },
131
+ body: JSON.stringify({
132
+ model: "gpt-5",
133
+ messages: [{ role: "user", content: "hi" }],
134
+ }),
135
+ }),
136
+ incomingAuth: new Map(),
137
+ env: {},
138
+ };
139
+
140
+ const response = await handleChatCompletions({ runtime, ctx });
141
+ expect(response.status).toBe(200);
142
+ expect(capturedUrl).toBe("https://api.openai.com/v1/responses");
143
+ expect(capturedBody).toEqual({
144
+ model: "gpt-5",
145
+ input: [{ role: "user", content: "hi" }],
146
+ });
147
+ expect(await response.json()).toEqual({
148
+ object: "chat.completion",
149
+ id: "resp_1",
150
+ model: "gpt-5",
151
+ choices: [{
152
+ index: 0,
153
+ message: { role: "assistant", content: "hello" },
154
+ finish_reason: "stop",
155
+ }],
156
+ });
157
+ });
158
+
159
+ it("routes anthropic-style providers through provider transport", async () => {
160
+ let capturedUrl = "";
161
+ let capturedBody: Record<string, unknown> | null = null;
162
+
163
+ const runtime = createRouterRuntime({
164
+ config: {
165
+ providers: {
166
+ anthropic: {
167
+ api_base_url: "https://api.anthropic.com",
168
+ style: "anthropic-messages",
169
+ allow_anonymous: true,
170
+ exports: ["claude-*"],
171
+ no_prefix: true,
172
+ },
173
+ },
174
+ executors: {},
175
+ },
176
+ fetchImpl: vi.fn(async (input, init) => {
177
+ capturedUrl = String(input);
178
+ capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
179
+ return new Response(
180
+ JSON.stringify({
181
+ id: "msg_1",
182
+ model: "claude-sonnet-4",
183
+ content: [{ type: "text", text: "hello" }],
184
+ stop_reason: "end_turn",
185
+ usage: { input_tokens: 10, output_tokens: 5 },
186
+ }),
187
+ { headers: { "content-type": "application/json; charset=utf-8" } },
188
+ );
189
+ }),
190
+ });
191
+
192
+ const ctx: RequestContext = {
193
+ request: new Request("http://local/v1/chat/completions", {
194
+ method: "POST",
195
+ headers: { "content-type": "application/json" },
196
+ body: JSON.stringify({
197
+ model: "claude-sonnet-4",
198
+ messages: [{ role: "user", content: "hi" }],
199
+ }),
200
+ }),
201
+ incomingAuth: new Map(),
202
+ env: {},
203
+ };
204
+
205
+ const response = await handleChatCompletions({ runtime, ctx });
206
+ expect(response.status).toBe(200);
207
+ expect(capturedUrl).toBe("https://api.anthropic.com/v1/messages");
208
+ expect(capturedBody).toEqual({
209
+ model: "claude-sonnet-4",
210
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
211
+ });
212
+ expect(await response.json()).toEqual({
213
+ object: "chat.completion",
214
+ id: "msg_1",
215
+ model: "claude-sonnet-4",
216
+ usage: {
217
+ prompt_tokens: 10,
218
+ completion_tokens: 5,
219
+ total_tokens: 15,
220
+ },
221
+ choices: [{
222
+ index: 0,
223
+ message: { role: "assistant", content: "hello" },
224
+ finish_reason: "stop",
225
+ }],
226
+ });
227
+ });
228
+
229
+ it("routes google-genai providers through provider transport", async () => {
230
+ let capturedUrl = "";
231
+ let capturedBody: Record<string, unknown> | null = null;
232
+
233
+ const runtime = createRouterRuntime({
234
+ config: {
235
+ providers: {
236
+ google: {
237
+ api_base_url: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
238
+ style: "google-genai",
239
+ allow_anonymous: true,
240
+ exports: ["gemini-*"],
241
+ no_prefix: true,
242
+ },
243
+ },
244
+ executors: {},
245
+ },
246
+ fetchImpl: vi.fn(async (input, init) => {
247
+ capturedUrl = String(input);
248
+ capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
249
+ return new Response(
250
+ JSON.stringify({
251
+ modelVersion: "gemini-2.5-flash",
252
+ candidates: [{
253
+ content: { parts: [{ text: "hello" }] },
254
+ finishReason: "STOP",
255
+ }],
256
+ }),
257
+ { headers: { "content-type": "application/json; charset=utf-8" } },
258
+ );
259
+ }),
260
+ });
261
+
262
+ const ctx: RequestContext = {
263
+ request: new Request("http://local/v1/chat/completions", {
264
+ method: "POST",
265
+ headers: { "content-type": "application/json" },
266
+ body: JSON.stringify({
267
+ model: "gemini-2.5-flash",
268
+ messages: [{ role: "user", content: "hi" }],
269
+ }),
270
+ }),
271
+ incomingAuth: new Map(),
272
+ env: {},
273
+ };
274
+
275
+ const response = await handleChatCompletions({ runtime, ctx });
276
+ expect(response.status).toBe(200);
277
+ expect(capturedUrl).toBe("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent");
278
+ expect(capturedBody).toEqual({
279
+ model: "gemini-2.5-flash",
280
+ contents: [{ role: "user", parts: [{ text: "hi" }] }],
281
+ });
282
+ expect(await response.json()).toEqual({
283
+ object: "chat.completion",
284
+ model: "gemini-2.5-flash",
285
+ choices: [{
286
+ index: 0,
287
+ message: { role: "assistant", content: "hello" },
288
+ finish_reason: "stop",
289
+ }],
290
+ });
291
+ });
292
+
293
+ it("routes executor-backed aliases without re-entering provider transport", async () => {
294
+ const fetchImpl = vi.fn(async () => {
295
+ throw new Error("fetch should not be called");
296
+ });
297
+
298
+ const runtime = createRouterRuntime({
299
+ config: {
300
+ providers: {},
301
+ executors: {
302
+ enei: {
303
+ exports: ["enei-*"],
304
+ models: {
305
+ "enei-1": { executor: "enei-1" },
306
+ },
307
+ },
308
+ },
309
+ },
310
+ fetchImpl,
311
+ });
312
+
313
+ const ctx: RequestContext = {
314
+ request: new Request("http://local/v1/chat/completions", {
315
+ method: "POST",
316
+ headers: { "content-type": "application/json" },
317
+ body: JSON.stringify({
318
+ model: "enei-1",
319
+ messages: [{ role: "user", content: "hi" }],
320
+ }),
321
+ }),
322
+ incomingAuth: new Map(),
323
+ env: {},
324
+ };
325
+
326
+ const response = await handleChatCompletions({
327
+ runtime,
328
+ ctx,
329
+ executors: {
330
+ "enei-1": {
331
+ async execute(request) {
332
+ return appendAssistantMessage(request, "done");
333
+ },
334
+ async *stream(request) {
335
+ yield appendAssistantMessage(request, "done");
336
+ },
337
+ },
338
+ },
339
+ });
340
+
341
+ expect(fetchImpl).not.toHaveBeenCalled();
342
+ expect(response.headers.get("x-openairouter-executor-id")).toBe("enei-1");
343
+ expect(await response.json()).toEqual({
344
+ object: "chat.completion",
345
+ choices: [
346
+ {
347
+ index: 0,
348
+ message: { role: "assistant", content: "done" },
349
+ finish_reason: "stop",
350
+ },
351
+ ],
352
+ });
353
+ });
354
+
355
+ it("streams provider chunks as openai sse output", async () => {
356
+ const runtime = createRouterRuntime({
357
+ config: {
358
+ providers: {
359
+ openrouter: {
360
+ api_base_url: "https://openrouter.ai/api/v1",
361
+ style: "chat-completions",
362
+ allow_anonymous: true,
363
+ exports: ["gpt-4o"],
364
+ no_prefix: true,
365
+ },
366
+ },
367
+ executors: {},
368
+ },
369
+ fetchImpl: vi.fn(async () => {
370
+ const stream = new ReadableStream<Uint8Array>({
371
+ start(controller) {
372
+ controller.enqueue(
373
+ encoder.encode(
374
+ `data: ${JSON.stringify({
375
+ id: "chunk_1",
376
+ model: "gpt-4o",
377
+ choices: [{ index: 0, delta: { role: "assistant", content: "Hi" } }],
378
+ })}\n\n`,
379
+ ),
380
+ );
381
+ controller.enqueue(
382
+ encoder.encode(
383
+ `data: ${JSON.stringify({
384
+ id: "chunk_1",
385
+ model: "gpt-4o",
386
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
387
+ })}\n\n`,
388
+ ),
389
+ );
390
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
391
+ controller.close();
392
+ },
393
+ });
394
+
395
+ return new Response(stream, {
396
+ headers: { "content-type": "text/event-stream; charset=utf-8" },
397
+ });
398
+ }),
399
+ });
400
+
401
+ const ctx: RequestContext = {
402
+ request: new Request("http://local/v1/chat/completions", {
403
+ method: "POST",
404
+ headers: { "content-type": "application/json" },
405
+ body: JSON.stringify({
406
+ model: "gpt-4o",
407
+ stream: true,
408
+ messages: [{ role: "user", content: "hi" }],
409
+ }),
410
+ }),
411
+ incomingAuth: new Map(),
412
+ env: {},
413
+ };
414
+
415
+ const response = await handleChatCompletions({ runtime, ctx });
416
+ const text = await response.text();
417
+
418
+ expect(response.headers.get("content-type")).toBe("text/event-stream; charset=utf-8");
419
+ expect(text).toContain("\"content\":\"Hi\"");
420
+ expect(text).toContain("\"finish_reason\":\"stop\"");
421
+ expect(text).toContain("data: [DONE]");
422
+ });
423
+
424
+ it("returns a gateway timeout when the upstream provider hangs", async () => {
425
+ const runtime = createRouterRuntime({
426
+ config: {
427
+ providers: {
428
+ openrouter: {
429
+ api_base_url: "https://openrouter.ai/api/v1",
430
+ style: "chat-completions",
431
+ allow_anonymous: true,
432
+ exports: ["gpt-4o"],
433
+ no_prefix: true,
434
+ },
435
+ },
436
+ executors: {},
437
+ },
438
+ fetchImpl: vi.fn(async (_input, init) => {
439
+ return await new Promise<Response>((_resolve, reject) => {
440
+ init?.signal?.addEventListener(
441
+ "abort",
442
+ () => reject(init.signal?.reason ?? new Error("aborted")),
443
+ { once: true },
444
+ );
445
+ });
446
+ }),
447
+ });
448
+
449
+ const ctx: RequestContext = {
450
+ request: new Request("http://local/v1/chat/completions", {
451
+ method: "POST",
452
+ headers: { "content-type": "application/json" },
453
+ body: JSON.stringify({
454
+ model: "gpt-4o",
455
+ messages: [{ role: "user", content: "hi" }],
456
+ }),
457
+ }),
458
+ incomingAuth: new Map(),
459
+ env: {},
460
+ };
461
+
462
+ const response = await handleChatCompletions({
463
+ runtime,
464
+ ctx,
465
+ upstreamTimeoutMs: 5,
466
+ });
467
+
468
+ expect(response.status).toBe(504);
469
+ expect(await response.json()).toEqual({
470
+ error: {
471
+ message: "Upstream provider timed out after 5ms",
472
+ type: "invalid_request_error",
473
+ param: null,
474
+ code: "upstream_timeout",
475
+ },
476
+ });
477
+ });
478
+ });