@neutrome/open-ai-router 0.1.10 → 0.1.12

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 CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@neutrome/open-ai-router",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
7
7
  },
8
8
  "dependencies": {
9
9
  "hono": "^4.12.18",
10
- "@neutrome/lil-engine": "0.1.3"
10
+ "@neutrome/lil-engine": "0.1.4"
11
11
  },
12
12
  "devDependencies": {
13
13
  "@types/node": "^25.9.3",
@@ -3,6 +3,7 @@ import type { Executor, ProgramTransform } from "@neutrome/lil-engine";
3
3
  import type { ExecutionRuntimeOptions } from "@neutrome/lil-engine";
4
4
  import { cors } from "./cors.ts";
5
5
  import { healthHandler } from "./health.ts";
6
+ import { createTraceCollector, type ExecutionTrace } from "../observer.ts";
6
7
  import {
7
8
  createRouterRuntime,
8
9
  handleAnthropicMessages,
@@ -14,7 +15,6 @@ import {
14
15
  } from "../router/index.ts";
15
16
  import type { RequestContext } from "../auth/types.ts";
16
17
  import type { RouterConfig } from "../router/config.ts";
17
- import type { UsageReport, ProviderCallReport } from "../router/execute.ts";
18
18
 
19
19
  export type RouterAppAuth = {
20
20
  authenticate(request: Request, env: Record<string, unknown>): Promise<object | null>;
@@ -23,9 +23,7 @@ export type RouterAppAuth = {
23
23
 
24
24
  export type RouterAppHooks = {
25
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
- onProviderCall?(report: ProviderCallReport, env: Record<string, unknown>): void;
28
- onToolCall?(report: { toolName: string }, env: Record<string, unknown>): void;
26
+ onTrace?(trace: ExecutionTrace, env: Record<string, unknown>): void | Promise<void>;
29
27
  };
30
28
 
31
29
  export type RouterAppOptions = {
@@ -113,24 +111,39 @@ export function createRouterApp(options: RouterAppOptions) {
113
111
  driver.collectIncoming?.(ctx);
114
112
  }
115
113
 
116
- const onUsage = options.hooks?.onUsage
117
- ? (report: UsageReport) => options.hooks!.onUsage!(report, env)
118
- : undefined;
119
- const onProviderCall = options.hooks?.onProviderCall
120
- ? (report: ProviderCallReport) => options.hooks!.onProviderCall!(report, env)
121
- : undefined;
122
-
123
- return handler({
124
- runtime,
125
- ctx,
126
- executors: options.executors,
127
- ...(options.transforms ? { transforms: options.transforms } : {}),
128
- ...(options.observe ? { observe: options.observe } : {}),
129
- ...(options.upstreamTimeoutMs !== undefined ? { upstreamTimeoutMs: options.upstreamTimeoutMs } : {}),
130
- ...(onUsage ? { onUsage } : {}),
131
- ...(onProviderCall ? { onProviderCall } : {}),
132
- ...(forceStreaming ? { forceStreaming: true } : {}),
133
- });
114
+ const collector = createTraceCollector();
115
+ const observe = (event: Parameters<NonNullable<ExecutionRuntimeOptions["observe"]>>[0]) => {
116
+ collector.observe(event);
117
+ options.observe?.(event);
118
+ };
119
+ let traceFinalized = false;
120
+ const finalizeTrace = () => {
121
+ if (traceFinalized) return;
122
+ traceFinalized = true;
123
+ Promise.resolve(options.hooks?.onTrace?.(collector.getTrace(), env)).catch((error) => {
124
+ console.error("[open-ai-router] onTrace hook failed:", error);
125
+ });
126
+ };
127
+
128
+ try {
129
+ const response = await handler({
130
+ runtime,
131
+ ctx,
132
+ executors: options.executors,
133
+ ...(options.transforms ? { transforms: options.transforms } : {}),
134
+ observe,
135
+ ...(options.upstreamTimeoutMs !== undefined ? { upstreamTimeoutMs: options.upstreamTimeoutMs } : {}),
136
+ ...(forceStreaming ? { forceStreaming: true } : {}),
137
+ });
138
+ if (!isEventStreamResponse(response) || !response.body) {
139
+ finalizeTrace();
140
+ return response;
141
+ }
142
+ return wrapResponseWithFinalize(response, finalizeTrace);
143
+ } catch (error) {
144
+ finalizeTrace();
145
+ throw error;
146
+ }
134
147
  }
135
148
 
136
149
  return app;
@@ -189,3 +202,37 @@ function errorResponse(message: string, status: number, code: string): Response
189
202
  { status, headers: { "content-type": "application/json; charset=utf-8" } },
190
203
  );
191
204
  }
205
+
206
+ function isEventStreamResponse(response: Response): boolean {
207
+ const contentType = response.headers.get("content-type") ?? "";
208
+ return contentType.startsWith("text/event-stream");
209
+ }
210
+
211
+ function wrapResponseWithFinalize(response: Response, finalize: () => void): Response {
212
+ const reader = response.body!.getReader();
213
+ const body = new ReadableStream<Uint8Array>({
214
+ async pull(controller) {
215
+ try {
216
+ const { done, value } = await reader.read();
217
+ if (done) {
218
+ finalize();
219
+ controller.close();
220
+ return;
221
+ }
222
+ controller.enqueue(value);
223
+ } catch (error) {
224
+ finalize();
225
+ controller.error(error);
226
+ }
227
+ },
228
+ async cancel(reason) {
229
+ finalize();
230
+ await reader.cancel(reason).catch(() => {});
231
+ },
232
+ });
233
+ return new Response(body, {
234
+ status: response.status,
235
+ statusText: response.statusText,
236
+ headers: new Headers(response.headers),
237
+ });
238
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  export { createRouterApp } from "./app/factory.ts";
2
2
  export type { RouterAppAuth, RouterAppHooks, RouterAppOptions } from "./app/factory.ts";
3
+ export {
4
+ createTraceCollector,
5
+ renderProgramAsLil,
6
+ } from "./observer.ts";
7
+ export type {
8
+ ExecutionTrace,
9
+ TraceSpan,
10
+ TraceSpanKind,
11
+ } from "./observer.ts";
3
12
 
4
13
  export { createApp } from "./example.ts";
5
14
  export type { CreateAppOptions } from "./example.ts";
@@ -32,15 +41,12 @@ export type {
32
41
  HandleResponsesOptions,
33
42
  ProviderApiStyle,
34
43
  ProviderApiStyle as ProviderStyle,
35
- ProviderCallReport,
36
- ProviderErrorReport,
37
44
  ProviderRuntime,
38
45
  ProviderTargetConfig,
39
46
  ResolvedTarget,
40
47
  RouterConfig,
41
48
  RouterExecutionOptions,
42
49
  RouterRuntime,
43
- UsageReport,
44
50
  } from "./router/index.ts";
45
51
 
46
52
  export type {
@@ -0,0 +1,250 @@
1
+ import { opcodeName, type AuditEvent, type Program } from "@neutrome/lil-engine";
2
+
3
+ const decoder = new TextDecoder();
4
+
5
+ export type TraceSpanKind = "request" | "provider" | "tool" | "executor";
6
+
7
+ export type TraceSpan = {
8
+ kind: TraceSpanKind;
9
+ event: string;
10
+ requestId: string;
11
+ executionId: string;
12
+ parentExecutionId?: string;
13
+ model?: string;
14
+ provider?: string;
15
+ toolName?: string;
16
+ executor?: string;
17
+ startedAt: string;
18
+ finishedAt?: string;
19
+ tokensInput: number;
20
+ tokensOutput: number;
21
+ cachedTokensInput: number;
22
+ requestBytes: number;
23
+ responseBytes: number;
24
+ error?: string;
25
+ };
26
+
27
+ export type ExecutionTrace = {
28
+ spanId: string;
29
+ startedAt: string;
30
+ finishedAt?: string;
31
+ spans: TraceSpan[];
32
+ totalTokensInput: number;
33
+ totalTokensOutput: number;
34
+ totalCachedTokensInput: number;
35
+ rawTrace: string;
36
+ error?: string;
37
+ };
38
+
39
+ export function createTraceCollector(spanId = crypto.randomUUID()): {
40
+ observe: (event: AuditEvent) => void;
41
+ getTrace: () => ExecutionTrace;
42
+ } {
43
+ const spans: TraceSpan[] = [];
44
+ const providerSpans = new Map<string, TraceSpan>();
45
+ const executorSpans = new Map<string, TraceSpan>();
46
+ const rawSegments: string[] = [];
47
+ const startedAt = new Date().toISOString();
48
+ let finishedAt: string | undefined;
49
+ let error: string | undefined;
50
+
51
+ return {
52
+ observe(event) {
53
+ appendLilSegment(event);
54
+
55
+ switch (event.kind) {
56
+ case "request.received":
57
+ spans.push(createSpan("request", event, {
58
+ finishedAt: event.timestamp,
59
+ }));
60
+ break;
61
+ case "provider.started":
62
+ case "provider.stream_started": {
63
+ const span = createSpan("provider", event, {
64
+ event: event.kind,
65
+ ...withMaybeString("model", asString(event.data?.model)),
66
+ ...withMaybeString("provider", asString(event.data?.provider)),
67
+ });
68
+ spans.push(span);
69
+ providerSpans.set(event.executionId, span);
70
+ break;
71
+ }
72
+ case "provider.usage": {
73
+ const span = providerSpans.get(event.executionId);
74
+ if (!span) break;
75
+ span.tokensInput = asNumber(event.data?.tokensInput);
76
+ span.tokensOutput = asNumber(event.data?.tokensOutput);
77
+ span.cachedTokensInput = asNumber(event.data?.cachedTokensInput);
78
+ span.requestBytes = asNumber(event.data?.requestBytes);
79
+ span.responseBytes = asNumber(event.data?.responseBytes);
80
+ const provider = asString(event.data?.provider);
81
+ if (!span.provider && provider) {
82
+ span.provider = provider;
83
+ }
84
+ const model = asString(event.data?.model);
85
+ if (!span.model && model) {
86
+ span.model = model;
87
+ }
88
+ break;
89
+ }
90
+ case "provider.finished":
91
+ case "provider.stream_finished": {
92
+ const span = providerSpans.get(event.executionId);
93
+ if (!span) break;
94
+ span.finishedAt = event.timestamp;
95
+ break;
96
+ }
97
+ case "executor.started":
98
+ case "executor.stream_started": {
99
+ const span = createSpan("executor", event, {
100
+ event: event.kind,
101
+ ...withMaybeString("executor", asString(event.data?.executor) || asString(event.target?.id)),
102
+ });
103
+ spans.push(span);
104
+ executorSpans.set(event.executionId, span);
105
+ break;
106
+ }
107
+ case "executor.finished":
108
+ case "executor.stream_finished": {
109
+ const span = executorSpans.get(event.executionId);
110
+ if (!span) break;
111
+ span.finishedAt = event.timestamp;
112
+ break;
113
+ }
114
+ case "tool.executed":
115
+ spans.push(createSpan("tool", event, {
116
+ finishedAt: event.timestamp,
117
+ ...withMaybeString("toolName", asString(event.data?.toolName)),
118
+ }));
119
+ break;
120
+ case "execution.failed": {
121
+ const message = asString(event.data?.message) || `Execution failed (${event.errorKind ?? "unknown"})`;
122
+ error = error ?? message;
123
+ const span = providerSpans.get(event.executionId) ?? executorSpans.get(event.executionId);
124
+ if (span) {
125
+ span.error = message;
126
+ span.finishedAt = span.finishedAt ?? event.timestamp;
127
+ }
128
+ break;
129
+ }
130
+ }
131
+ },
132
+
133
+ getTrace() {
134
+ const totalTokensInput = spans.reduce((sum, span) => sum + span.tokensInput, 0);
135
+ const totalTokensOutput = spans.reduce((sum, span) => sum + span.tokensOutput, 0);
136
+ const totalCachedTokensInput = spans.reduce((sum, span) => sum + span.cachedTokensInput, 0);
137
+ finishedAt ??= new Date().toISOString();
138
+ return {
139
+ spanId,
140
+ startedAt,
141
+ finishedAt,
142
+ spans,
143
+ totalTokensInput,
144
+ totalTokensOutput,
145
+ totalCachedTokensInput,
146
+ rawTrace: rawSegments.join("\n\n"),
147
+ ...(error ? { error } : {}),
148
+ };
149
+ },
150
+ };
151
+
152
+ function appendLilSegment(event: AuditEvent): void {
153
+ const lilText = asString(event.data?.lilText)?.trim();
154
+ if (!lilText) return;
155
+ rawSegments.push(lilText);
156
+ }
157
+ }
158
+
159
+ export function renderProgramAsLil(program: Program): string {
160
+ return program.code
161
+ .map((instruction) => {
162
+ const name = opcodeName(instruction.opcode) ?? `OP_${instruction.opcode}`;
163
+ const value = formatInstructionValue(program, instruction.value);
164
+ return value ? `${name} ${value}` : name;
165
+ })
166
+ .join("\n");
167
+ }
168
+
169
+ function createSpan(
170
+ kind: TraceSpanKind,
171
+ event: AuditEvent,
172
+ input: Partial<Omit<TraceSpan, "kind" | "requestId" | "executionId" | "startedAt">> = {},
173
+ ): TraceSpan {
174
+ return {
175
+ kind,
176
+ event: input.event ?? event.kind,
177
+ requestId: event.requestId,
178
+ executionId: event.executionId,
179
+ ...(event.parentExecutionId ? { parentExecutionId: event.parentExecutionId } : {}),
180
+ startedAt: event.timestamp,
181
+ ...withMaybeString("model", input.model),
182
+ ...withMaybeString("provider", input.provider),
183
+ ...withMaybeString("toolName", input.toolName),
184
+ ...withMaybeString("executor", input.executor),
185
+ ...(input.finishedAt ? { finishedAt: input.finishedAt } : {}),
186
+ tokensInput: input.tokensInput ?? 0,
187
+ tokensOutput: input.tokensOutput ?? 0,
188
+ cachedTokensInput: input.cachedTokensInput ?? 0,
189
+ requestBytes: input.requestBytes ?? 0,
190
+ responseBytes: input.responseBytes ?? 0,
191
+ ...withMaybeString("error", input.error),
192
+ };
193
+ }
194
+
195
+ function formatInstructionValue(program: Program, value: Program["code"][number]["value"]): string {
196
+ switch (value.kind) {
197
+ case "none":
198
+ return "";
199
+ case "string":
200
+ return JSON.stringify(value.value);
201
+ case "float":
202
+ case "int":
203
+ return String(value.value);
204
+ case "json":
205
+ return formatBytes(value.value);
206
+ case "ref": {
207
+ const bytes = program.buffers[value.value];
208
+ if (!bytes) {
209
+ return `@${value.value}`;
210
+ }
211
+ return `@${value.value}=${formatBytes(bytes)}`;
212
+ }
213
+ case "key_string":
214
+ return `${JSON.stringify(value.key)}=${JSON.stringify(value.value)}`;
215
+ case "key_json":
216
+ return `${JSON.stringify(value.key)}=${formatBytes(value.value)}`;
217
+ }
218
+ }
219
+
220
+ function formatBytes(bytes: Uint8Array): string {
221
+ const text = decoder.decode(bytes);
222
+ if (text.includes("\uFFFD")) {
223
+ return `base64:${toBase64(bytes)}`;
224
+ }
225
+ const trimmed = text.trim();
226
+ if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"")) {
227
+ return trimmed;
228
+ }
229
+ return JSON.stringify(text);
230
+ }
231
+
232
+ function toBase64(bytes: Uint8Array): string {
233
+ let binary = "";
234
+ for (const byte of bytes) {
235
+ binary += String.fromCharCode(byte);
236
+ }
237
+ return btoa(binary);
238
+ }
239
+
240
+ function asString(value: unknown): string | undefined {
241
+ return typeof value === "string" && value.length > 0 ? value : undefined;
242
+ }
243
+
244
+ function asNumber(value: unknown): number {
245
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
246
+ }
247
+
248
+ function withMaybeString<K extends string>(key: K, value: string | undefined): Partial<Record<K, string>> {
249
+ return value ? { [key]: value } as Record<K, string> : {};
250
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ createAuditEvent,
2
3
  emitChatCompletionsResponse,
3
4
  emitProviderRequest,
4
5
  emitProviderResponse,
@@ -27,6 +28,7 @@ import {
27
28
  import type { RequestContext, TargetAuthContext } from "../auth/types.ts";
28
29
  import { cloneForwardHeaders, isSse } from "../util/headers.ts";
29
30
  import { eventDataLines, splitSseEvents } from "../util/sse.ts";
31
+ import { renderProgramAsLil } from "../observer.ts";
30
32
  import { resolveInvocationTarget, type ResolvedTarget } from "./resolve.ts";
31
33
  import type { ProviderRuntime, RouterRuntime } from "./runtime.ts";
32
34
 
@@ -34,25 +36,6 @@ const encoder = new TextEncoder();
34
36
  const decoder = new TextDecoder();
35
37
  const DEFAULT_UPSTREAM_TIMEOUT_MS = 120_000;
36
38
 
37
- export type UsageReport = {
38
- model: string;
39
- provider: string;
40
- tokensInput: number;
41
- tokensOutput: number;
42
- cachedTokensInput: number;
43
- };
44
-
45
- export type ProviderErrorReport = {
46
- model: string;
47
- provider: string;
48
- status: number;
49
- message: string;
50
- };
51
-
52
- export type ProviderCallReport =
53
- | { kind: "usage"; } & UsageReport
54
- | { kind: "error"; } & ProviderErrorReport;
55
-
56
39
  export type RouterExecutionOptions = Pick<
57
40
  ExecutionRuntimeOptions,
58
41
  "observe" | "requestIdFactory" | "executionIdFactory" | "resolveExecutor"
@@ -60,8 +43,6 @@ export type RouterExecutionOptions = Pick<
60
43
  executors?: Readonly<Record<string, Executor>>;
61
44
  transforms?: Readonly<Record<string, ProgramTransform>>;
62
45
  upstreamTimeoutMs?: number;
63
- onUsage?: (report: UsageReport) => void;
64
- onProviderCall?: (report: ProviderCallReport) => void;
65
46
  };
66
47
 
67
48
  export type HandleChatCompletionsOptions = RouterExecutionOptions & {
@@ -121,7 +102,6 @@ export function createRouterExecutionRuntime(
121
102
  runtime,
122
103
  ctx,
123
104
  options.upstreamTimeoutMs,
124
- options.onProviderCall,
125
105
  ),
126
106
  };
127
107
 
@@ -182,6 +162,11 @@ export async function handleChatCompletions(
182
162
  try {
183
163
  const requestBody = new Uint8Array(await options.ctx.request.arrayBuffer());
184
164
  const request = parseChatCompletionsRequest(requestBody);
165
+ const incomingExecutionId = `incoming_${crypto.randomUUID()}`;
166
+ emitObservedProgram(options.observe, "request.received", request, {
167
+ requestId: incomingExecutionId,
168
+ executionId: incomingExecutionId,
169
+ });
185
170
  const resolved = resolveRequestTarget(options.runtime, request);
186
171
  const execution = createRouterExecutionRuntime(
187
172
  options.runtime,
@@ -191,15 +176,12 @@ export async function handleChatCompletions(
191
176
 
192
177
  const startTime = Date.now();
193
178
  if (isStreaming(request)) {
194
- const userWantsUsage = requestedStreamUsage(requestBody);
195
179
  const chunks = execution.stream(request, { target: resolved.target });
196
180
  return await streamExecutionResponse(
197
181
  "chat-completions",
198
182
  chunks,
199
183
  resolved,
200
184
  startTime,
201
- options.onUsage,
202
- userWantsUsage,
203
185
  );
204
186
  }
205
187
 
@@ -207,7 +189,6 @@ export async function handleChatCompletions(
207
189
  target: resolved.target,
208
190
  });
209
191
  const userTime = Date.now() - startTime;
210
- reportUsage(response, resolved, options.onUsage);
211
192
  return jsonExecutionResponse(
212
193
  emitChatCompletionsResponse(response),
213
194
  resolved,
@@ -253,6 +234,11 @@ async function handleProviderStyle(
253
234
  const request = mutateRequest
254
235
  ? mutateRequest(parseProviderRequest(style, requestBody))
255
236
  : parseProviderRequest(style, requestBody);
237
+ const incomingExecutionId = `incoming_${crypto.randomUUID()}`;
238
+ emitObservedProgram(options.observe, "request.received", request, {
239
+ requestId: incomingExecutionId,
240
+ executionId: incomingExecutionId,
241
+ });
256
242
  const resolved = resolveRequestTarget(options.runtime, request);
257
243
  const execution = createRouterExecutionRuntime(
258
244
  options.runtime,
@@ -268,7 +254,6 @@ async function handleProviderStyle(
268
254
  chunks,
269
255
  resolved,
270
256
  startTime,
271
- options.onUsage,
272
257
  );
273
258
  }
274
259
 
@@ -276,7 +261,6 @@ async function handleProviderStyle(
276
261
  target: resolved.target,
277
262
  });
278
263
  const userTime = Date.now() - startTime;
279
- reportUsage(response, resolved, options.onUsage);
280
264
  return jsonExecutionResponse(
281
265
  emitProviderResponse(style, response),
282
266
  resolved,
@@ -311,7 +295,6 @@ export function createFetchProviderInvoker(
311
295
  runtime: RouterRuntime,
312
296
  ctx: RequestContext,
313
297
  upstreamTimeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS,
314
- onProviderCall?: (report: ProviderCallReport) => void,
315
298
  ): ProviderInvoker {
316
299
  return {
317
300
  async execute(request, providerCtx) {
@@ -319,22 +302,37 @@ export function createFetchProviderInvoker(
319
302
  const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
320
303
 
321
304
  try {
305
+ const resolvedRequest = setModel(request, providerCtx.target.model);
306
+ emitObservedProgram(providerCtx.observe, "provider.request", resolvedRequest, {
307
+ requestId: providerCtx.requestId,
308
+ executionId: providerCtx.executionId,
309
+ ...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
310
+ provider: provider.name,
311
+ model: providerCtx.target.model,
312
+ });
313
+ const reqBody = emitProviderRequest(provider.style, resolvedRequest);
314
+ const reqBytes = reqBody.byteLength;
322
315
  const upstream = await fetchProvider(
323
316
  runtime,
324
317
  ctx,
325
318
  provider,
326
- setModel(request, providerCtx.target.model),
319
+ resolvedRequest,
327
320
  timed.signal,
328
321
  );
329
322
  if (!upstream.ok) {
330
- const err = await toProviderError(provider.name, upstream);
331
- reportProviderError(onProviderCall, providerCtx.target, err);
332
- throw err;
323
+ throw await toProviderError(provider.name, upstream);
333
324
  }
334
325
 
335
326
  const bytes = new Uint8Array(await upstream.arrayBuffer());
336
327
  const response = parseProviderResponse(provider.style, bytes);
337
- reportProviderUsage(onProviderCall, response, providerCtx.target);
328
+ emitObservedProgram(providerCtx.observe, "provider.response", response, {
329
+ requestId: providerCtx.requestId,
330
+ executionId: providerCtx.executionId,
331
+ ...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
332
+ provider: provider.name,
333
+ model: providerCtx.target.model,
334
+ });
335
+ emitProviderUsage(providerCtx, response, provider.name, reqBytes, bytes.byteLength);
338
336
  return response;
339
337
  } catch (error) {
340
338
  throw normalizeUpstreamAbort(error, timed.signal);
@@ -350,17 +348,25 @@ export function createFetchProviderInvoker(
350
348
  let buffer = "";
351
349
 
352
350
  try {
351
+ const resolvedRequest = setModel(request, providerCtx.target.model);
352
+ emitObservedProgram(providerCtx.observe, "provider.request", resolvedRequest, {
353
+ requestId: providerCtx.requestId,
354
+ executionId: providerCtx.executionId,
355
+ ...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
356
+ provider: provider.name,
357
+ model: providerCtx.target.model,
358
+ });
359
+ const reqBody = emitProviderRequest(provider.style, resolvedRequest);
360
+ const reqBytes = reqBody.byteLength;
353
361
  const upstream = await fetchProvider(
354
362
  runtime,
355
363
  ctx,
356
364
  provider,
357
- setModel(request, providerCtx.target.model),
365
+ resolvedRequest,
358
366
  timed.signal,
359
367
  );
360
368
  if (!upstream.ok) {
361
- const err = await toProviderError(provider.name, upstream);
362
- reportProviderError(onProviderCall, providerCtx.target, err);
363
- throw err;
369
+ throw await toProviderError(provider.name, upstream);
364
370
  }
365
371
  if (!isSse(upstream.headers) || !upstream.body) {
366
372
  throw new RouterError(
@@ -372,18 +378,20 @@ export function createFetchProviderInvoker(
372
378
 
373
379
  reader = upstream.body.getReader();
374
380
  let lastChunkWithUsage: Program | null = null;
381
+ let respBytes = 0;
375
382
  while (true) {
376
383
  const { done, value } = await reader.read();
377
384
  if (done) {
378
385
  break;
379
386
  }
387
+ respBytes += value.byteLength;
380
388
  buffer += decoder.decode(value, { stream: true });
381
389
  const split = splitSseEvents(buffer);
382
390
  buffer = split.remainder;
383
391
  for (const event of split.events) {
384
392
  for (const data of eventDataLines(event)) {
385
393
  if (data === "[DONE]") {
386
- reportProviderUsage(onProviderCall, lastChunkWithUsage, providerCtx.target);
394
+ emitProviderUsage(providerCtx, lastChunkWithUsage, provider.name, reqBytes, respBytes);
387
395
  return;
388
396
  }
389
397
  if (data[0] !== "{") continue;
@@ -392,6 +400,13 @@ export function createFetchProviderInvoker(
392
400
  encoder.encode(data),
393
401
  );
394
402
  if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
403
+ emitObservedProgram(providerCtx.observe, "provider.response_chunk", chunk, {
404
+ requestId: providerCtx.requestId,
405
+ executionId: providerCtx.executionId,
406
+ ...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
407
+ provider: provider.name,
408
+ model: providerCtx.target.model,
409
+ });
395
410
  yield chunk;
396
411
  }
397
412
  }
@@ -400,7 +415,7 @@ export function createFetchProviderInvoker(
400
415
  if (buffer.length > 0) {
401
416
  for (const data of eventDataLines(buffer)) {
402
417
  if (data === "[DONE]") {
403
- reportProviderUsage(onProviderCall, lastChunkWithUsage, providerCtx.target);
418
+ emitProviderUsage(providerCtx, lastChunkWithUsage, provider.name, reqBytes, respBytes);
404
419
  return;
405
420
  }
406
421
  if (data[0] !== "{") continue;
@@ -409,10 +424,17 @@ export function createFetchProviderInvoker(
409
424
  encoder.encode(data),
410
425
  );
411
426
  if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
427
+ emitObservedProgram(providerCtx.observe, "provider.response_chunk", chunk, {
428
+ requestId: providerCtx.requestId,
429
+ executionId: providerCtx.executionId,
430
+ ...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
431
+ provider: provider.name,
432
+ model: providerCtx.target.model,
433
+ });
412
434
  yield chunk;
413
435
  }
414
436
  }
415
- reportProviderUsage(onProviderCall, lastChunkWithUsage, providerCtx.target);
437
+ emitProviderUsage(providerCtx, lastChunkWithUsage, provider.name, reqBytes, respBytes);
416
438
  } catch (error) {
417
439
  throw normalizeUpstreamAbort(error, timed.signal);
418
440
  } finally {
@@ -429,8 +451,6 @@ async function streamExecutionResponse(
429
451
  chunks: AsyncIterable<Program>,
430
452
  resolved: ResolvedTarget,
431
453
  startTime: number,
432
- onUsage?: (report: UsageReport) => void,
433
- emitUsageToClient = true,
434
454
  ): Promise<Response> {
435
455
  const iterator = chunks[Symbol.asyncIterator]();
436
456
 
@@ -440,12 +460,7 @@ async function streamExecutionResponse(
440
460
  const stream = new ReadableStream<Uint8Array>({
441
461
  async start(controller) {
442
462
  try {
443
- let lastChunkWithUsage: Program | null = null;
444
-
445
463
  function emitChunk(chunk: Program): void {
446
- const hasUsage = usageObject(chunk) !== undefined;
447
- if (hasUsage) lastChunkWithUsage = chunk;
448
- if (hasUsage && !emitUsageToClient) return;
449
464
  const bytes = emitProviderStreamChunk(responseStyle, chunk);
450
465
  controller.enqueue(
451
466
  encoder.encode(`data: ${decoder.decode(bytes)}\n\n`),
@@ -466,9 +481,6 @@ async function streamExecutionResponse(
466
481
  if (responseStyle === "chat-completions") {
467
482
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
468
483
  }
469
- if (lastChunkWithUsage) {
470
- reportUsage(lastChunkWithUsage, resolved, onUsage);
471
- }
472
484
  controller.close();
473
485
  } catch (error) {
474
486
  controller.enqueue(
@@ -741,87 +753,40 @@ function createUpstreamSignal(
741
753
  };
742
754
  }
743
755
 
744
- function reportUsage(
745
- response: Program,
746
- resolved: ResolvedTarget,
747
- onUsage?: (report: UsageReport) => void,
748
- ): void {
749
- if (!onUsage) return;
750
- const usage = usageObject(response);
751
- if (!usage || typeof usage !== "object") return;
752
- const u = usage as Record<string, unknown>;
753
- const tokensInput = asNum(u.prompt_tokens) || asNum(u.input_tokens);
754
- const tokensOutput = asNum(u.completion_tokens) || asNum(u.output_tokens);
755
- if (tokensInput === 0 && tokensOutput === 0) return;
756
- const details = u.prompt_tokens_details as Record<string, unknown> | undefined;
757
- const cachedTokensInput = details ? asNum(details.cached_tokens) : 0;
758
- const model = resolved.target.kind === "provider" ? resolved.target.model : resolved.target.alias;
759
- const provider = resolved.target.kind === "provider" ? (resolved.target.provider ?? "") : "";
760
- try {
761
- onUsage({ model, provider, tokensInput, tokensOutput, cachedTokensInput });
762
- } catch {}
763
- }
764
-
765
- function reportProviderUsage(
766
- onProviderCall: ((report: ProviderCallReport) => void) | undefined,
756
+ function emitProviderUsage(
757
+ providerCtx: import("@neutrome/lil-engine").ProviderInvocationContext,
767
758
  response: Program | null,
768
- target: Extract<import("@neutrome/lil-engine").ExecutionTarget, { kind: "provider" }>,
759
+ provider: string,
760
+ requestBytes: number,
761
+ responseBytes: number,
769
762
  ): void {
770
- if (!onProviderCall) return;
771
763
  const usage = response ? usageObject(response) : undefined;
772
764
  const u = (usage && typeof usage === "object" ? usage : {}) as Record<string, unknown>;
773
765
  const tokensInput = asNum(u.prompt_tokens) || asNum(u.input_tokens);
774
766
  const tokensOutput = asNum(u.completion_tokens) || asNum(u.output_tokens);
775
767
  const details = u.prompt_tokens_details as Record<string, unknown> | undefined;
776
768
  const cachedTokensInput = details ? asNum(details.cached_tokens) : 0;
777
- try {
778
- onProviderCall({
779
- kind: "usage",
780
- model: target.model,
781
- provider: target.provider ?? "",
769
+ providerCtx.observe(createAuditEvent({
770
+ kind: "provider.usage",
771
+ requestId: providerCtx.requestId,
772
+ executionId: providerCtx.executionId,
773
+ ...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
774
+ data: {
775
+ provider,
776
+ model: providerCtx.target.model,
782
777
  tokensInput,
783
778
  tokensOutput,
784
779
  cachedTokensInput,
785
- });
786
- } catch {}
787
- }
788
-
789
- function reportProviderError(
790
- onProviderCall: ((report: ProviderCallReport) => void) | undefined,
791
- target: Extract<import("@neutrome/lil-engine").ExecutionTarget, { kind: "provider" }>,
792
- error: ProviderHttpError,
793
- ): void {
794
- if (!onProviderCall) return;
795
- let message = `HTTP ${error.status}`;
796
- try {
797
- const text = decoder.decode(error.body).slice(0, 500);
798
- message = text || message;
799
- } catch {}
800
- try {
801
- onProviderCall({
802
- kind: "error",
803
- model: target.model,
804
- provider: target.provider ?? "",
805
- status: error.status,
806
- message,
807
- });
808
- } catch {}
780
+ requestBytes,
781
+ responseBytes,
782
+ },
783
+ }));
809
784
  }
810
785
 
811
786
  function asNum(v: unknown): number {
812
787
  return typeof v === "number" && v >= 0 ? v : 0;
813
788
  }
814
789
 
815
- function requestedStreamUsage(body: Uint8Array): boolean {
816
- try {
817
- const obj = JSON.parse(decoder.decode(body)) as Record<string, unknown>;
818
- const opts = obj.stream_options as Record<string, unknown> | undefined;
819
- return opts?.include_usage === true;
820
- } catch {
821
- return false;
822
- }
823
- }
824
-
825
790
  function injectStreamUsage(body: Uint8Array): Uint8Array {
826
791
  try {
827
792
  const obj = JSON.parse(decoder.decode(body)) as Record<string, unknown>;
@@ -838,3 +803,29 @@ function normalizeUpstreamAbort(error: unknown, signal: AbortSignal): unknown {
838
803
  }
839
804
  return error;
840
805
  }
806
+
807
+ function emitObservedProgram(
808
+ observe: ExecutionRuntimeOptions["observe"],
809
+ kind: string,
810
+ program: Program,
811
+ input: {
812
+ requestId: string;
813
+ executionId: string;
814
+ parentExecutionId?: string;
815
+ provider?: string;
816
+ model?: string;
817
+ },
818
+ ): void {
819
+ if (!observe) return;
820
+ observe(createAuditEvent({
821
+ kind,
822
+ requestId: input.requestId,
823
+ executionId: input.executionId,
824
+ ...(input.parentExecutionId ? { parentExecutionId: input.parentExecutionId } : {}),
825
+ data: {
826
+ ...(input.provider ? { provider: input.provider } : {}),
827
+ ...(input.model ? { model: input.model } : {}),
828
+ lilText: renderProgramAsLil(program),
829
+ },
830
+ }));
831
+ }
@@ -32,8 +32,5 @@ export type {
32
32
  HandleGoogleGenAIOptions,
33
33
  HandleResponsesOptions,
34
34
  RouterExecutionOptions,
35
- UsageReport,
36
- ProviderCallReport,
37
- ProviderErrorReport,
38
35
  } from "./execute.ts";
39
36
  export type { ResolvedTarget } from "./resolve.ts";
package/src/worker.ts CHANGED
@@ -38,6 +38,6 @@ export default {
38
38
  throw error;
39
39
  });
40
40
  const app = await appPromise;
41
- return app.fetch(request, env, ctx);
41
+ return app.fetch(request, env, ctx as never);
42
42
  },
43
43
  };