@neutrome/open-ai-router 0.1.18 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # `@neutrome/open-ai-router`
2
2
 
3
- Deployable router app and reusable router/runtime library surface built on `lil-core` + `lil-exec`.
3
+ Deployable router app and reusable router/runtime library built on
4
+ `@neutrome/lil-engine` for protocol conversion and `@neutrome/lilsdk-ts` for
5
+ executor/tool contracts.
4
6
 
5
7
  ```ts
6
8
  import { createApp, createRouterRuntime, type RouterConfig } from "@neutrome/open-ai-router";
@@ -8,8 +10,9 @@ import { createApp, createRouterRuntime, type RouterConfig } from "@neutrome/ope
8
10
 
9
11
  Primary runtime model:
10
12
 
11
- - client JSON/SSE -> `lil-core` IR
12
- - target resolution -> `lil-exec`
13
+ - client JSON/SSE -> `@neutrome/lil-engine` IR
14
+ - target resolution -> router execution runtime
15
+ - SDK executor or upstream provider invocation
13
16
  - provider transport -> router host
14
17
 
15
18
  Development commands:
@@ -22,4 +25,5 @@ pnpm --filter @neutrome/open-ai-router typecheck
22
25
  Notes:
23
26
 
24
27
  - `RouterConfig` is the canonical router config surface.
25
- - `/v1/models` lists configured executor aliases plus provider `catalog` entries.
28
+ - `/v1/models` lists configured executor aliases.
29
+ - Executor contracts and `connectTools` come from `@neutrome/lilsdk-ts`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neutrome/open-ai-router",
3
- "version": "0.1.18",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -8,7 +8,8 @@
8
8
  "dependencies": {
9
9
  "@modelcontextprotocol/sdk": "^1.29.0",
10
10
  "hono": "^4.12.18",
11
- "@neutrome/lil-engine": "0.1.5"
11
+ "@neutrome/lil-engine": "0.2.0",
12
+ "@neutrome/lilsdk-ts": "0.2.0"
12
13
  },
13
14
  "devDependencies": {
14
15
  "@types/node": "^25.9.3",
@@ -0,0 +1,14 @@
1
+ import type { AuthPrincipal } from "./types.ts";
2
+
3
+ export function canAccessPrincipalModel(
4
+ principal: AuthPrincipal,
5
+ modelId: string,
6
+ ): boolean {
7
+ const hasModelRestriction = principal.allowedModels !== null;
8
+ const hasNamespaceRestriction = principal.allowedNamespaces !== null;
9
+ if (!hasModelRestriction && !hasNamespaceRestriction) return true;
10
+ if (principal.allowedModels?.includes(modelId)) return true;
11
+ const namespace = modelId.split("/")[0] ?? "";
12
+ return principal.allowedNamespaces?.includes(namespace) ?? false;
13
+ }
14
+
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createRuntimeJwtAuthenticator } from "./jwt.ts";
3
+
4
+ describe("runtime JWT admission", () => {
5
+ it("accepts a valid ES256 runtime token", async () => {
6
+ const auth = await testAuth();
7
+ const principal = await createRuntimeJwtAuthenticator({
8
+ workspaceId: "workspace_1",
9
+ }).authenticate(request(auth.token), env(auth.jwksJson));
10
+
11
+ expect(principal).toEqual({
12
+ workspaceId: "workspace_1",
13
+ keyId: "key_1",
14
+ allowedModels: ["support/router"],
15
+ allowedNamespaces: null,
16
+ });
17
+ });
18
+
19
+ it("rejects revoked key ids", async () => {
20
+ const auth = await testAuth();
21
+ const principal = await createRuntimeJwtAuthenticator({
22
+ workspaceId: "workspace_1",
23
+ }).authenticate(
24
+ request(auth.token),
25
+ env(auth.jwksJson, JSON.stringify(["key_1"])),
26
+ );
27
+
28
+ expect(principal).toBeNull();
29
+ });
30
+
31
+ it("rejects wrong workspace tokens", async () => {
32
+ const auth = await testAuth({ workspaceId: "workspace_2" });
33
+ const principal = await createRuntimeJwtAuthenticator({
34
+ workspaceId: "workspace_1",
35
+ }).authenticate(request(auth.token), env(auth.jwksJson));
36
+
37
+ expect(principal).toBeNull();
38
+ });
39
+ });
40
+
41
+ async function testAuth(
42
+ options: { workspaceId?: string } = {},
43
+ ): Promise<{ token: string; jwksJson: string }> {
44
+ const keyPair = await crypto.subtle.generateKey(
45
+ { name: "ECDSA", namedCurve: "P-256" },
46
+ true,
47
+ ["sign", "verify"],
48
+ );
49
+ const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
50
+ const header = { alg: "ES256", typ: "JWT", kid: "test-key" };
51
+ const claims = {
52
+ iss: "neutrome-platform",
53
+ aud: "workspace-runtime",
54
+ sub: options.workspaceId ?? "workspace_1",
55
+ jti: "key_1",
56
+ models: ["support/router"],
57
+ iat: Math.floor(Date.now() / 1000),
58
+ exp: Math.floor(Date.now() / 1000) + 3600,
59
+ };
60
+ const signingInput = `${base64UrlJson(header)}.${base64UrlJson(claims)}`;
61
+ const signature = new Uint8Array(await crypto.subtle.sign(
62
+ { name: "ECDSA", hash: "SHA-256" },
63
+ keyPair.privateKey,
64
+ new TextEncoder().encode(signingInput),
65
+ ));
66
+ return {
67
+ token: `${signingInput}.${base64UrlBytes(signature)}`,
68
+ jwksJson: JSON.stringify({ keys: [{ ...publicJwk, kid: "test-key", alg: "ES256", use: "sig" }] }),
69
+ };
70
+ }
71
+
72
+ function request(token: string): Request {
73
+ return new Request("http://runtime.test/v1/models", {
74
+ headers: { authorization: `Bearer ${token}` },
75
+ });
76
+ }
77
+
78
+ function env(jwksJson: string, revoked = "[]"): Record<string, unknown> {
79
+ return {
80
+ LIL_RUNTIME_AUTH_JWKS_JSON: jwksJson,
81
+ LIL_RUNTIME_AUTH_ISSUER: "neutrome-platform",
82
+ LIL_RUNTIME_AUTH_AUDIENCE: "workspace-runtime",
83
+ LIL_RUNTIME_REVOKED_KEY_IDS_JSON: revoked,
84
+ };
85
+ }
86
+
87
+ function base64UrlJson(value: unknown): string {
88
+ return base64UrlBytes(new TextEncoder().encode(JSON.stringify(value)));
89
+ }
90
+
91
+ function base64UrlBytes(value: Uint8Array): string {
92
+ let binary = "";
93
+ for (const byte of value) binary += String.fromCharCode(byte);
94
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
95
+ }
96
+
@@ -0,0 +1,221 @@
1
+ import type { AuthPrincipal, RuntimeAuthenticator } from "./types.ts";
2
+
3
+ export const DEFAULT_RUNTIME_AUTH_ISSUER = "neutrome-platform";
4
+ export const DEFAULT_RUNTIME_AUTH_AUDIENCE = "workspace-runtime";
5
+
6
+ export type RuntimeJwtAuthConfig = {
7
+ jwksJson?: string;
8
+ issuer?: string;
9
+ audience?: string;
10
+ revokedKeyIdsJson?: string;
11
+ workspaceId?: string;
12
+ };
13
+
14
+ type JsonWebKeySet = {
15
+ keys?: JsonWebKey[];
16
+ };
17
+
18
+ type JwtHeader = {
19
+ alg?: unknown;
20
+ kid?: unknown;
21
+ };
22
+
23
+ type RuntimeJwtClaims = {
24
+ iss?: unknown;
25
+ aud?: unknown;
26
+ sub?: unknown;
27
+ jti?: unknown;
28
+ models?: unknown;
29
+ namespaces?: unknown;
30
+ exp?: unknown;
31
+ nbf?: unknown;
32
+ };
33
+
34
+ type RuntimeJwtEnv = Record<string, unknown> & {
35
+ LIL_RUNTIME_AUTH_JWKS_JSON?: unknown;
36
+ LIL_RUNTIME_AUTH_ISSUER?: unknown;
37
+ LIL_RUNTIME_AUTH_AUDIENCE?: unknown;
38
+ LIL_RUNTIME_REVOKED_KEY_IDS_JSON?: unknown;
39
+ };
40
+
41
+ const encoder = new TextEncoder();
42
+ const importedKeys = new Map<string, Promise<CryptoKey>>();
43
+
44
+ export function createRuntimeJwtAuthenticator(
45
+ config: RuntimeJwtAuthConfig = {},
46
+ ): RuntimeAuthenticator {
47
+ return {
48
+ async authenticate(request, env) {
49
+ const token = readRuntimeToken(request);
50
+ if (!token) return null;
51
+ return verifyRuntimeJwt(token, readConfig(config, env));
52
+ },
53
+ };
54
+ }
55
+
56
+ export async function verifyRuntimeJwt(
57
+ token: string,
58
+ config: Required<RuntimeJwtAuthConfig>,
59
+ ): Promise<AuthPrincipal | null> {
60
+ const parts = token.split(".");
61
+ if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) return null;
62
+
63
+ const header = parseJsonPart<JwtHeader>(parts[0]);
64
+ const claims = parseJsonPart<RuntimeJwtClaims>(parts[1]);
65
+ if (!header || !claims || header.alg !== "ES256" || typeof header.kid !== "string") return null;
66
+
67
+ const key = findJwk(config.jwksJson, header.kid);
68
+ if (!key) return null;
69
+
70
+ const signature = base64UrlToBytes(parts[2]);
71
+ const signingInput = `${parts[0]}.${parts[1]}`;
72
+ const verified = await crypto.subtle.verify(
73
+ { name: "ECDSA", hash: "SHA-256" },
74
+ await importVerifyKey(key),
75
+ toArrayBuffer(derToJose(signature)),
76
+ toArrayBuffer(encoder.encode(signingInput)),
77
+ ).catch(() => false);
78
+ if (!verified) return null;
79
+
80
+ const now = Math.floor(Date.now() / 1000);
81
+ if (claims.iss !== config.issuer) return null;
82
+ if (!audienceMatches(claims.aud, config.audience)) return null;
83
+ if (typeof claims.sub !== "string" || !claims.sub) return null;
84
+ if (typeof claims.jti !== "string" || !claims.jti) return null;
85
+ if (typeof claims.exp !== "number" || claims.exp <= now) return null;
86
+ if (typeof claims.nbf === "number" && claims.nbf > now) return null;
87
+ if (config.workspaceId && claims.sub !== config.workspaceId) return null;
88
+ if (revokedKeyIds(config.revokedKeyIdsJson).has(claims.jti)) return null;
89
+
90
+ const allowedModels = readStringArrayClaim(claims.models);
91
+ const allowedNamespaces = readStringArrayClaim(claims.namespaces);
92
+ if (allowedModels === undefined || allowedNamespaces === undefined) return null;
93
+
94
+ return {
95
+ workspaceId: claims.sub,
96
+ keyId: claims.jti,
97
+ allowedModels,
98
+ allowedNamespaces,
99
+ };
100
+ }
101
+
102
+ export function readRuntimeToken(request: Request): string | null {
103
+ const authHeader = request.headers.get("authorization");
104
+ const bearerMatch = authHeader?.match(/^Bearer\s+(.+)$/i);
105
+ if (bearerMatch?.[1]) return bearerMatch[1];
106
+
107
+ const headerToken = request.headers.get("x-api-key")
108
+ ?? request.headers.get("x-goog-api-key");
109
+ if (headerToken) return headerToken;
110
+
111
+ const url = new URL(request.url);
112
+ return url.searchParams.get("key") ?? url.searchParams.get("api_key");
113
+ }
114
+
115
+ function readConfig(
116
+ config: RuntimeJwtAuthConfig,
117
+ env: Record<string, unknown>,
118
+ ): Required<RuntimeJwtAuthConfig> {
119
+ const runtimeEnv = env as RuntimeJwtEnv;
120
+ return {
121
+ jwksJson: config.jwksJson ?? stringBinding(runtimeEnv.LIL_RUNTIME_AUTH_JWKS_JSON) ?? "",
122
+ issuer: config.issuer ?? stringBinding(runtimeEnv.LIL_RUNTIME_AUTH_ISSUER) ?? DEFAULT_RUNTIME_AUTH_ISSUER,
123
+ audience: config.audience ?? stringBinding(runtimeEnv.LIL_RUNTIME_AUTH_AUDIENCE) ?? DEFAULT_RUNTIME_AUTH_AUDIENCE,
124
+ revokedKeyIdsJson: config.revokedKeyIdsJson ?? stringBinding(runtimeEnv.LIL_RUNTIME_REVOKED_KEY_IDS_JSON) ?? "",
125
+ workspaceId: config.workspaceId ?? "",
126
+ };
127
+ }
128
+
129
+ function stringBinding(value: unknown): string | undefined {
130
+ return typeof value === "string" && value ? value : undefined;
131
+ }
132
+
133
+ function findJwk(jwksJson: string, kid: string): JsonWebKey | null {
134
+ try {
135
+ const parsed = JSON.parse(jwksJson) as JsonWebKeySet;
136
+ const key = parsed.keys?.find((item) => (item as { kid?: unknown }).kid === kid);
137
+ return key ?? null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ function revokedKeyIds(value: string): Set<string> {
144
+ try {
145
+ const parsed = JSON.parse(value) as unknown;
146
+ return new Set(Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : []);
147
+ } catch {
148
+ return new Set();
149
+ }
150
+ }
151
+
152
+ function audienceMatches(value: unknown, expected: string): boolean {
153
+ return value === expected || (Array.isArray(value) && value.includes(expected));
154
+ }
155
+
156
+ function readStringArrayClaim(value: unknown): readonly string[] | null | undefined {
157
+ if (value === undefined || value === null) return null;
158
+ if (!Array.isArray(value)) return undefined;
159
+ return value.every((item) => typeof item === "string") ? value : undefined;
160
+ }
161
+
162
+ function parseJsonPart<T>(part: string): T | null {
163
+ try {
164
+ return JSON.parse(new TextDecoder().decode(base64UrlToBytes(part))) as T;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ function importVerifyKey(jwk: JsonWebKey): Promise<CryptoKey> {
171
+ const cacheKey = JSON.stringify(jwk);
172
+ let imported = importedKeys.get(cacheKey);
173
+ if (!imported) {
174
+ imported = crypto.subtle.importKey(
175
+ "jwk",
176
+ jwk,
177
+ { name: "ECDSA", namedCurve: "P-256" },
178
+ false,
179
+ ["verify"],
180
+ );
181
+ importedKeys.set(cacheKey, imported);
182
+ }
183
+ return imported;
184
+ }
185
+
186
+ function base64UrlToBytes(value: string): Uint8Array {
187
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
188
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
189
+ const binary = atob(padded);
190
+ const bytes = new Uint8Array(binary.length);
191
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
192
+ return bytes;
193
+ }
194
+
195
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
196
+ return bytes.buffer.slice(
197
+ bytes.byteOffset,
198
+ bytes.byteOffset + bytes.byteLength,
199
+ ) as ArrayBuffer;
200
+ }
201
+
202
+ function derToJose(signature: Uint8Array): Uint8Array {
203
+ if (signature.byteLength === 64) return signature;
204
+ const bytes = [...signature];
205
+ if (bytes[0] !== 0x30) return signature;
206
+ let offset = 2;
207
+ if (bytes[1] === 0x81) offset = 3;
208
+ if (bytes[offset] !== 0x02) return signature;
209
+ const rLength = bytes[offset + 1] ?? 0;
210
+ const r = bytes.slice(offset + 2, offset + 2 + rLength);
211
+ offset += 2 + rLength;
212
+ if (bytes[offset] !== 0x02) return signature;
213
+ const sLength = bytes[offset + 1] ?? 0;
214
+ const s = bytes.slice(offset + 2, offset + 2 + sLength);
215
+ return new Uint8Array([...normalizeInteger(r), ...normalizeInteger(s)]);
216
+ }
217
+
218
+ function normalizeInteger(value: number[]): number[] {
219
+ const trimmed = value[0] === 0 ? value.slice(1) : value;
220
+ return [...new Array(Math.max(0, 32 - trimmed.length)).fill(0), ...trimmed].slice(-32);
221
+ }
@@ -0,0 +1,14 @@
1
+ export type AuthPrincipal = {
2
+ workspaceId: string;
3
+ keyId: string;
4
+ allowedModels: readonly string[] | null;
5
+ allowedNamespaces: readonly string[] | null;
6
+ };
7
+
8
+ export type RuntimeAuthenticator = {
9
+ authenticate(
10
+ request: Request,
11
+ env: Record<string, unknown>,
12
+ ): Promise<AuthPrincipal | null>;
13
+ };
14
+
@@ -12,7 +12,7 @@ const config: RouterConfig = {
12
12
  exports: ["*"],
13
13
  },
14
14
  },
15
- executors: {
15
+ modelNamespaces: {
16
16
  default: {
17
17
  exports: ["*"],
18
18
  models: {
@@ -28,13 +28,10 @@ describe("createRouterApp trace finalization", () => {
28
28
  const onTrace = vi.fn(async () => {});
29
29
  const app = createRouterApp({
30
30
  config,
31
- executors: {},
31
+ executorImplementations: {},
32
32
  auth: {
33
33
  async authenticate() {
34
- return {};
35
- },
36
- canAccess() {
37
- return true;
34
+ return principal();
38
35
  },
39
36
  },
40
37
  hooks: { onTrace },
@@ -79,13 +76,10 @@ describe("createRouterApp trace finalization", () => {
79
76
  const onTrace = vi.fn(async () => {});
80
77
  const app = createRouterApp({
81
78
  config,
82
- executors: {},
79
+ executorImplementations: {},
83
80
  auth: {
84
81
  async authenticate() {
85
- return {};
86
- },
87
- canAccess() {
88
- return true;
82
+ return principal();
89
83
  },
90
84
  },
91
85
  hooks: { onTrace },
@@ -154,3 +148,12 @@ function createExecutionContext(waited: Promise<unknown>[]) {
154
148
  passThroughOnException() {},
155
149
  };
156
150
  }
151
+
152
+ function principal() {
153
+ return {
154
+ workspaceId: "workspace_1",
155
+ keyId: "key_1",
156
+ allowedModels: null,
157
+ allowedNamespaces: null,
158
+ };
159
+ }
@@ -1,35 +1,40 @@
1
1
  import { Hono, type Context } from "hono";
2
- import type { Executor, ProgramTransform } from "@neutrome/lil-engine";
3
- import type { ExecutionRuntimeOptions } from "@neutrome/lil-engine";
2
+ import type { Executor, ProgramTransform } from "@neutrome/lilsdk-ts";
3
+ import { getModel, setStreaming, type Program, type ProviderStyle } from "@neutrome/lil-engine";
4
4
  import { cors } from "./cors.ts";
5
5
  import { healthHandler } from "./health.ts";
6
6
  import { createTraceCollector, type ExecutionTrace } from "../observer.ts";
7
+ import { canAccessPrincipalModel } from "../admission/access.ts";
7
8
  import {
8
9
  createRouterRuntime,
9
10
  handleAnthropicMessages,
10
11
  handleChatCompletions,
11
12
  handleGoogleGenAI,
13
+ parseRequestProgram,
12
14
  handleResponses,
13
15
  listConfiguredModels,
14
- type RouterRuntime,
15
16
  } from "../router/index.ts";
16
- import type { RequestContext } from "../auth/types.ts";
17
+ import type { AuthPrincipal, RuntimeAuthenticator } from "../admission/types.ts";
18
+ import type { RequestContext } from "../upstream-auth/types.ts";
17
19
  import type { RouterConfig } from "../router/config.ts";
18
20
  import type { RemoteMcpClientFactory } from "../router/mcp.ts";
21
+ import type { ExecutionRuntimeOptions } from "../router/execution-types.ts";
19
22
 
20
- export type RouterAppAuth = {
21
- authenticate(request: Request, env: Record<string, unknown>): Promise<object | null>;
22
- canAccess(principal: object, modelId: string): boolean;
23
- };
23
+ export type RouterAppAuth = RuntimeAuthenticator;
24
24
 
25
25
  export type RouterAppHooks = {
26
- beforeRequest?(ctx: { request: Request; model: string | null; env: Record<string, unknown> }): Promise<Response | void>;
26
+ beforeRequest?(ctx: {
27
+ request: Request;
28
+ model: string | null;
29
+ principal: AuthPrincipal;
30
+ env: Record<string, unknown>;
31
+ }): Promise<Response | void>;
27
32
  onTrace?(trace: ExecutionTrace, env: Record<string, unknown>): void | Promise<void>;
28
33
  };
29
34
 
30
35
  export type RouterAppOptions = {
31
36
  config: RouterConfig;
32
- executors: Readonly<Record<string, Executor>>;
37
+ executorImplementations: Readonly<Record<string, Executor>>;
33
38
  transforms?: Readonly<Record<string, ProgramTransform>>;
34
39
  auth: RouterAppAuth;
35
40
  hooks?: RouterAppHooks;
@@ -60,7 +65,7 @@ export function createRouterApp(options: RouterAppOptions) {
60
65
  if (!principal) return unauthorized();
61
66
 
62
67
  const models = listConfiguredModels(runtime)
63
- .filter((id) => options.auth.canAccess(principal, id))
68
+ .filter((id) => canAccessPrincipalModel(principal, id))
64
69
  .map((id) => ({
65
70
  id,
66
71
  object: "model" as const,
@@ -70,15 +75,15 @@ export function createRouterApp(options: RouterAppOptions) {
70
75
  return c.json({ object: "list", data: models });
71
76
  });
72
77
 
73
- app.post("/v1/chat/completions", (c) => runAuthorized(c, c.req.raw, handleChatCompletions));
74
- app.post("/v1/responses", (c) => runAuthorized(c, c.req.raw, handleResponses));
75
- app.post("/v1/messages", (c) => runAuthorized(c, c.req.raw, handleAnthropicMessages));
78
+ app.post("/v1/chat/completions", (c) => runAuthorized(c, c.req.raw, "chat-completions", handleChatCompletions));
79
+ app.post("/v1/responses", (c) => runAuthorized(c, c.req.raw, "responses", handleResponses));
80
+ app.post("/v1/messages", (c) => runAuthorized(c, c.req.raw, "anthropic-messages", handleAnthropicMessages));
76
81
 
77
82
  app.post("/v1/models/*", async (c) => {
78
83
  const route = parseGoogleGenAIRoute(c.req.raw);
79
84
  if (!route) return c.text("Not Found", 404);
80
85
  const request = await rewriteGoogleGenAIRequest(c.req.raw, route);
81
- return runAuthorized(c, request, handleGoogleGenAI, route.stream);
86
+ return runAuthorized(c, request, "google-genai", handleGoogleGenAI, route.stream);
82
87
  });
83
88
 
84
89
  app.all("*", (c) => c.text("Not Found", 404));
@@ -86,6 +91,7 @@ export function createRouterApp(options: RouterAppOptions) {
86
91
  async function runAuthorized(
87
92
  c: Context,
88
93
  request: Request,
94
+ style: ProviderStyle,
89
95
  handler: (opts: any) => Promise<Response>,
90
96
  forceStreaming?: boolean,
91
97
  ): Promise<Response> {
@@ -93,13 +99,21 @@ export function createRouterApp(options: RouterAppOptions) {
93
99
  const principal = await options.auth.authenticate(request, env);
94
100
  if (!principal) return unauthorized();
95
101
 
96
- const model = await readModel(request.clone());
97
- if (model && !options.auth.canAccess(principal, model)) {
102
+ let program: Program;
103
+ try {
104
+ program = await parseRequestProgram(style, request.clone());
105
+ if (forceStreaming) program = setStreaming(program, true);
106
+ } catch {
107
+ return errorResponse("Invalid request body", 400, "invalid_request_body");
108
+ }
109
+
110
+ const model = getModel(program) || null;
111
+ if (model && !canAccessPrincipalModel(principal, model)) {
98
112
  return errorResponse(`Model \`${model}\` does not exist or you do not have access.`, 404, "model_not_found");
99
113
  }
100
114
 
101
115
  if (options.hooks?.beforeRequest) {
102
- const hookResponse = await options.hooks.beforeRequest({ request, model, env });
116
+ const hookResponse = await options.hooks.beforeRequest({ request, model, principal, env });
103
117
  if (hookResponse) return hookResponse;
104
118
  }
105
119
 
@@ -109,7 +123,7 @@ export function createRouterApp(options: RouterAppOptions) {
109
123
  env,
110
124
  state: new Map(),
111
125
  };
112
- for (const driver of runtime.authChain) {
126
+ for (const driver of runtime.upstreamAuthChain) {
113
127
  driver.collectIncoming?.(ctx);
114
128
  }
115
129
 
@@ -135,12 +149,12 @@ export function createRouterApp(options: RouterAppOptions) {
135
149
  const response = await handler({
136
150
  runtime,
137
151
  ctx,
138
- executors: options.executors,
152
+ program,
153
+ executorImplementations: options.executorImplementations,
139
154
  ...(options.transforms ? { transforms: options.transforms } : {}),
140
155
  observe,
141
156
  ...(options.upstreamTimeoutMs !== undefined ? { upstreamTimeoutMs: options.upstreamTimeoutMs } : {}),
142
157
  ...(options.remoteMcpClientFactory ? { remoteMcpClientFactory: options.remoteMcpClientFactory } : {}),
143
- ...(forceStreaming ? { forceStreaming: true } : {}),
144
158
  });
145
159
  if (!isEventStreamResponse(response) || !response.body) {
146
160
  finalizeTrace();
@@ -190,15 +204,6 @@ async function rewriteGoogleGenAIRequest(
190
204
  return new Request(request.url, { method: request.method, headers, body: bodyText });
191
205
  }
192
206
 
193
- async function readModel(request: Request): Promise<string | null> {
194
- try {
195
- const body = (await request.json()) as { model?: unknown };
196
- return typeof body.model === "string" ? body.model : null;
197
- } catch {
198
- return null;
199
- }
200
- }
201
-
202
207
  function unauthorized(): Response {
203
208
  return errorResponse("Authentication required", 401, "auth_required");
204
209
  }
@@ -221,7 +226,12 @@ function runBackgroundTask(c: Context, task: () => unknown, label: string): void
221
226
  .catch((error) => {
222
227
  console.error(`[open-ai-router] ${label} failed:`, error);
223
228
  });
224
- const executionCtx = c.executionCtx as { waitUntil?(promise: Promise<unknown>): void } | undefined;
229
+ let executionCtx: { waitUntil?(promise: Promise<unknown>): void } | undefined;
230
+ try {
231
+ executionCtx = c.executionCtx as { waitUntil?(promise: Promise<unknown>): void } | undefined;
232
+ } catch {
233
+ executionCtx = undefined;
234
+ }
225
235
  if (executionCtx?.waitUntil) {
226
236
  executionCtx.waitUntil(tracked);
227
237
  return;
@@ -1,6 +1,5 @@
1
1
  import type { Context } from "hono";
2
- import type { Executor, ProgramTransform } from "@neutrome/lil-engine";
3
- import type { RequestContext } from "../auth/types.ts";
2
+ import type { RequestContext } from "../upstream-auth/types.ts";
4
3
  import {
5
4
  handleAnthropicMessages,
6
5
  handleChatCompletions,
@@ -18,10 +17,7 @@ type OpenAiModel = {
18
17
  owned_by: string;
19
18
  };
20
19
 
21
- export type ChatCompletionsHandlerOptions = RouterExecutionOptions & {
22
- executors?: Readonly<Record<string, Executor>>;
23
- transforms?: Readonly<Record<string, ProgramTransform>>;
24
- };
20
+ export type ChatCompletionsHandlerOptions = RouterExecutionOptions;
25
21
 
26
22
  export type ResponsesHandlerOptions = ChatCompletionsHandlerOptions;
27
23
  export type AnthropicMessagesHandlerOptions = ChatCompletionsHandlerOptions;
@@ -111,8 +107,8 @@ function createExecutionHandlerOptions(
111
107
  ctx,
112
108
  } as Parameters<typeof handleChatCompletions>[0];
113
109
 
114
- if (options.executors) {
115
- handlerOptions.executors = options.executors;
110
+ if (options.executorImplementations) {
111
+ handlerOptions.executorImplementations = options.executorImplementations;
116
112
  }
117
113
  if (options.resolveExecutor) {
118
114
  handlerOptions.resolveExecutor = options.resolveExecutor;
@@ -149,7 +145,7 @@ function createRequestContext(c: Context, request = c.req.raw): RequestContext {
149
145
  }
150
146
 
151
147
  function collectIncomingAuth(ctx: RequestContext, runtime: RouterRuntime): void {
152
- for (const driver of runtime.authChain) {
148
+ for (const driver of runtime.upstreamAuthChain) {
153
149
  driver.collectIncoming?.(ctx);
154
150
  }
155
151
  }