@neutrome/open-ai-router 0.4.2 → 0.5.1

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 +155 -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 +78 -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
@@ -0,0 +1,82 @@
1
+ import { cloneProgram } from "@neutrome/lil-engine";
2
+ import type { Program } from "@neutrome/lil-engine";
3
+ import { createExecutionEvent } from "@neutrome/lilsdk-ts";
4
+ import type { ExecutorContext } from "@neutrome/lilsdk-ts";
5
+ import { ExecutionError } from "./errors.ts";
6
+ import { executionTargetAuditRef, executionTargetId } from "./targets.ts";
7
+ import type { ExecutionTarget, ProgramTransform } from "./execution-types.ts";
8
+
9
+ export async function applyExecutionTransforms(options: {
10
+ program: Program;
11
+ target: ExecutionTarget;
12
+ transforms: ReadonlyMap<string, ProgramTransform>;
13
+ context: ExecutorContext;
14
+ requestId: string;
15
+ executionId: string;
16
+ parentExecutionId: string | undefined;
17
+ depth: number;
18
+ maxDepth: number;
19
+ observe: ExecutorContext["observe"];
20
+ validate(program: Program): void;
21
+ }): Promise<Program> {
22
+ let current = cloneProgram(options.program);
23
+ for (const transformName of options.target.transforms ?? []) {
24
+ const transform = options.transforms.get(transformName);
25
+ if (!transform) {
26
+ throw new ExecutionError(
27
+ "transform",
28
+ `Unknown transform: ${transformName}`,
29
+ {
30
+ stage: "transform",
31
+ details: transformDetails(transformName, options),
32
+ },
33
+ );
34
+ }
35
+ try {
36
+ current = await transform.apply(current, options.context);
37
+ } catch (cause) {
38
+ throw new ExecutionError(
39
+ "transform",
40
+ `Transform ${transform.name} failed`,
41
+ {
42
+ stage: "transform",
43
+ details: transformDetails(transform.name, options),
44
+ cause,
45
+ },
46
+ );
47
+ }
48
+ options.validate(current);
49
+ options.observe(
50
+ createExecutionEvent({
51
+ kind: "transform.applied",
52
+ requestId: options.requestId,
53
+ executionId: options.executionId,
54
+ target: executionTargetAuditRef(options.target),
55
+ data: transformDetails(transform.name, options),
56
+ ...parent(options.parentExecutionId),
57
+ }),
58
+ );
59
+ }
60
+ return current;
61
+ }
62
+
63
+ function transformDetails(
64
+ transform: string,
65
+ options: Pick<
66
+ Parameters<typeof applyExecutionTransforms>[0],
67
+ "target" | "depth" | "maxDepth"
68
+ >,
69
+ ): Record<string, string | number> {
70
+ return {
71
+ transform,
72
+ target: executionTargetId(options.target),
73
+ depth: options.depth,
74
+ maxDepth: options.maxDepth,
75
+ };
76
+ }
77
+
78
+ function parent(parentExecutionId: string | undefined): {
79
+ parentExecutionId?: string;
80
+ } {
81
+ return parentExecutionId === undefined ? {} : { parentExecutionId };
82
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Program, ValidationIssue } from "@neutrome/lil-engine";
2
2
  import type {
3
3
  ExecutionTarget,
4
+ ExecutionEvent,
4
5
  Executor,
5
6
  InvokeOptions,
6
7
  ProgramTransform,
@@ -11,33 +12,21 @@ import type {
11
12
  ExecutionErrorStage,
12
13
  } from "./errors.ts";
13
14
 
14
- export type AuditEvent = {
15
- kind: string;
16
- requestId: string;
17
- executionId: string;
18
- timestamp: string;
19
- parentExecutionId?: string;
20
- target?: { kind: "provider" | "executor"; id: string };
21
- errorKind?: string;
22
- data?: Record<string, unknown>;
23
- };
24
-
25
- export type AuditEventInput = Omit<AuditEvent, "timestamp"> & {
26
- timestamp?: string;
27
- };
28
-
29
15
  export type ProviderInvocationContext = {
30
16
  requestId: string;
31
17
  executionId: string;
32
18
  parentExecutionId?: string;
33
19
  target: Extract<ExecutionTarget, { kind: "provider" }>;
34
20
  signal: AbortSignal;
35
- observe(event: AuditEvent): void;
21
+ observe(event: ExecutionEvent): void;
36
22
  };
37
23
 
38
24
  export type ProviderInvoker = {
39
25
  execute(request: Program, ctx: ProviderInvocationContext): Promise<Program>;
40
- stream(request: Program, ctx: ProviderInvocationContext): AsyncIterable<Program>;
26
+ stream(
27
+ request: Program,
28
+ ctx: ProviderInvocationContext,
29
+ ): AsyncIterable<Program>;
41
30
  };
42
31
 
43
32
  export type ResolveTarget = (
@@ -55,7 +44,7 @@ export type ExecutionRuntimeOptions = {
55
44
  transforms?: Readonly<Record<string, ProgramTransform>>;
56
45
  providerInvoker?: ProviderInvoker;
57
46
  resolveTarget?: ResolveTarget;
58
- observe?: (event: AuditEvent) => void;
47
+ observe?: (event: ExecutionEvent) => void;
59
48
  signal?: AbortSignal;
60
49
  requestIdFactory?: () => string;
61
50
  executionIdFactory?: () => string;
@@ -73,8 +62,8 @@ export type {
73
62
  ExecutionErrorKind,
74
63
  ExecutionErrorStage,
75
64
  ExecutionTarget,
65
+ ExecutionEvent,
76
66
  Executor,
77
67
  InvokeOptions,
78
68
  ProgramTransform,
79
- ValidationIssue,
80
69
  };
@@ -1,4 +1,4 @@
1
- export { createAuditEvent, createInMemoryAuditSink } from "./audit.ts";
1
+ export { createInMemoryAuditSink } from "./audit.ts";
2
2
  export { ExecutionError, isExecutionError } from "./errors.ts";
3
3
  export { createExecutionRuntime } from "./execution-runtime.ts";
4
4
  export { createRouterRuntime } from "./runtime.ts";
@@ -14,7 +14,11 @@ export {
14
14
  resolveRequestTarget,
15
15
  RouterError,
16
16
  } from "./execute.ts";
17
- export { listConfiguredModels, resolveInvocationTarget, resolveInvocationTargets } from "./resolve.ts";
17
+ export {
18
+ listConfiguredModels,
19
+ resolveInvocationTarget,
20
+ resolveInvocationTargets,
21
+ } from "./resolve.ts";
18
22
  export { executionTargetAuditRef, executionTargetId } from "./targets.ts";
19
23
  export {
20
24
  createRemoteMcpToolSet,
@@ -37,12 +41,9 @@ export type {
37
41
  ModelNamespaceRuntime,
38
42
  ModelRouteRuntime,
39
43
  ProviderRuntime,
40
- RemoteMcpServerRuntime,
41
44
  RouterRuntime,
42
45
  } from "./runtime.ts";
43
46
  export type {
44
- AuditEvent,
45
- AuditEventInput,
46
47
  ExecutionErrorDetails,
47
48
  ExecutionErrorKind,
48
49
  ExecutionErrorStage,
package/src/router/mcp.ts CHANGED
@@ -3,7 +3,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
3
3
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
4
  import type { ExecutorContext, Tool } from "@neutrome/lilsdk-ts";
5
5
  import type { RemoteMcpServerRuntime, RouterRuntime } from "./runtime.ts";
6
- import { observeTimingEvent } from "../timing.ts";
6
+ import { observeTimedExecution } from "./execution-events.ts";
7
7
 
8
8
  export type RemoteMcpToolCallClient = {
9
9
  callTool(name: string, args: Record<string, unknown>): Promise<unknown>;
@@ -26,7 +26,9 @@ export function createRemoteMcpToolSet(
26
26
  factory?: RemoteMcpClientFactory,
27
27
  ): RemoteMcpToolSet {
28
28
  const pool = new RemoteMcpSessionPool(
29
- factory ?? ((server, signal) => createStreamableHttpMcpClient(server, signal, runtime.fetchImpl)),
29
+ factory ??
30
+ ((server, signal) =>
31
+ createStreamableHttpMcpClient(server, signal, runtime.fetchImpl)),
30
32
  );
31
33
  const tools: Tool[] = [];
32
34
  const usedNames = new Map<string, number>();
@@ -43,12 +45,14 @@ export function createRemoteMcpToolSet(
43
45
 
44
46
  tools.push({
45
47
  name,
46
- description: snapshot.description || `Remote MCP tool ${server.name}/${snapshot.name}`,
48
+ description:
49
+ snapshot.description ||
50
+ `Remote MCP tool ${server.name}/${snapshot.name}`,
47
51
  schema: normalizeSchema(snapshot.inputSchema),
48
52
  async execute(args: Record<string, unknown>, ctx: ExecutorContext) {
49
53
  const connectStartedMs = Date.now();
50
54
  const client = await pool.get(server, ctx.signal);
51
- observeTimingEvent({
55
+ observeTimedExecution({
52
56
  observe: ctx.observe,
53
57
  kind: "remote_mcp.connect",
54
58
  requestId: ctx.requestId,
@@ -63,7 +67,7 @@ export function createRemoteMcpToolSet(
63
67
  });
64
68
  const callStartedMs = Date.now();
65
69
  const result = await client.callTool(snapshot.name, args);
66
- observeTimingEvent({
70
+ observeTimedExecution({
67
71
  observe: ctx.observe,
68
72
  kind: "remote_mcp.tool_call",
69
73
  requestId: ctx.requestId,
@@ -126,7 +130,9 @@ export function stringifyMcpToolResult(result: unknown): string {
126
130
  }
127
131
  const body = chunks.filter(Boolean).join("\n");
128
132
  if (result.isError === true) {
129
- return body ? `MCP tool returned an error:\n${body}` : "MCP tool returned an error.";
133
+ return body
134
+ ? `MCP tool returned an error:\n${body}`
135
+ : "MCP tool returned an error.";
130
136
  }
131
137
  return body || JSON.stringify(result);
132
138
  }
@@ -0,0 +1,134 @@
1
+ import {
2
+ emitProviderRequest,
3
+ isStreaming,
4
+ parseProviderResponse,
5
+ setModel,
6
+ } from "@neutrome/lil-engine";
7
+ import { toProviderError } from "./error-codec.ts";
8
+ import {
9
+ createUpstreamSignal,
10
+ fetchProvider,
11
+ getProviderOrThrow,
12
+ normalizeUpstreamAbort,
13
+ } from "./upstream-client.ts";
14
+ import { emitProviderUsage, injectStreamUsage } from "./provider-telemetry.ts";
15
+ import type { ProviderInvoker } from "./execution-types.ts";
16
+ import type { RequestContext } from "../upstream-auth/types.ts";
17
+ import { streamProvider } from "./provider-stream.ts";
18
+ import type { RouterRuntime } from "./runtime.ts";
19
+ import { observeTimedExecution } from "./execution-events.ts";
20
+
21
+ const DEFAULT_UPSTREAM_TIMEOUT_MS = 120_000;
22
+
23
+ export function createFetchProviderInvoker(
24
+ runtime: RouterRuntime,
25
+ ctx: RequestContext,
26
+ upstreamTimeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS,
27
+ ): ProviderInvoker {
28
+ return {
29
+ async execute(request, providerCtx) {
30
+ const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
31
+ const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
32
+
33
+ try {
34
+ const resolvedRequest = setModel(request, providerCtx.target.model);
35
+ const encodeStartedMs = Date.now();
36
+ let reqBody = emitProviderRequest(provider.style, resolvedRequest);
37
+ if (
38
+ provider.style === "chat-completions" &&
39
+ isStreaming(resolvedRequest)
40
+ ) {
41
+ reqBody = injectStreamUsage(reqBody);
42
+ }
43
+ observeTimedExecution({
44
+ observe: providerCtx.observe,
45
+ kind: "provider.request_encoded",
46
+ requestId: providerCtx.requestId,
47
+ executionId: providerCtx.executionId,
48
+ ...(providerCtx.parentExecutionId
49
+ ? { parentExecutionId: providerCtx.parentExecutionId }
50
+ : {}),
51
+ startedAtMs: encodeStartedMs,
52
+ finishedAtMs: Date.now(),
53
+ data: {
54
+ provider: provider.name,
55
+ model: providerCtx.target.model,
56
+ bytes: reqBody.byteLength,
57
+ },
58
+ });
59
+ const reqBytes = reqBody.byteLength;
60
+ const fetchStartedMs = Date.now();
61
+ const upstream = await fetchProvider(
62
+ runtime,
63
+ ctx,
64
+ provider,
65
+ reqBody,
66
+ timed.signal,
67
+ providerCtx,
68
+ );
69
+ observeTimedExecution({
70
+ observe: providerCtx.observe,
71
+ kind: "provider.fetch_headers",
72
+ requestId: providerCtx.requestId,
73
+ executionId: providerCtx.executionId,
74
+ ...(providerCtx.parentExecutionId
75
+ ? { parentExecutionId: providerCtx.parentExecutionId }
76
+ : {}),
77
+ startedAtMs: fetchStartedMs,
78
+ finishedAtMs: Date.now(),
79
+ data: {
80
+ provider: provider.name,
81
+ model: providerCtx.target.model,
82
+ status: upstream.status,
83
+ },
84
+ });
85
+ if (!upstream.ok) {
86
+ const providerError = await toProviderError(provider, upstream);
87
+ throw providerError;
88
+ }
89
+
90
+ const readStartedMs = Date.now();
91
+ const bytes = new Uint8Array(await upstream.arrayBuffer());
92
+ observeTimedExecution({
93
+ observe: providerCtx.observe,
94
+ kind: "provider.response_body_read",
95
+ requestId: providerCtx.requestId,
96
+ executionId: providerCtx.executionId,
97
+ ...(providerCtx.parentExecutionId
98
+ ? { parentExecutionId: providerCtx.parentExecutionId }
99
+ : {}),
100
+ startedAtMs: readStartedMs,
101
+ finishedAtMs: Date.now(),
102
+ data: {
103
+ provider: provider.name,
104
+ model: providerCtx.target.model,
105
+ bytes: bytes.byteLength,
106
+ },
107
+ });
108
+ const response = parseProviderResponse(provider.style, bytes);
109
+ emitProviderUsage(
110
+ providerCtx,
111
+ response,
112
+ provider.name,
113
+ reqBytes,
114
+ bytes.byteLength,
115
+ );
116
+ return response;
117
+ } catch (error) {
118
+ throw normalizeUpstreamAbort(error, timed.signal);
119
+ } finally {
120
+ timed.cleanup();
121
+ }
122
+ },
123
+
124
+ stream(request, providerCtx) {
125
+ return streamProvider(
126
+ runtime,
127
+ ctx,
128
+ upstreamTimeoutMs,
129
+ request,
130
+ providerCtx,
131
+ );
132
+ },
133
+ };
134
+ }
@@ -0,0 +1,192 @@
1
+ import {
2
+ emitProviderRequest,
3
+ isStreaming,
4
+ parseProviderStreamChunk,
5
+ setModel,
6
+ type Program,
7
+ usageObject,
8
+ } from "@neutrome/lil-engine";
9
+ import { RouterError } from "./errors.ts";
10
+ import { toProviderError } from "./error-codec.ts";
11
+ import {
12
+ createUpstreamSignal,
13
+ fetchProvider,
14
+ getProviderOrThrow,
15
+ normalizeUpstreamAbort,
16
+ } from "./upstream-client.ts";
17
+ import { emitProviderUsage, injectStreamUsage } from "./provider-telemetry.ts";
18
+ import type { ProviderInvocationContext } from "./execution-types.ts";
19
+ import type { RequestContext } from "../upstream-auth/types.ts";
20
+ import { isSse } from "../util/headers.ts";
21
+ import { eventDataLines, splitSseEvents } from "../util/sse.ts";
22
+ import type { RouterRuntime } from "./runtime.ts";
23
+ import { observeTimedExecution } from "./execution-events.ts";
24
+
25
+ const encoder = new TextEncoder();
26
+ const decoder = new TextDecoder();
27
+
28
+ export async function* streamProvider(
29
+ runtime: RouterRuntime,
30
+ ctx: RequestContext,
31
+ upstreamTimeoutMs: number,
32
+ request: Program,
33
+ providerCtx: ProviderInvocationContext,
34
+ ): AsyncGenerator<Program> {
35
+ const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
36
+ const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
37
+ let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
38
+ let buffer = "";
39
+
40
+ try {
41
+ const resolvedRequest = setModel(request, providerCtx.target.model);
42
+ const encodeStartedMs = Date.now();
43
+ let reqBody = emitProviderRequest(provider.style, resolvedRequest);
44
+ if (provider.style === "chat-completions" && isStreaming(resolvedRequest)) {
45
+ reqBody = injectStreamUsage(reqBody);
46
+ }
47
+ observeTimedExecution({
48
+ observe: providerCtx.observe,
49
+ kind: "provider.request_encoded",
50
+ requestId: providerCtx.requestId,
51
+ executionId: providerCtx.executionId,
52
+ ...(providerCtx.parentExecutionId
53
+ ? { parentExecutionId: providerCtx.parentExecutionId }
54
+ : {}),
55
+ startedAtMs: encodeStartedMs,
56
+ finishedAtMs: Date.now(),
57
+ data: {
58
+ provider: provider.name,
59
+ model: providerCtx.target.model,
60
+ bytes: reqBody.byteLength,
61
+ },
62
+ });
63
+ const reqBytes = reqBody.byteLength;
64
+ const fetchStartedMs = Date.now();
65
+ const upstream = await fetchProvider(
66
+ runtime,
67
+ ctx,
68
+ provider,
69
+ reqBody,
70
+ timed.signal,
71
+ providerCtx,
72
+ );
73
+ observeTimedExecution({
74
+ observe: providerCtx.observe,
75
+ kind: "provider.fetch_headers",
76
+ requestId: providerCtx.requestId,
77
+ executionId: providerCtx.executionId,
78
+ ...(providerCtx.parentExecutionId
79
+ ? { parentExecutionId: providerCtx.parentExecutionId }
80
+ : {}),
81
+ startedAtMs: fetchStartedMs,
82
+ finishedAtMs: Date.now(),
83
+ data: {
84
+ provider: provider.name,
85
+ model: providerCtx.target.model,
86
+ status: upstream.status,
87
+ },
88
+ });
89
+ if (!upstream.ok) {
90
+ const providerError = await toProviderError(provider, upstream);
91
+ throw providerError;
92
+ }
93
+ if (!isSse(upstream.headers) || !upstream.body) {
94
+ throw new RouterError(
95
+ `Provider ${provider.name} did not return an event stream`,
96
+ 502,
97
+ "invalid_upstream_stream",
98
+ );
99
+ }
100
+
101
+ reader = upstream.body.getReader();
102
+ let firstByteObserved = false;
103
+ const streamReadStartedMs = Date.now();
104
+ let lastChunkWithUsage: Program | null = null;
105
+ let respBytes = 0;
106
+ while (true) {
107
+ const { done, value } = await reader.read();
108
+ if (done) {
109
+ break;
110
+ }
111
+ if (!firstByteObserved) {
112
+ firstByteObserved = true;
113
+ observeTimedExecution({
114
+ observe: providerCtx.observe,
115
+ kind: "provider.stream_first_byte",
116
+ requestId: providerCtx.requestId,
117
+ executionId: providerCtx.executionId,
118
+ ...(providerCtx.parentExecutionId
119
+ ? { parentExecutionId: providerCtx.parentExecutionId }
120
+ : {}),
121
+ startedAtMs: streamReadStartedMs,
122
+ finishedAtMs: Date.now(),
123
+ data: {
124
+ provider: provider.name,
125
+ model: providerCtx.target.model,
126
+ bytes: value.byteLength,
127
+ },
128
+ });
129
+ }
130
+ respBytes += value.byteLength;
131
+ buffer += decoder.decode(value, { stream: true });
132
+ const split = splitSseEvents(buffer);
133
+ buffer = split.remainder;
134
+ for (const event of split.events) {
135
+ for (const data of eventDataLines(event)) {
136
+ if (data === "[DONE]") {
137
+ emitProviderUsage(
138
+ providerCtx,
139
+ lastChunkWithUsage,
140
+ provider.name,
141
+ reqBytes,
142
+ respBytes,
143
+ );
144
+ return;
145
+ }
146
+ if (data[0] !== "{") continue;
147
+ const chunk = parseProviderStreamChunk(
148
+ provider.style,
149
+ encoder.encode(data),
150
+ );
151
+ if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
152
+ yield chunk;
153
+ }
154
+ }
155
+ }
156
+
157
+ if (buffer.length > 0) {
158
+ for (const data of eventDataLines(buffer)) {
159
+ if (data === "[DONE]") {
160
+ emitProviderUsage(
161
+ providerCtx,
162
+ lastChunkWithUsage,
163
+ provider.name,
164
+ reqBytes,
165
+ respBytes,
166
+ );
167
+ return;
168
+ }
169
+ if (data[0] !== "{") continue;
170
+ const chunk = parseProviderStreamChunk(
171
+ provider.style,
172
+ encoder.encode(data),
173
+ );
174
+ if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
175
+ yield chunk;
176
+ }
177
+ }
178
+ emitProviderUsage(
179
+ providerCtx,
180
+ lastChunkWithUsage,
181
+ provider.name,
182
+ reqBytes,
183
+ respBytes,
184
+ );
185
+ } catch (error) {
186
+ throw normalizeUpstreamAbort(error, timed.signal);
187
+ } finally {
188
+ await reader?.cancel().catch(() => {});
189
+ reader?.releaseLock();
190
+ timed.cleanup();
191
+ }
192
+ }
@@ -0,0 +1,59 @@
1
+ import { usageObject, type Program } from "@neutrome/lil-engine";
2
+ import { createExecutionEvent } from "@neutrome/lilsdk-ts";
3
+ import type { ProviderInvocationContext } from "./execution-types.ts";
4
+
5
+ const encoder = new TextEncoder();
6
+ const decoder = new TextDecoder();
7
+
8
+ export function emitProviderUsage(
9
+ providerCtx: ProviderInvocationContext,
10
+ response: Program | null,
11
+ provider: string,
12
+ requestBytes: number,
13
+ responseBytes: number,
14
+ ): void {
15
+ const usage = response ? usageObject(response) : undefined;
16
+ const u = (usage && typeof usage === "object" ? usage : {}) as Record<
17
+ string,
18
+ unknown
19
+ >;
20
+ const tokensInput = asNum(u.prompt_tokens) || asNum(u.input_tokens);
21
+ const tokensOutput = asNum(u.completion_tokens) || asNum(u.output_tokens);
22
+ const details = u.prompt_tokens_details as
23
+ | Record<string, unknown>
24
+ | undefined;
25
+ const cachedTokensInput = details ? asNum(details.cached_tokens) : 0;
26
+ providerCtx.observe(
27
+ createExecutionEvent({
28
+ kind: "provider.usage",
29
+ requestId: providerCtx.requestId,
30
+ executionId: providerCtx.executionId,
31
+ ...(providerCtx.parentExecutionId
32
+ ? { parentExecutionId: providerCtx.parentExecutionId }
33
+ : {}),
34
+ data: {
35
+ provider,
36
+ model: providerCtx.target.model,
37
+ tokensInput,
38
+ tokensOutput,
39
+ cachedTokensInput,
40
+ requestBytes,
41
+ responseBytes,
42
+ },
43
+ }),
44
+ );
45
+ }
46
+
47
+ function asNum(v: unknown): number {
48
+ return typeof v === "number" && v >= 0 ? v : 0;
49
+ }
50
+
51
+ export function injectStreamUsage(body: Uint8Array): Uint8Array {
52
+ try {
53
+ const obj = JSON.parse(decoder.decode(body)) as Record<string, unknown>;
54
+ obj.stream_options = { include_usage: true };
55
+ return encoder.encode(JSON.stringify(obj));
56
+ } catch {
57
+ return body;
58
+ }
59
+ }
@@ -0,0 +1,61 @@
1
+ import { createTargetExecutor } from "@neutrome/lilsdk-ts/managed";
2
+ import { connectTools } from "@neutrome/lilsdk-ts/tools";
3
+ import type {
4
+ ExecutionTarget,
5
+ Executor,
6
+ ProgramTransform,
7
+ } from "./execution-types.ts";
8
+ import { createRemoteMcpToolSet, type RemoteMcpClientFactory } from "./mcp.ts";
9
+ import type { ResolvedTarget } from "./resolve.ts";
10
+ import type { RouterRuntime } from "./runtime.ts";
11
+
12
+ type RemoteMcpExecutionOptions = {
13
+ executorImplementations?: Readonly<Record<string, Executor>>;
14
+ transforms?: Readonly<Record<string, ProgramTransform>>;
15
+ upstreamTimeoutMs?: number;
16
+ remoteMcpClientFactory?: RemoteMcpClientFactory;
17
+ incomingExecutionId?: string;
18
+ };
19
+
20
+ export function prepareRemoteMcpExecution(
21
+ runtime: RouterRuntime,
22
+ resolved: ResolvedTarget,
23
+ options: RemoteMcpExecutionOptions,
24
+ ): {
25
+ target: ExecutionTarget;
26
+ options: RemoteMcpExecutionOptions;
27
+ close: () => Promise<void>;
28
+ } {
29
+ if (resolved.mcps.length === 0) {
30
+ return { target: resolved.target, options, close: async () => {} };
31
+ }
32
+
33
+ const remoteMcp = createRemoteMcpToolSet(
34
+ runtime,
35
+ resolved.mcps,
36
+ options.remoteMcpClientFactory,
37
+ );
38
+ if (remoteMcp.tools.length === 0) {
39
+ return { target: resolved.target, options, close: remoteMcp.close };
40
+ }
41
+
42
+ const executorName = `__remote_mcp_${crypto.randomUUID()}`;
43
+ return {
44
+ target: {
45
+ kind: "executor",
46
+ executorId: executorName,
47
+ alias: resolved.requestedModel,
48
+ },
49
+ options: {
50
+ ...options,
51
+ executorImplementations: {
52
+ ...(options.executorImplementations ?? {}),
53
+ [executorName]: connectTools(
54
+ remoteMcp.tools,
55
+ createTargetExecutor(resolved.target),
56
+ ),
57
+ },
58
+ },
59
+ close: remoteMcp.close,
60
+ };
61
+ }
@@ -0,0 +1,16 @@
1
+ import {
2
+ parseChatCompletionsRequest,
3
+ parseProviderRequest,
4
+ type Program,
5
+ type ProviderStyle,
6
+ } from "@neutrome/lil-engine";
7
+
8
+ export async function parseRequestProgram(
9
+ style: ProviderStyle,
10
+ request: Request,
11
+ ): Promise<Program> {
12
+ const body = new Uint8Array(await request.arrayBuffer());
13
+ return style === "chat-completions"
14
+ ? parseChatCompletionsRequest(body)
15
+ : parseProviderRequest(style, body);
16
+ }