@neutrome/open-ai-router 0.1.5 → 0.1.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 +2 -2
- package/src/app/factory.ts +186 -0
- package/src/app/handlers.ts +4 -56
- package/src/auth/apikey.ts +51 -0
- package/src/example.test.ts +29 -59
- package/src/index.ts +5 -0
- package/src/router/config.ts +0 -4
- package/src/router/execute.test.ts +54 -24
- package/src/router/execute.ts +26 -64
- package/src/router/resolve.test.ts +20 -20
- package/src/router/resolve.ts +0 -33
- package/src/router/runtime.ts +1 -7
- package/src/worker.ts +2 -1
- package/tsconfig.json +16 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/open-ai-router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"deploy": "wrangler deploy"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@neutrome/lil-engine": "
|
|
17
|
+
"@neutrome/lil-engine": "workspace:*",
|
|
18
18
|
"hono": "^4.12.18"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Hono, type Context } from "hono";
|
|
2
|
+
import type { Executor, ProgramTransform } from "@neutrome/lil-engine";
|
|
3
|
+
import type { ExecutionRuntimeOptions } from "@neutrome/lil-engine";
|
|
4
|
+
import { cors } from "./cors.ts";
|
|
5
|
+
import { healthHandler } from "./health.ts";
|
|
6
|
+
import {
|
|
7
|
+
createRouterRuntime,
|
|
8
|
+
handleAnthropicMessages,
|
|
9
|
+
handleChatCompletions,
|
|
10
|
+
handleGoogleGenAI,
|
|
11
|
+
handleResponses,
|
|
12
|
+
listConfiguredModels,
|
|
13
|
+
type RouterRuntime,
|
|
14
|
+
} from "../router/index.ts";
|
|
15
|
+
import type { RequestContext } from "../auth/types.ts";
|
|
16
|
+
import type { RouterConfig } from "../router/config.ts";
|
|
17
|
+
import type { UsageReport } from "../router/execute.ts";
|
|
18
|
+
|
|
19
|
+
export type RouterAppAuth = {
|
|
20
|
+
authenticate(request: Request, env: Record<string, unknown>): Promise<object | null>;
|
|
21
|
+
canAccess(principal: object, modelId: string): boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RouterAppHooks = {
|
|
25
|
+
beforeRequest?(ctx: { request: Request; model: string | null; env: Record<string, unknown> }): Promise<Response | void>;
|
|
26
|
+
onUsage?(report: UsageReport, env: Record<string, unknown>): void;
|
|
27
|
+
onToolCall?(report: { toolName: string }, env: Record<string, unknown>): void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type RouterAppOptions = {
|
|
31
|
+
config: RouterConfig;
|
|
32
|
+
executors: Readonly<Record<string, Executor>>;
|
|
33
|
+
transforms?: Readonly<Record<string, ProgramTransform>>;
|
|
34
|
+
auth: RouterAppAuth;
|
|
35
|
+
hooks?: RouterAppHooks;
|
|
36
|
+
fetchImpl?: typeof fetch;
|
|
37
|
+
knownSuffixes?: string[];
|
|
38
|
+
observe?: ExecutionRuntimeOptions["observe"];
|
|
39
|
+
requestIdFactory?: ExecutionRuntimeOptions["requestIdFactory"];
|
|
40
|
+
executionIdFactory?: ExecutionRuntimeOptions["executionIdFactory"];
|
|
41
|
+
upstreamTimeoutMs?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function createRouterApp(options: RouterAppOptions) {
|
|
45
|
+
const runtime = createRouterRuntime({
|
|
46
|
+
config: options.config,
|
|
47
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
48
|
+
...(options.knownSuffixes ? { knownSuffixes: options.knownSuffixes } : {}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const app = new Hono();
|
|
52
|
+
|
|
53
|
+
app.use("/v1/*", cors());
|
|
54
|
+
app.get("/health", healthHandler());
|
|
55
|
+
|
|
56
|
+
app.get("/v1/models", async (c) => {
|
|
57
|
+
const env = honoEnv(c);
|
|
58
|
+
const principal = await options.auth.authenticate(c.req.raw, env);
|
|
59
|
+
if (!principal) return unauthorized();
|
|
60
|
+
|
|
61
|
+
const models = listConfiguredModels(runtime)
|
|
62
|
+
.filter((id) => options.auth.canAccess(principal, id))
|
|
63
|
+
.map((id) => ({
|
|
64
|
+
id,
|
|
65
|
+
object: "model" as const,
|
|
66
|
+
created: 0,
|
|
67
|
+
owned_by: id.split("/")[0] || "unknown",
|
|
68
|
+
}));
|
|
69
|
+
return c.json({ object: "list", data: models });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.post("/v1/chat/completions", (c) => runAuthorized(c, c.req.raw, handleChatCompletions));
|
|
73
|
+
app.post("/v1/responses", (c) => runAuthorized(c, c.req.raw, handleResponses));
|
|
74
|
+
app.post("/v1/messages", (c) => runAuthorized(c, c.req.raw, handleAnthropicMessages));
|
|
75
|
+
|
|
76
|
+
app.post("/v1/models/*", async (c) => {
|
|
77
|
+
const route = parseGoogleGenAIRoute(c.req.raw);
|
|
78
|
+
if (!route) return c.text("Not Found", 404);
|
|
79
|
+
const request = await rewriteGoogleGenAIRequest(c.req.raw, route);
|
|
80
|
+
return runAuthorized(c, request, handleGoogleGenAI, route.stream);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
app.all("*", (c) => c.text("Not Found", 404));
|
|
84
|
+
|
|
85
|
+
async function runAuthorized(
|
|
86
|
+
c: Context,
|
|
87
|
+
request: Request,
|
|
88
|
+
handler: (opts: any) => Promise<Response>,
|
|
89
|
+
forceStreaming?: boolean,
|
|
90
|
+
): Promise<Response> {
|
|
91
|
+
const env = honoEnv(c);
|
|
92
|
+
const principal = await options.auth.authenticate(request, env);
|
|
93
|
+
if (!principal) return unauthorized();
|
|
94
|
+
|
|
95
|
+
const model = await readModel(request.clone());
|
|
96
|
+
if (model && !options.auth.canAccess(principal, model)) {
|
|
97
|
+
return errorResponse(`Model \`${model}\` does not exist or you do not have access.`, 404, "model_not_found");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (options.hooks?.beforeRequest) {
|
|
101
|
+
const hookResponse = await options.hooks.beforeRequest({ request, model, env });
|
|
102
|
+
if (hookResponse) return hookResponse;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ctx: RequestContext = {
|
|
106
|
+
request,
|
|
107
|
+
incomingAuth: new Map(),
|
|
108
|
+
env,
|
|
109
|
+
state: new Map(),
|
|
110
|
+
};
|
|
111
|
+
for (const driver of runtime.authChain) {
|
|
112
|
+
driver.collectIncoming?.(ctx);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const onUsage = options.hooks?.onUsage
|
|
116
|
+
? (report: UsageReport) => options.hooks!.onUsage!(report, env)
|
|
117
|
+
: undefined;
|
|
118
|
+
|
|
119
|
+
return handler({
|
|
120
|
+
runtime,
|
|
121
|
+
ctx,
|
|
122
|
+
executors: options.executors,
|
|
123
|
+
...(options.transforms ? { transforms: options.transforms } : {}),
|
|
124
|
+
...(options.observe ? { observe: options.observe } : {}),
|
|
125
|
+
...(options.upstreamTimeoutMs !== undefined ? { upstreamTimeoutMs: options.upstreamTimeoutMs } : {}),
|
|
126
|
+
...(onUsage ? { onUsage } : {}),
|
|
127
|
+
...(forceStreaming ? { forceStreaming: true } : {}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return app;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function honoEnv(c: Context): Record<string, unknown> {
|
|
135
|
+
return (c.env ?? {}) as Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseGoogleGenAIRoute(request: Request): { model: string; stream: boolean } | null {
|
|
139
|
+
const pathname = new URL(request.url).pathname;
|
|
140
|
+
const match = pathname.match(/^\/v1\/models\/(.+):(generateContent|streamGenerateContent)$/);
|
|
141
|
+
if (!match?.[1] || !match[2]) return null;
|
|
142
|
+
try {
|
|
143
|
+
return { model: decodeURIComponent(match[1]), stream: match[2] === "streamGenerateContent" };
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function rewriteGoogleGenAIRequest(
|
|
150
|
+
request: Request,
|
|
151
|
+
route: { model: string; stream: boolean },
|
|
152
|
+
): Promise<Request> {
|
|
153
|
+
const bodyText = await request.text();
|
|
154
|
+
const headers = new Headers(request.headers);
|
|
155
|
+
try {
|
|
156
|
+
const raw = JSON.parse(bodyText) as Record<string, unknown>;
|
|
157
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
158
|
+
return new Request(request.url, {
|
|
159
|
+
method: request.method,
|
|
160
|
+
headers,
|
|
161
|
+
body: JSON.stringify({ ...raw, model: route.model, ...(route.stream ? { stream: true } : {}) }),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
return new Request(request.url, { method: request.method, headers, body: bodyText });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function readModel(request: Request): Promise<string | null> {
|
|
169
|
+
try {
|
|
170
|
+
const body = (await request.json()) as { model?: unknown };
|
|
171
|
+
return typeof body.model === "string" ? body.model : null;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function unauthorized(): Response {
|
|
178
|
+
return errorResponse("Authentication required", 401, "auth_required");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function errorResponse(message: string, status: number, code: string): Response {
|
|
182
|
+
return new Response(
|
|
183
|
+
JSON.stringify({ error: { message, type: "invalid_request_error", param: null, code } }),
|
|
184
|
+
{ status, headers: { "content-type": "application/json; charset=utf-8" } },
|
|
185
|
+
);
|
|
186
|
+
}
|
package/src/app/handlers.ts
CHANGED
|
@@ -28,8 +28,8 @@ export type AnthropicMessagesHandlerOptions = ChatCompletionsHandlerOptions;
|
|
|
28
28
|
export type GoogleGenAIHandlerOptions = ChatCompletionsHandlerOptions;
|
|
29
29
|
|
|
30
30
|
export function listModelsHandler(runtime: RouterRuntime) {
|
|
31
|
-
return
|
|
32
|
-
const modelIds =
|
|
31
|
+
return (c: Context) => {
|
|
32
|
+
const modelIds = listAvailableModels(runtime);
|
|
33
33
|
const models: OpenAiModel[] = modelIds.map((id) => ({
|
|
34
34
|
id,
|
|
35
35
|
object: "model",
|
|
@@ -40,60 +40,8 @@ export function listModelsHandler(runtime: RouterRuntime) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const models: string[] = [];
|
|
46
|
-
|
|
47
|
-
for (const id of listConfiguredModels(runtime)) {
|
|
48
|
-
addModel(id);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
await Promise.all(runtime.providerOrder.map(async (namespace) => {
|
|
52
|
-
const provider = runtime.providers.get(namespace);
|
|
53
|
-
if (!provider || provider.catalog.length > 0) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
for (const model of await fetchProviderCatalog(runtime, provider.apiBaseUrl)) {
|
|
58
|
-
if (provider.exportsMatcher(model)) {
|
|
59
|
-
addModel(`${namespace}/${model}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
return models;
|
|
65
|
-
|
|
66
|
-
function addModel(id: string): void {
|
|
67
|
-
if (seen.has(id)) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
seen.add(id);
|
|
71
|
-
models.push(id);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function fetchProviderCatalog(runtime: RouterRuntime, apiBaseUrl: string): Promise<string[]> {
|
|
76
|
-
try {
|
|
77
|
-
const response = await runtime.fetchImpl(`${apiBaseUrl}/models`, {
|
|
78
|
-
method: "GET",
|
|
79
|
-
headers: { accept: "application/json" },
|
|
80
|
-
});
|
|
81
|
-
if (!response.ok) {
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const raw = await response.json() as { data?: unknown };
|
|
86
|
-
const data = raw.data;
|
|
87
|
-
if (!Array.isArray(data)) {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return data
|
|
92
|
-
.map((model) => (model as { id?: unknown } | null | undefined)?.id)
|
|
93
|
-
.filter((id): id is string => typeof id === "string");
|
|
94
|
-
} catch {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
43
|
+
function listAvailableModels(runtime: RouterRuntime): string[] {
|
|
44
|
+
return listConfiguredModels(runtime);
|
|
97
45
|
}
|
|
98
46
|
|
|
99
47
|
export function chatCompletionsHandler(
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type ApiKeyEntry = {
|
|
2
|
+
tokenHash: string;
|
|
3
|
+
allowedModels: string[] | null;
|
|
4
|
+
allowedNamespaces: string[] | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type ApiKeyAuth = {
|
|
8
|
+
authenticate(request: Request): Promise<ApiKeyEntry | null>;
|
|
9
|
+
canAccess(key: ApiKeyEntry, modelId: string): boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function createApiKeyAuth(keys: readonly ApiKeyEntry[]): ApiKeyAuth {
|
|
13
|
+
return {
|
|
14
|
+
async authenticate(request) {
|
|
15
|
+
const token = readApiToken(request);
|
|
16
|
+
if (!token) return null;
|
|
17
|
+
const hash = await sha256Hex(token);
|
|
18
|
+
return keys.find((k) => k.tokenHash === hash) ?? null;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
canAccess(key, modelId) {
|
|
22
|
+
const hasModelRestriction = key.allowedModels && key.allowedModels.length > 0;
|
|
23
|
+
const hasNamespaceRestriction = key.allowedNamespaces && key.allowedNamespaces.length > 0;
|
|
24
|
+
if (!hasModelRestriction && !hasNamespaceRestriction) return true;
|
|
25
|
+
if (hasModelRestriction && key.allowedModels!.includes(modelId)) return true;
|
|
26
|
+
const ns = modelId.split("/")[0] ?? "";
|
|
27
|
+
return !!(hasNamespaceRestriction && key.allowedNamespaces!.includes(ns));
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readApiToken(request: Request): string | null {
|
|
33
|
+
const authHeader = request.headers.get("authorization");
|
|
34
|
+
const bearerMatch = authHeader?.match(/^Bearer\s+(.+)$/i);
|
|
35
|
+
if (bearerMatch?.[1]) return bearerMatch[1];
|
|
36
|
+
|
|
37
|
+
const headerToken = request.headers.get("x-api-key")
|
|
38
|
+
?? request.headers.get("x-goog-api-key");
|
|
39
|
+
if (headerToken) return headerToken;
|
|
40
|
+
|
|
41
|
+
const url = new URL(request.url);
|
|
42
|
+
return url.searchParams.get("key") ?? url.searchParams.get("api_key");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sha256Hex(value: string): Promise<string> {
|
|
46
|
+
const bytes = new TextEncoder().encode(value);
|
|
47
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
48
|
+
return Array.from(new Uint8Array(digest))
|
|
49
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
50
|
+
.join("");
|
|
51
|
+
}
|
package/src/example.test.ts
CHANGED
|
@@ -9,15 +9,14 @@ import { createApp } from "./example.ts";
|
|
|
9
9
|
const decoder = new TextDecoder();
|
|
10
10
|
|
|
11
11
|
describe("open-ai-router createApp", () => {
|
|
12
|
-
it("lists configured
|
|
12
|
+
it("lists configured executor aliases", async () => {
|
|
13
13
|
const app = createApp({
|
|
14
14
|
config: {
|
|
15
15
|
providers: {
|
|
16
16
|
openrouter: {
|
|
17
17
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
18
18
|
style: "chat-completions",
|
|
19
|
-
exports: ["
|
|
20
|
-
catalog: ["gpt-4o"],
|
|
19
|
+
exports: ["*"],
|
|
21
20
|
},
|
|
22
21
|
},
|
|
23
22
|
executors: {
|
|
@@ -38,50 +37,6 @@ describe("open-ai-router createApp", () => {
|
|
|
38
37
|
object: "list",
|
|
39
38
|
data: [
|
|
40
39
|
{ id: "semantyka/enei-1", object: "model", created: 0, owned_by: "semantyka" },
|
|
41
|
-
{ id: "openrouter/gpt-4o", object: "model", created: 0, owned_by: "openrouter" },
|
|
42
|
-
],
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("lists provider /models entries when provider catalog is not configured", async () => {
|
|
47
|
-
const app = createApp({
|
|
48
|
-
config: {
|
|
49
|
-
providers: {
|
|
50
|
-
openrouter: {
|
|
51
|
-
api_base_url: "https://openrouter.ai/api/v1",
|
|
52
|
-
style: "chat-completions",
|
|
53
|
-
exports: ["*:free"],
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
executors: {},
|
|
57
|
-
},
|
|
58
|
-
fetchImpl: vi.fn(async () =>
|
|
59
|
-
new Response(
|
|
60
|
-
JSON.stringify({
|
|
61
|
-
data: [
|
|
62
|
-
{ id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free" },
|
|
63
|
-
{ id: "openai/gpt-4.1-mini" },
|
|
64
|
-
],
|
|
65
|
-
}),
|
|
66
|
-
{
|
|
67
|
-
headers: { "content-type": "application/json; charset=utf-8" },
|
|
68
|
-
},
|
|
69
|
-
)
|
|
70
|
-
),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const response = await app.request("/v1/models");
|
|
74
|
-
|
|
75
|
-
expect(response.status).toBe(200);
|
|
76
|
-
expect(await response.json()).toEqual({
|
|
77
|
-
object: "list",
|
|
78
|
-
data: [
|
|
79
|
-
{
|
|
80
|
-
id: "openrouter/nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
|
|
81
|
-
object: "model",
|
|
82
|
-
created: 0,
|
|
83
|
-
owned_by: "openrouter",
|
|
84
|
-
},
|
|
85
40
|
],
|
|
86
41
|
});
|
|
87
42
|
});
|
|
@@ -101,12 +56,17 @@ describe("open-ai-router createApp", () => {
|
|
|
101
56
|
openrouter: {
|
|
102
57
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
103
58
|
style: "chat-completions",
|
|
104
|
-
exports: ["
|
|
105
|
-
|
|
106
|
-
|
|
59
|
+
exports: ["*"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
executors: {
|
|
63
|
+
default: {
|
|
64
|
+
exports: ["*"],
|
|
65
|
+
models: {
|
|
66
|
+
"gpt-4o": { provider: "openrouter", model: "gpt-4o" },
|
|
67
|
+
},
|
|
107
68
|
},
|
|
108
69
|
},
|
|
109
|
-
executors: {},
|
|
110
70
|
},
|
|
111
71
|
fetchImpl: vi.fn(async (_input, init) => {
|
|
112
72
|
capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
|
|
@@ -172,12 +132,17 @@ describe("open-ai-router createApp", () => {
|
|
|
172
132
|
openrouter: {
|
|
173
133
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
174
134
|
style: "chat-completions",
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
135
|
+
exports: ["*"],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
executors: {
|
|
139
|
+
default: {
|
|
140
|
+
exports: ["*"],
|
|
141
|
+
models: {
|
|
142
|
+
"gpt-4o": { provider: "openrouter", model: "gpt-4o" },
|
|
143
|
+
},
|
|
178
144
|
},
|
|
179
145
|
},
|
|
180
|
-
executors: {},
|
|
181
146
|
},
|
|
182
147
|
fetchImpl: vi.fn(async (_input, init) => {
|
|
183
148
|
capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
|
|
@@ -241,12 +206,17 @@ describe("open-ai-router createApp", () => {
|
|
|
241
206
|
openrouter: {
|
|
242
207
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
243
208
|
style: "chat-completions",
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
209
|
+
exports: ["*"],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
executors: {
|
|
213
|
+
default: {
|
|
214
|
+
exports: ["*"],
|
|
215
|
+
models: {
|
|
216
|
+
"gpt-4o": { provider: "openrouter", model: "gpt-4o" },
|
|
217
|
+
},
|
|
247
218
|
},
|
|
248
219
|
},
|
|
249
|
-
executors: {},
|
|
250
220
|
},
|
|
251
221
|
fetchImpl: vi.fn(async (_input, init) => {
|
|
252
222
|
capturedBody = JSON.parse(decoder.decode(new Uint8Array(init?.body as ArrayBuffer)));
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { createRouterApp } from "./app/factory.ts";
|
|
2
|
+
export type { RouterAppAuth, RouterAppHooks, RouterAppOptions } from "./app/factory.ts";
|
|
3
|
+
|
|
1
4
|
export { createApp } from "./example.ts";
|
|
2
5
|
export type { CreateAppOptions } from "./example.ts";
|
|
3
6
|
|
|
@@ -47,6 +50,8 @@ export type {
|
|
|
47
50
|
export { buildAuthChain } from "./auth/registry.ts";
|
|
48
51
|
export { envAuthDriver } from "./auth/env.ts";
|
|
49
52
|
export { proxyAuthDriver } from "./auth/proxy.ts";
|
|
53
|
+
export { createApiKeyAuth } from "./auth/apikey.ts";
|
|
54
|
+
export type { ApiKeyAuth, ApiKeyEntry } from "./auth/apikey.ts";
|
|
50
55
|
|
|
51
56
|
export { cors } from "./app/cors.ts";
|
|
52
57
|
export { healthHandler } from "./app/health.ts";
|
package/src/router/config.ts
CHANGED
|
@@ -9,11 +9,7 @@ export type ProviderTargetConfig = {
|
|
|
9
9
|
api_base_url: string;
|
|
10
10
|
style: ProviderApiStyle;
|
|
11
11
|
exports?: string[] | null;
|
|
12
|
-
endpoint_path?: string;
|
|
13
|
-
allow_anonymous?: boolean;
|
|
14
|
-
no_prefix?: boolean;
|
|
15
12
|
headers?: Record<string, string>;
|
|
16
|
-
catalog?: string[];
|
|
17
13
|
};
|
|
18
14
|
|
|
19
15
|
export type ExecutorRouteConfig =
|
|
@@ -19,12 +19,17 @@ describe("router execution", () => {
|
|
|
19
19
|
openrouter: {
|
|
20
20
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
21
21
|
style: "chat-completions",
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
exports: ["*"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
executors: {
|
|
26
|
+
default: {
|
|
27
|
+
exports: ["*"],
|
|
28
|
+
models: {
|
|
29
|
+
"gpt-4o": { provider: "openrouter", model: "gpt-4o" },
|
|
30
|
+
},
|
|
25
31
|
},
|
|
26
32
|
},
|
|
27
|
-
executors: {},
|
|
28
33
|
},
|
|
29
34
|
fetchImpl: vi.fn(async (input, init) => {
|
|
30
35
|
capturedUrl = String(input);
|
|
@@ -98,12 +103,17 @@ describe("router execution", () => {
|
|
|
98
103
|
openai: {
|
|
99
104
|
api_base_url: "https://api.openai.com/v1",
|
|
100
105
|
style: "responses",
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
exports: ["*"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
executors: {
|
|
110
|
+
default: {
|
|
111
|
+
exports: ["*"],
|
|
112
|
+
models: {
|
|
113
|
+
"gpt-5": { provider: "openai", model: "gpt-5" },
|
|
114
|
+
},
|
|
104
115
|
},
|
|
105
116
|
},
|
|
106
|
-
executors: {},
|
|
107
117
|
},
|
|
108
118
|
fetchImpl: vi.fn(async (input, init) => {
|
|
109
119
|
capturedUrl = String(input);
|
|
@@ -166,12 +176,17 @@ describe("router execution", () => {
|
|
|
166
176
|
anthropic: {
|
|
167
177
|
api_base_url: "https://api.anthropic.com",
|
|
168
178
|
style: "anthropic-messages",
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
179
|
+
exports: ["*"],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
executors: {
|
|
183
|
+
default: {
|
|
184
|
+
exports: ["*"],
|
|
185
|
+
models: {
|
|
186
|
+
"claude-sonnet-4": { provider: "anthropic", model: "claude-sonnet-4" },
|
|
187
|
+
},
|
|
172
188
|
},
|
|
173
189
|
},
|
|
174
|
-
executors: {},
|
|
175
190
|
},
|
|
176
191
|
fetchImpl: vi.fn(async (input, init) => {
|
|
177
192
|
capturedUrl = String(input);
|
|
@@ -236,12 +251,17 @@ describe("router execution", () => {
|
|
|
236
251
|
google: {
|
|
237
252
|
api_base_url: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
|
|
238
253
|
style: "google-genai",
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
exports: ["*"],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
executors: {
|
|
258
|
+
default: {
|
|
259
|
+
exports: ["*"],
|
|
260
|
+
models: {
|
|
261
|
+
"gemini-2.5-flash": { provider: "google", model: "gemini-2.5-flash" },
|
|
262
|
+
},
|
|
242
263
|
},
|
|
243
264
|
},
|
|
244
|
-
executors: {},
|
|
245
265
|
},
|
|
246
266
|
fetchImpl: vi.fn(async (input, init) => {
|
|
247
267
|
capturedUrl = String(input);
|
|
@@ -359,12 +379,17 @@ describe("router execution", () => {
|
|
|
359
379
|
openrouter: {
|
|
360
380
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
361
381
|
style: "chat-completions",
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
382
|
+
exports: ["*"],
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
executors: {
|
|
386
|
+
default: {
|
|
387
|
+
exports: ["*"],
|
|
388
|
+
models: {
|
|
389
|
+
"gpt-4o": { provider: "openrouter", model: "gpt-4o" },
|
|
390
|
+
},
|
|
365
391
|
},
|
|
366
392
|
},
|
|
367
|
-
executors: {},
|
|
368
393
|
},
|
|
369
394
|
fetchImpl: vi.fn(async () => {
|
|
370
395
|
const stream = new ReadableStream<Uint8Array>({
|
|
@@ -428,12 +453,17 @@ describe("router execution", () => {
|
|
|
428
453
|
openrouter: {
|
|
429
454
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
430
455
|
style: "chat-completions",
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
456
|
+
exports: ["*"],
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
executors: {
|
|
460
|
+
default: {
|
|
461
|
+
exports: ["*"],
|
|
462
|
+
models: {
|
|
463
|
+
"gpt-4o": { provider: "openrouter", model: "gpt-4o" },
|
|
464
|
+
},
|
|
434
465
|
},
|
|
435
466
|
},
|
|
436
|
-
executors: {},
|
|
437
467
|
},
|
|
438
468
|
fetchImpl: vi.fn(async (_input, init) => {
|
|
439
469
|
return await new Promise<Response>((_resolve, reject) => {
|
package/src/router/execute.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type { RequestContext, TargetAuthContext } from "../auth/types.ts";
|
|
|
28
28
|
import { cloneForwardHeaders, isSse } from "../util/headers.ts";
|
|
29
29
|
import { eventDataLines, splitSseEvents } from "../util/sse.ts";
|
|
30
30
|
import { resolveInvocationTarget, type ResolvedTarget } from "./resolve.ts";
|
|
31
|
-
import type { RouterRuntime } from "./runtime.ts";
|
|
31
|
+
import type { ProviderRuntime, RouterRuntime } from "./runtime.ts";
|
|
32
32
|
|
|
33
33
|
const encoder = new TextEncoder();
|
|
34
34
|
const decoder = new TextDecoder();
|
|
@@ -300,19 +300,14 @@ export function createFetchProviderInvoker(
|
|
|
300
300
|
): ProviderInvoker {
|
|
301
301
|
return {
|
|
302
302
|
async execute(request, providerCtx) {
|
|
303
|
-
const provider = getProviderOrThrow(runtime, providerCtx.target.provider
|
|
303
|
+
const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
|
|
304
304
|
const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
|
|
305
305
|
|
|
306
306
|
try {
|
|
307
307
|
const upstream = await fetchProvider(
|
|
308
308
|
runtime,
|
|
309
309
|
ctx,
|
|
310
|
-
provider
|
|
311
|
-
provider.allowAnonymous,
|
|
312
|
-
provider.headers,
|
|
313
|
-
provider.apiBaseUrl,
|
|
314
|
-
provider.endpointPath,
|
|
315
|
-
provider.style,
|
|
310
|
+
provider,
|
|
316
311
|
setModel(request, providerCtx.target.model),
|
|
317
312
|
timed.signal,
|
|
318
313
|
);
|
|
@@ -330,7 +325,7 @@ export function createFetchProviderInvoker(
|
|
|
330
325
|
},
|
|
331
326
|
|
|
332
327
|
async *stream(request, providerCtx) {
|
|
333
|
-
const provider = getProviderOrThrow(runtime, providerCtx.target.provider
|
|
328
|
+
const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
|
|
334
329
|
const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
|
|
335
330
|
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
336
331
|
let buffer = "";
|
|
@@ -339,12 +334,7 @@ export function createFetchProviderInvoker(
|
|
|
339
334
|
const upstream = await fetchProvider(
|
|
340
335
|
runtime,
|
|
341
336
|
ctx,
|
|
342
|
-
provider
|
|
343
|
-
provider.allowAnonymous,
|
|
344
|
-
provider.headers,
|
|
345
|
-
provider.apiBaseUrl,
|
|
346
|
-
provider.endpointPath,
|
|
347
|
-
provider.style,
|
|
337
|
+
provider,
|
|
348
338
|
setModel(request, providerCtx.target.model),
|
|
349
339
|
timed.signal,
|
|
350
340
|
);
|
|
@@ -516,50 +506,31 @@ function timingHeaders(userTimeMs: number): Record<string, string> {
|
|
|
516
506
|
async function fetchProvider(
|
|
517
507
|
runtime: RouterRuntime,
|
|
518
508
|
ctx: RequestContext,
|
|
519
|
-
|
|
520
|
-
allowAnonymous: boolean,
|
|
521
|
-
providerHeaders: Readonly<Record<string, string>>,
|
|
522
|
-
apiBaseUrl: string,
|
|
523
|
-
endpointPath: string,
|
|
524
|
-
providerStyle: RouterRuntime["providers"] extends ReadonlyMap<string, infer T>
|
|
525
|
-
? T extends { style: infer S }
|
|
526
|
-
? S
|
|
527
|
-
: never
|
|
528
|
-
: never,
|
|
509
|
+
provider: ProviderRuntime,
|
|
529
510
|
request: Program,
|
|
530
511
|
signal: AbortSignal,
|
|
531
512
|
): Promise<Response> {
|
|
532
513
|
const headers = cloneForwardHeaders(ctx.request.headers);
|
|
533
514
|
headers.set("content-type", "application/json");
|
|
534
|
-
for (const [key, value] of Object.entries(
|
|
515
|
+
for (const [key, value] of Object.entries(provider.headers)) {
|
|
535
516
|
headers.set(key, value);
|
|
536
517
|
}
|
|
537
518
|
|
|
538
|
-
let hasAuth = false;
|
|
539
519
|
for (const driver of runtime.authChain) {
|
|
540
|
-
const targetCtx: TargetAuthContext = { ...ctx, providerName };
|
|
520
|
+
const targetCtx: TargetAuthContext = { ...ctx, providerName: provider.name };
|
|
541
521
|
const auth = await driver.collectTarget(targetCtx);
|
|
542
522
|
if (!auth) {
|
|
543
523
|
continue;
|
|
544
524
|
}
|
|
545
525
|
new Headers(auth.headers).forEach((value, key) => headers.set(key, value));
|
|
546
|
-
hasAuth = true;
|
|
547
526
|
break;
|
|
548
527
|
}
|
|
549
528
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
`No auth available for provider ${providerName}`,
|
|
553
|
-
401,
|
|
554
|
-
"provider_auth_missing",
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
let body = emitProviderRequest(providerStyle, request);
|
|
559
|
-
if (providerStyle === "chat-completions" && isStreaming(request)) {
|
|
529
|
+
let body = emitProviderRequest(provider.style, request);
|
|
530
|
+
if (provider.style === "chat-completions" && isStreaming(request)) {
|
|
560
531
|
body = injectStreamUsage(body);
|
|
561
532
|
}
|
|
562
|
-
return runtime.fetchImpl(`${apiBaseUrl}${endpointPath}`, {
|
|
533
|
+
return runtime.fetchImpl(`${provider.apiBaseUrl}${provider.endpointPath}`, {
|
|
563
534
|
method: "POST",
|
|
564
535
|
headers,
|
|
565
536
|
body: toBody(body),
|
|
@@ -567,32 +538,23 @@ async function fetchProvider(
|
|
|
567
538
|
});
|
|
568
539
|
}
|
|
569
540
|
|
|
570
|
-
function getProviderOrThrow(runtime: RouterRuntime, providerName: string | undefined
|
|
571
|
-
if (providerName) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
"provider_not_configured",
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
return provider;
|
|
541
|
+
function getProviderOrThrow(runtime: RouterRuntime, providerName: string | undefined) {
|
|
542
|
+
if (!providerName) {
|
|
543
|
+
throw new RouterError(
|
|
544
|
+
"No provider specified on execution target",
|
|
545
|
+
500,
|
|
546
|
+
"provider_not_configured",
|
|
547
|
+
);
|
|
581
548
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
549
|
+
const provider = runtime.providers.get(providerName);
|
|
550
|
+
if (!provider) {
|
|
551
|
+
throw new RouterError(
|
|
552
|
+
`Provider ${providerName} is not configured`,
|
|
553
|
+
500,
|
|
554
|
+
"provider_not_configured",
|
|
555
|
+
);
|
|
589
556
|
}
|
|
590
|
-
|
|
591
|
-
throw new RouterError(
|
|
592
|
-
`No provider found for model \`${model ?? "(unknown)"}\``,
|
|
593
|
-
404,
|
|
594
|
-
"provider_not_found",
|
|
595
|
-
);
|
|
557
|
+
return provider;
|
|
596
558
|
}
|
|
597
559
|
|
|
598
560
|
async function toProviderError(
|
|
@@ -10,8 +10,7 @@ describe("router resolution", () => {
|
|
|
10
10
|
openrouter: {
|
|
11
11
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
12
12
|
style: "chat-completions",
|
|
13
|
-
exports: ["
|
|
14
|
-
catalog: ["gemma-4-31b-it"],
|
|
13
|
+
exports: ["*"],
|
|
15
14
|
},
|
|
16
15
|
},
|
|
17
16
|
executors: {
|
|
@@ -45,7 +44,6 @@ describe("router resolution", () => {
|
|
|
45
44
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
46
45
|
style: "chat-completions",
|
|
47
46
|
exports: ["google/*"],
|
|
48
|
-
catalog: ["google/gemma-4-31b-it"],
|
|
49
47
|
},
|
|
50
48
|
},
|
|
51
49
|
executors: {},
|
|
@@ -71,7 +69,6 @@ describe("router resolution", () => {
|
|
|
71
69
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
72
70
|
style: "chat-completions",
|
|
73
71
|
exports: ["openai/gpt-4.1-mini"],
|
|
74
|
-
catalog: ["openai/gpt-4.1-mini"],
|
|
75
72
|
},
|
|
76
73
|
},
|
|
77
74
|
executors: {
|
|
@@ -96,15 +93,14 @@ describe("router resolution", () => {
|
|
|
96
93
|
});
|
|
97
94
|
});
|
|
98
95
|
|
|
99
|
-
it("lists configured executor
|
|
96
|
+
it("lists configured executor models as qualified IDs", () => {
|
|
100
97
|
const runtime = createRouterRuntime({
|
|
101
98
|
config: {
|
|
102
99
|
providers: {
|
|
103
100
|
openrouter: {
|
|
104
101
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
105
102
|
style: "chat-completions",
|
|
106
|
-
exports: ["
|
|
107
|
-
catalog: ["google/gemma-4-31b-it", "openai/gpt-4o", "anthropic/claude-sonnet-4"],
|
|
103
|
+
exports: ["*"],
|
|
108
104
|
},
|
|
109
105
|
},
|
|
110
106
|
executors: {
|
|
@@ -122,34 +118,38 @@ describe("router resolution", () => {
|
|
|
122
118
|
expect(listConfiguredModels(runtime)).toEqual([
|
|
123
119
|
"enei/enei-1",
|
|
124
120
|
"enei/enei-1-pro",
|
|
125
|
-
"openrouter/google/gemma-4-31b-it",
|
|
126
|
-
"openrouter/openai/gpt-4o",
|
|
127
121
|
]);
|
|
128
122
|
});
|
|
129
123
|
|
|
130
|
-
it("
|
|
124
|
+
it("resolves unqualified model through executor routes", () => {
|
|
131
125
|
const runtime = createRouterRuntime({
|
|
132
126
|
config: {
|
|
133
127
|
providers: {
|
|
134
128
|
openrouter: {
|
|
135
129
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
136
130
|
style: "chat-completions",
|
|
137
|
-
exports: ["
|
|
138
|
-
no_prefix: true,
|
|
131
|
+
exports: ["*"],
|
|
139
132
|
},
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
exports: ["
|
|
144
|
-
|
|
133
|
+
},
|
|
134
|
+
executors: {
|
|
135
|
+
default: {
|
|
136
|
+
exports: ["*"],
|
|
137
|
+
models: {
|
|
138
|
+
"gpt-4o": { provider: "openrouter", model: "openai/gpt-4o" },
|
|
139
|
+
},
|
|
145
140
|
},
|
|
146
141
|
},
|
|
147
|
-
executors: {},
|
|
148
142
|
},
|
|
149
143
|
});
|
|
150
144
|
|
|
151
145
|
const resolved = resolveInvocationTargets(runtime, "gpt-4o");
|
|
152
|
-
expect(resolved).toHaveLength(
|
|
153
|
-
expect(resolved
|
|
146
|
+
expect(resolved).toHaveLength(1);
|
|
147
|
+
expect(resolved[0]!.namespace).toBe("default");
|
|
148
|
+
expect(resolved[0]!.target).toEqual({
|
|
149
|
+
kind: "provider",
|
|
150
|
+
provider: "openrouter",
|
|
151
|
+
model: "openai/gpt-4o",
|
|
152
|
+
transforms: [],
|
|
153
|
+
});
|
|
154
154
|
});
|
|
155
155
|
});
|
package/src/router/resolve.ts
CHANGED
|
@@ -48,31 +48,11 @@ export function resolveInvocationTargets(
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
for (const namespace of runtime.providerOrder) {
|
|
52
|
-
const provider = runtime.providers.get(namespace)!;
|
|
53
|
-
if (!provider.noPrefix) continue;
|
|
54
|
-
if (provider.exportsMatcher(parsed.baseModel)) {
|
|
55
|
-
matches.push({
|
|
56
|
-
requestedModel,
|
|
57
|
-
namespace,
|
|
58
|
-
source: "provider",
|
|
59
|
-
suffixes: parsed.suffixes,
|
|
60
|
-
target: {
|
|
61
|
-
kind: "provider",
|
|
62
|
-
provider: namespace,
|
|
63
|
-
model: parsed.baseModel,
|
|
64
|
-
transforms: parsed.suffixes.map((suffix) => suffix.raw),
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
51
|
return matches;
|
|
71
52
|
}
|
|
72
53
|
|
|
73
54
|
export function listConfiguredModels(runtime: RouterRuntime): string[] {
|
|
74
55
|
const models: string[] = [];
|
|
75
|
-
const seen = new Set<string>();
|
|
76
56
|
|
|
77
57
|
for (const namespace of runtime.executorOrder) {
|
|
78
58
|
const executor = runtime.executors.get(namespace)!;
|
|
@@ -83,19 +63,6 @@ export function listConfiguredModels(runtime: RouterRuntime): string[] {
|
|
|
83
63
|
}
|
|
84
64
|
}
|
|
85
65
|
|
|
86
|
-
for (const namespace of runtime.providerOrder) {
|
|
87
|
-
const provider = runtime.providers.get(namespace)!;
|
|
88
|
-
for (const model of provider.catalog) {
|
|
89
|
-
if (provider.exportsMatcher(model)) {
|
|
90
|
-
models.push(`${namespace}/${model}`);
|
|
91
|
-
if (provider.noPrefix && !seen.has(model)) {
|
|
92
|
-
seen.add(model);
|
|
93
|
-
models.push(model);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
66
|
return models;
|
|
100
67
|
}
|
|
101
68
|
|
package/src/router/runtime.ts
CHANGED
|
@@ -14,11 +14,8 @@ export type ProviderRuntime = {
|
|
|
14
14
|
style: ProviderTargetConfig["style"];
|
|
15
15
|
apiBaseUrl: string;
|
|
16
16
|
endpointPath: string;
|
|
17
|
-
allowAnonymous: boolean;
|
|
18
|
-
noPrefix: boolean;
|
|
19
17
|
headers: Readonly<Record<string, string>>;
|
|
20
18
|
exportsMatcher: (model: string) => boolean;
|
|
21
|
-
catalog: readonly string[];
|
|
22
19
|
};
|
|
23
20
|
|
|
24
21
|
export type ExecutorRouteRuntime = {
|
|
@@ -84,12 +81,9 @@ function buildProviderRuntime(name: string, config: ProviderTargetConfig): Provi
|
|
|
84
81
|
name,
|
|
85
82
|
style: config.style,
|
|
86
83
|
apiBaseUrl: config.api_base_url,
|
|
87
|
-
endpointPath:
|
|
88
|
-
allowAnonymous: config.allow_anonymous ?? false,
|
|
89
|
-
noPrefix: config.no_prefix ?? false,
|
|
84
|
+
endpointPath: defaultEndpointPath(config.style),
|
|
90
85
|
headers: config.headers ?? {},
|
|
91
86
|
exportsMatcher: createExportsMatcher(config.exports),
|
|
92
|
-
catalog: config.catalog ?? [],
|
|
93
87
|
};
|
|
94
88
|
}
|
|
95
89
|
|
package/src/worker.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
type ExecutionContext = { waitUntil(p: Promise<unknown>): void; passThroughOnException(): void };
|
|
2
|
+
|
|
1
3
|
import type { RouterConfig } from "./router/index.ts";
|
|
2
4
|
import { createApp } from "./example.ts";
|
|
3
5
|
|
|
@@ -13,7 +15,6 @@ const config: RouterConfig = {
|
|
|
13
15
|
openrouter: {
|
|
14
16
|
api_base_url: "https://openrouter.ai/api/v1",
|
|
15
17
|
style: "chat-completions",
|
|
16
|
-
allow_anonymous: false,
|
|
17
18
|
exports: ["*:free"],
|
|
18
19
|
},
|
|
19
20
|
},
|
package/tsconfig.json
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "../../tsconfig.base.json",
|
|
3
2
|
"compilerOptions": {
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"allowArbitraryExtensions": true,
|
|
10
|
+
"rewriteRelativeImportExtensions": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"exactOptionalPropertyTypes": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"types": ["node", "vitest/globals"]
|
|
6
18
|
},
|
|
7
19
|
"include": ["src/**/*.ts", "src/**/*.d.ts"],
|
|
8
|
-
"exclude": ["test", "dist", "node_modules"]
|
|
20
|
+
"exclude": ["test", "dist", "node_modules"]
|
|
9
21
|
}
|