@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
package/src/observer.ts DELETED
@@ -1,315 +0,0 @@
1
- import { opcodeName, type Program } from "@neutrome/lil-engine";
2
- import type { AuditEvent } from "./router/execution-types.ts";
3
-
4
- const decoder = new TextDecoder();
5
-
6
- export type TraceSpanKind = "request" | "provider" | "tool" | "executor";
7
-
8
- export type TraceSpan = {
9
- kind: TraceSpanKind;
10
- event: string;
11
- requestId: string;
12
- executionId: string;
13
- parentExecutionId?: string;
14
- model?: string;
15
- provider?: string;
16
- toolName?: string;
17
- executor?: string;
18
- startedAt: string;
19
- finishedAt?: string;
20
- tokensInput: number;
21
- tokensOutput: number;
22
- cachedTokensInput: number;
23
- requestBytes: number;
24
- responseBytes: number;
25
- error?: string;
26
- };
27
-
28
- export type ExecutionTrace = {
29
- spanId: string;
30
- startedAt: string;
31
- finishedAt?: string;
32
- spans: TraceSpan[];
33
- totalTokensInput: number;
34
- totalTokensOutput: number;
35
- totalCachedTokensInput: number;
36
- rawTrace: string;
37
- error?: string;
38
- };
39
-
40
- export function createTraceCollector(spanId = crypto.randomUUID()): {
41
- observe: (event: AuditEvent) => void;
42
- getTrace: () => ExecutionTrace;
43
- } {
44
- const spans: TraceSpan[] = [];
45
- const providerSpans = new Map<string, TraceSpan>();
46
- const executorSpans = new Map<string, TraceSpan>();
47
- const rawSegments: string[] = [];
48
- const startedAt = new Date().toISOString();
49
- let rawSegmentIndex = 0;
50
- let finishedAt: string | undefined;
51
- let error: string | undefined;
52
-
53
- return {
54
- observe(event) {
55
- appendTimingComment(event);
56
- appendLilSegment(event);
57
-
58
- switch (event.kind) {
59
- case "request.received":
60
- spans.push(createSpan("request", event, {
61
- finishedAt: event.timestamp,
62
- }));
63
- break;
64
- case "provider.started":
65
- case "provider.stream_started": {
66
- const span = createSpan("provider", event, {
67
- event: event.kind,
68
- ...withMaybeString("model", asString(event.data?.model)),
69
- ...withMaybeString("provider", asString(event.data?.provider)),
70
- });
71
- spans.push(span);
72
- providerSpans.set(event.executionId, span);
73
- break;
74
- }
75
- case "provider.usage": {
76
- const span = providerSpans.get(event.executionId);
77
- if (!span) break;
78
- span.tokensInput = asNumber(event.data?.tokensInput);
79
- span.tokensOutput = asNumber(event.data?.tokensOutput);
80
- span.cachedTokensInput = asNumber(event.data?.cachedTokensInput);
81
- span.requestBytes = asNumber(event.data?.requestBytes);
82
- span.responseBytes = asNumber(event.data?.responseBytes);
83
- const provider = asString(event.data?.provider);
84
- if (!span.provider && provider) {
85
- span.provider = provider;
86
- }
87
- const model = asString(event.data?.model);
88
- if (!span.model && model) {
89
- span.model = model;
90
- }
91
- break;
92
- }
93
- case "provider.finished":
94
- case "provider.stream_finished": {
95
- const span = providerSpans.get(event.executionId);
96
- if (!span) break;
97
- span.finishedAt = event.timestamp;
98
- break;
99
- }
100
- case "executor.started":
101
- case "executor.stream_started": {
102
- const span = createSpan("executor", event, {
103
- event: event.kind,
104
- ...withMaybeString("executor", asString(event.data?.executor) || asString(event.target?.id)),
105
- });
106
- spans.push(span);
107
- executorSpans.set(event.executionId, span);
108
- break;
109
- }
110
- case "executor.finished":
111
- case "executor.stream_finished": {
112
- const span = executorSpans.get(event.executionId);
113
- if (!span) break;
114
- span.finishedAt = event.timestamp;
115
- break;
116
- }
117
- case "tool.executed":
118
- spans.push(createSpan("tool", event, {
119
- finishedAt: event.timestamp,
120
- ...withMaybeString("toolName", asString(event.data?.toolName)),
121
- }));
122
- break;
123
- case "execution.failed": {
124
- const message = asString(event.data?.message) || `Execution failed (${event.errorKind ?? "unknown"})`;
125
- error = error ?? message;
126
- const span = providerSpans.get(event.executionId) ?? executorSpans.get(event.executionId);
127
- if (span) {
128
- span.error = message;
129
- span.finishedAt = span.finishedAt ?? event.timestamp;
130
- }
131
- break;
132
- }
133
- }
134
- },
135
-
136
- getTrace() {
137
- const totalTokensInput = spans.reduce((sum, span) => sum + span.tokensInput, 0);
138
- const totalTokensOutput = spans.reduce((sum, span) => sum + span.tokensOutput, 0);
139
- const totalCachedTokensInput = spans.reduce((sum, span) => sum + span.cachedTokensInput, 0);
140
- finishedAt ??= new Date().toISOString();
141
- return {
142
- spanId,
143
- startedAt,
144
- finishedAt,
145
- spans,
146
- totalTokensInput,
147
- totalTokensOutput,
148
- totalCachedTokensInput,
149
- rawTrace: rawSegments.join("\n\n"),
150
- ...(error ? { error } : {}),
151
- };
152
- },
153
- };
154
-
155
- function appendLilSegment(event: AuditEvent): void {
156
- const lilText = asString(event.data?.lilText)?.trim();
157
- if (!lilText) return;
158
- rawSegments.push(`;${traceSegmentLabel(event)}:${rawSegmentIndex++}\n${lilText}`);
159
- }
160
-
161
- function appendTimingComment(event: AuditEvent): void {
162
- const startedAt = asString(event.data?.startedAt);
163
- const finishedAt = asString(event.data?.finishedAt);
164
- const rawDurationMs = event.data?.durationMs;
165
- const durationMs = typeof rawDurationMs === "number" && Number.isFinite(rawDurationMs) && rawDurationMs >= 0
166
- ? Math.round(rawDurationMs)
167
- : undefined;
168
- if (!startedAt && !finishedAt && durationMs === undefined) return;
169
-
170
- rawSegments.push(`;timing ${JSON.stringify(compactObject({
171
- kind: traceTimingKind(event),
172
- event: event.kind,
173
- requestId: event.requestId,
174
- executionId: event.executionId,
175
- ...(event.parentExecutionId ? { parentExecutionId: event.parentExecutionId } : {}),
176
- startedAt,
177
- finishedAt: finishedAt ?? (durationMs !== undefined ? event.timestamp : undefined),
178
- durationMs,
179
- provider: asString(event.data?.provider),
180
- model: asString(event.data?.model),
181
- toolName: asString(event.data?.toolName),
182
- executor: asString(event.data?.executor) || asString(event.data?.executorId),
183
- driver: asString(event.data?.driver),
184
- source: asString(event.data?.source),
185
- matched: typeof event.data?.matched === "boolean" ? event.data.matched : undefined,
186
- status: typeof event.data?.status === "number" ? event.data.status : undefined,
187
- bytes: typeof event.data?.bytes === "number" ? event.data.bytes : undefined,
188
- }))}`);
189
- }
190
- }
191
-
192
- function traceTimingKind(event: AuditEvent): string {
193
- if (event.kind.startsWith("upstream_auth.")) return "upstream_auth";
194
- if (event.kind.startsWith("admission.")) return "admission";
195
- if (event.kind.startsWith("response.")) return "response";
196
- if (event.kind.startsWith("provider.")) return "provider";
197
- if (event.kind.startsWith("executor.")) return "executor";
198
- if (event.kind.startsWith("remote_mcp.")) return "remote_mcp";
199
- if (event.kind.startsWith("tool.")) return "tool";
200
- return "request";
201
- }
202
-
203
- function traceSegmentLabel(event: AuditEvent): string {
204
- switch (event.kind) {
205
- case "request.received":
206
- return "request";
207
- case "provider.request":
208
- return "provider-request";
209
- case "provider.response":
210
- return "provider-response";
211
- case "provider.response_chunk":
212
- return "provider-response-chunk";
213
- case "tool.executed":
214
- return "tool";
215
- default:
216
- return event.kind.replace(/\./g, "-") || "trace";
217
- }
218
- }
219
-
220
- export function renderProgramAsLil(program: Program): string {
221
- return program.code
222
- .map((instruction) => {
223
- const name = opcodeName(instruction.opcode) ?? `OP_${instruction.opcode}`;
224
- const value = formatInstructionValue(program, instruction.value);
225
- return value ? `${name} ${value}` : name;
226
- })
227
- .join("\n");
228
- }
229
-
230
- function createSpan(
231
- kind: TraceSpanKind,
232
- event: AuditEvent,
233
- input: Partial<Omit<TraceSpan, "kind" | "requestId" | "executionId" | "startedAt">> = {},
234
- ): TraceSpan {
235
- return {
236
- kind,
237
- event: input.event ?? event.kind,
238
- requestId: event.requestId,
239
- executionId: event.executionId,
240
- ...(event.parentExecutionId ? { parentExecutionId: event.parentExecutionId } : {}),
241
- startedAt: event.timestamp,
242
- ...withMaybeString("model", input.model),
243
- ...withMaybeString("provider", input.provider),
244
- ...withMaybeString("toolName", input.toolName),
245
- ...withMaybeString("executor", input.executor),
246
- ...(input.finishedAt ? { finishedAt: input.finishedAt } : {}),
247
- tokensInput: input.tokensInput ?? 0,
248
- tokensOutput: input.tokensOutput ?? 0,
249
- cachedTokensInput: input.cachedTokensInput ?? 0,
250
- requestBytes: input.requestBytes ?? 0,
251
- responseBytes: input.responseBytes ?? 0,
252
- ...withMaybeString("error", input.error),
253
- };
254
- }
255
-
256
- function formatInstructionValue(program: Program, value: Program["code"][number]["value"]): string {
257
- switch (value.kind) {
258
- case "none":
259
- return "";
260
- case "string":
261
- return JSON.stringify(value.value);
262
- case "float":
263
- case "int":
264
- return String(value.value);
265
- case "json":
266
- return formatBytes(value.value);
267
- case "ref": {
268
- const bytes = program.buffers[value.value];
269
- if (!bytes) {
270
- return `@${value.value}`;
271
- }
272
- return `@${value.value}=${formatBytes(bytes)}`;
273
- }
274
- case "key_string":
275
- return `${JSON.stringify(value.key)}=${JSON.stringify(value.value)}`;
276
- case "key_json":
277
- return `${JSON.stringify(value.key)}=${formatBytes(value.value)}`;
278
- }
279
- }
280
-
281
- function formatBytes(bytes: Uint8Array): string {
282
- const text = decoder.decode(bytes);
283
- if (text.includes("\uFFFD")) {
284
- return `base64:${toBase64(bytes)}`;
285
- }
286
- const trimmed = text.trim();
287
- if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"")) {
288
- return trimmed;
289
- }
290
- return JSON.stringify(text);
291
- }
292
-
293
- function toBase64(bytes: Uint8Array): string {
294
- let binary = "";
295
- for (const byte of bytes) {
296
- binary += String.fromCharCode(byte);
297
- }
298
- return btoa(binary);
299
- }
300
-
301
- function asString(value: unknown): string | undefined {
302
- return typeof value === "string" && value.length > 0 ? value : undefined;
303
- }
304
-
305
- function asNumber(value: unknown): number {
306
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
307
- }
308
-
309
- function withMaybeString<K extends string>(key: K, value: string | undefined): Partial<Record<K, string>> {
310
- return value ? { [key]: value } as Record<K, string> : {};
311
- }
312
-
313
- function compactObject<T extends Record<string, unknown>>(value: T): Record<string, unknown> {
314
- return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
315
- }
package/src/timing.ts DELETED
@@ -1,36 +0,0 @@
1
- import { createAuditEvent } from "./router/audit.ts";
2
- import type { AuditEvent } from "./router/execution-types.ts";
3
-
4
- export type TimingEventInput = {
5
- observe?: ((event: AuditEvent) => void) | undefined;
6
- kind: string;
7
- requestId: string;
8
- executionId: string;
9
- parentExecutionId?: string | undefined;
10
- startedAtMs: number;
11
- finishedAtMs?: number | undefined;
12
- data?: Record<string, unknown> | undefined;
13
- };
14
-
15
- export function observeTimingEvent(input: TimingEventInput): void {
16
- if (!input.observe) return;
17
-
18
- const finishedAtMs = input.finishedAtMs;
19
- const data: Record<string, unknown> = {
20
- ...(input.data ?? {}),
21
- startedAt: new Date(input.startedAtMs).toISOString(),
22
- };
23
- if (finishedAtMs !== undefined) {
24
- data.finishedAt = new Date(finishedAtMs).toISOString();
25
- data.durationMs = Math.max(0, finishedAtMs - input.startedAtMs);
26
- }
27
-
28
- input.observe(createAuditEvent({
29
- kind: input.kind,
30
- requestId: input.requestId,
31
- executionId: input.executionId,
32
- ...(input.parentExecutionId ? { parentExecutionId: input.parentExecutionId } : {}),
33
- timestamp: new Date(finishedAtMs ?? input.startedAtMs).toISOString(),
34
- data,
35
- }));
36
- }