@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neutrome/open-ai-router",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
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: ["Content-Type", "Authorization", "X-OpenAIRouter-Token"],
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
  });
@@ -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 ctx = createRequestContext(c);
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 ctx = createRequestContext(c);
139
- collectIncomingAuth(ctx, runtime);
140
- const handlerOptions = {
141
- runtime,
142
- ctx,
143
- } as Parameters<typeof handleResponses>[0];
114
+ const handlerOptions = createExecutionHandlerOptions(c, runtime, options);
115
+ return handleResponses(handlerOptions);
116
+ };
117
+ }
144
118
 
145
- if (options.executors) {
146
- handlerOptions.executors = options.executors;
147
- }
148
- if (options.resolveExecutor) {
149
- handlerOptions.resolveExecutor = options.resolveExecutor;
150
- }
151
- if (options.transforms) {
152
- handlerOptions.transforms = options.transforms;
153
- }
154
- if (options.observe) {
155
- handlerOptions.observe = options.observe;
156
- }
157
- if (options.requestIdFactory) {
158
- handlerOptions.requestIdFactory = options.requestIdFactory;
159
- }
160
- if (options.executionIdFactory) {
161
- handlerOptions.executionIdFactory = options.executionIdFactory;
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
- return handleResponses(handlerOptions);
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 createRequestContext(c: Context): RequestContext {
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: c.req.raw,
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
+ }
@@ -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 { chatCompletionsHandler, listModelsHandler, responsesHandler } from "./app/handlers.ts";
50
- export type { ChatCompletionsHandlerOptions, ResponsesHandlerOptions } from "./app/handlers.ts";
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";
@@ -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
- usageObject,
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 = parseProviderRequest("responses", requestBody);
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
- "responses",
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("responses", response),
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: "chat-completions" | "responses",
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
  );
@@ -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,