@neutrome/open-ai-router 0.1.3 → 0.1.5
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/example.test.ts +178 -0
- package/src/example.ts +8 -0
- package/src/index.ts +17 -2
- package/src/router/execute.ts +81 -14
- 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/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,
|
|
@@ -102,9 +114,6 @@ export function createRouterExecutionRuntime(
|
|
|
102
114
|
if (options.executors) {
|
|
103
115
|
runtimeOptions.executors = options.executors;
|
|
104
116
|
}
|
|
105
|
-
if (options.resolveExecutor) {
|
|
106
|
-
runtimeOptions.resolveExecutor = options.resolveExecutor;
|
|
107
|
-
}
|
|
108
117
|
if (options.transforms) {
|
|
109
118
|
runtimeOptions.transforms = options.transforms;
|
|
110
119
|
}
|
|
@@ -118,9 +127,41 @@ export function createRouterExecutionRuntime(
|
|
|
118
127
|
runtimeOptions.executionIdFactory = options.executionIdFactory;
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
const userResolveExecutor = options.resolveExecutor;
|
|
131
|
+
runtimeOptions.resolveExecutor = (name) => {
|
|
132
|
+
if (userResolveExecutor) {
|
|
133
|
+
const result = userResolveExecutor(name);
|
|
134
|
+
if (result) return result;
|
|
135
|
+
}
|
|
136
|
+
return resolveNamespaceExecutor(runtime, name);
|
|
137
|
+
};
|
|
138
|
+
|
|
121
139
|
return createExecutionRuntime(runtimeOptions);
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
function resolveNamespaceExecutor(
|
|
143
|
+
runtime: RouterRuntime,
|
|
144
|
+
name: string,
|
|
145
|
+
): Executor | null {
|
|
146
|
+
const executor = runtime.executors.get(name);
|
|
147
|
+
if (!executor) return null;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
async execute(request, ctx) {
|
|
151
|
+
const model = getModel(request);
|
|
152
|
+
if (!model) throw new ExecutionError("resolution", "Request is missing model for namespace executor resolution", { stage: "target_resolution" });
|
|
153
|
+
const resolved = resolveInvocationTarget(runtime, `${name}/${model}`);
|
|
154
|
+
return ctx.invoke(request, { target: resolved.target });
|
|
155
|
+
},
|
|
156
|
+
async *stream(request, ctx) {
|
|
157
|
+
const model = getModel(request);
|
|
158
|
+
if (!model) throw new ExecutionError("resolution", "Request is missing model for namespace executor resolution", { stage: "target_resolution" });
|
|
159
|
+
const resolved = resolveInvocationTarget(runtime, `${name}/${model}`);
|
|
160
|
+
yield* ctx.invokeStream(request, { target: resolved.target });
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
124
165
|
export async function handleChatCompletions(
|
|
125
166
|
options: HandleChatCompletionsOptions,
|
|
126
167
|
): Promise<Response> {
|
|
@@ -165,10 +206,39 @@ export async function handleChatCompletions(
|
|
|
165
206
|
|
|
166
207
|
export async function handleResponses(
|
|
167
208
|
options: HandleResponsesOptions,
|
|
209
|
+
): Promise<Response> {
|
|
210
|
+
return handleProviderStyle("responses", options);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function handleAnthropicMessages(
|
|
214
|
+
options: HandleAnthropicMessagesOptions,
|
|
215
|
+
): Promise<Response> {
|
|
216
|
+
return handleProviderStyle("anthropic-messages", options);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function handleGoogleGenAI(
|
|
220
|
+
options: HandleGoogleGenAIOptions,
|
|
221
|
+
): Promise<Response> {
|
|
222
|
+
return handleProviderStyle(
|
|
223
|
+
"google-genai",
|
|
224
|
+
options,
|
|
225
|
+
options.forceStreaming ? (request) => setStreaming(request, true) : undefined,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function handleProviderStyle(
|
|
230
|
+
style: Exclude<ProviderStyle, "chat-completions">,
|
|
231
|
+
options: RouterExecutionOptions & {
|
|
232
|
+
runtime: RouterRuntime;
|
|
233
|
+
ctx: RequestContext;
|
|
234
|
+
},
|
|
235
|
+
mutateRequest?: (request: Program) => Program,
|
|
168
236
|
): Promise<Response> {
|
|
169
237
|
try {
|
|
170
238
|
const requestBody = new Uint8Array(await options.ctx.request.arrayBuffer());
|
|
171
|
-
const request =
|
|
239
|
+
const request = mutateRequest
|
|
240
|
+
? mutateRequest(parseProviderRequest(style, requestBody))
|
|
241
|
+
: parseProviderRequest(style, requestBody);
|
|
172
242
|
const resolved = resolveRequestTarget(options.runtime, request);
|
|
173
243
|
const execution = createRouterExecutionRuntime(
|
|
174
244
|
options.runtime,
|
|
@@ -180,7 +250,7 @@ export async function handleResponses(
|
|
|
180
250
|
if (isStreaming(request)) {
|
|
181
251
|
const chunks = execution.stream(request, { target: resolved.target });
|
|
182
252
|
return await streamExecutionResponse(
|
|
183
|
-
|
|
253
|
+
style,
|
|
184
254
|
chunks,
|
|
185
255
|
resolved,
|
|
186
256
|
startTime,
|
|
@@ -194,7 +264,7 @@ export async function handleResponses(
|
|
|
194
264
|
const userTime = Date.now() - startTime;
|
|
195
265
|
reportUsage(response, resolved, options.onUsage);
|
|
196
266
|
return jsonExecutionResponse(
|
|
197
|
-
emitProviderResponse(
|
|
267
|
+
emitProviderResponse(style, response),
|
|
198
268
|
resolved,
|
|
199
269
|
userTime,
|
|
200
270
|
);
|
|
@@ -336,7 +406,7 @@ export function createFetchProviderInvoker(
|
|
|
336
406
|
}
|
|
337
407
|
|
|
338
408
|
async function streamExecutionResponse(
|
|
339
|
-
responseStyle:
|
|
409
|
+
responseStyle: ProviderStyle,
|
|
340
410
|
chunks: AsyncIterable<Program>,
|
|
341
411
|
resolved: ResolvedTarget,
|
|
342
412
|
startTime: number,
|
|
@@ -357,10 +427,7 @@ async function streamExecutionResponse(
|
|
|
357
427
|
const hasUsage = usageObject(chunk) !== undefined;
|
|
358
428
|
if (hasUsage) lastChunkWithUsage = chunk;
|
|
359
429
|
if (hasUsage && !emitUsageToClient) return;
|
|
360
|
-
const bytes =
|
|
361
|
-
responseStyle === "chat-completions"
|
|
362
|
-
? emitChatCompletionsStreamChunk(chunk)
|
|
363
|
-
: emitProviderStreamChunk("responses", chunk);
|
|
430
|
+
const bytes = emitProviderStreamChunk(responseStyle, chunk);
|
|
364
431
|
controller.enqueue(
|
|
365
432
|
encoder.encode(`data: ${decoder.decode(bytes)}\n\n`),
|
|
366
433
|
);
|
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,
|