@neutrome/open-ai-router 0.5.6 → 0.5.7

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.5.6",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -11,8 +11,8 @@
11
11
  "hono": "^4.12.18",
12
12
  "posthog-node": "^5.41.0",
13
13
  "zod": "^4.2.1",
14
- "@neutrome/lil-engine": "0.3.1",
15
- "@neutrome/lilsdk-ts": "0.3.2"
14
+ "@neutrome/lil-engine": "0.3.2",
15
+ "@neutrome/lilsdk-ts": "0.3.3"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/node": "^25.9.3",
@@ -24,6 +24,79 @@ const config: RouterConfig = {
24
24
  };
25
25
 
26
26
  describe("createRouterApp trace finalization", () => {
27
+ it("returns routing headers for versioned Google GenAI requests", async () => {
28
+ const app = createRouterApp({
29
+ config: {
30
+ providers: {
31
+ google: {
32
+ apiBaseUrl: "https://google.example.test/v1beta/openai",
33
+ style: "chat-completions",
34
+ exports: ["*"],
35
+ },
36
+ },
37
+ modelNamespaces: {},
38
+ },
39
+ executorImplementations: {},
40
+ auth: { authenticate: async () => principal() },
41
+ fetchImpl: vi.fn(async () =>
42
+ Response.json({
43
+ id: "chat_1",
44
+ model: "gemini-2.5-flash-lite",
45
+ choices: [
46
+ {
47
+ index: 0,
48
+ message: { role: "assistant", content: "hello" },
49
+ finish_reason: "stop",
50
+ },
51
+ ],
52
+ }),
53
+ ),
54
+ });
55
+
56
+ const response = await app.fetch(
57
+ new Request(
58
+ "http://local/v1beta/models/google%2Fgemini-2.5-flash-lite:generateContent",
59
+ {
60
+ method: "POST",
61
+ headers: { "content-type": "application/json" },
62
+ body: JSON.stringify({
63
+ contents: [{ role: "user", parts: [{ text: "hi" }] }],
64
+ }),
65
+ },
66
+ ),
67
+ {},
68
+ );
69
+
70
+ expect(response.status).toBe(200);
71
+ expect(response.headers.get("x-openairouter-provider-id")).toBe("google");
72
+ expect(response.headers.get("x-openairouter-model-id")).toBe(
73
+ "gemini-2.5-flash-lite",
74
+ );
75
+ });
76
+
77
+ it("rejects Responses state IDs instead of silently dropping provider state", async () => {
78
+ const app = createRouterApp({
79
+ config: { providers: {}, modelNamespaces: {} },
80
+ executorImplementations: {},
81
+ auth: { authenticate: async () => principal() },
82
+ });
83
+ const response = await app.request("http://local/v1/responses", {
84
+ method: "POST",
85
+ headers: { "content-type": "application/json" },
86
+ body: JSON.stringify({
87
+ model: "ignored",
88
+ previous_response_id: "resp_1",
89
+ }),
90
+ });
91
+ expect(response.status).toBe(400);
92
+ await expect(response.json()).resolves.toMatchObject({
93
+ error: {
94
+ code: "unsupported_response_state",
95
+ type: "invalid_request_error",
96
+ },
97
+ });
98
+ });
99
+
27
100
  it("keeps non-stream trace hooks alive with waitUntil", async () => {
28
101
  const waited: Promise<unknown>[] = [];
29
102
  const onExecutionSummary = vi.fn(async (_summary: ExecutionSummary) => {});
@@ -148,7 +221,6 @@ describe("createRouterApp trace finalization", () => {
148
221
  (span) => span.event,
149
222
  );
150
223
  expect(events).toContain("admission.authenticate");
151
- expect(events).toContain("upstream_auth.collect_incoming");
152
224
  expect(events).toContain("upstream_auth.resolve_target");
153
225
  expect(events).toContain("provider.stream_first_byte");
154
226
  expect(events).toContain("response.stream_first_chunk");
@@ -21,6 +21,8 @@ import type { RequestContext } from "../upstream-auth/types.ts";
21
21
  import type { ExecutionRuntimeOptions } from "../router/execution-types.ts";
22
22
  import { observeTimedExecution } from "../router/execution-events.ts";
23
23
  import { createOpenAiErrorResponse } from "../router/errors.ts";
24
+ import { errorResponse } from "../router/error-codec.ts";
25
+ import { RouterError } from "../router/errors.ts";
24
26
  import {
25
27
  parseGoogleGenAIRoute,
26
28
  rewriteGoogleGenAIRequest,
@@ -67,7 +69,7 @@ export function createRouterApp(options: RouterAppOptions) {
67
69
  runAuthorized(c, c.req.raw, "anthropic-messages", handleAnthropicMessages),
68
70
  );
69
71
 
70
- app.post("/v1/models/*", async (c) => {
72
+ app.post("/:version/models/*", async (c) => {
71
73
  const route = parseGoogleGenAIRoute(c.req.raw);
72
74
  if (!route) return c.text("Not Found", 404);
73
75
  const request = await rewriteGoogleGenAIRequest(c.req.raw, route);
@@ -242,26 +244,20 @@ export function createRouterApp(options: RouterAppOptions) {
242
244
  }
243
245
  }
244
246
 
245
- const ctx: RequestContext = {
246
- request,
247
- incomingAuth: new Map(),
248
- env,
249
- state: new Map(),
250
- };
251
- for (const driver of runtime.upstreamAuthChain) {
252
- const collectStartedMs = Date.now();
253
- driver.collectIncoming?.(ctx);
254
- observeTimedExecution({
255
- observe,
256
- kind: "upstream_auth.collect_incoming",
257
- requestId: incomingExecutionId,
258
- executionId: incomingExecutionId,
259
- startedAtMs: collectStartedMs,
260
- finishedAtMs: Date.now(),
261
- data: { driver: driver.name },
262
- });
247
+ if (style === "responses" && (await hasPreviousResponseId(request))) {
248
+ finalizeTrace();
249
+ return errorResponse(
250
+ new RouterError(
251
+ "previous_response_id is not supported because the router does not retain provider response state.",
252
+ 400,
253
+ "unsupported_response_state",
254
+ ),
255
+ style,
256
+ );
263
257
  }
264
258
 
259
+ const ctx: RequestContext = { request, env };
260
+
265
261
  try {
266
262
  const handlerStartedMs = Date.now();
267
263
  const response = await handler({
@@ -311,3 +307,17 @@ export function createRouterApp(options: RouterAppOptions) {
311
307
 
312
308
  return app;
313
309
  }
310
+
311
+ async function hasPreviousResponseId(request: Request): Promise<boolean> {
312
+ try {
313
+ const body: unknown = await request.clone().json();
314
+ return !!(
315
+ body &&
316
+ typeof body === "object" &&
317
+ "previous_response_id" in body &&
318
+ (body as { previous_response_id?: unknown }).previous_response_id
319
+ );
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseGoogleGenAIRoute } from "./google-genai-route.ts";
3
+
4
+ describe("Google GenAI routes", () => {
5
+ it("accepts the API version selected by the client SDK", () => {
6
+ expect(
7
+ parseGoogleGenAIRoute(
8
+ new Request(
9
+ "http://local/v1beta/models/google%2Fgemini-2.5-flash:streamGenerateContent",
10
+ ),
11
+ ),
12
+ ).toEqual({ model: "google/gemini-2.5-flash", stream: true });
13
+ });
14
+ });
@@ -5,7 +5,7 @@ export function parseGoogleGenAIRoute(
5
5
  ): GoogleGenAIRoute | null {
6
6
  const pathname = new URL(request.url).pathname;
7
7
  const match = pathname.match(
8
- /^\/v1\/models\/(.+):(generateContent|streamGenerateContent)$/,
8
+ /^\/[^/]+\/models\/(.+):(generateContent|streamGenerateContent)$/,
9
9
  );
10
10
  if (!match?.[1] || !match[2]) return null;
11
11
  try {
package/src/index.ts CHANGED
@@ -71,7 +71,9 @@ export type {
71
71
  RouterConfig,
72
72
  RouterExecutionOptions,
73
73
  RouterRuntime,
74
- UpstreamAuthConfig,
74
+ ProviderAuthDriverConfig,
75
+ ProviderEnvAuthDriverConfig,
76
+ ProviderProxyAuthDriverConfig,
75
77
  } from "./router/index.ts";
76
78
  export type {
77
79
  RemoteMcpClientFactory,
@@ -85,14 +87,5 @@ export {
85
87
  wireToolName,
86
88
  } from "./router/index.ts";
87
89
 
88
- export type {
89
- AuthResult,
90
- RequestContext,
91
- TargetAuthContext,
92
- UpstreamAuthDriver,
93
- } from "./upstream-auth/types.ts";
94
- export { buildUpstreamAuthChain } from "./upstream-auth/registry.ts";
95
- export { envAuthDriver } from "./upstream-auth/env.ts";
96
- export { proxyAuthDriver } from "./upstream-auth/proxy.ts";
97
90
  export { canAccessPrincipalModel } from "./admission/access.ts";
98
91
  export type { AuthPrincipal, RuntimeAuthenticator } from "./admission/types.ts";
@@ -1,15 +1,31 @@
1
1
  import type { ProviderStyle } from "@neutrome/lil-engine";
2
- import type { UpstreamAuthConfigShape } from "../upstream-auth/config.ts";
3
-
4
2
  export type ProviderApiStyle = ProviderStyle;
5
3
 
6
- export type UpstreamAuthConfig = UpstreamAuthConfigShape;
4
+ export type ProviderEnvAuthDriverConfig = {
5
+ kind: "env";
6
+ env: string;
7
+ header: string;
8
+ prefix?: string;
9
+ };
10
+
11
+ export type ProviderProxyAuthDriverConfig = {
12
+ kind: "proxy";
13
+ sourceHeader?: string;
14
+ sourcePrefix?: string;
15
+ header: string;
16
+ prefix?: string;
17
+ };
18
+
19
+ export type ProviderAuthDriverConfig =
20
+ | ProviderEnvAuthDriverConfig
21
+ | ProviderProxyAuthDriverConfig;
7
22
 
8
23
  export type ProviderTargetConfig = {
9
24
  apiBaseUrl: string;
10
25
  style: ProviderApiStyle;
11
26
  exports?: string[];
12
27
  headers?: Record<string, string>;
28
+ auth?: ProviderAuthDriverConfig[];
13
29
  };
14
30
 
15
31
  export type ModelRouteConfig =
@@ -46,7 +62,6 @@ export type RemoteMcpServerConfig = {
46
62
 
47
63
  export type RouterConfig = {
48
64
  modelSuffixes?: string[];
49
- upstreamAuth?: UpstreamAuthConfig;
50
65
  providers: Record<string, ProviderTargetConfig>;
51
66
  modelNamespaces: Record<string, ModelNamespaceConfig>;
52
67
  mcps?: Record<string, RemoteMcpServerConfig>;
@@ -75,7 +75,7 @@ describe("router execution", () => {
75
75
 
76
76
  expect(response.status).toBe(200);
77
77
  expect(capturedUrl).toBe("https://openrouter.ai/api/v1/chat/completions");
78
- expect(capturedAuth).toBe("Bearer incoming");
78
+ expect(capturedAuth).toBe("");
79
79
  expect(capturedBody).toEqual({
80
80
  model: "gpt-4o",
81
81
  messages: [{ role: "user", content: "hi" }],
@@ -435,8 +435,7 @@ describe("router execution", () => {
435
435
  config: {
436
436
  providers: {
437
437
  google: {
438
- apiBaseUrl:
439
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
438
+ apiBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
440
439
  style: "google-genai",
441
440
  exports: ["*"],
442
441
  },
@@ -772,6 +771,12 @@ describe("router execution", () => {
772
771
  });
773
772
 
774
773
  expect(response.status).toBe(529);
774
+ expect(response.headers.get("x-openairouter-provider-id")).toBe(
775
+ "anthropic",
776
+ );
777
+ expect(response.headers.get("x-openairouter-model-id")).toBe(
778
+ "claude-sonnet-4",
779
+ );
775
780
  expect(await response.json()).toEqual({
776
781
  error: {
777
782
  message: "Provider is overloaded",
@@ -33,6 +33,7 @@ import {
33
33
  } from "./upstream-client.ts";
34
34
  import { emitProviderUsage, injectStreamUsage } from "./provider-telemetry.ts";
35
35
  import {
36
+ applyRoutingHeaders,
36
37
  jsonExecutionResponse,
37
38
  streamExecutionResponse,
38
39
  } from "./response-delivery.ts";
@@ -195,6 +196,7 @@ async function handleRequestStyle(
195
196
  emitResponse: (program: Program) => Uint8Array,
196
197
  mutateRequest?: (request: Program) => Program,
197
198
  ): Promise<Response> {
199
+ let resolved: ResolvedTarget | undefined;
198
200
  try {
199
201
  const baseRequest =
200
202
  options.program ??
@@ -202,7 +204,7 @@ async function handleRequestStyle(
202
204
  const request = mutateRequest ? mutateRequest(baseRequest) : baseRequest;
203
205
  const incomingExecutionId =
204
206
  options.incomingExecutionId ?? `incoming_${crypto.randomUUID()}`;
205
- const resolved = resolveRequestTarget(options.runtime, request);
207
+ resolved = resolveRequestTarget(options.runtime, request);
206
208
  const remoteMcp = prepareRemoteMcpExecution(
207
209
  options.runtime,
208
210
  resolved,
@@ -247,7 +249,8 @@ async function handleRequestStyle(
247
249
  await remoteMcp.close();
248
250
  }
249
251
  } catch (error) {
250
- return errorResponse(error, style);
252
+ const response = errorResponse(error, style);
253
+ return resolved ? applyRoutingHeaders(response, resolved) : response;
251
254
  }
252
255
  }
253
256
 
@@ -34,7 +34,9 @@ export type {
34
34
  RemoteMcpServerConfig,
35
35
  RemoteMcpToolConfig,
36
36
  RouterConfig,
37
- UpstreamAuthConfig,
37
+ ProviderAuthDriverConfig,
38
+ ProviderEnvAuthDriverConfig,
39
+ ProviderProxyAuthDriverConfig,
38
40
  } from "./config.ts";
39
41
  export type {
40
42
  CreateRouterOptions,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  emitProviderError,
3
3
  emitProviderStreamChunk,
4
+ repackStream,
4
5
  type Program,
5
6
  type ProviderStyle,
6
7
  } from "@neutrome/lil-engine";
@@ -20,7 +21,9 @@ export async function streamExecutionResponse(
20
21
  input: { requestId: string; executionId: string; startedAtMs: number },
21
22
  cleanup?: () => Promise<void>,
22
23
  ): Promise<Response> {
23
- const iterator = chunks[Symbol.asyncIterator]();
24
+ const iterator = repackStream(chunks, { target: responseStyle })[
25
+ Symbol.asyncIterator
26
+ ]();
24
27
 
25
28
  const first = await iterator.next();
26
29
  observeTimedExecution({
@@ -57,7 +60,7 @@ export async function streamExecutionResponse(
57
60
  const stream = new ReadableStream<Uint8Array>({
58
61
  start(controller) {
59
62
  const bytes = emitProviderStreamChunk(responseStyle, first.value);
60
- controller.enqueue(encoder.encode(`data: ${decoder.decode(bytes)}\n\n`));
63
+ controller.enqueue(encoder.encode(streamEvent(responseStyle, bytes)));
61
64
  },
62
65
  async pull(controller) {
63
66
  if (done) return;
@@ -73,9 +76,7 @@ export async function streamExecutionResponse(
73
76
  return;
74
77
  }
75
78
  const bytes = emitProviderStreamChunk(responseStyle, result.value);
76
- controller.enqueue(
77
- encoder.encode(`data: ${decoder.decode(bytes)}\n\n`),
78
- );
79
+ controller.enqueue(encoder.encode(streamEvent(responseStyle, bytes)));
79
80
  } catch (error) {
80
81
  done = true;
81
82
  await cleanup?.();
@@ -89,7 +90,7 @@ export async function streamExecutionResponse(
89
90
  },
90
91
  async cancel() {
91
92
  await cleanup?.();
92
- await iterator.return?.();
93
+ await iterator.return?.(undefined);
93
94
  },
94
95
  });
95
96
 
@@ -101,6 +102,14 @@ export async function streamExecutionResponse(
101
102
  });
102
103
  }
103
104
 
105
+ function streamEvent(style: ProviderStyle, bytes: Uint8Array): string {
106
+ const data = decoder.decode(bytes);
107
+ if (style !== "anthropic-messages") return `data: ${data}\n\n`;
108
+ const parsed = JSON.parse(data) as { type?: unknown };
109
+ if (typeof parsed.type !== "string") return `data: ${data}\n\n`;
110
+ return `event: ${parsed.type}\ndata: ${data}\n\n`;
111
+ }
112
+
104
113
  export function jsonExecutionResponse(
105
114
  body: Uint8Array,
106
115
  resolved: ResolvedTarget,
@@ -115,6 +124,16 @@ export function jsonExecutionResponse(
115
124
  });
116
125
  }
117
126
 
127
+ export function applyRoutingHeaders(
128
+ response: Response,
129
+ resolved: ResolvedTarget,
130
+ ): Response {
131
+ for (const [name, value] of Object.entries(routingHeaders(resolved))) {
132
+ response.headers.set(name, value);
133
+ }
134
+ return response;
135
+ }
136
+
118
137
  function routingHeaders(resolved: ResolvedTarget): Record<string, string> {
119
138
  if (resolved.target.kind === "provider") {
120
139
  return {
@@ -1,6 +1,4 @@
1
1
  import type { ExecutionTarget } from "@neutrome/lilsdk-ts";
2
- import type { UpstreamAuthDriver } from "../upstream-auth/types.ts";
3
- import { buildUpstreamAuthChain } from "../upstream-auth/registry.ts";
4
2
  import { createExportsMatcher } from "../util/glob.ts";
5
3
  import type {
6
4
  ModelNamespaceConfig,
@@ -16,6 +14,7 @@ export type ProviderRuntime = {
16
14
  apiBaseUrl: string;
17
15
  endpointPath: string;
18
16
  headers: Readonly<Record<string, string>>;
17
+ auth?: ProviderTargetConfig["auth"];
19
18
  exportsMatcher: (model: string) => boolean;
20
19
  };
21
20
 
@@ -39,7 +38,6 @@ export type RouterRuntime = {
39
38
  modelNamespaces: ReadonlyMap<string, ModelNamespaceRuntime>;
40
39
  modelNamespaceOrder: readonly string[];
41
40
  mcps: ReadonlyMap<string, RemoteMcpServerRuntime>;
42
- upstreamAuthChain: readonly UpstreamAuthDriver[];
43
41
  modelSuffixes: ReadonlySet<string>;
44
42
  fetchImpl: typeof fetch;
45
43
  };
@@ -51,7 +49,6 @@ export type RemoteMcpServerRuntime = RemoteMcpServerConfig & {
51
49
  export type CreateRouterOptions = {
52
50
  config: RouterConfig;
53
51
  fetchImpl?: typeof fetch;
54
- upstreamAuthDrivers?: readonly UpstreamAuthDriver[];
55
52
  };
56
53
 
57
54
  export function createRouterRuntime(
@@ -84,9 +81,6 @@ export function createRouterRuntime(
84
81
  modelNamespaces,
85
82
  modelNamespaceOrder,
86
83
  mcps,
87
- upstreamAuthChain:
88
- options.upstreamAuthDrivers ??
89
- buildUpstreamAuthChain(options.config.upstreamAuth),
90
84
  modelSuffixes: new Set(options.config.modelSuffixes ?? []),
91
85
  fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis),
92
86
  };
@@ -159,6 +153,7 @@ function buildProviderRuntime(
159
153
  apiBaseUrl: config.apiBaseUrl,
160
154
  endpointPath: defaultEndpointPath(config.style),
161
155
  headers: config.headers ?? {},
156
+ auth: config.auth,
162
157
  exportsMatcher: createExportsMatcher(config.exports),
163
158
  };
164
159
  }
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyProviderAuth, providerUpstreamUrl } from "./upstream-client.ts";
3
+ import type { ProviderRuntime } from "./runtime.ts";
4
+
5
+ const provider = (
6
+ overrides: Partial<ProviderRuntime> = {},
7
+ ): ProviderRuntime => ({
8
+ name: "google",
9
+ style: "google-genai",
10
+ apiBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
11
+ endpointPath: "",
12
+ headers: {},
13
+ auth: [{ kind: "env", env: "GEMINI_API_KEY", header: "x-goog-api-key" }],
14
+ exportsMatcher: () => true,
15
+ ...overrides,
16
+ });
17
+
18
+ describe("provider upstream configuration", () => {
19
+ it("uses provider credentials in preference to incoming client credentials", () => {
20
+ const headers = new Headers({ authorization: "Bearer client" });
21
+ applyProviderAuth(
22
+ provider({
23
+ auth: [
24
+ {
25
+ kind: "env",
26
+ env: "OPENAI_API_KEY",
27
+ header: "authorization",
28
+ prefix: "Bearer ",
29
+ },
30
+ { kind: "proxy", header: "authorization", sourcePrefix: "Bearer " },
31
+ ],
32
+ }),
33
+ headers,
34
+ new Headers({ authorization: "Bearer client" }),
35
+ { OPENAI_API_KEY: "provider" },
36
+ );
37
+ expect(headers.get("authorization")).toBe("Bearer provider");
38
+ });
39
+
40
+ it("injects explicitly enabled proxy credentials into the provider header", () => {
41
+ const headers = new Headers({ authorization: "Bearer test-token" });
42
+ applyProviderAuth(
43
+ provider({
44
+ auth: [
45
+ {
46
+ kind: "proxy",
47
+ sourceHeader: "authorization",
48
+ sourcePrefix: "Bearer ",
49
+ header: "x-goog-api-key",
50
+ },
51
+ ],
52
+ }),
53
+ headers,
54
+ new Headers({ authorization: "Bearer provider-key" }),
55
+ {},
56
+ );
57
+
58
+ expect(headers.get("authorization")).toBe("Bearer test-token");
59
+ expect(headers.get("x-goog-api-key")).toBe("provider-key");
60
+ });
61
+
62
+ it("builds Google generate and SSE stream URLs from the routed model", () => {
63
+ expect(
64
+ providerUpstreamUrl(
65
+ provider(),
66
+ "gemini-2.5-flash-lite",
67
+ new TextEncoder().encode('{"stream":false}'),
68
+ ),
69
+ ).toBe(
70
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent",
71
+ );
72
+ expect(
73
+ providerUpstreamUrl(
74
+ provider(),
75
+ "gemini-2.5-flash-lite",
76
+ new TextEncoder().encode('{"stream":true}'),
77
+ ),
78
+ ).toBe(
79
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent?alt=sse",
80
+ );
81
+ });
82
+ });
@@ -2,10 +2,7 @@ import type { ProviderInvocationContext } from "./execution-types.ts";
2
2
  import { RouterError } from "./errors.ts";
3
3
  import { toBody } from "./error-codec.ts";
4
4
  import type { ProviderRuntime, RouterRuntime } from "./runtime.ts";
5
- import type {
6
- RequestContext,
7
- TargetAuthContext,
8
- } from "../upstream-auth/types.ts";
5
+ import type { RequestContext } from "../upstream-auth/types.ts";
9
6
  import { cloneForwardHeaders } from "../util/headers.ts";
10
7
  import { observeTimedExecution } from "./execution-events.ts";
11
8
 
@@ -18,49 +15,90 @@ export async function fetchProvider(
18
15
  providerCtx: ProviderInvocationContext,
19
16
  ): Promise<Response> {
20
17
  const headers = cloneForwardHeaders(ctx.request.headers);
18
+ headers.delete("authorization");
19
+ headers.delete("x-api-key");
20
+ headers.delete("x-goog-api-key");
21
21
  headers.set("content-type", "application/json");
22
22
  for (const [key, value] of Object.entries(provider.headers)) {
23
23
  headers.set(key, value);
24
24
  }
25
25
 
26
- for (const driver of runtime.upstreamAuthChain) {
27
- const targetCtx: TargetAuthContext = {
28
- ...ctx,
29
- providerName: provider.name,
30
- };
31
- const authStartedMs = Date.now();
32
- const auth = await driver.collectTarget(targetCtx);
33
- observeTimedExecution({
34
- observe: providerCtx.observe,
35
- kind: "upstream_auth.resolve_target",
36
- requestId: providerCtx.requestId,
37
- executionId: providerCtx.executionId,
38
- ...(providerCtx.parentExecutionId
39
- ? { parentExecutionId: providerCtx.parentExecutionId }
40
- : {}),
41
- startedAtMs: authStartedMs,
42
- finishedAtMs: Date.now(),
43
- data: {
44
- provider: provider.name,
45
- model: providerCtx.target.model,
46
- driver: driver.name,
47
- source: auth?.source,
48
- matched: !!auth,
49
- },
50
- });
51
- if (!auth) {
52
- continue;
26
+ const authStartedMs = Date.now();
27
+ const authSource = applyProviderAuth(
28
+ provider,
29
+ headers,
30
+ ctx.request.headers,
31
+ ctx.env,
32
+ );
33
+ observeTimedExecution({
34
+ observe: providerCtx.observe,
35
+ kind: "upstream_auth.resolve_target",
36
+ requestId: providerCtx.requestId,
37
+ executionId: providerCtx.executionId,
38
+ ...(providerCtx.parentExecutionId
39
+ ? { parentExecutionId: providerCtx.parentExecutionId }
40
+ : {}),
41
+ startedAtMs: authStartedMs,
42
+ finishedAtMs: Date.now(),
43
+ data: {
44
+ provider: provider.name,
45
+ model: providerCtx.target.model,
46
+ source: authSource ?? undefined,
47
+ matched: authSource !== null,
48
+ },
49
+ });
50
+
51
+ return runtime.fetchImpl(
52
+ providerUpstreamUrl(provider, providerCtx.target.model, body),
53
+ {
54
+ method: "POST",
55
+ headers,
56
+ body: toBody(body),
57
+ signal,
58
+ },
59
+ );
60
+ }
61
+
62
+ export function applyProviderAuth(
63
+ provider: ProviderRuntime,
64
+ headers: Headers,
65
+ incomingHeaders: Headers,
66
+ env: Record<string, unknown>,
67
+ ): "env" | "proxy" | null {
68
+ for (const driver of provider.auth ?? []) {
69
+ if (driver.kind === "env") {
70
+ const value = env[driver.env];
71
+ if (typeof value !== "string" || !value) continue;
72
+ headers.set(driver.header, `${driver.prefix ?? ""}${value}`);
73
+ return "env";
53
74
  }
54
- new Headers(auth.headers).forEach((value, key) => headers.set(key, value));
55
- break;
75
+
76
+ const incoming = incomingHeaders.get(
77
+ driver.sourceHeader ?? "authorization",
78
+ );
79
+ if (!incoming) continue;
80
+ const sourcePrefix = driver.sourcePrefix ?? "";
81
+ if (sourcePrefix && !incoming.startsWith(sourcePrefix)) continue;
82
+ headers.set(
83
+ driver.header,
84
+ `${driver.prefix ?? ""}${incoming.slice(sourcePrefix.length)}`,
85
+ );
86
+ return "proxy";
56
87
  }
88
+ return null;
89
+ }
57
90
 
58
- return runtime.fetchImpl(`${provider.apiBaseUrl}${provider.endpointPath}`, {
59
- method: "POST",
60
- headers,
61
- body: toBody(body),
62
- signal,
63
- });
91
+ export function providerUpstreamUrl(
92
+ provider: ProviderRuntime,
93
+ model: string,
94
+ body: Uint8Array,
95
+ ): string {
96
+ if (provider.style !== "google-genai") {
97
+ return `${provider.apiBaseUrl}${provider.endpointPath}`;
98
+ }
99
+ const stream = JSON.parse(new TextDecoder().decode(body)).stream === true;
100
+ const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
101
+ return `${provider.apiBaseUrl}/models/${encodeURIComponent(model)}:${action}`;
64
102
  }
65
103
 
66
104
  export function getProviderOrThrow(
@@ -71,12 +71,14 @@ const EventDataSchema = z
71
71
  })
72
72
  .passthrough();
73
73
 
74
- export function createRouterTelemetry(input: {
75
- env?: Record<string, unknown>;
76
- distinctId?: string;
77
- traceDriver?: TraceTelemetryDriver;
78
- errorDriver?: ErrorTelemetryDriver;
79
- } = {}): RouterTelemetry {
74
+ export function createRouterTelemetry(
75
+ input: {
76
+ env?: Record<string, unknown>;
77
+ distinctId?: string;
78
+ traceDriver?: TraceTelemetryDriver;
79
+ errorDriver?: ErrorTelemetryDriver;
80
+ } = {},
81
+ ): RouterTelemetry {
80
82
  const drivers = createPosthogTelemetryDrivers({
81
83
  apiKey: input.env?.POSTHOG_API_KEY,
82
84
  host: input.env?.POSTHOG_HOST,
@@ -97,7 +99,9 @@ export function createRouterTelemetry(input: {
97
99
  function observe(event: ExecutionEvent): void {
98
100
  const timing = eventTiming(event);
99
101
  if (isInvocationStarted(event.kind)) {
100
- const active = { summary: createSpan(event, timing.startedAt ?? event.timestamp) };
102
+ const active = {
103
+ summary: createSpan(event, timing.startedAt ?? event.timestamp),
104
+ };
101
105
  spans.push(active.summary);
102
106
  activeSpans.set(event.executionId, active);
103
107
  return;
@@ -107,7 +111,8 @@ export function createRouterTelemetry(input: {
107
111
  if (active) {
108
112
  active.summary.finishedAt = timing.finishedAt ?? event.timestamp;
109
113
  activeSpans.delete(event.executionId);
110
- if (active.summary.kind === "provider") providerGenerations.push(active);
114
+ if (active.summary.kind === "provider")
115
+ providerGenerations.push(active);
111
116
  }
112
117
  return;
113
118
  }
@@ -138,7 +143,11 @@ export function createRouterTelemetry(input: {
138
143
  active.summary.error = message;
139
144
  active.summary.finishedAt ??= event.timestamp;
140
145
  }
141
- recordError({ message, requestId: event.requestId, executionId: event.executionId });
146
+ recordError({
147
+ message,
148
+ requestId: event.requestId,
149
+ executionId: event.executionId,
150
+ });
142
151
  return;
143
152
  }
144
153
  if (!timing.startedAt) return;
@@ -163,11 +172,15 @@ export function createRouterTelemetry(input: {
163
172
  }
164
173
  const generations = [
165
174
  ...providerGenerations,
166
- ...[...activeSpans.values()].filter(({ summary }) => summary.kind === "provider"),
175
+ ...[...activeSpans.values()].filter(
176
+ ({ summary }) => summary.kind === "provider",
177
+ ),
167
178
  ].map(toGeneration);
168
179
  activeSpans.clear();
169
180
  await reportAll([
170
- ...generations.map((generation) => traceDriver?.recordGeneration(generation)),
181
+ ...generations.map((generation) =>
182
+ traceDriver?.recordGeneration(generation),
183
+ ),
171
184
  ...[...errors.values()].map((telemetryError) =>
172
185
  errorDriver?.recordError(telemetryError),
173
186
  ),
@@ -181,8 +194,14 @@ export function createRouterTelemetry(input: {
181
194
  startedAt,
182
195
  finishedAt,
183
196
  spans,
184
- totalTokensInput: spans.reduce((total, span) => total + span.tokensInput, 0),
185
- totalTokensOutput: spans.reduce((total, span) => total + span.tokensOutput, 0),
197
+ totalTokensInput: spans.reduce(
198
+ (total, span) => total + span.tokensInput,
199
+ 0,
200
+ ),
201
+ totalTokensOutput: spans.reduce(
202
+ (total, span) => total + span.tokensOutput,
203
+ 0,
204
+ ),
186
205
  totalCachedTokensInput: spans.reduce(
187
206
  (total, span) => total + span.cachedTokensInput,
188
207
  0,
@@ -218,13 +237,18 @@ export function createRouterTelemetry(input: {
218
237
  }
219
238
  }
220
239
 
221
- function createSpan(event: ExecutionEvent, startedAt: string): ExecutionSummarySpan {
240
+ function createSpan(
241
+ event: ExecutionEvent,
242
+ startedAt: string,
243
+ ): ExecutionSummarySpan {
222
244
  return {
223
245
  kind: spanKind(event.kind),
224
246
  event: event.kind,
225
247
  requestId: event.requestId,
226
248
  executionId: event.executionId,
227
- ...(event.parentExecutionId ? { parentExecutionId: event.parentExecutionId } : {}),
249
+ ...(event.parentExecutionId
250
+ ? { parentExecutionId: event.parentExecutionId }
251
+ : {}),
228
252
  ...eventDetails(event),
229
253
  startedAt,
230
254
  tokensInput: 0,
@@ -271,10 +295,15 @@ function eventDetails(event: ExecutionEvent): Partial<ExecutionSummarySpan> {
271
295
  ...optional("model", eventData(event).model),
272
296
  ...optional("provider", eventData(event).provider),
273
297
  ...optional("toolName", eventData(event).toolName),
274
- ...optional("executor", eventData(event).executor ?? eventData(event).executorId),
298
+ ...optional(
299
+ "executor",
300
+ eventData(event).executor ?? eventData(event).executorId,
301
+ ),
275
302
  };
276
303
  }
277
- async function reportAll(reports: Array<Promise<void> | undefined>): Promise<void> {
304
+ async function reportAll(
305
+ reports: Array<Promise<void> | undefined>,
306
+ ): Promise<void> {
278
307
  const results = await Promise.allSettled(reports.filter(Boolean));
279
308
  for (const result of results) {
280
309
  if (result.status === "rejected") {
@@ -286,6 +315,9 @@ function eventData(event: ExecutionEvent): z.infer<typeof EventDataSchema> {
286
315
  const parsed = EventDataSchema.safeParse(event.data ?? {});
287
316
  return parsed.success ? parsed.data : {};
288
317
  }
289
- function optional<K extends string>(key: K, value: string | undefined): Partial<Record<K, string>> {
318
+ function optional<K extends string>(
319
+ key: K,
320
+ value: string | undefined,
321
+ ): Partial<Record<K, string>> {
290
322
  return value ? ({ [key]: value } as Record<K, string>) : {};
291
323
  }
@@ -5,19 +5,9 @@ export type AuthResult = {
5
5
 
6
6
  export type RequestContext = {
7
7
  request: Request;
8
- incomingAuth: Map<string, string>;
9
8
  env: Record<string, unknown>;
9
+ /** @internal Retained only for source compatibility with custom callers. */
10
+ incomingAuth?: Map<string, string>;
11
+ /** @internal Retained only for source compatibility with custom callers. */
10
12
  state?: Map<string, unknown>;
11
13
  };
12
-
13
- export type TargetAuthContext = RequestContext & {
14
- providerName: string;
15
- };
16
-
17
- export interface UpstreamAuthDriver {
18
- name: string;
19
- collectIncoming?(ctx: RequestContext): void;
20
- collectTarget(
21
- ctx: TargetAuthContext,
22
- ): Promise<AuthResult | null> | AuthResult | null;
23
- }
package/src/worker.ts CHANGED
@@ -7,17 +7,19 @@ import type { RouterConfig } from "./router/index.ts";
7
7
  import { createRouterApp } from "./app/factory.ts";
8
8
 
9
9
  const config: RouterConfig = {
10
- upstreamAuth: {
11
- order: ["proxy", "env"],
12
- proxy: {
13
- headers: ["authorization", "x-openairouter-token"],
14
- },
15
- },
16
10
  providers: {
17
11
  openrouter: {
18
12
  apiBaseUrl: "https://openrouter.ai/api/v1",
19
13
  style: "chat-completions",
20
14
  exports: ["*:free"],
15
+ auth: [
16
+ {
17
+ kind: "env",
18
+ env: "OPENROUTER_API_KEY",
19
+ header: "authorization",
20
+ prefix: "Bearer ",
21
+ },
22
+ ],
21
23
  },
22
24
  },
23
25
  modelNamespaces: {},
@@ -1,10 +0,0 @@
1
- export type UpstreamAuthConfigShape = {
2
- order?: string[];
3
- proxy?: {
4
- headers?: string[];
5
- };
6
- env?: {
7
- suffix?: string;
8
- prefix?: string;
9
- };
10
- };
@@ -1,53 +0,0 @@
1
- import type {
2
- AuthResult,
3
- TargetAuthContext,
4
- UpstreamAuthDriver,
5
- } from "./types.ts";
6
-
7
- export function envAuthDriver(
8
- suffix = "_API_KEY",
9
- prefix = "",
10
- ): UpstreamAuthDriver {
11
- const counters = new Map<string, number>();
12
-
13
- return {
14
- name: "env",
15
- collectTarget(ctx: TargetAuthContext): AuthResult | null {
16
- const base = `${prefix}${ctx.providerName.toUpperCase()}`;
17
- const key = resolveKey(ctx.env, base, suffix, counters);
18
- if (!key) return null;
19
- return {
20
- headers: { authorization: `Bearer ${key}` },
21
- source: "env",
22
- };
23
- },
24
- };
25
- }
26
-
27
- function resolveKey(
28
- env: Record<string, unknown>,
29
- base: string,
30
- suffix: string,
31
- counters: Map<string, number>,
32
- ): string | null {
33
- const nValue = env[`${base}${suffix}S_N`];
34
- if (nValue) {
35
- const n =
36
- typeof nValue === "string"
37
- ? parseInt(nValue, 10)
38
- : typeof nValue === "number"
39
- ? nValue
40
- : 0;
41
- if (n > 0) {
42
- const prev = counters.get(base) ?? 0;
43
- const index = (prev % n) + 1;
44
- counters.set(base, prev + 1);
45
- const key = env[`${base}${suffix}_${index}`];
46
- if (typeof key === "string" && key) return key;
47
- }
48
- }
49
-
50
- const single = env[`${base}${suffix}`];
51
- if (typeof single === "string" && single) return single;
52
- return null;
53
- }
@@ -1,26 +0,0 @@
1
- import type {
2
- AuthResult,
3
- RequestContext,
4
- TargetAuthContext,
5
- UpstreamAuthDriver,
6
- } from "./types.ts";
7
-
8
- export function proxyAuthDriver(headers: string[]): UpstreamAuthDriver {
9
- return {
10
- name: "proxy",
11
- collectIncoming(ctx: RequestContext) {
12
- for (const name of headers) {
13
- const value = ctx.request.headers.get(name);
14
- if (value) ctx.incomingAuth.set(name.toLowerCase(), value);
15
- }
16
- },
17
- collectTarget(ctx: TargetAuthContext): AuthResult | null {
18
- const authHeader = ctx.incomingAuth.get("authorization");
19
- if (!authHeader) return null;
20
- return {
21
- headers: { authorization: authHeader },
22
- source: "proxy",
23
- };
24
- },
25
- };
26
- }
@@ -1,27 +0,0 @@
1
- import type { UpstreamAuthConfigShape } from "./config.ts";
2
- import type { UpstreamAuthDriver } from "./types.ts";
3
- import { proxyAuthDriver } from "./proxy.ts";
4
- import { envAuthDriver } from "./env.ts";
5
-
6
- export function buildUpstreamAuthChain(
7
- config?: UpstreamAuthConfigShape,
8
- ): UpstreamAuthDriver[] {
9
- if (!config) return [proxyAuthDriver(["authorization"]), envAuthDriver()];
10
- const order = config.order ?? ["proxy", "env"];
11
- const drivers: UpstreamAuthDriver[] = [];
12
- for (const name of order) {
13
- switch (name) {
14
- case "proxy":
15
- drivers.push(
16
- proxyAuthDriver(config.proxy?.headers ?? ["authorization"]),
17
- );
18
- break;
19
- case "env":
20
- drivers.push(
21
- envAuthDriver(config.env?.suffix ?? "_API_KEY", config.env?.prefix),
22
- );
23
- break;
24
- }
25
- }
26
- return drivers;
27
- }