@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 +2 -2
- package/src/app/factory.ts +69 -22
- package/src/index.ts +9 -3
- package/src/observer.ts +250 -0
- package/src/router/execute.ts +106 -115
- package/src/router/index.ts +0 -3
- package/src/worker.ts +1 -1
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/open-ai-router",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
10
|
+
"@neutrome/lil-engine": "0.1.4"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@types/node": "^25.9.3",
|
package/src/app/factory.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 {
|
package/src/observer.ts
ADDED
|
@@ -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
|
+
}
|
package/src/router/execute.ts
CHANGED
|
@@ -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
|
-
|
|
319
|
+
resolvedRequest,
|
|
327
320
|
timed.signal,
|
|
328
321
|
);
|
|
329
322
|
if (!upstream.ok) {
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
+
resolvedRequest,
|
|
358
366
|
timed.signal,
|
|
359
367
|
);
|
|
360
368
|
if (!upstream.ok) {
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/router/index.ts
CHANGED