@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
package/src/router/execute.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
appendErrorBlock,
|
|
3
|
-
createProgram,
|
|
4
2
|
emitChatCompletionsResponse,
|
|
5
3
|
emitProviderError,
|
|
6
4
|
emitProviderRequest,
|
|
7
5
|
emitProviderResponse,
|
|
8
6
|
emitProviderStreamChunk,
|
|
9
|
-
firstError,
|
|
10
7
|
getModel,
|
|
11
8
|
isStreaming,
|
|
12
9
|
parseChatCompletionsRequest,
|
|
13
|
-
parseProviderError,
|
|
14
10
|
parseProviderRequest,
|
|
15
11
|
parseProviderResponse,
|
|
16
12
|
parseProviderStreamChunk,
|
|
@@ -20,11 +16,32 @@ import {
|
|
|
20
16
|
type ProviderStyle,
|
|
21
17
|
usageObject,
|
|
22
18
|
} from "@neutrome/lil-engine";
|
|
23
|
-
import {
|
|
19
|
+
import { createExecutionRuntime } from "./execution-runtime.ts";
|
|
20
|
+
import { createFetchProviderInvoker } from "./provider-invoker.ts";
|
|
21
|
+
import { ExecutionError, ProviderHttpError, RouterError } from "./errors.ts";
|
|
24
22
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
errorProgramFromUnknown,
|
|
24
|
+
errorResponse,
|
|
25
|
+
toBody,
|
|
26
|
+
toProviderError,
|
|
27
|
+
} from "./error-codec.ts";
|
|
28
|
+
import {
|
|
29
|
+
createUpstreamSignal,
|
|
30
|
+
fetchProvider,
|
|
31
|
+
getProviderOrThrow,
|
|
32
|
+
normalizeUpstreamAbort,
|
|
33
|
+
} from "./upstream-client.ts";
|
|
34
|
+
import { emitProviderUsage, injectStreamUsage } from "./provider-telemetry.ts";
|
|
35
|
+
import {
|
|
36
|
+
jsonExecutionResponse,
|
|
37
|
+
streamExecutionResponse,
|
|
38
|
+
} from "./response-delivery.ts";
|
|
39
|
+
import { prepareRemoteMcpExecution } from "./remote-mcp-execution.ts";
|
|
40
|
+
import { parseRequestProgram } from "./request-program.ts";
|
|
41
|
+
import {
|
|
42
|
+
resolveModelNamespaceExecutor,
|
|
43
|
+
resolveRequestTarget,
|
|
44
|
+
} from "./request-target.ts";
|
|
28
45
|
import type {
|
|
29
46
|
ExecutionTarget,
|
|
30
47
|
ExecutionRuntime,
|
|
@@ -34,18 +51,13 @@ import type {
|
|
|
34
51
|
ProviderInvocationContext,
|
|
35
52
|
ProviderInvoker,
|
|
36
53
|
} from "./execution-types.ts";
|
|
37
|
-
import {
|
|
38
|
-
import
|
|
39
|
-
import { cloneForwardHeaders, isSse } from "../util/headers.ts";
|
|
54
|
+
import type { RequestContext } from "../upstream-auth/types.ts";
|
|
55
|
+
import { isSse } from "../util/headers.ts";
|
|
40
56
|
import { eventDataLines, splitSseEvents } from "../util/sse.ts";
|
|
41
|
-
import { renderProgramAsLil } from "../observer.ts";
|
|
42
57
|
import { resolveInvocationTarget, type ResolvedTarget } from "./resolve.ts";
|
|
43
|
-
import {
|
|
44
|
-
createRemoteMcpToolSet,
|
|
45
|
-
type RemoteMcpClientFactory,
|
|
46
|
-
} from "./mcp.ts";
|
|
58
|
+
import type { RemoteMcpClientFactory } from "./mcp.ts";
|
|
47
59
|
import type { ProviderRuntime, RouterRuntime } from "./runtime.ts";
|
|
48
|
-
import {
|
|
60
|
+
import { observeTimedExecution } from "./execution-events.ts";
|
|
49
61
|
|
|
50
62
|
const encoder = new TextEncoder();
|
|
51
63
|
const decoder = new TextDecoder();
|
|
@@ -87,26 +99,7 @@ export type HandleGoogleGenAIOptions = RouterExecutionOptions & {
|
|
|
87
99
|
forceStreaming?: boolean;
|
|
88
100
|
};
|
|
89
101
|
|
|
90
|
-
export
|
|
91
|
-
constructor(
|
|
92
|
-
message: string,
|
|
93
|
-
readonly status: number,
|
|
94
|
-
readonly code: string,
|
|
95
|
-
readonly type = "invalid_request_error",
|
|
96
|
-
) {
|
|
97
|
-
super(message);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export class ProviderHttpError extends Error {
|
|
102
|
-
constructor(
|
|
103
|
-
message: string,
|
|
104
|
-
readonly status: number,
|
|
105
|
-
readonly program: Program,
|
|
106
|
-
) {
|
|
107
|
-
super(message);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
102
|
+
export { ProviderHttpError, RouterError } from "./errors.ts";
|
|
110
103
|
|
|
111
104
|
export function createRouterExecutionRuntime(
|
|
112
105
|
runtime: RouterRuntime,
|
|
@@ -153,126 +146,73 @@ export function createRouterExecutionRuntime(
|
|
|
153
146
|
return createExecutionRuntime(runtimeOptions);
|
|
154
147
|
}
|
|
155
148
|
|
|
156
|
-
function resolveModelNamespaceExecutor(
|
|
157
|
-
runtime: RouterRuntime,
|
|
158
|
-
name: string,
|
|
159
|
-
): Executor | null {
|
|
160
|
-
const modelNamespace = runtime.modelNamespaces.get(name);
|
|
161
|
-
if (!modelNamespace) return null;
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
async execute(request, ctx) {
|
|
165
|
-
const model = getModel(request);
|
|
166
|
-
if (!model) throw new ExecutionError("resolution", "Request is missing model for namespace executor resolution", { stage: "target_resolution" });
|
|
167
|
-
const resolved = resolveInvocationTarget(runtime, `${name}/${model}`);
|
|
168
|
-
return ctx.invoke(request, { target: resolved.target });
|
|
169
|
-
},
|
|
170
|
-
async *stream(request, ctx) {
|
|
171
|
-
const model = getModel(request);
|
|
172
|
-
if (!model) throw new ExecutionError("resolution", "Request is missing model for namespace executor resolution", { stage: "target_resolution" });
|
|
173
|
-
const resolved = resolveInvocationTarget(runtime, `${name}/${model}`);
|
|
174
|
-
yield* ctx.invokeStream(request, { target: resolved.target });
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
149
|
export async function handleChatCompletions(
|
|
180
150
|
options: HandleChatCompletionsOptions,
|
|
181
151
|
): Promise<Response> {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
executionId: incomingExecutionId,
|
|
188
|
-
});
|
|
189
|
-
const resolved = resolveRequestTarget(options.runtime, request);
|
|
190
|
-
const remoteMcp = prepareRemoteMcpExecution(options.runtime, resolved, options);
|
|
191
|
-
const execution = createRouterExecutionRuntime(options.runtime, options.ctx, remoteMcp.options);
|
|
192
|
-
|
|
193
|
-
if (isStreaming(request)) {
|
|
194
|
-
try {
|
|
195
|
-
const responseStartedMs = Date.now();
|
|
196
|
-
const chunks = execution.stream(request, { target: remoteMcp.target });
|
|
197
|
-
return await streamExecutionResponse(
|
|
198
|
-
"chat-completions",
|
|
199
|
-
chunks,
|
|
200
|
-
resolved,
|
|
201
|
-
options.observe,
|
|
202
|
-
{
|
|
203
|
-
requestId: incomingExecutionId,
|
|
204
|
-
executionId: incomingExecutionId,
|
|
205
|
-
startedAtMs: responseStartedMs,
|
|
206
|
-
},
|
|
207
|
-
remoteMcp.close,
|
|
208
|
-
);
|
|
209
|
-
} catch (error) {
|
|
210
|
-
await remoteMcp.close();
|
|
211
|
-
throw error;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const startTime = Date.now();
|
|
216
|
-
try {
|
|
217
|
-
const response = await execution.execute(request, {
|
|
218
|
-
target: remoteMcp.target,
|
|
219
|
-
});
|
|
220
|
-
const userTime = Date.now() - startTime;
|
|
221
|
-
return jsonExecutionResponse(
|
|
222
|
-
emitChatCompletionsResponse(response),
|
|
223
|
-
resolved,
|
|
224
|
-
userTime,
|
|
225
|
-
);
|
|
226
|
-
} finally {
|
|
227
|
-
await remoteMcp.close();
|
|
228
|
-
}
|
|
229
|
-
} catch (error) {
|
|
230
|
-
return errorResponse(error, "chat-completions");
|
|
231
|
-
}
|
|
152
|
+
return handleRequestStyle(
|
|
153
|
+
"chat-completions",
|
|
154
|
+
options,
|
|
155
|
+
emitChatCompletionsResponse,
|
|
156
|
+
);
|
|
232
157
|
}
|
|
233
158
|
|
|
234
159
|
export async function handleResponses(
|
|
235
160
|
options: HandleResponsesOptions,
|
|
236
161
|
): Promise<Response> {
|
|
237
|
-
return
|
|
162
|
+
return handleRequestStyle("responses", options, (program) =>
|
|
163
|
+
emitProviderResponse("responses", program),
|
|
164
|
+
);
|
|
238
165
|
}
|
|
239
166
|
|
|
240
167
|
export async function handleAnthropicMessages(
|
|
241
168
|
options: HandleAnthropicMessagesOptions,
|
|
242
169
|
): Promise<Response> {
|
|
243
|
-
return
|
|
170
|
+
return handleRequestStyle("anthropic-messages", options, (program) =>
|
|
171
|
+
emitProviderResponse("anthropic-messages", program),
|
|
172
|
+
);
|
|
244
173
|
}
|
|
245
174
|
|
|
246
175
|
export async function handleGoogleGenAI(
|
|
247
176
|
options: HandleGoogleGenAIOptions,
|
|
248
177
|
): Promise<Response> {
|
|
249
|
-
return
|
|
178
|
+
return handleRequestStyle(
|
|
250
179
|
"google-genai",
|
|
251
180
|
options,
|
|
252
|
-
|
|
181
|
+
(program) => emitProviderResponse("google-genai", program),
|
|
182
|
+
options.forceStreaming
|
|
183
|
+
? (request) => setStreaming(request, true)
|
|
184
|
+
: undefined,
|
|
253
185
|
);
|
|
254
186
|
}
|
|
255
187
|
|
|
256
|
-
async function
|
|
257
|
-
style:
|
|
188
|
+
async function handleRequestStyle(
|
|
189
|
+
style: ProviderStyle,
|
|
258
190
|
options: RouterExecutionOptions & {
|
|
259
191
|
runtime: RouterRuntime;
|
|
260
192
|
ctx: RequestContext;
|
|
261
193
|
program?: Program;
|
|
262
194
|
},
|
|
195
|
+
emitResponse: (program: Program) => Uint8Array,
|
|
263
196
|
mutateRequest?: (request: Program) => Program,
|
|
264
197
|
): Promise<Response> {
|
|
265
198
|
try {
|
|
266
|
-
const baseRequest =
|
|
199
|
+
const baseRequest =
|
|
200
|
+
options.program ??
|
|
201
|
+
(await parseRequestProgram(style, options.ctx.request));
|
|
267
202
|
const request = mutateRequest ? mutateRequest(baseRequest) : baseRequest;
|
|
268
|
-
const incomingExecutionId =
|
|
269
|
-
|
|
270
|
-
requestId: incomingExecutionId,
|
|
271
|
-
executionId: incomingExecutionId,
|
|
272
|
-
});
|
|
203
|
+
const incomingExecutionId =
|
|
204
|
+
options.incomingExecutionId ?? `incoming_${crypto.randomUUID()}`;
|
|
273
205
|
const resolved = resolveRequestTarget(options.runtime, request);
|
|
274
|
-
const remoteMcp = prepareRemoteMcpExecution(
|
|
275
|
-
|
|
206
|
+
const remoteMcp = prepareRemoteMcpExecution(
|
|
207
|
+
options.runtime,
|
|
208
|
+
resolved,
|
|
209
|
+
options,
|
|
210
|
+
);
|
|
211
|
+
const execution = createRouterExecutionRuntime(
|
|
212
|
+
options.runtime,
|
|
213
|
+
options.ctx,
|
|
214
|
+
remoteMcp.options,
|
|
215
|
+
);
|
|
276
216
|
|
|
277
217
|
if (isStreaming(request)) {
|
|
278
218
|
try {
|
|
@@ -302,11 +242,7 @@ async function handleProviderStyle(
|
|
|
302
242
|
target: remoteMcp.target,
|
|
303
243
|
});
|
|
304
244
|
const userTime = Date.now() - startTime;
|
|
305
|
-
return jsonExecutionResponse(
|
|
306
|
-
emitProviderResponse(style, response),
|
|
307
|
-
resolved,
|
|
308
|
-
userTime,
|
|
309
|
-
);
|
|
245
|
+
return jsonExecutionResponse(emitResponse(response), resolved, userTime);
|
|
310
246
|
} finally {
|
|
311
247
|
await remoteMcp.close();
|
|
312
248
|
}
|
|
@@ -315,798 +251,6 @@ async function handleProviderStyle(
|
|
|
315
251
|
}
|
|
316
252
|
}
|
|
317
253
|
|
|
318
|
-
export
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
): ResolvedTarget {
|
|
322
|
-
const model = getModel(request);
|
|
323
|
-
if (!model) {
|
|
324
|
-
throw new RouterError("Request is missing `model`", 400, "missing_model");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
return resolveInvocationTarget(runtime, model);
|
|
329
|
-
} catch (error) {
|
|
330
|
-
throw new RouterError(
|
|
331
|
-
error instanceof Error ? error.message : "Model resolution failed",
|
|
332
|
-
404,
|
|
333
|
-
"model_not_found",
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export async function parseRequestProgram(
|
|
339
|
-
style: ProviderStyle,
|
|
340
|
-
request: Request,
|
|
341
|
-
): Promise<Program> {
|
|
342
|
-
const body = new Uint8Array(await request.arrayBuffer());
|
|
343
|
-
return style === "chat-completions"
|
|
344
|
-
? parseChatCompletionsRequest(body)
|
|
345
|
-
: parseProviderRequest(style, body);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
export function createFetchProviderInvoker(
|
|
349
|
-
runtime: RouterRuntime,
|
|
350
|
-
ctx: RequestContext,
|
|
351
|
-
upstreamTimeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS,
|
|
352
|
-
): ProviderInvoker {
|
|
353
|
-
return {
|
|
354
|
-
async execute(request, providerCtx) {
|
|
355
|
-
const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
|
|
356
|
-
const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
const resolvedRequest = setModel(request, providerCtx.target.model);
|
|
360
|
-
emitObservedProgram(providerCtx.observe, "provider.request", resolvedRequest, {
|
|
361
|
-
requestId: providerCtx.requestId,
|
|
362
|
-
executionId: providerCtx.executionId,
|
|
363
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
364
|
-
provider: provider.name,
|
|
365
|
-
model: providerCtx.target.model,
|
|
366
|
-
});
|
|
367
|
-
const encodeStartedMs = Date.now();
|
|
368
|
-
let reqBody = emitProviderRequest(provider.style, resolvedRequest);
|
|
369
|
-
if (provider.style === "chat-completions" && isStreaming(resolvedRequest)) {
|
|
370
|
-
reqBody = injectStreamUsage(reqBody);
|
|
371
|
-
}
|
|
372
|
-
observeTimingEvent({
|
|
373
|
-
observe: providerCtx.observe,
|
|
374
|
-
kind: "provider.request_encoded",
|
|
375
|
-
requestId: providerCtx.requestId,
|
|
376
|
-
executionId: providerCtx.executionId,
|
|
377
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
378
|
-
startedAtMs: encodeStartedMs,
|
|
379
|
-
finishedAtMs: Date.now(),
|
|
380
|
-
data: {
|
|
381
|
-
provider: provider.name,
|
|
382
|
-
model: providerCtx.target.model,
|
|
383
|
-
bytes: reqBody.byteLength,
|
|
384
|
-
},
|
|
385
|
-
});
|
|
386
|
-
const reqBytes = reqBody.byteLength;
|
|
387
|
-
const fetchStartedMs = Date.now();
|
|
388
|
-
const upstream = await fetchProvider(
|
|
389
|
-
runtime,
|
|
390
|
-
ctx,
|
|
391
|
-
provider,
|
|
392
|
-
reqBody,
|
|
393
|
-
timed.signal,
|
|
394
|
-
providerCtx,
|
|
395
|
-
);
|
|
396
|
-
observeTimingEvent({
|
|
397
|
-
observe: providerCtx.observe,
|
|
398
|
-
kind: "provider.fetch_headers",
|
|
399
|
-
requestId: providerCtx.requestId,
|
|
400
|
-
executionId: providerCtx.executionId,
|
|
401
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
402
|
-
startedAtMs: fetchStartedMs,
|
|
403
|
-
finishedAtMs: Date.now(),
|
|
404
|
-
data: {
|
|
405
|
-
provider: provider.name,
|
|
406
|
-
model: providerCtx.target.model,
|
|
407
|
-
status: upstream.status,
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
|
-
if (!upstream.ok) {
|
|
411
|
-
const providerError = await toProviderError(provider, upstream);
|
|
412
|
-
emitObservedProgram(providerCtx.observe, "provider.response", providerError.program, {
|
|
413
|
-
requestId: providerCtx.requestId,
|
|
414
|
-
executionId: providerCtx.executionId,
|
|
415
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
416
|
-
provider: provider.name,
|
|
417
|
-
model: providerCtx.target.model,
|
|
418
|
-
});
|
|
419
|
-
throw providerError;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const readStartedMs = Date.now();
|
|
423
|
-
const bytes = new Uint8Array(await upstream.arrayBuffer());
|
|
424
|
-
observeTimingEvent({
|
|
425
|
-
observe: providerCtx.observe,
|
|
426
|
-
kind: "provider.response_body_read",
|
|
427
|
-
requestId: providerCtx.requestId,
|
|
428
|
-
executionId: providerCtx.executionId,
|
|
429
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
430
|
-
startedAtMs: readStartedMs,
|
|
431
|
-
finishedAtMs: Date.now(),
|
|
432
|
-
data: {
|
|
433
|
-
provider: provider.name,
|
|
434
|
-
model: providerCtx.target.model,
|
|
435
|
-
bytes: bytes.byteLength,
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
|
-
const response = parseProviderResponse(provider.style, bytes);
|
|
439
|
-
emitObservedProgram(providerCtx.observe, "provider.response", response, {
|
|
440
|
-
requestId: providerCtx.requestId,
|
|
441
|
-
executionId: providerCtx.executionId,
|
|
442
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
443
|
-
provider: provider.name,
|
|
444
|
-
model: providerCtx.target.model,
|
|
445
|
-
});
|
|
446
|
-
emitProviderUsage(providerCtx, response, provider.name, reqBytes, bytes.byteLength);
|
|
447
|
-
return response;
|
|
448
|
-
} catch (error) {
|
|
449
|
-
throw normalizeUpstreamAbort(error, timed.signal);
|
|
450
|
-
} finally {
|
|
451
|
-
timed.cleanup();
|
|
452
|
-
}
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
async *stream(request, providerCtx) {
|
|
456
|
-
const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
|
|
457
|
-
const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
|
|
458
|
-
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
459
|
-
let buffer = "";
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
const resolvedRequest = setModel(request, providerCtx.target.model);
|
|
463
|
-
emitObservedProgram(providerCtx.observe, "provider.request", resolvedRequest, {
|
|
464
|
-
requestId: providerCtx.requestId,
|
|
465
|
-
executionId: providerCtx.executionId,
|
|
466
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
467
|
-
provider: provider.name,
|
|
468
|
-
model: providerCtx.target.model,
|
|
469
|
-
});
|
|
470
|
-
const encodeStartedMs = Date.now();
|
|
471
|
-
let reqBody = emitProviderRequest(provider.style, resolvedRequest);
|
|
472
|
-
if (provider.style === "chat-completions" && isStreaming(resolvedRequest)) {
|
|
473
|
-
reqBody = injectStreamUsage(reqBody);
|
|
474
|
-
}
|
|
475
|
-
observeTimingEvent({
|
|
476
|
-
observe: providerCtx.observe,
|
|
477
|
-
kind: "provider.request_encoded",
|
|
478
|
-
requestId: providerCtx.requestId,
|
|
479
|
-
executionId: providerCtx.executionId,
|
|
480
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
481
|
-
startedAtMs: encodeStartedMs,
|
|
482
|
-
finishedAtMs: Date.now(),
|
|
483
|
-
data: {
|
|
484
|
-
provider: provider.name,
|
|
485
|
-
model: providerCtx.target.model,
|
|
486
|
-
bytes: reqBody.byteLength,
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
const reqBytes = reqBody.byteLength;
|
|
490
|
-
const fetchStartedMs = Date.now();
|
|
491
|
-
const upstream = await fetchProvider(
|
|
492
|
-
runtime,
|
|
493
|
-
ctx,
|
|
494
|
-
provider,
|
|
495
|
-
reqBody,
|
|
496
|
-
timed.signal,
|
|
497
|
-
providerCtx,
|
|
498
|
-
);
|
|
499
|
-
observeTimingEvent({
|
|
500
|
-
observe: providerCtx.observe,
|
|
501
|
-
kind: "provider.fetch_headers",
|
|
502
|
-
requestId: providerCtx.requestId,
|
|
503
|
-
executionId: providerCtx.executionId,
|
|
504
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
505
|
-
startedAtMs: fetchStartedMs,
|
|
506
|
-
finishedAtMs: Date.now(),
|
|
507
|
-
data: {
|
|
508
|
-
provider: provider.name,
|
|
509
|
-
model: providerCtx.target.model,
|
|
510
|
-
status: upstream.status,
|
|
511
|
-
},
|
|
512
|
-
});
|
|
513
|
-
if (!upstream.ok) {
|
|
514
|
-
const providerError = await toProviderError(provider, upstream);
|
|
515
|
-
emitObservedProgram(providerCtx.observe, "provider.response", providerError.program, {
|
|
516
|
-
requestId: providerCtx.requestId,
|
|
517
|
-
executionId: providerCtx.executionId,
|
|
518
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
519
|
-
provider: provider.name,
|
|
520
|
-
model: providerCtx.target.model,
|
|
521
|
-
});
|
|
522
|
-
throw providerError;
|
|
523
|
-
}
|
|
524
|
-
if (!isSse(upstream.headers) || !upstream.body) {
|
|
525
|
-
throw new RouterError(
|
|
526
|
-
`Provider ${provider.name} did not return an event stream`,
|
|
527
|
-
502,
|
|
528
|
-
"invalid_upstream_stream",
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
reader = upstream.body.getReader();
|
|
533
|
-
let firstByteObserved = false;
|
|
534
|
-
const streamReadStartedMs = Date.now();
|
|
535
|
-
let lastChunkWithUsage: Program | null = null;
|
|
536
|
-
let respBytes = 0;
|
|
537
|
-
while (true) {
|
|
538
|
-
const { done, value } = await reader.read();
|
|
539
|
-
if (done) {
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
if (!firstByteObserved) {
|
|
543
|
-
firstByteObserved = true;
|
|
544
|
-
observeTimingEvent({
|
|
545
|
-
observe: providerCtx.observe,
|
|
546
|
-
kind: "provider.stream_first_byte",
|
|
547
|
-
requestId: providerCtx.requestId,
|
|
548
|
-
executionId: providerCtx.executionId,
|
|
549
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
550
|
-
startedAtMs: streamReadStartedMs,
|
|
551
|
-
finishedAtMs: Date.now(),
|
|
552
|
-
data: {
|
|
553
|
-
provider: provider.name,
|
|
554
|
-
model: providerCtx.target.model,
|
|
555
|
-
bytes: value.byteLength,
|
|
556
|
-
},
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
respBytes += value.byteLength;
|
|
560
|
-
buffer += decoder.decode(value, { stream: true });
|
|
561
|
-
const split = splitSseEvents(buffer);
|
|
562
|
-
buffer = split.remainder;
|
|
563
|
-
for (const event of split.events) {
|
|
564
|
-
for (const data of eventDataLines(event)) {
|
|
565
|
-
if (data === "[DONE]") {
|
|
566
|
-
emitProviderUsage(providerCtx, lastChunkWithUsage, provider.name, reqBytes, respBytes);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
if (data[0] !== "{") continue;
|
|
570
|
-
const chunk = parseProviderStreamChunk(
|
|
571
|
-
provider.style,
|
|
572
|
-
encoder.encode(data),
|
|
573
|
-
);
|
|
574
|
-
if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
|
|
575
|
-
emitObservedProgram(providerCtx.observe, "provider.response_chunk", chunk, {
|
|
576
|
-
requestId: providerCtx.requestId,
|
|
577
|
-
executionId: providerCtx.executionId,
|
|
578
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
579
|
-
provider: provider.name,
|
|
580
|
-
model: providerCtx.target.model,
|
|
581
|
-
});
|
|
582
|
-
yield chunk;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (buffer.length > 0) {
|
|
588
|
-
for (const data of eventDataLines(buffer)) {
|
|
589
|
-
if (data === "[DONE]") {
|
|
590
|
-
emitProviderUsage(providerCtx, lastChunkWithUsage, provider.name, reqBytes, respBytes);
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
if (data[0] !== "{") continue;
|
|
594
|
-
const chunk = parseProviderStreamChunk(
|
|
595
|
-
provider.style,
|
|
596
|
-
encoder.encode(data),
|
|
597
|
-
);
|
|
598
|
-
if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
|
|
599
|
-
emitObservedProgram(providerCtx.observe, "provider.response_chunk", chunk, {
|
|
600
|
-
requestId: providerCtx.requestId,
|
|
601
|
-
executionId: providerCtx.executionId,
|
|
602
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
603
|
-
provider: provider.name,
|
|
604
|
-
model: providerCtx.target.model,
|
|
605
|
-
});
|
|
606
|
-
yield chunk;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
emitProviderUsage(providerCtx, lastChunkWithUsage, provider.name, reqBytes, respBytes);
|
|
610
|
-
} catch (error) {
|
|
611
|
-
throw normalizeUpstreamAbort(error, timed.signal);
|
|
612
|
-
} finally {
|
|
613
|
-
await reader?.cancel().catch(() => {});
|
|
614
|
-
reader?.releaseLock();
|
|
615
|
-
timed.cleanup();
|
|
616
|
-
}
|
|
617
|
-
},
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
async function streamExecutionResponse(
|
|
622
|
-
responseStyle: ProviderStyle,
|
|
623
|
-
chunks: AsyncIterable<Program>,
|
|
624
|
-
resolved: ResolvedTarget,
|
|
625
|
-
observe: ExecutionRuntimeOptions["observe"] | undefined,
|
|
626
|
-
input: { requestId: string; executionId: string; startedAtMs: number },
|
|
627
|
-
cleanup?: () => Promise<void>,
|
|
628
|
-
): Promise<Response> {
|
|
629
|
-
const iterator = chunks[Symbol.asyncIterator]();
|
|
630
|
-
|
|
631
|
-
const first = await iterator.next();
|
|
632
|
-
observeTimingEvent({
|
|
633
|
-
observe,
|
|
634
|
-
kind: "response.stream_first_chunk",
|
|
635
|
-
requestId: input.requestId,
|
|
636
|
-
executionId: input.executionId,
|
|
637
|
-
startedAtMs: input.startedAtMs,
|
|
638
|
-
finishedAtMs: Date.now(),
|
|
639
|
-
data: {
|
|
640
|
-
streaming: true,
|
|
641
|
-
source: first.done ? "empty" : "chunk",
|
|
642
|
-
},
|
|
643
|
-
});
|
|
644
|
-
if (first.done) {
|
|
645
|
-
await cleanup?.();
|
|
646
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
647
|
-
start(controller) {
|
|
648
|
-
if (responseStyle === "chat-completions") {
|
|
649
|
-
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
650
|
-
}
|
|
651
|
-
controller.close();
|
|
652
|
-
},
|
|
653
|
-
});
|
|
654
|
-
return new Response(stream, {
|
|
655
|
-
headers: {
|
|
656
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
657
|
-
...routingHeaders(resolved),
|
|
658
|
-
},
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
let done = false;
|
|
663
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
664
|
-
start(controller) {
|
|
665
|
-
const bytes = emitProviderStreamChunk(responseStyle, first.value);
|
|
666
|
-
controller.enqueue(encoder.encode(`data: ${decoder.decode(bytes)}\n\n`));
|
|
667
|
-
},
|
|
668
|
-
async pull(controller) {
|
|
669
|
-
if (done) return;
|
|
670
|
-
try {
|
|
671
|
-
const result = await iterator.next();
|
|
672
|
-
if (result.done) {
|
|
673
|
-
done = true;
|
|
674
|
-
if (responseStyle === "chat-completions") {
|
|
675
|
-
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
676
|
-
}
|
|
677
|
-
await cleanup?.();
|
|
678
|
-
controller.close();
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
const bytes = emitProviderStreamChunk(responseStyle, result.value);
|
|
682
|
-
controller.enqueue(encoder.encode(`data: ${decoder.decode(bytes)}\n\n`));
|
|
683
|
-
} catch (error) {
|
|
684
|
-
done = true;
|
|
685
|
-
await cleanup?.();
|
|
686
|
-
const program = errorProgramFromUnknown(error);
|
|
687
|
-
const bytes = emitProviderError(responseStyle, program);
|
|
688
|
-
controller.enqueue(encoder.encode(`event: error\ndata: ${decoder.decode(bytes)}\n\n`));
|
|
689
|
-
controller.close();
|
|
690
|
-
}
|
|
691
|
-
},
|
|
692
|
-
async cancel() {
|
|
693
|
-
await cleanup?.();
|
|
694
|
-
await iterator.return?.();
|
|
695
|
-
},
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
return new Response(stream, {
|
|
699
|
-
headers: {
|
|
700
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
701
|
-
...routingHeaders(resolved),
|
|
702
|
-
},
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function prepareRemoteMcpExecution(
|
|
707
|
-
runtime: RouterRuntime,
|
|
708
|
-
resolved: ResolvedTarget,
|
|
709
|
-
options: RouterExecutionOptions,
|
|
710
|
-
): {
|
|
711
|
-
target: ExecutionTarget;
|
|
712
|
-
options: RouterExecutionOptions;
|
|
713
|
-
close: () => Promise<void>;
|
|
714
|
-
} {
|
|
715
|
-
if (resolved.mcps.length === 0) {
|
|
716
|
-
return { target: resolved.target, options, close: async () => {} };
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const remoteMcp = createRemoteMcpToolSet(
|
|
720
|
-
runtime,
|
|
721
|
-
resolved.mcps,
|
|
722
|
-
options.remoteMcpClientFactory,
|
|
723
|
-
);
|
|
724
|
-
if (remoteMcp.tools.length === 0) {
|
|
725
|
-
return { target: resolved.target, options, close: remoteMcp.close };
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const executorName = `__remote_mcp_${crypto.randomUUID()}`;
|
|
729
|
-
return {
|
|
730
|
-
target: {
|
|
731
|
-
kind: "executor",
|
|
732
|
-
executorId: executorName,
|
|
733
|
-
alias: resolved.requestedModel,
|
|
734
|
-
},
|
|
735
|
-
options: {
|
|
736
|
-
...options,
|
|
737
|
-
executorImplementations: {
|
|
738
|
-
...(options.executorImplementations ?? {}),
|
|
739
|
-
[executorName]: withTools(remoteMcp.tools, resolved.target),
|
|
740
|
-
},
|
|
741
|
-
},
|
|
742
|
-
close: remoteMcp.close,
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function jsonExecutionResponse(
|
|
747
|
-
body: Uint8Array,
|
|
748
|
-
resolved: ResolvedTarget,
|
|
749
|
-
userTime: number,
|
|
750
|
-
): Response {
|
|
751
|
-
return new Response(toBody(body), {
|
|
752
|
-
headers: {
|
|
753
|
-
"content-type": "application/json; charset=utf-8",
|
|
754
|
-
...routingHeaders(resolved),
|
|
755
|
-
...timingHeaders(userTime),
|
|
756
|
-
},
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function routingHeaders(resolved: ResolvedTarget): Record<string, string> {
|
|
761
|
-
if (resolved.target.kind === "provider") {
|
|
762
|
-
return {
|
|
763
|
-
"x-openairouter-source": resolved.source,
|
|
764
|
-
"x-openairouter-namespace": resolved.namespace,
|
|
765
|
-
"x-openairouter-provider-id": resolved.target.provider ?? "",
|
|
766
|
-
"x-openairouter-model-id": resolved.target.model,
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
return {
|
|
771
|
-
"x-openairouter-engine": "open-ai-router",
|
|
772
|
-
"x-openairouter-source": resolved.source,
|
|
773
|
-
"x-openairouter-namespace": resolved.namespace,
|
|
774
|
-
"x-openairouter-executor-id": resolved.target.executorId,
|
|
775
|
-
"x-openairouter-model-id": resolved.target.alias,
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function timingHeaders(userTimeMs: number): Record<string, string> {
|
|
780
|
-
return {
|
|
781
|
-
"x-openairouter-user-time": String(userTimeMs),
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
async function fetchProvider(
|
|
786
|
-
runtime: RouterRuntime,
|
|
787
|
-
ctx: RequestContext,
|
|
788
|
-
provider: ProviderRuntime,
|
|
789
|
-
body: Uint8Array,
|
|
790
|
-
signal: AbortSignal,
|
|
791
|
-
providerCtx: ProviderInvocationContext,
|
|
792
|
-
): Promise<Response> {
|
|
793
|
-
const headers = cloneForwardHeaders(ctx.request.headers);
|
|
794
|
-
headers.set("content-type", "application/json");
|
|
795
|
-
for (const [key, value] of Object.entries(provider.headers)) {
|
|
796
|
-
headers.set(key, value);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
for (const driver of runtime.upstreamAuthChain) {
|
|
800
|
-
const targetCtx: TargetAuthContext = { ...ctx, providerName: provider.name };
|
|
801
|
-
const authStartedMs = Date.now();
|
|
802
|
-
const auth = await driver.collectTarget(targetCtx);
|
|
803
|
-
observeTimingEvent({
|
|
804
|
-
observe: providerCtx.observe,
|
|
805
|
-
kind: "upstream_auth.resolve_target",
|
|
806
|
-
requestId: providerCtx.requestId,
|
|
807
|
-
executionId: providerCtx.executionId,
|
|
808
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
809
|
-
startedAtMs: authStartedMs,
|
|
810
|
-
finishedAtMs: Date.now(),
|
|
811
|
-
data: {
|
|
812
|
-
provider: provider.name,
|
|
813
|
-
model: providerCtx.target.model,
|
|
814
|
-
driver: driver.name,
|
|
815
|
-
source: auth?.source,
|
|
816
|
-
matched: !!auth,
|
|
817
|
-
},
|
|
818
|
-
});
|
|
819
|
-
if (!auth) {
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
822
|
-
new Headers(auth.headers).forEach((value, key) => headers.set(key, value));
|
|
823
|
-
break;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
return runtime.fetchImpl(`${provider.apiBaseUrl}${provider.endpointPath}`, {
|
|
827
|
-
method: "POST",
|
|
828
|
-
headers,
|
|
829
|
-
body: toBody(body),
|
|
830
|
-
signal,
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
function getProviderOrThrow(runtime: RouterRuntime, providerName: string | undefined) {
|
|
835
|
-
if (!providerName) {
|
|
836
|
-
throw new RouterError(
|
|
837
|
-
"No provider specified on execution target",
|
|
838
|
-
500,
|
|
839
|
-
"provider_not_configured",
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
const provider = runtime.providers.get(providerName);
|
|
843
|
-
if (!provider) {
|
|
844
|
-
throw new RouterError(
|
|
845
|
-
`Provider ${providerName} is not configured`,
|
|
846
|
-
500,
|
|
847
|
-
"provider_not_configured",
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
return provider;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
async function toProviderError(
|
|
854
|
-
provider: ProviderRuntime,
|
|
855
|
-
response: Response,
|
|
856
|
-
): Promise<ProviderHttpError> {
|
|
857
|
-
const body = new Uint8Array(await response.arrayBuffer());
|
|
858
|
-
const program = parseProviderError(provider.style, body, {
|
|
859
|
-
provider: provider.name,
|
|
860
|
-
status: response.status,
|
|
861
|
-
contentType: response.headers.get("content-type"),
|
|
862
|
-
});
|
|
863
|
-
const summary = firstError(program)?.message || `Provider ${provider.name} returned ${response.status}`;
|
|
864
|
-
return new ProviderHttpError(
|
|
865
|
-
summary,
|
|
866
|
-
response.status,
|
|
867
|
-
program,
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
function errorResponse(error: unknown, responseStyle: ProviderStyle): Response {
|
|
872
|
-
if (error instanceof ProviderHttpError) {
|
|
873
|
-
return new Response(toBody(emitProviderError(responseStyle, error.program)), {
|
|
874
|
-
status: error.status,
|
|
875
|
-
headers: {
|
|
876
|
-
"content-type": "application/json; charset=utf-8",
|
|
877
|
-
},
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
if (error instanceof RouterError) {
|
|
882
|
-
return emitErrorProgramResponse(routerErrorProgram(error), error.status, responseStyle);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
if (error instanceof ExecutionError) {
|
|
886
|
-
const providerError = unwrapCause(error.cause, ProviderHttpError);
|
|
887
|
-
if (providerError) {
|
|
888
|
-
return errorResponse(providerError, responseStyle);
|
|
889
|
-
}
|
|
890
|
-
const nestedRouterError = unwrapCause(error.cause, RouterError);
|
|
891
|
-
if (nestedRouterError) {
|
|
892
|
-
return errorResponse(nestedRouterError, responseStyle);
|
|
893
|
-
}
|
|
894
|
-
const routerError = routerErrorFromExecutionError(error);
|
|
895
|
-
return emitErrorProgramResponse(routerErrorProgram(routerError), routerError.status, responseStyle);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
console.error("[open-ai-router] unhandled error:", error);
|
|
899
|
-
return emitErrorProgramResponse(
|
|
900
|
-
appendErrorBlock(createProgram(), {
|
|
901
|
-
source: "router.internal",
|
|
902
|
-
kind: "invalid_request_error",
|
|
903
|
-
status: 500,
|
|
904
|
-
message: "Internal router error",
|
|
905
|
-
data: { code: "internal_error", param: null },
|
|
906
|
-
}),
|
|
907
|
-
500,
|
|
908
|
-
responseStyle,
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function emitErrorProgramResponse(program: Program, status: number, responseStyle: ProviderStyle): Response {
|
|
913
|
-
return new Response(toBody(emitProviderError(responseStyle, program)), {
|
|
914
|
-
status,
|
|
915
|
-
headers: { "content-type": "application/json; charset=utf-8" },
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function routerErrorProgram(error: RouterError): Program {
|
|
920
|
-
return appendErrorBlock(createProgram(), {
|
|
921
|
-
source: "router",
|
|
922
|
-
kind: error.type,
|
|
923
|
-
status: error.status,
|
|
924
|
-
message: error.message,
|
|
925
|
-
data: { code: error.code, param: null },
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
function errorProgramFromUnknown(error: unknown): Program {
|
|
930
|
-
if (error instanceof ProviderHttpError) {
|
|
931
|
-
return error.program;
|
|
932
|
-
}
|
|
933
|
-
if (error instanceof RouterError) {
|
|
934
|
-
return routerErrorProgram(error);
|
|
935
|
-
}
|
|
936
|
-
if (error instanceof ExecutionError) {
|
|
937
|
-
const providerError = unwrapCause(error.cause, ProviderHttpError);
|
|
938
|
-
if (providerError) return providerError.program;
|
|
939
|
-
const nestedRouterError = unwrapCause(error.cause, RouterError);
|
|
940
|
-
if (nestedRouterError) return routerErrorProgram(nestedRouterError);
|
|
941
|
-
return routerErrorProgram(routerErrorFromExecutionError(error));
|
|
942
|
-
}
|
|
943
|
-
return appendErrorBlock(createProgram(), {
|
|
944
|
-
source: "router.stream",
|
|
945
|
-
kind: "stream_error",
|
|
946
|
-
status: 500,
|
|
947
|
-
message: error instanceof Error ? error.message : String(error),
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function routerErrorFromExecutionError(error: ExecutionError): RouterError {
|
|
952
|
-
switch (error.kind) {
|
|
953
|
-
case "validation":
|
|
954
|
-
return new RouterError(error.message, 400, "invalid_program");
|
|
955
|
-
case "resolution":
|
|
956
|
-
return new RouterError(error.message, 400, "target_resolution_failed");
|
|
957
|
-
case "cancellation":
|
|
958
|
-
return new RouterError(error.message, 499, "request_cancelled");
|
|
959
|
-
case "provider":
|
|
960
|
-
return new RouterError(error.message, 502, "provider_execution_failed");
|
|
961
|
-
case "transform":
|
|
962
|
-
return new RouterError(error.message, 500, "transform_failed");
|
|
963
|
-
case "executor":
|
|
964
|
-
return new RouterError(error.message, 500, "executor_failed");
|
|
965
|
-
case "stream":
|
|
966
|
-
return new RouterError(error.message, 500, "stream_failed");
|
|
967
|
-
case "recursion_limit":
|
|
968
|
-
return new RouterError(error.message, 500, "recursion_limit");
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
function unwrapCause<T extends Error>(
|
|
973
|
-
error: unknown,
|
|
974
|
-
ctor: new (...args: any[]) => T,
|
|
975
|
-
): T | null {
|
|
976
|
-
let current = error;
|
|
977
|
-
while (current && typeof current === "object") {
|
|
978
|
-
if (current instanceof ctor) {
|
|
979
|
-
return current;
|
|
980
|
-
}
|
|
981
|
-
if (!("cause" in current)) {
|
|
982
|
-
return null;
|
|
983
|
-
}
|
|
984
|
-
current = (current as { cause?: unknown }).cause;
|
|
985
|
-
}
|
|
986
|
-
return null;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
function toBody(bytes: Uint8Array): ArrayBuffer {
|
|
990
|
-
return bytes.buffer.slice(
|
|
991
|
-
bytes.byteOffset,
|
|
992
|
-
bytes.byteOffset + bytes.byteLength,
|
|
993
|
-
) as ArrayBuffer;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
function createUpstreamSignal(
|
|
997
|
-
signal: AbortSignal,
|
|
998
|
-
timeoutMs: number,
|
|
999
|
-
): {
|
|
1000
|
-
signal: AbortSignal;
|
|
1001
|
-
cleanup(): void;
|
|
1002
|
-
} {
|
|
1003
|
-
if (timeoutMs <= 0) {
|
|
1004
|
-
return {
|
|
1005
|
-
signal,
|
|
1006
|
-
cleanup() {},
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
const controller = new AbortController();
|
|
1011
|
-
const onAbort = () => controller.abort(signal.reason);
|
|
1012
|
-
if (signal.aborted) {
|
|
1013
|
-
onAbort();
|
|
1014
|
-
} else {
|
|
1015
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const timer = setTimeout(() => {
|
|
1019
|
-
controller.abort(
|
|
1020
|
-
new RouterError(
|
|
1021
|
-
`Upstream provider timed out after ${timeoutMs}ms`,
|
|
1022
|
-
504,
|
|
1023
|
-
"upstream_timeout",
|
|
1024
|
-
),
|
|
1025
|
-
);
|
|
1026
|
-
}, timeoutMs);
|
|
1027
|
-
|
|
1028
|
-
return {
|
|
1029
|
-
signal: controller.signal,
|
|
1030
|
-
cleanup() {
|
|
1031
|
-
clearTimeout(timer);
|
|
1032
|
-
signal.removeEventListener("abort", onAbort);
|
|
1033
|
-
},
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
function emitProviderUsage(
|
|
1038
|
-
providerCtx: ProviderInvocationContext,
|
|
1039
|
-
response: Program | null,
|
|
1040
|
-
provider: string,
|
|
1041
|
-
requestBytes: number,
|
|
1042
|
-
responseBytes: number,
|
|
1043
|
-
): void {
|
|
1044
|
-
const usage = response ? usageObject(response) : undefined;
|
|
1045
|
-
const u = (usage && typeof usage === "object" ? usage : {}) as Record<string, unknown>;
|
|
1046
|
-
const tokensInput = asNum(u.prompt_tokens) || asNum(u.input_tokens);
|
|
1047
|
-
const tokensOutput = asNum(u.completion_tokens) || asNum(u.output_tokens);
|
|
1048
|
-
const details = u.prompt_tokens_details as Record<string, unknown> | undefined;
|
|
1049
|
-
const cachedTokensInput = details ? asNum(details.cached_tokens) : 0;
|
|
1050
|
-
providerCtx.observe(createAuditEvent({
|
|
1051
|
-
kind: "provider.usage",
|
|
1052
|
-
requestId: providerCtx.requestId,
|
|
1053
|
-
executionId: providerCtx.executionId,
|
|
1054
|
-
...(providerCtx.parentExecutionId ? { parentExecutionId: providerCtx.parentExecutionId } : {}),
|
|
1055
|
-
data: {
|
|
1056
|
-
provider,
|
|
1057
|
-
model: providerCtx.target.model,
|
|
1058
|
-
tokensInput,
|
|
1059
|
-
tokensOutput,
|
|
1060
|
-
cachedTokensInput,
|
|
1061
|
-
requestBytes,
|
|
1062
|
-
responseBytes,
|
|
1063
|
-
},
|
|
1064
|
-
}));
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function asNum(v: unknown): number {
|
|
1068
|
-
return typeof v === "number" && v >= 0 ? v : 0;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
function injectStreamUsage(body: Uint8Array): Uint8Array {
|
|
1072
|
-
try {
|
|
1073
|
-
const obj = JSON.parse(decoder.decode(body)) as Record<string, unknown>;
|
|
1074
|
-
obj.stream_options = { include_usage: true };
|
|
1075
|
-
return encoder.encode(JSON.stringify(obj));
|
|
1076
|
-
} catch {
|
|
1077
|
-
return body;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
function normalizeUpstreamAbort(error: unknown, signal: AbortSignal): unknown {
|
|
1082
|
-
if (signal.aborted && signal.reason instanceof RouterError) {
|
|
1083
|
-
return signal.reason;
|
|
1084
|
-
}
|
|
1085
|
-
return error;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
function emitObservedProgram(
|
|
1089
|
-
observe: ExecutionRuntimeOptions["observe"],
|
|
1090
|
-
kind: string,
|
|
1091
|
-
program: Program,
|
|
1092
|
-
input: {
|
|
1093
|
-
requestId: string;
|
|
1094
|
-
executionId: string;
|
|
1095
|
-
parentExecutionId?: string;
|
|
1096
|
-
provider?: string;
|
|
1097
|
-
model?: string;
|
|
1098
|
-
},
|
|
1099
|
-
): void {
|
|
1100
|
-
if (!observe) return;
|
|
1101
|
-
observe(createAuditEvent({
|
|
1102
|
-
kind,
|
|
1103
|
-
requestId: input.requestId,
|
|
1104
|
-
executionId: input.executionId,
|
|
1105
|
-
...(input.parentExecutionId ? { parentExecutionId: input.parentExecutionId } : {}),
|
|
1106
|
-
data: {
|
|
1107
|
-
...(input.provider ? { provider: input.provider } : {}),
|
|
1108
|
-
...(input.model ? { model: input.model } : {}),
|
|
1109
|
-
lilText: renderProgramAsLil(program),
|
|
1110
|
-
},
|
|
1111
|
-
}));
|
|
1112
|
-
}
|
|
254
|
+
export { parseRequestProgram } from "./request-program.ts";
|
|
255
|
+
export { resolveRequestTarget } from "./request-target.ts";
|
|
256
|
+
export { createFetchProviderInvoker } from "./provider-invoker.ts";
|