@neutrome/open-ai-router 0.1.2 → 0.1.4
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/package.json +1 -1
- package/src/app/cors.ts +8 -1
- package/src/app/handlers.ts +117 -58
- package/src/auth/env.ts +28 -3
- package/src/example.test.ts +178 -0
- package/src/example.ts +8 -0
- package/src/index.ts +17 -2
- package/src/router/execute.ts +49 -11
- package/src/router/index.ts +4 -0
package/package.json
CHANGED
package/src/app/cors.ts
CHANGED
|
@@ -4,5 +4,12 @@ export const cors = () =>
|
|
|
4
4
|
honoCors({
|
|
5
5
|
origin: "*",
|
|
6
6
|
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
7
|
-
allowHeaders: [
|
|
7
|
+
allowHeaders: [
|
|
8
|
+
"Content-Type",
|
|
9
|
+
"Authorization",
|
|
10
|
+
"X-API-Key",
|
|
11
|
+
"Anthropic-Version",
|
|
12
|
+
"X-Goog-Api-Key",
|
|
13
|
+
"X-OpenAIRouter-Token",
|
|
14
|
+
],
|
|
8
15
|
});
|
package/src/app/handlers.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { Context } from "hono";
|
|
|
2
2
|
import type { Executor, ProgramTransform } from "@neutrome/lil-engine";
|
|
3
3
|
import type { RequestContext } from "../auth/types.ts";
|
|
4
4
|
import {
|
|
5
|
+
handleAnthropicMessages,
|
|
5
6
|
handleChatCompletions,
|
|
7
|
+
handleGoogleGenAI,
|
|
6
8
|
handleResponses,
|
|
7
9
|
listConfiguredModels,
|
|
8
10
|
type RouterExecutionOptions,
|
|
@@ -22,6 +24,8 @@ export type ChatCompletionsHandlerOptions = RouterExecutionOptions & {
|
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
export type ResponsesHandlerOptions = ChatCompletionsHandlerOptions;
|
|
27
|
+
export type AnthropicMessagesHandlerOptions = ChatCompletionsHandlerOptions;
|
|
28
|
+
export type GoogleGenAIHandlerOptions = ChatCompletionsHandlerOptions;
|
|
25
29
|
|
|
26
30
|
export function listModelsHandler(runtime: RouterRuntime) {
|
|
27
31
|
return async (c: Context) => {
|
|
@@ -97,35 +101,7 @@ export function chatCompletionsHandler(
|
|
|
97
101
|
options: ChatCompletionsHandlerOptions = {},
|
|
98
102
|
) {
|
|
99
103
|
return async (c: Context) => {
|
|
100
|
-
const
|
|
101
|
-
collectIncomingAuth(ctx, runtime);
|
|
102
|
-
const handlerOptions = {
|
|
103
|
-
runtime,
|
|
104
|
-
ctx,
|
|
105
|
-
} as Parameters<typeof handleChatCompletions>[0];
|
|
106
|
-
|
|
107
|
-
if (options.executors) {
|
|
108
|
-
handlerOptions.executors = options.executors;
|
|
109
|
-
}
|
|
110
|
-
if (options.resolveExecutor) {
|
|
111
|
-
handlerOptions.resolveExecutor = options.resolveExecutor;
|
|
112
|
-
}
|
|
113
|
-
if (options.transforms) {
|
|
114
|
-
handlerOptions.transforms = options.transforms;
|
|
115
|
-
}
|
|
116
|
-
if (options.observe) {
|
|
117
|
-
handlerOptions.observe = options.observe;
|
|
118
|
-
}
|
|
119
|
-
if (options.requestIdFactory) {
|
|
120
|
-
handlerOptions.requestIdFactory = options.requestIdFactory;
|
|
121
|
-
}
|
|
122
|
-
if (options.executionIdFactory) {
|
|
123
|
-
handlerOptions.executionIdFactory = options.executionIdFactory;
|
|
124
|
-
}
|
|
125
|
-
if (options.upstreamTimeoutMs !== undefined) {
|
|
126
|
-
handlerOptions.upstreamTimeoutMs = options.upstreamTimeoutMs;
|
|
127
|
-
}
|
|
128
|
-
|
|
104
|
+
const handlerOptions = createExecutionHandlerOptions(c, runtime, options);
|
|
129
105
|
return handleChatCompletions(handlerOptions);
|
|
130
106
|
};
|
|
131
107
|
}
|
|
@@ -135,42 +111,86 @@ export function responsesHandler(
|
|
|
135
111
|
options: ResponsesHandlerOptions = {},
|
|
136
112
|
) {
|
|
137
113
|
return async (c: Context) => {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
ctx,
|
|
143
|
-
} as Parameters<typeof handleResponses>[0];
|
|
114
|
+
const handlerOptions = createExecutionHandlerOptions(c, runtime, options);
|
|
115
|
+
return handleResponses(handlerOptions);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
144
118
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (options.upstreamTimeoutMs !== undefined) {
|
|
164
|
-
handlerOptions.upstreamTimeoutMs = options.upstreamTimeoutMs;
|
|
119
|
+
export function anthropicMessagesHandler(
|
|
120
|
+
runtime: RouterRuntime,
|
|
121
|
+
options: AnthropicMessagesHandlerOptions = {},
|
|
122
|
+
) {
|
|
123
|
+
return async (c: Context) => {
|
|
124
|
+
const handlerOptions = createExecutionHandlerOptions(c, runtime, options);
|
|
125
|
+
return handleAnthropicMessages(handlerOptions);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function googleGenAIHandler(
|
|
130
|
+
runtime: RouterRuntime,
|
|
131
|
+
options: GoogleGenAIHandlerOptions = {},
|
|
132
|
+
) {
|
|
133
|
+
return async (c: Context) => {
|
|
134
|
+
const route = parseGoogleGenAIRoute(c.req.raw);
|
|
135
|
+
if (!route) {
|
|
136
|
+
return c.text("Not Found", 404);
|
|
165
137
|
}
|
|
166
138
|
|
|
167
|
-
|
|
139
|
+
const request = await rewriteGoogleGenAIRequest(c.req.raw, route);
|
|
140
|
+
const handlerOptions = createExecutionHandlerOptions(
|
|
141
|
+
c,
|
|
142
|
+
runtime,
|
|
143
|
+
options,
|
|
144
|
+
request,
|
|
145
|
+
) as Parameters<typeof handleGoogleGenAI>[0];
|
|
146
|
+
if (route.stream) {
|
|
147
|
+
handlerOptions.forceStreaming = true;
|
|
148
|
+
}
|
|
149
|
+
return handleGoogleGenAI(handlerOptions);
|
|
168
150
|
};
|
|
169
151
|
}
|
|
170
152
|
|
|
171
|
-
function
|
|
153
|
+
function createExecutionHandlerOptions(
|
|
154
|
+
c: Context,
|
|
155
|
+
runtime: RouterRuntime,
|
|
156
|
+
options: ChatCompletionsHandlerOptions,
|
|
157
|
+
request = c.req.raw,
|
|
158
|
+
) {
|
|
159
|
+
const ctx = createRequestContext(c, request);
|
|
160
|
+
collectIncomingAuth(ctx, runtime);
|
|
161
|
+
const handlerOptions = {
|
|
162
|
+
runtime,
|
|
163
|
+
ctx,
|
|
164
|
+
} as Parameters<typeof handleChatCompletions>[0];
|
|
165
|
+
|
|
166
|
+
if (options.executors) {
|
|
167
|
+
handlerOptions.executors = options.executors;
|
|
168
|
+
}
|
|
169
|
+
if (options.resolveExecutor) {
|
|
170
|
+
handlerOptions.resolveExecutor = options.resolveExecutor;
|
|
171
|
+
}
|
|
172
|
+
if (options.transforms) {
|
|
173
|
+
handlerOptions.transforms = options.transforms;
|
|
174
|
+
}
|
|
175
|
+
if (options.observe) {
|
|
176
|
+
handlerOptions.observe = options.observe;
|
|
177
|
+
}
|
|
178
|
+
if (options.requestIdFactory) {
|
|
179
|
+
handlerOptions.requestIdFactory = options.requestIdFactory;
|
|
180
|
+
}
|
|
181
|
+
if (options.executionIdFactory) {
|
|
182
|
+
handlerOptions.executionIdFactory = options.executionIdFactory;
|
|
183
|
+
}
|
|
184
|
+
if (options.upstreamTimeoutMs !== undefined) {
|
|
185
|
+
handlerOptions.upstreamTimeoutMs = options.upstreamTimeoutMs;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return handlerOptions;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createRequestContext(c: Context, request = c.req.raw): RequestContext {
|
|
172
192
|
return {
|
|
173
|
-
request
|
|
193
|
+
request,
|
|
174
194
|
incomingAuth: new Map(),
|
|
175
195
|
env: (c.env ?? {}) as Record<string, unknown>,
|
|
176
196
|
state: new Map(),
|
|
@@ -190,3 +210,42 @@ function ownerFromModelId(id: string): string {
|
|
|
190
210
|
}
|
|
191
211
|
return id.slice(0, slash);
|
|
192
212
|
}
|
|
213
|
+
|
|
214
|
+
function parseGoogleGenAIRoute(request: Request): { model: string; stream: boolean } | null {
|
|
215
|
+
const pathname = new URL(request.url).pathname;
|
|
216
|
+
const match = pathname.match(/^\/v1\/models\/(.+):(generateContent|streamGenerateContent)$/);
|
|
217
|
+
if (!match?.[1] || !match[2]) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
return {
|
|
223
|
+
model: decodeURIComponent(match[1]),
|
|
224
|
+
stream: match[2] === "streamGenerateContent",
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function rewriteGoogleGenAIRequest(
|
|
232
|
+
request: Request,
|
|
233
|
+
route: { model: string; stream: boolean },
|
|
234
|
+
): Promise<Request> {
|
|
235
|
+
const bodyText = await request.text();
|
|
236
|
+
const headers = new Headers(request.headers);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const raw = JSON.parse(bodyText) as Record<string, unknown>;
|
|
240
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
241
|
+
const body = JSON.stringify({
|
|
242
|
+
...raw,
|
|
243
|
+
model: route.model,
|
|
244
|
+
...(route.stream ? { stream: true } : {}),
|
|
245
|
+
});
|
|
246
|
+
return new Request(request.url, { method: request.method, headers, body });
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
|
|
250
|
+
return new Request(request.url, { method: request.method, headers, body: bodyText });
|
|
251
|
+
}
|
package/src/auth/env.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { AuthDriver, TargetAuthContext, AuthResult } from "./types.ts";
|
|
2
2
|
|
|
3
3
|
export function envAuthDriver(suffix = "_API_KEY", prefix = ""): AuthDriver {
|
|
4
|
+
const counters = new Map<string, number>();
|
|
5
|
+
|
|
4
6
|
return {
|
|
5
7
|
name: "env",
|
|
6
8
|
collectTarget(ctx: TargetAuthContext): AuthResult | null {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
if (
|
|
9
|
+
const base = `${prefix}${ctx.providerName.toUpperCase()}`;
|
|
10
|
+
const key = resolveKey(ctx.env, base, suffix, counters);
|
|
11
|
+
if (!key) return null;
|
|
10
12
|
return {
|
|
11
13
|
headers: { authorization: `Bearer ${key}` },
|
|
12
14
|
source: "env",
|
|
@@ -14,3 +16,26 @@ export function envAuthDriver(suffix = "_API_KEY", prefix = ""): AuthDriver {
|
|
|
14
16
|
},
|
|
15
17
|
};
|
|
16
18
|
}
|
|
19
|
+
|
|
20
|
+
function resolveKey(
|
|
21
|
+
env: Record<string, unknown>,
|
|
22
|
+
base: string,
|
|
23
|
+
suffix: string,
|
|
24
|
+
counters: Map<string, number>,
|
|
25
|
+
): string | null {
|
|
26
|
+
const nValue = env[`${base}${suffix}S_N`];
|
|
27
|
+
if (nValue) {
|
|
28
|
+
const n = typeof nValue === "string" ? parseInt(nValue, 10) : typeof nValue === "number" ? nValue : 0;
|
|
29
|
+
if (n > 0) {
|
|
30
|
+
const prev = counters.get(base) ?? 0;
|
|
31
|
+
const index = (prev % n) + 1;
|
|
32
|
+
counters.set(base, prev + 1);
|
|
33
|
+
const key = env[`${base}${suffix}_${index}`];
|
|
34
|
+
if (typeof key === "string" && key) return key;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const single = env[`${base}${suffix}`];
|
|
39
|
+
if (typeof single === "string" && single) return single;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
package/src/example.test.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
appendAssistantMessage,
|
|
4
|
+
getModel,
|
|
5
|
+
streamTextResponse,
|
|
6
|
+
} from "@neutrome/lil-engine";
|
|
2
7
|
import { createApp } from "./example.ts";
|
|
3
8
|
|
|
4
9
|
const decoder = new TextDecoder();
|
|
@@ -226,4 +231,177 @@ describe("open-ai-router createApp", () => {
|
|
|
226
231
|
],
|
|
227
232
|
});
|
|
228
233
|
});
|
|
234
|
+
|
|
235
|
+
it("serves anthropic messages through the app surface", async () => {
|
|
236
|
+
let capturedBody: Record<string, unknown> | null = null;
|
|
237
|
+
|
|
238
|
+
const app = createApp({
|
|
239
|
+
config: {
|
|
240
|
+
providers: {
|
|
241
|
+
openrouter: {
|
|
242
|
+
api_base_url: "https://openrouter.ai/api/v1",
|
|
243
|
+
style: "chat-completions",
|
|
244
|
+
allow_anonymous: true,
|
|
245
|
+
exports: ["gpt-4o"],
|
|
246
|
+
no_prefix: true,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
executors: {},
|
|
250
|
+
},
|
|
251
|
+
fetchImpl: vi.fn(async (_input, init) => {
|
|
252
|
+
capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
|
|
253
|
+
return new Response(
|
|
254
|
+
JSON.stringify({
|
|
255
|
+
id: "resp_3",
|
|
256
|
+
model: "gpt-4o",
|
|
257
|
+
choices: [
|
|
258
|
+
{
|
|
259
|
+
index: 0,
|
|
260
|
+
message: { role: "assistant", content: "hello" },
|
|
261
|
+
finish_reason: "stop",
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
}),
|
|
265
|
+
{
|
|
266
|
+
headers: {
|
|
267
|
+
"content-type": "application/json; charset=utf-8",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const response = await app.request("/v1/messages", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: {
|
|
277
|
+
"content-type": "application/json",
|
|
278
|
+
},
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
model: "gpt-4o",
|
|
281
|
+
messages: [
|
|
282
|
+
{
|
|
283
|
+
role: "user",
|
|
284
|
+
content: [{ type: "text", text: "hi" }],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(response.status).toBe(200);
|
|
291
|
+
expect(capturedBody).toEqual({
|
|
292
|
+
model: "gpt-4o",
|
|
293
|
+
messages: [{ role: "user", content: "hi" }],
|
|
294
|
+
});
|
|
295
|
+
expect(await response.json()).toEqual({
|
|
296
|
+
id: "resp_3",
|
|
297
|
+
model: "gpt-4o",
|
|
298
|
+
role: "assistant",
|
|
299
|
+
content: [{ type: "text", text: "hello" }],
|
|
300
|
+
stop_reason: "end_turn",
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("serves google generateContent routes and decodes namespaced model paths", async () => {
|
|
305
|
+
let capturedModel: string | null = null;
|
|
306
|
+
let executeCalled = false;
|
|
307
|
+
|
|
308
|
+
const app = createApp({
|
|
309
|
+
config: {
|
|
310
|
+
providers: {},
|
|
311
|
+
executors: {
|
|
312
|
+
support: {
|
|
313
|
+
exports: ["*"],
|
|
314
|
+
models: {
|
|
315
|
+
"refund-router": { executor: "support/refund-router" },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
executors: {
|
|
321
|
+
"support/refund-router": {
|
|
322
|
+
async execute(request) {
|
|
323
|
+
executeCalled = true;
|
|
324
|
+
capturedModel = getModel(request) ?? null;
|
|
325
|
+
return appendAssistantMessage(request, "hello");
|
|
326
|
+
},
|
|
327
|
+
async *stream(request) {
|
|
328
|
+
capturedModel = getModel(request) ?? null;
|
|
329
|
+
yield* streamTextResponse("hello");
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const response = await app.request("/v1/models/support%2Frefund-router:generateContent", {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: {
|
|
338
|
+
"content-type": "application/json",
|
|
339
|
+
},
|
|
340
|
+
body: JSON.stringify({
|
|
341
|
+
contents: [{ role: "user", parts: [{ text: "hi" }] }],
|
|
342
|
+
}),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(response.status).toBe(200);
|
|
346
|
+
expect(executeCalled).toBe(true);
|
|
347
|
+
expect(capturedModel).toBe("support/refund-router");
|
|
348
|
+
expect(await response.json()).toEqual({
|
|
349
|
+
candidates: [
|
|
350
|
+
{
|
|
351
|
+
content: {
|
|
352
|
+
role: "model",
|
|
353
|
+
parts: [{ text: "hello" }],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("serves google streamGenerateContent routes as event streams", async () => {
|
|
361
|
+
let capturedModel: string | null = null;
|
|
362
|
+
|
|
363
|
+
const app = createApp({
|
|
364
|
+
config: {
|
|
365
|
+
providers: {},
|
|
366
|
+
executors: {
|
|
367
|
+
support: {
|
|
368
|
+
exports: ["*"],
|
|
369
|
+
models: {
|
|
370
|
+
"refund-router": { executor: "support/refund-router" },
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
executors: {
|
|
376
|
+
"support/refund-router": {
|
|
377
|
+
async execute(request) {
|
|
378
|
+
capturedModel = getModel(request) ?? null;
|
|
379
|
+
return appendAssistantMessage(request, "hello");
|
|
380
|
+
},
|
|
381
|
+
async *stream(request) {
|
|
382
|
+
capturedModel = getModel(request) ?? null;
|
|
383
|
+
yield* streamTextResponse("hello");
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const response = await app.request("/v1/models/support%2Frefund-router:streamGenerateContent", {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: {
|
|
392
|
+
"content-type": "application/json",
|
|
393
|
+
},
|
|
394
|
+
body: JSON.stringify({
|
|
395
|
+
contents: [{ role: "user", parts: [{ text: "hi" }] }],
|
|
396
|
+
}),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(response.status).toBe(200);
|
|
400
|
+
expect(capturedModel).toBe("support/refund-router");
|
|
401
|
+
expect(response.headers.get("content-type")).toContain("text/event-stream");
|
|
402
|
+
const body = await response.text();
|
|
403
|
+
expect(body).toContain("\"candidates\"");
|
|
404
|
+
expect(body).toContain("\"text\":\"hello\"");
|
|
405
|
+
expect(body).toContain("\"finishReason\":\"STOP\"");
|
|
406
|
+
});
|
|
229
407
|
});
|
package/src/example.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { cors } from "./app/cors.ts";
|
|
3
3
|
import {
|
|
4
|
+
anthropicMessagesHandler,
|
|
4
5
|
chatCompletionsHandler,
|
|
6
|
+
googleGenAIHandler,
|
|
5
7
|
listModelsHandler,
|
|
6
8
|
responsesHandler,
|
|
7
9
|
type ChatCompletionsHandlerOptions,
|
|
@@ -55,6 +57,12 @@ export function createApp(options: CreateAppOptions) {
|
|
|
55
57
|
app.use("/v1/responses", cors());
|
|
56
58
|
app.post("/v1/responses", responsesHandler(runtime, handlerOptions));
|
|
57
59
|
|
|
60
|
+
app.use("/v1/messages", cors());
|
|
61
|
+
app.post("/v1/messages", anthropicMessagesHandler(runtime, handlerOptions));
|
|
62
|
+
|
|
63
|
+
app.use("/v1/models/*", cors());
|
|
64
|
+
app.post("/v1/models/*", googleGenAIHandler(runtime, handlerOptions));
|
|
65
|
+
|
|
58
66
|
app.get("/health", healthHandler());
|
|
59
67
|
app.all("*", (c) => c.text("Not Found", 404));
|
|
60
68
|
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,9 @@ export {
|
|
|
5
5
|
createFetchProviderInvoker,
|
|
6
6
|
createRouterExecutionRuntime,
|
|
7
7
|
createRouterRuntime,
|
|
8
|
+
handleAnthropicMessages,
|
|
8
9
|
handleChatCompletions,
|
|
10
|
+
handleGoogleGenAI,
|
|
9
11
|
handleResponses,
|
|
10
12
|
listConfiguredModels,
|
|
11
13
|
ProviderHttpError,
|
|
@@ -21,7 +23,9 @@ export type {
|
|
|
21
23
|
ExecutorRouteConfig,
|
|
22
24
|
ExecutorRouteRuntime,
|
|
23
25
|
ExecutorRuntime,
|
|
26
|
+
HandleAnthropicMessagesOptions,
|
|
24
27
|
HandleChatCompletionsOptions,
|
|
28
|
+
HandleGoogleGenAIOptions,
|
|
25
29
|
HandleResponsesOptions,
|
|
26
30
|
ProviderApiStyle,
|
|
27
31
|
ProviderApiStyle as ProviderStyle,
|
|
@@ -46,5 +50,16 @@ export { proxyAuthDriver } from "./auth/proxy.ts";
|
|
|
46
50
|
|
|
47
51
|
export { cors } from "./app/cors.ts";
|
|
48
52
|
export { healthHandler } from "./app/health.ts";
|
|
49
|
-
export {
|
|
50
|
-
|
|
53
|
+
export {
|
|
54
|
+
anthropicMessagesHandler,
|
|
55
|
+
chatCompletionsHandler,
|
|
56
|
+
googleGenAIHandler,
|
|
57
|
+
listModelsHandler,
|
|
58
|
+
responsesHandler,
|
|
59
|
+
} from "./app/handlers.ts";
|
|
60
|
+
export type {
|
|
61
|
+
AnthropicMessagesHandlerOptions,
|
|
62
|
+
ChatCompletionsHandlerOptions,
|
|
63
|
+
GoogleGenAIHandlerOptions,
|
|
64
|
+
ResponsesHandlerOptions,
|
|
65
|
+
} from "./app/handlers.ts";
|
package/src/router/execute.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
emitChatCompletionsResponse,
|
|
3
|
-
emitChatCompletionsStreamChunk,
|
|
4
|
-
emitProviderResponse,
|
|
5
3
|
emitProviderRequest,
|
|
4
|
+
emitProviderResponse,
|
|
6
5
|
emitProviderStreamChunk,
|
|
7
6
|
getModel,
|
|
8
7
|
isStreaming,
|
|
@@ -11,8 +10,10 @@ import {
|
|
|
11
10
|
parseProviderResponse,
|
|
12
11
|
parseProviderStreamChunk,
|
|
13
12
|
setModel,
|
|
14
|
-
|
|
13
|
+
setStreaming,
|
|
15
14
|
type Program,
|
|
15
|
+
type ProviderStyle,
|
|
16
|
+
usageObject,
|
|
16
17
|
} from "@neutrome/lil-engine";
|
|
17
18
|
import {
|
|
18
19
|
createExecutionRuntime,
|
|
@@ -60,6 +61,17 @@ export type HandleResponsesOptions = RouterExecutionOptions & {
|
|
|
60
61
|
ctx: RequestContext;
|
|
61
62
|
};
|
|
62
63
|
|
|
64
|
+
export type HandleAnthropicMessagesOptions = RouterExecutionOptions & {
|
|
65
|
+
runtime: RouterRuntime;
|
|
66
|
+
ctx: RequestContext;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type HandleGoogleGenAIOptions = RouterExecutionOptions & {
|
|
70
|
+
runtime: RouterRuntime;
|
|
71
|
+
ctx: RequestContext;
|
|
72
|
+
forceStreaming?: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
63
75
|
export class RouterError extends Error {
|
|
64
76
|
constructor(
|
|
65
77
|
message: string,
|
|
@@ -165,10 +177,39 @@ export async function handleChatCompletions(
|
|
|
165
177
|
|
|
166
178
|
export async function handleResponses(
|
|
167
179
|
options: HandleResponsesOptions,
|
|
180
|
+
): Promise<Response> {
|
|
181
|
+
return handleProviderStyle("responses", options);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function handleAnthropicMessages(
|
|
185
|
+
options: HandleAnthropicMessagesOptions,
|
|
186
|
+
): Promise<Response> {
|
|
187
|
+
return handleProviderStyle("anthropic-messages", options);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function handleGoogleGenAI(
|
|
191
|
+
options: HandleGoogleGenAIOptions,
|
|
192
|
+
): Promise<Response> {
|
|
193
|
+
return handleProviderStyle(
|
|
194
|
+
"google-genai",
|
|
195
|
+
options,
|
|
196
|
+
options.forceStreaming ? (request) => setStreaming(request, true) : undefined,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function handleProviderStyle(
|
|
201
|
+
style: Exclude<ProviderStyle, "chat-completions">,
|
|
202
|
+
options: RouterExecutionOptions & {
|
|
203
|
+
runtime: RouterRuntime;
|
|
204
|
+
ctx: RequestContext;
|
|
205
|
+
},
|
|
206
|
+
mutateRequest?: (request: Program) => Program,
|
|
168
207
|
): Promise<Response> {
|
|
169
208
|
try {
|
|
170
209
|
const requestBody = new Uint8Array(await options.ctx.request.arrayBuffer());
|
|
171
|
-
const request =
|
|
210
|
+
const request = mutateRequest
|
|
211
|
+
? mutateRequest(parseProviderRequest(style, requestBody))
|
|
212
|
+
: parseProviderRequest(style, requestBody);
|
|
172
213
|
const resolved = resolveRequestTarget(options.runtime, request);
|
|
173
214
|
const execution = createRouterExecutionRuntime(
|
|
174
215
|
options.runtime,
|
|
@@ -180,7 +221,7 @@ export async function handleResponses(
|
|
|
180
221
|
if (isStreaming(request)) {
|
|
181
222
|
const chunks = execution.stream(request, { target: resolved.target });
|
|
182
223
|
return await streamExecutionResponse(
|
|
183
|
-
|
|
224
|
+
style,
|
|
184
225
|
chunks,
|
|
185
226
|
resolved,
|
|
186
227
|
startTime,
|
|
@@ -194,7 +235,7 @@ export async function handleResponses(
|
|
|
194
235
|
const userTime = Date.now() - startTime;
|
|
195
236
|
reportUsage(response, resolved, options.onUsage);
|
|
196
237
|
return jsonExecutionResponse(
|
|
197
|
-
emitProviderResponse(
|
|
238
|
+
emitProviderResponse(style, response),
|
|
198
239
|
resolved,
|
|
199
240
|
userTime,
|
|
200
241
|
);
|
|
@@ -336,7 +377,7 @@ export function createFetchProviderInvoker(
|
|
|
336
377
|
}
|
|
337
378
|
|
|
338
379
|
async function streamExecutionResponse(
|
|
339
|
-
responseStyle:
|
|
380
|
+
responseStyle: ProviderStyle,
|
|
340
381
|
chunks: AsyncIterable<Program>,
|
|
341
382
|
resolved: ResolvedTarget,
|
|
342
383
|
startTime: number,
|
|
@@ -357,10 +398,7 @@ async function streamExecutionResponse(
|
|
|
357
398
|
const hasUsage = usageObject(chunk) !== undefined;
|
|
358
399
|
if (hasUsage) lastChunkWithUsage = chunk;
|
|
359
400
|
if (hasUsage && !emitUsageToClient) return;
|
|
360
|
-
const bytes =
|
|
361
|
-
responseStyle === "chat-completions"
|
|
362
|
-
? emitChatCompletionsStreamChunk(chunk)
|
|
363
|
-
: emitProviderStreamChunk("responses", chunk);
|
|
401
|
+
const bytes = emitProviderStreamChunk(responseStyle, chunk);
|
|
364
402
|
controller.enqueue(
|
|
365
403
|
encoder.encode(`data: ${decoder.decode(bytes)}\n\n`),
|
|
366
404
|
);
|
package/src/router/index.ts
CHANGED
|
@@ -2,7 +2,9 @@ export { createRouterRuntime } from "./runtime.ts";
|
|
|
2
2
|
export {
|
|
3
3
|
createFetchProviderInvoker,
|
|
4
4
|
createRouterExecutionRuntime,
|
|
5
|
+
handleAnthropicMessages,
|
|
5
6
|
handleChatCompletions,
|
|
7
|
+
handleGoogleGenAI,
|
|
6
8
|
handleResponses,
|
|
7
9
|
resolveRequestTarget,
|
|
8
10
|
RouterError,
|
|
@@ -25,7 +27,9 @@ export type {
|
|
|
25
27
|
RouterRuntime,
|
|
26
28
|
} from "./runtime.ts";
|
|
27
29
|
export type {
|
|
30
|
+
HandleAnthropicMessagesOptions,
|
|
28
31
|
HandleChatCompletionsOptions,
|
|
32
|
+
HandleGoogleGenAIOptions,
|
|
29
33
|
HandleResponsesOptions,
|
|
30
34
|
RouterExecutionOptions,
|
|
31
35
|
UsageReport,
|