@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.
- package/.dev.vars.example +2 -0
- package/README.md +5 -1
- package/package.json +5 -4
- package/src/admission/access.ts +0 -1
- package/src/admission/types.ts +0 -1
- package/src/app/factory.test.ts +40 -34
- package/src/app/factory.ts +120 -173
- package/src/app/google-genai-route.ts +48 -0
- package/src/app/http-utils.ts +14 -0
- package/src/app/model-listing.ts +29 -0
- package/src/app/response-lifecycle.ts +66 -0
- package/src/app/types.ts +38 -0
- package/src/index.ts +11 -43
- package/src/otlp.ts +148 -0
- package/src/router/audit.ts +5 -12
- package/src/router/config.ts +4 -4
- package/src/router/error-codec.ts +170 -0
- package/src/router/errors.ts +38 -1
- package/src/router/execute.test.ts +177 -107
- package/src/router/execute.ts +68 -924
- package/src/router/execution-events.ts +39 -0
- package/src/router/execution-invocation.ts +144 -0
- package/src/router/execution-observation.ts +161 -0
- package/src/router/execution-resolution.ts +120 -0
- package/src/router/execution-runtime.test.ts +156 -83
- package/src/router/execution-runtime.ts +144 -542
- package/src/router/execution-transforms.ts +82 -0
- package/src/router/execution-types.ts +8 -19
- package/src/router/index.ts +6 -5
- package/src/router/mcp.ts +12 -6
- package/src/router/provider-invoker.ts +134 -0
- package/src/router/provider-stream.ts +192 -0
- package/src/router/provider-telemetry.ts +59 -0
- package/src/router/remote-mcp-execution.ts +61 -0
- package/src/router/request-program.ts +16 -0
- package/src/router/request-target.ts +56 -0
- package/src/router/resolve.test.ts +43 -10
- package/src/router/resolve.ts +50 -28
- package/src/router/response-delivery.ts +141 -0
- package/src/router/runtime.test.ts +56 -0
- package/src/router/runtime.ts +79 -9
- package/src/router/targets.ts +3 -1
- package/src/router/upstream-client.ts +137 -0
- package/src/telemetry.test.ts +50 -0
- package/src/telemetry.ts +316 -0
- package/src/trace-labels.ts +29 -0
- package/src/upstream-auth/env.ts +15 -3
- package/src/upstream-auth/proxy.ts +6 -1
- package/src/upstream-auth/registry.ts +3 -1
- package/src/util/glob.ts +1 -1
- package/src/util/model-syntax.ts +3 -3
- package/src/worker.ts +12 -6
- package/worker-configuration.d.ts +195 -164
- package/pnpm-workspace.yaml +0 -4
- package/src/admission/jwt.test.ts +0 -96
- package/src/admission/jwt.ts +0 -221
- package/src/app/handlers.ts +0 -198
- package/src/example.test.ts +0 -377
- package/src/example.ts +0 -73
- package/src/observer.test.ts +0 -54
- package/src/observer.ts +0 -315
- 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
|
+
});
|
package/src/telemetry.ts
ADDED
|
@@ -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
|
+
}
|
package/src/upstream-auth/env.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AuthResult,
|
|
3
|
+
TargetAuthContext,
|
|
4
|
+
UpstreamAuthDriver,
|
|
5
|
+
} from "./types.ts";
|
|
2
6
|
|
|
3
|
-
export function envAuthDriver(
|
|
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 =
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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);
|
package/src/util/model-syntax.ts
CHANGED
|
@@ -4,20 +4,20 @@ export type ModelSuffixSpec = {
|
|
|
4
4
|
raw: string;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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 (
|
|
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 = {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
26
|
+
let appPromise: Promise<ReturnType<typeof createRouterApp>> | undefined;
|
|
25
27
|
|
|
26
28
|
async function getApp() {
|
|
27
|
-
return
|
|
29
|
+
return createRouterApp({
|
|
30
|
+
config,
|
|
31
|
+
executorImplementations: {},
|
|
32
|
+
auth: { authenticate: async () => null },
|
|
33
|
+
});
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export default {
|