@neutrome/open-ai-router 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.dev.vars.example +2 -0
  2. package/README.md +5 -1
  3. package/package.json +5 -4
  4. package/src/admission/access.ts +0 -1
  5. package/src/admission/types.ts +0 -1
  6. package/src/app/factory.test.ts +40 -34
  7. package/src/app/factory.ts +120 -173
  8. package/src/app/google-genai-route.ts +48 -0
  9. package/src/app/http-utils.ts +14 -0
  10. package/src/app/model-listing.ts +29 -0
  11. package/src/app/response-lifecycle.ts +66 -0
  12. package/src/app/types.ts +38 -0
  13. package/src/index.ts +11 -43
  14. package/src/otlp.ts +148 -0
  15. package/src/router/audit.ts +5 -12
  16. package/src/router/config.ts +4 -4
  17. package/src/router/error-codec.ts +170 -0
  18. package/src/router/errors.ts +38 -1
  19. package/src/router/execute.test.ts +177 -107
  20. package/src/router/execute.ts +68 -924
  21. package/src/router/execution-events.ts +39 -0
  22. package/src/router/execution-invocation.ts +144 -0
  23. package/src/router/execution-observation.ts +161 -0
  24. package/src/router/execution-resolution.ts +120 -0
  25. package/src/router/execution-runtime.test.ts +156 -83
  26. package/src/router/execution-runtime.ts +144 -542
  27. package/src/router/execution-transforms.ts +82 -0
  28. package/src/router/execution-types.ts +8 -19
  29. package/src/router/index.ts +6 -5
  30. package/src/router/mcp.ts +12 -6
  31. package/src/router/provider-invoker.ts +134 -0
  32. package/src/router/provider-stream.ts +192 -0
  33. package/src/router/provider-telemetry.ts +59 -0
  34. package/src/router/remote-mcp-execution.ts +61 -0
  35. package/src/router/request-program.ts +16 -0
  36. package/src/router/request-target.ts +56 -0
  37. package/src/router/resolve.test.ts +43 -10
  38. package/src/router/resolve.ts +50 -28
  39. package/src/router/response-delivery.ts +141 -0
  40. package/src/router/runtime.test.ts +56 -0
  41. package/src/router/runtime.ts +79 -9
  42. package/src/router/targets.ts +3 -1
  43. package/src/router/upstream-client.ts +137 -0
  44. package/src/telemetry.test.ts +50 -0
  45. package/src/telemetry.ts +316 -0
  46. package/src/trace-labels.ts +29 -0
  47. package/src/upstream-auth/env.ts +15 -3
  48. package/src/upstream-auth/proxy.ts +6 -1
  49. package/src/upstream-auth/registry.ts +3 -1
  50. package/src/util/glob.ts +1 -1
  51. package/src/util/model-syntax.ts +3 -3
  52. package/src/worker.ts +12 -6
  53. package/worker-configuration.d.ts +195 -164
  54. package/pnpm-workspace.yaml +0 -4
  55. package/src/admission/jwt.test.ts +0 -96
  56. package/src/admission/jwt.ts +0 -221
  57. package/src/app/handlers.ts +0 -198
  58. package/src/example.test.ts +0 -377
  59. package/src/example.ts +0 -73
  60. package/src/observer.test.ts +0 -54
  61. package/src/observer.ts +0 -315
  62. package/src/timing.ts +0 -36
@@ -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 { withTools } from "@neutrome/lilsdk-ts/tools";
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
- createExecutionRuntime,
26
- } from "./execution-runtime.ts";
27
- import { ExecutionError } from "./errors.ts";
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 { createAuditEvent } from "./audit.ts";
38
- import type { RequestContext, TargetAuthContext } from "../upstream-auth/types.ts";
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 { observeTimingEvent } from "../timing.ts";
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 class RouterError extends Error {
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
- try {
183
- const request = options.program ?? await parseRequestProgram("chat-completions", options.ctx.request);
184
- const incomingExecutionId = options.incomingExecutionId ?? `incoming_${crypto.randomUUID()}`;
185
- emitObservedProgram(options.observe, "request.received", request, {
186
- requestId: incomingExecutionId,
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 handleProviderStyle("responses", options);
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 handleProviderStyle("anthropic-messages", options);
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 handleProviderStyle(
178
+ return handleRequestStyle(
250
179
  "google-genai",
251
180
  options,
252
- options.forceStreaming ? (request) => setStreaming(request, true) : undefined,
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 handleProviderStyle(
257
- style: Exclude<ProviderStyle, "chat-completions">,
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 = options.program ?? await parseRequestProgram(style, options.ctx.request);
199
+ const baseRequest =
200
+ options.program ??
201
+ (await parseRequestProgram(style, options.ctx.request));
267
202
  const request = mutateRequest ? mutateRequest(baseRequest) : baseRequest;
268
- const incomingExecutionId = options.incomingExecutionId ?? `incoming_${crypto.randomUUID()}`;
269
- emitObservedProgram(options.observe, "request.received", request, {
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(options.runtime, resolved, options);
275
- const execution = createRouterExecutionRuntime(options.runtime, options.ctx, remoteMcp.options);
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 function resolveRequestTarget(
319
- runtime: RouterRuntime,
320
- request: Program,
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";