@neutrome/open-ai-router 0.4.2 → 0.5.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.
Files changed (62) hide show
  1. package/.dev.vars.example +2 -0
  2. package/README.md +5 -1
  3. package/package.json +5 -4
  4. package/src/admission/access.ts +0 -1
  5. package/src/admission/types.ts +0 -1
  6. package/src/app/factory.test.ts +40 -34
  7. package/src/app/factory.ts +120 -173
  8. package/src/app/google-genai-route.ts +48 -0
  9. package/src/app/http-utils.ts +14 -0
  10. package/src/app/model-listing.ts +29 -0
  11. package/src/app/response-lifecycle.ts +66 -0
  12. package/src/app/types.ts +38 -0
  13. package/src/index.ts +11 -43
  14. package/src/otlp.ts +148 -0
  15. package/src/router/audit.ts +5 -12
  16. package/src/router/config.ts +4 -4
  17. package/src/router/error-codec.ts +170 -0
  18. package/src/router/errors.ts +38 -1
  19. package/src/router/execute.test.ts +177 -107
  20. package/src/router/execute.ts +68 -924
  21. package/src/router/execution-events.ts +39 -0
  22. package/src/router/execution-invocation.ts +144 -0
  23. package/src/router/execution-observation.ts +161 -0
  24. package/src/router/execution-resolution.ts +120 -0
  25. package/src/router/execution-runtime.test.ts +156 -83
  26. package/src/router/execution-runtime.ts +144 -542
  27. package/src/router/execution-transforms.ts +82 -0
  28. package/src/router/execution-types.ts +8 -19
  29. package/src/router/index.ts +6 -5
  30. package/src/router/mcp.ts +12 -6
  31. package/src/router/provider-invoker.ts +134 -0
  32. package/src/router/provider-stream.ts +192 -0
  33. package/src/router/provider-telemetry.ts +59 -0
  34. package/src/router/remote-mcp-execution.ts +61 -0
  35. package/src/router/request-program.ts +16 -0
  36. package/src/router/request-target.ts +56 -0
  37. package/src/router/resolve.test.ts +43 -10
  38. package/src/router/resolve.ts +50 -28
  39. package/src/router/response-delivery.ts +141 -0
  40. package/src/router/runtime.test.ts +56 -0
  41. package/src/router/runtime.ts +79 -9
  42. package/src/router/targets.ts +3 -1
  43. package/src/router/upstream-client.ts +137 -0
  44. package/src/telemetry.test.ts +50 -0
  45. package/src/telemetry.ts +316 -0
  46. package/src/trace-labels.ts +29 -0
  47. package/src/upstream-auth/env.ts +15 -3
  48. package/src/upstream-auth/proxy.ts +6 -1
  49. package/src/upstream-auth/registry.ts +3 -1
  50. package/src/util/glob.ts +1 -1
  51. package/src/util/model-syntax.ts +3 -3
  52. package/src/worker.ts +12 -6
  53. package/worker-configuration.d.ts +195 -164
  54. package/pnpm-workspace.yaml +0 -4
  55. package/src/admission/jwt.test.ts +0 -96
  56. package/src/admission/jwt.ts +0 -221
  57. package/src/app/handlers.ts +0 -198
  58. package/src/example.test.ts +0 -377
  59. package/src/example.ts +0 -73
  60. package/src/observer.test.ts +0 -54
  61. package/src/observer.ts +0 -315
  62. package/src/timing.ts +0 -36
@@ -0,0 +1,137 @@
1
+ import type { ProviderInvocationContext } from "./execution-types.ts";
2
+ import { RouterError } from "./errors.ts";
3
+ import { toBody } from "./error-codec.ts";
4
+ import type { ProviderRuntime, RouterRuntime } from "./runtime.ts";
5
+ import type {
6
+ RequestContext,
7
+ TargetAuthContext,
8
+ } from "../upstream-auth/types.ts";
9
+ import { cloneForwardHeaders } from "../util/headers.ts";
10
+ import { observeTimedExecution } from "./execution-events.ts";
11
+
12
+ export async function fetchProvider(
13
+ runtime: RouterRuntime,
14
+ ctx: RequestContext,
15
+ provider: ProviderRuntime,
16
+ body: Uint8Array,
17
+ signal: AbortSignal,
18
+ providerCtx: ProviderInvocationContext,
19
+ ): Promise<Response> {
20
+ const headers = cloneForwardHeaders(ctx.request.headers);
21
+ headers.set("content-type", "application/json");
22
+ for (const [key, value] of Object.entries(provider.headers)) {
23
+ headers.set(key, value);
24
+ }
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;
53
+ }
54
+ new Headers(auth.headers).forEach((value, key) => headers.set(key, value));
55
+ break;
56
+ }
57
+
58
+ return runtime.fetchImpl(`${provider.apiBaseUrl}${provider.endpointPath}`, {
59
+ method: "POST",
60
+ headers,
61
+ body: toBody(body),
62
+ signal,
63
+ });
64
+ }
65
+
66
+ export function getProviderOrThrow(
67
+ runtime: RouterRuntime,
68
+ providerName: string | undefined,
69
+ ) {
70
+ if (!providerName) {
71
+ throw new RouterError(
72
+ "No provider specified on execution target",
73
+ 500,
74
+ "provider_not_configured",
75
+ );
76
+ }
77
+ const provider = runtime.providers.get(providerName);
78
+ if (!provider) {
79
+ throw new RouterError(
80
+ `Provider ${providerName} is not configured`,
81
+ 500,
82
+ "provider_not_configured",
83
+ );
84
+ }
85
+ return provider;
86
+ }
87
+
88
+ export function createUpstreamSignal(
89
+ signal: AbortSignal,
90
+ timeoutMs: number,
91
+ ): {
92
+ signal: AbortSignal;
93
+ cleanup(): void;
94
+ } {
95
+ if (timeoutMs <= 0) {
96
+ return {
97
+ signal,
98
+ cleanup() {},
99
+ };
100
+ }
101
+
102
+ const controller = new AbortController();
103
+ const onAbort = () => controller.abort(signal.reason);
104
+ if (signal.aborted) {
105
+ onAbort();
106
+ } else {
107
+ signal.addEventListener("abort", onAbort, { once: true });
108
+ }
109
+
110
+ const timer = setTimeout(() => {
111
+ controller.abort(
112
+ new RouterError(
113
+ `Upstream provider timed out after ${timeoutMs}ms`,
114
+ 504,
115
+ "upstream_timeout",
116
+ ),
117
+ );
118
+ }, timeoutMs);
119
+
120
+ return {
121
+ signal: controller.signal,
122
+ cleanup() {
123
+ clearTimeout(timer);
124
+ signal.removeEventListener("abort", onAbort);
125
+ },
126
+ };
127
+ }
128
+
129
+ export function normalizeUpstreamAbort(
130
+ error: unknown,
131
+ signal: AbortSignal,
132
+ ): unknown {
133
+ if (signal.aborted && signal.reason instanceof RouterError) {
134
+ return signal.reason;
135
+ }
136
+ return error;
137
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createExecutionEvent } from "@neutrome/lilsdk-ts";
3
+ import { createRouterTelemetry } from "./telemetry.ts";
4
+
5
+ describe("createRouterTelemetry", () => {
6
+ it("exports OTLP spans and errors to their separate endpoints", async () => {
7
+ const fetchMock = vi
8
+ .spyOn(globalThis, "fetch")
9
+ .mockResolvedValue(new Response(null, { status: 200 }));
10
+ const telemetry = createRouterTelemetry({
11
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://traces.example/v1/traces",
12
+ OTEL_EXPORTER_OTLP_TRACES_HEADERS: "authorization=Bearer%20trace-token",
13
+ OTEL_EXPORTER_OTLP_ERRORS_ENDPOINT: "https://errors.example/v1/traces",
14
+ OTEL_EXPORTER_OTLP_ERRORS_HEADERS: "authorization=Bearer%20error-token",
15
+ });
16
+ telemetry.observe(
17
+ createExecutionEvent({
18
+ kind: "provider.started",
19
+ requestId: "request-1",
20
+ executionId: "provider-1",
21
+ data: { startedAt: "2026-07-13T12:00:00.000Z", provider: "openai" },
22
+ }),
23
+ );
24
+ telemetry.observe(
25
+ createExecutionEvent({
26
+ kind: "execution.failed",
27
+ requestId: "request-1",
28
+ executionId: "provider-1",
29
+ data: { message: "upstream unavailable" },
30
+ }),
31
+ );
32
+
33
+ await telemetry.flush();
34
+
35
+ expect(fetchMock).toHaveBeenCalledTimes(2);
36
+ expect(fetchMock.mock.calls[0]?.[0]).toBe(
37
+ "https://traces.example/v1/traces",
38
+ );
39
+ expect(fetchMock.mock.calls[1]?.[0]).toBe(
40
+ "https://errors.example/v1/traces",
41
+ );
42
+ expect(fetchMock.mock.calls[0]?.[1]?.headers).toMatchObject({
43
+ authorization: "Bearer trace-token",
44
+ });
45
+ expect(fetchMock.mock.calls[1]?.[1]?.headers).toMatchObject({
46
+ authorization: "Bearer error-token",
47
+ });
48
+ fetchMock.mockRestore();
49
+ });
50
+ });
@@ -0,0 +1,316 @@
1
+ import type { ExecutionEvent } from "@neutrome/lilsdk-ts";
2
+ import {
3
+ attributes,
4
+ exportOtlp,
5
+ randomHex,
6
+ rootOtlpSpan,
7
+ unixNanos,
8
+ type OtlpSpan,
9
+ } from "./otlp.ts";
10
+
11
+ type TelemetryEnvironment = Record<string, unknown>;
12
+
13
+ export type ExecutionSummary = {
14
+ spanId: string;
15
+ startedAt: string;
16
+ finishedAt: string;
17
+ spans: ExecutionSummarySpan[];
18
+ totalTokensInput: number;
19
+ totalTokensOutput: number;
20
+ totalCachedTokensInput: number;
21
+ error?: string;
22
+ };
23
+
24
+ export type ExecutionSummarySpan = {
25
+ kind: "request" | "provider" | "tool" | "executor";
26
+ event: string;
27
+ requestId: string;
28
+ executionId: string;
29
+ parentExecutionId?: string;
30
+ model?: string;
31
+ provider?: string;
32
+ toolName?: string;
33
+ executor?: string;
34
+ startedAt: string;
35
+ finishedAt?: string;
36
+ tokensInput: number;
37
+ tokensOutput: number;
38
+ cachedTokensInput: number;
39
+ requestBytes: number;
40
+ responseBytes: number;
41
+ error?: string;
42
+ };
43
+
44
+ export type RouterTelemetry = {
45
+ observe(event: ExecutionEvent): void;
46
+ recordError(input: {
47
+ message: string;
48
+ requestId: string;
49
+ executionId: string;
50
+ }): void;
51
+ flush(): Promise<void>;
52
+ summary(): ExecutionSummary;
53
+ };
54
+
55
+ export function createRouterTelemetry(
56
+ env: TelemetryEnvironment,
57
+ ): RouterTelemetry {
58
+ const traceId = randomHex(16);
59
+ const rootSpanId = randomHex(8);
60
+ const startedAt = new Date().toISOString();
61
+ const spans: ExecutionSummarySpan[] = [];
62
+ const otlpSpans: OtlpSpan[] = [];
63
+ const activeSpans = new Map<
64
+ string,
65
+ { summary: ExecutionSummarySpan; otlp: OtlpSpan }
66
+ >();
67
+ const executionSpanIds = new Map<string, string>();
68
+ const errors: OtlpSpan[] = [];
69
+ let error: string | undefined;
70
+
71
+ return { observe, recordError, flush, summary };
72
+
73
+ function observe(event: ExecutionEvent): void {
74
+ const timing = eventTiming(event);
75
+ if (isInvocationStarted(event.kind)) {
76
+ const pair = createSpan(
77
+ event,
78
+ timing.startedAt ?? event.timestamp,
79
+ undefined,
80
+ );
81
+ spans.push(pair.summary);
82
+ activeSpans.set(event.executionId, pair);
83
+ executionSpanIds.set(event.executionId, pair.otlp.spanId);
84
+ return;
85
+ }
86
+ if (isInvocationFinished(event.kind)) {
87
+ const active = activeSpans.get(event.executionId);
88
+ if (active) {
89
+ const finishedAt = timing.finishedAt ?? event.timestamp;
90
+ active.summary.finishedAt = finishedAt;
91
+ active.otlp.endTimeUnixNano = unixNanos(finishedAt);
92
+ activeSpans.delete(event.executionId);
93
+ }
94
+ return;
95
+ }
96
+ if (event.kind === "provider.usage") {
97
+ const active = activeSpans.get(event.executionId);
98
+ if (active) applyUsage(active.summary, active.otlp, event);
99
+ return;
100
+ }
101
+ if (event.kind === "execution.failed") {
102
+ const message = stringValue(event.data?.message) ?? "Execution failed";
103
+ error ??= message;
104
+ const active = activeSpans.get(event.executionId);
105
+ if (active)
106
+ markSpanError(active.summary, active.otlp, message, event.timestamp);
107
+ recordError({
108
+ message,
109
+ requestId: event.requestId,
110
+ executionId: event.executionId,
111
+ });
112
+ return;
113
+ }
114
+ if (!timing.startedAt) return;
115
+ const pair = createSpan(
116
+ event,
117
+ timing.startedAt,
118
+ timing.finishedAt ?? event.timestamp,
119
+ );
120
+ spans.push(pair.summary);
121
+ }
122
+
123
+ function recordError(input: {
124
+ message: string;
125
+ requestId: string;
126
+ executionId: string;
127
+ }): void {
128
+ error ??= input.message;
129
+ const now = new Date().toISOString();
130
+ errors.push({
131
+ traceId,
132
+ spanId: randomHex(8),
133
+ parentSpanId: executionSpanIds.get(input.executionId) ?? rootSpanId,
134
+ name: "router.error",
135
+ startTimeUnixNano: unixNanos(now),
136
+ endTimeUnixNano: unixNanos(now),
137
+ attributes: attributes({
138
+ "error.message": input.message,
139
+ "lil.request.id": input.requestId,
140
+ "lil.execution.id": input.executionId,
141
+ }),
142
+ status: { code: 2, message: input.message },
143
+ });
144
+ }
145
+
146
+ async function flush(): Promise<void> {
147
+ const finishedAt = new Date().toISOString();
148
+ for (const active of activeSpans.values()) {
149
+ active.summary.finishedAt ??= finishedAt;
150
+ active.otlp.endTimeUnixNano = unixNanos(active.summary.finishedAt);
151
+ }
152
+ activeSpans.clear();
153
+ const root = rootOtlpSpan({
154
+ traceId,
155
+ spanId: rootSpanId,
156
+ startedAt,
157
+ finishedAt,
158
+ ...(error ? { error } : {}),
159
+ });
160
+ await Promise.all([
161
+ exportOtlp(env, "traces", [root, ...otlpSpans]),
162
+ exportOtlp(env, "errors", errors),
163
+ ]);
164
+ }
165
+
166
+ function summary(): ExecutionSummary {
167
+ const finishedAt = new Date().toISOString();
168
+ return {
169
+ spanId: traceId,
170
+ startedAt,
171
+ finishedAt,
172
+ spans,
173
+ totalTokensInput: spans.reduce(
174
+ (total, span) => total + span.tokensInput,
175
+ 0,
176
+ ),
177
+ totalTokensOutput: spans.reduce(
178
+ (total, span) => total + span.tokensOutput,
179
+ 0,
180
+ ),
181
+ totalCachedTokensInput: spans.reduce(
182
+ (total, span) => total + span.cachedTokensInput,
183
+ 0,
184
+ ),
185
+ ...(error ? { error } : {}),
186
+ };
187
+ }
188
+
189
+ function createSpan(
190
+ event: ExecutionEvent,
191
+ startedAt: string,
192
+ finishedAt: string | undefined,
193
+ ) {
194
+ const kind = spanKind(event.kind);
195
+ const summary: ExecutionSummarySpan = {
196
+ kind,
197
+ event: event.kind,
198
+ requestId: event.requestId,
199
+ executionId: event.executionId,
200
+ ...(event.parentExecutionId
201
+ ? { parentExecutionId: event.parentExecutionId }
202
+ : {}),
203
+ ...eventDetails(event),
204
+ startedAt,
205
+ ...(finishedAt ? { finishedAt } : {}),
206
+ tokensInput: 0,
207
+ tokensOutput: 0,
208
+ cachedTokensInput: 0,
209
+ requestBytes: 0,
210
+ responseBytes: 0,
211
+ };
212
+ const parentSpanId = event.parentExecutionId
213
+ ? executionSpanIds.get(event.parentExecutionId)
214
+ : rootSpanId;
215
+ const otlp: OtlpSpan = {
216
+ traceId,
217
+ spanId: randomHex(8),
218
+ ...(parentSpanId ? { parentSpanId } : {}),
219
+ name: event.kind,
220
+ startTimeUnixNano: unixNanos(startedAt),
221
+ endTimeUnixNano: unixNanos(finishedAt ?? startedAt),
222
+ attributes: attributes({
223
+ "lil.request.id": event.requestId,
224
+ "lil.execution.id": event.executionId,
225
+ ...event.data,
226
+ }),
227
+ };
228
+ otlpSpans.push(otlp);
229
+ return { summary, otlp };
230
+ }
231
+ }
232
+
233
+ function eventTiming(event: ExecutionEvent): {
234
+ startedAt: string | undefined;
235
+ finishedAt: string | undefined;
236
+ } {
237
+ return {
238
+ startedAt: stringValue(event.data?.startedAt),
239
+ finishedAt: stringValue(event.data?.finishedAt),
240
+ };
241
+ }
242
+
243
+ function isInvocationStarted(kind: string): boolean {
244
+ return kind.endsWith(".started") || kind.endsWith(".stream_started");
245
+ }
246
+
247
+ function isInvocationFinished(kind: string): boolean {
248
+ return kind.endsWith(".finished") || kind.endsWith(".stream_finished");
249
+ }
250
+
251
+ function spanKind(kind: string): ExecutionSummarySpan["kind"] {
252
+ if (kind.startsWith("provider.")) return "provider";
253
+ if (kind.startsWith("tool.")) return "tool";
254
+ if (kind.startsWith("executor.")) return "executor";
255
+ return "request";
256
+ }
257
+
258
+ function eventDetails(event: ExecutionEvent): Partial<ExecutionSummarySpan> {
259
+ return {
260
+ ...optional("model", stringValue(event.data?.model)),
261
+ ...optional("provider", stringValue(event.data?.provider)),
262
+ ...optional("toolName", stringValue(event.data?.toolName)),
263
+ ...optional(
264
+ "executor",
265
+ stringValue(event.data?.executor) ?? stringValue(event.data?.executorId),
266
+ ),
267
+ };
268
+ }
269
+
270
+ function applyUsage(
271
+ summary: ExecutionSummarySpan,
272
+ span: OtlpSpan,
273
+ event: ExecutionEvent,
274
+ ): void {
275
+ summary.tokensInput = numberValue(event.data?.tokensInput);
276
+ summary.tokensOutput = numberValue(event.data?.tokensOutput);
277
+ summary.cachedTokensInput = numberValue(event.data?.cachedTokensInput);
278
+ summary.requestBytes = numberValue(event.data?.requestBytes);
279
+ summary.responseBytes = numberValue(event.data?.responseBytes);
280
+ span.attributes.push(
281
+ ...attributes({
282
+ "gen_ai.usage.input_tokens": summary.tokensInput,
283
+ "gen_ai.usage.output_tokens": summary.tokensOutput,
284
+ "lil.usage.cached_input_tokens": summary.cachedTokensInput,
285
+ "http.request.body.size": summary.requestBytes,
286
+ "http.response.body.size": summary.responseBytes,
287
+ }),
288
+ );
289
+ }
290
+
291
+ function markSpanError(
292
+ summary: ExecutionSummarySpan,
293
+ span: OtlpSpan,
294
+ message: string,
295
+ finishedAt: string,
296
+ ): void {
297
+ summary.error = message;
298
+ summary.finishedAt ??= finishedAt;
299
+ span.endTimeUnixNano = unixNanos(summary.finishedAt);
300
+ span.status = { code: 2, message };
301
+ }
302
+
303
+ function stringValue(value: unknown): string | undefined {
304
+ return typeof value === "string" && value.length > 0 ? value : undefined;
305
+ }
306
+ function numberValue(value: unknown): number {
307
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
308
+ ? value
309
+ : 0;
310
+ }
311
+ function optional<K extends string>(
312
+ key: K,
313
+ value: string | undefined,
314
+ ): Partial<Record<K, string>> {
315
+ return value ? ({ [key]: value } as Record<K, string>) : {};
316
+ }
@@ -0,0 +1,29 @@
1
+ import type { ExecutionEvent as AuditEvent } from "@neutrome/lilsdk-ts";
2
+
3
+ export function traceTimingKind(event: AuditEvent): string {
4
+ if (event.kind.startsWith("upstream_auth.")) return "upstream_auth";
5
+ if (event.kind.startsWith("admission.")) return "admission";
6
+ if (event.kind.startsWith("response.")) return "response";
7
+ if (event.kind.startsWith("provider.")) return "provider";
8
+ if (event.kind.startsWith("executor.")) return "executor";
9
+ if (event.kind.startsWith("remote_mcp.")) return "remote_mcp";
10
+ if (event.kind.startsWith("tool.")) return "tool";
11
+ return "request";
12
+ }
13
+
14
+ export function traceSegmentLabel(event: AuditEvent): string {
15
+ switch (event.kind) {
16
+ case "request.received":
17
+ return "request";
18
+ case "provider.request":
19
+ return "provider-request";
20
+ case "provider.response":
21
+ return "provider-response";
22
+ case "provider.response_chunk":
23
+ return "provider-response-chunk";
24
+ case "tool.executed":
25
+ return "tool";
26
+ default:
27
+ return event.kind.replace(/\./g, "-") || "trace";
28
+ }
29
+ }
@@ -1,6 +1,13 @@
1
- import type { AuthResult, TargetAuthContext, UpstreamAuthDriver } from "./types.ts";
1
+ import type {
2
+ AuthResult,
3
+ TargetAuthContext,
4
+ UpstreamAuthDriver,
5
+ } from "./types.ts";
2
6
 
3
- export function envAuthDriver(suffix = "_API_KEY", prefix = ""): UpstreamAuthDriver {
7
+ export function envAuthDriver(
8
+ suffix = "_API_KEY",
9
+ prefix = "",
10
+ ): UpstreamAuthDriver {
4
11
  const counters = new Map<string, number>();
5
12
 
6
13
  return {
@@ -25,7 +32,12 @@ function resolveKey(
25
32
  ): string | null {
26
33
  const nValue = env[`${base}${suffix}S_N`];
27
34
  if (nValue) {
28
- const n = typeof nValue === "string" ? parseInt(nValue, 10) : typeof nValue === "number" ? nValue : 0;
35
+ const n =
36
+ typeof nValue === "string"
37
+ ? parseInt(nValue, 10)
38
+ : typeof nValue === "number"
39
+ ? nValue
40
+ : 0;
29
41
  if (n > 0) {
30
42
  const prev = counters.get(base) ?? 0;
31
43
  const index = (prev % n) + 1;
@@ -1,4 +1,9 @@
1
- import type { AuthResult, RequestContext, TargetAuthContext, UpstreamAuthDriver } from "./types.ts";
1
+ import type {
2
+ AuthResult,
3
+ RequestContext,
4
+ TargetAuthContext,
5
+ UpstreamAuthDriver,
6
+ } from "./types.ts";
2
7
 
3
8
  export function proxyAuthDriver(headers: string[]): UpstreamAuthDriver {
4
9
  return {
@@ -3,7 +3,9 @@ import type { UpstreamAuthDriver } from "./types.ts";
3
3
  import { proxyAuthDriver } from "./proxy.ts";
4
4
  import { envAuthDriver } from "./env.ts";
5
5
 
6
- export function buildUpstreamAuthChain(config?: UpstreamAuthConfigShape): UpstreamAuthDriver[] {
6
+ export function buildUpstreamAuthChain(
7
+ config?: UpstreamAuthConfigShape,
8
+ ): UpstreamAuthDriver[] {
7
9
  if (!config) return [proxyAuthDriver(["authorization"]), envAuthDriver()];
8
10
  const order = config.order ?? ["proxy", "env"];
9
11
  const drivers: UpstreamAuthDriver[] = [];
package/src/util/glob.ts CHANGED
@@ -1,4 +1,4 @@
1
- export function matchesGlob(value: string, pattern: string): boolean {
1
+ function matchesGlob(value: string, pattern: string): boolean {
2
2
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
3
3
  const regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
4
4
  return regex.test(value);
@@ -4,20 +4,20 @@ export type ModelSuffixSpec = {
4
4
  raw: string;
5
5
  };
6
6
 
7
- export type ParsedModelSyntax = {
7
+ type ParsedModelSyntax = {
8
8
  baseModel: string;
9
9
  suffixes: ModelSuffixSpec[];
10
10
  };
11
11
 
12
12
  export function parseModelSyntax(
13
13
  model: string,
14
- knownSuffixes?: ReadonlySet<string>,
14
+ modelSuffixes?: ReadonlySet<string>,
15
15
  ): ParsedModelSyntax {
16
16
  const [baseModel = "", ...suffixParts] = model.split("+");
17
17
  const suffixes = suffixParts.filter(Boolean).map((part) => {
18
18
  const [namePart = "", ...args] = part.split(":");
19
19
  const name = decodeURIComponent(namePart).toLowerCase();
20
- if (knownSuffixes && !knownSuffixes.has(name)) {
20
+ if (modelSuffixes && !modelSuffixes.has(name)) {
21
21
  throw new Error(`Unknown model suffix: ${name}`);
22
22
  }
23
23
  return { name, args: args.map(decodeURIComponent), raw: part };
package/src/worker.ts CHANGED
@@ -1,10 +1,12 @@
1
- type ExecutionContext = { waitUntil(p: Promise<unknown>): void; passThroughOnException(): void };
1
+ type ExecutionContext = {
2
+ waitUntil(p: Promise<unknown>): void;
3
+ passThroughOnException(): void;
4
+ };
2
5
 
3
6
  import type { RouterConfig } from "./router/index.ts";
4
- import { createApp } from "./example.ts";
7
+ import { createRouterApp } from "./app/factory.ts";
5
8
 
6
9
  const config: RouterConfig = {
7
- trace: true,
8
10
  upstreamAuth: {
9
11
  order: ["proxy", "env"],
10
12
  proxy: {
@@ -13,7 +15,7 @@ const config: RouterConfig = {
13
15
  },
14
16
  providers: {
15
17
  openrouter: {
16
- api_base_url: "https://openrouter.ai/api/v1",
18
+ apiBaseUrl: "https://openrouter.ai/api/v1",
17
19
  style: "chat-completions",
18
20
  exports: ["*:free"],
19
21
  },
@@ -21,10 +23,14 @@ const config: RouterConfig = {
21
23
  modelNamespaces: {},
22
24
  };
23
25
 
24
- let appPromise: Promise<ReturnType<typeof createApp>> | undefined;
26
+ let appPromise: Promise<ReturnType<typeof createRouterApp>> | undefined;
25
27
 
26
28
  async function getApp() {
27
- return createApp({ config });
29
+ return createRouterApp({
30
+ config,
31
+ executorImplementations: {},
32
+ auth: { authenticate: async () => null },
33
+ });
28
34
  }
29
35
 
30
36
  export default {