@neutrome/open-ai-router 0.6.9 → 0.8.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/README.md CHANGED
@@ -30,4 +30,4 @@ Notes:
30
30
 
31
31
  - `RouterConfig` is the canonical router config surface.
32
32
  - `/v1/models` lists configured executor aliases.
33
- - Executor contracts and `connectTools` come from `@neutrome/lilsdk`.
33
+ - Executor contracts and `createToolsExecutor` come from `@neutrome/lilsdk`.
package/package.json CHANGED
@@ -1,18 +1,23 @@
1
1
  {
2
2
  "name": "@neutrome/open-ai-router",
3
- "version": "0.6.9",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
+ "files": [
6
+ "src",
7
+ "README.md"
8
+ ],
5
9
  "exports": {
6
10
  ".": "./src/index.ts"
7
11
  },
8
12
  "dependencies": {
9
- "@posthog/ai": "^8.3.0",
10
13
  "@modelcontextprotocol/sdk": "^1.29.0",
14
+ "@posthog/ai": "^8.3.0",
15
+ "gpt-tokenizer": "4.0.0",
11
16
  "hono": "^4.12.18",
12
17
  "posthog-node": "^5.41.0",
13
18
  "zod": "^4.2.1",
14
- "@neutrome/lil-engine": "0.4.6",
15
- "@neutrome/lilsdk": "0.4.6"
19
+ "@neutrome/lil-engine": "0.6.0",
20
+ "@neutrome/lilsdk": "0.6.0"
16
21
  },
17
22
  "devDependencies": {
18
23
  "@types/node": "^25.9.3",
@@ -100,6 +100,7 @@ describe("createRouterApp trace finalization", () => {
100
100
  it("keeps non-stream trace hooks alive with waitUntil", async () => {
101
101
  const waited: Promise<unknown>[] = [];
102
102
  const onExecutionSummary = vi.fn(async (_summary: ExecutionSummary) => {});
103
+ const onUserUsage = vi.fn(async () => {});
103
104
  const app = createRouterApp({
104
105
  config,
105
106
  executorImplementations: {},
@@ -108,7 +109,7 @@ describe("createRouterApp trace finalization", () => {
108
109
  return principal();
109
110
  },
110
111
  },
111
- hooks: { onExecutionSummary },
112
+ hooks: { onExecutionSummary, onUserUsage },
112
113
  fetchImpl: vi.fn(
113
114
  async () =>
114
115
  new Response(
@@ -145,11 +146,21 @@ describe("createRouterApp trace finalization", () => {
145
146
  expect(waited).toHaveLength(1);
146
147
  await waited[0];
147
148
  expect(onExecutionSummary).toHaveBeenCalledTimes(1);
149
+ expect(onUserUsage).toHaveBeenCalledWith(
150
+ expect.objectContaining({
151
+ requestedModel: "gpt-4o",
152
+ outcome: "ok",
153
+ inputTokens: expect.any(Number),
154
+ outputTokens: expect.any(Number),
155
+ }),
156
+ {},
157
+ );
148
158
  });
149
159
 
150
160
  it("keeps stream trace hooks alive until the stream closes", async () => {
151
161
  const waited: Promise<unknown>[] = [];
152
162
  const onExecutionSummary = vi.fn(async (_summary: ExecutionSummary) => {});
163
+ const onUserUsage = vi.fn(async () => {});
153
164
  const app = createRouterApp({
154
165
  config,
155
166
  executorImplementations: {},
@@ -158,7 +169,7 @@ describe("createRouterApp trace finalization", () => {
158
169
  return principal();
159
170
  },
160
171
  },
161
- hooks: { onExecutionSummary },
172
+ hooks: { onExecutionSummary, onUserUsage },
162
173
  fetchImpl: vi.fn(async () => {
163
174
  const stream = new ReadableStream<Uint8Array>({
164
175
  start(controller) {
@@ -217,6 +228,7 @@ describe("createRouterApp trace finalization", () => {
217
228
  expect(waited).toHaveLength(1);
218
229
  await waited[0];
219
230
  expect(onExecutionSummary).toHaveBeenCalledTimes(1);
231
+ expect(onUserUsage).toHaveBeenCalledTimes(1);
220
232
  const events = onExecutionSummary.mock.calls[0]?.[0].spans.flatMap((span) =>
221
233
  span.timeline.map((entry) => entry.event),
222
234
  );
@@ -1,9 +1,9 @@
1
1
  import { Hono, type Context } from "hono";
2
2
  import {
3
3
  getModel,
4
- setStreaming,
5
4
  type Program,
6
5
  type ProviderStyle,
6
+ setStreaming,
7
7
  } from "@neutrome/lil-engine";
8
8
  import { cors } from "./cors.ts";
9
9
  import { healthHandler } from "./health.ts";
@@ -35,6 +35,10 @@ import {
35
35
  import { createModelListingHandler } from "./model-listing.ts";
36
36
  import { honoEnv, unauthorizedResponse } from "./http-utils.ts";
37
37
  import type { RouterAppOptions } from "./types.ts";
38
+ import {
39
+ createUserUsageCollector,
40
+ responseResult,
41
+ } from "./user-usage-collector.ts";
38
42
 
39
43
  type ProtocolHandler = (
40
44
  options: Parameters<typeof handleChatCompletions>[0],
@@ -100,6 +104,13 @@ export function createRouterApp(options: RouterAppOptions) {
100
104
  : {}),
101
105
  });
102
106
  const onExecutionSummary = options.hooks?.onExecutionSummary;
107
+ const onUserUsage = options.hooks?.onUserUsage;
108
+ const requestStartedMs = Date.now();
109
+ const usage = createUserUsageCollector({
110
+ requestId: incomingExecutionId,
111
+ protocol: style,
112
+ startedAtMs: requestStartedMs,
113
+ });
103
114
  const observe = (
104
115
  event: Parameters<NonNullable<ExecutionRuntimeOptions["observe"]>>[0],
105
116
  ) => {
@@ -114,13 +125,14 @@ export function createRouterApp(options: RouterAppOptions) {
114
125
  c,
115
126
  async () => {
116
127
  await telemetry.flush();
117
- await onExecutionSummary?.(telemetry.summary(), env);
128
+ const summary = telemetry.summary();
129
+ await onExecutionSummary?.(summary, env);
130
+ await onUserUsage?.(usage.report(summary), env);
118
131
  },
119
132
  "onExecutionSummary hook",
120
133
  );
121
134
  };
122
135
 
123
- const requestStartedMs = Date.now();
124
136
  observeTimedExecution({
125
137
  observe,
126
138
  kind: "request.started",
@@ -142,6 +154,7 @@ export function createRouterApp(options: RouterAppOptions) {
142
154
  data: { authenticated: !!principal },
143
155
  });
144
156
  if (!principal) {
157
+ usage.setResult(responseResult(401, "Authentication failed"));
145
158
  telemetry.recordError({
146
159
  message: "Authentication failed",
147
160
  requestId: incomingExecutionId,
@@ -166,6 +179,7 @@ export function createRouterApp(options: RouterAppOptions) {
166
179
  data: { style, streaming: forceStreaming === true },
167
180
  });
168
181
  } catch {
182
+ usage.setResult(responseResult(400, "Invalid request body"));
169
183
  telemetry.recordError({
170
184
  message: "Invalid request body",
171
185
  requestId: incomingExecutionId,
@@ -189,6 +203,7 @@ export function createRouterApp(options: RouterAppOptions) {
189
203
  }
190
204
 
191
205
  const model = getModel(program) || null;
206
+ usage.setRequest(program, model);
192
207
  const accessStartedMs = Date.now();
193
208
  const hasModelAccess = !model || canAccessPrincipalModel(principal, model);
194
209
  observeTimedExecution({
@@ -201,6 +216,7 @@ export function createRouterApp(options: RouterAppOptions) {
201
216
  data: { model: model ?? undefined, allowed: hasModelAccess },
202
217
  });
203
218
  if (!hasModelAccess) {
219
+ usage.setResult(responseResult(404, "Model access denied"));
204
220
  telemetry.recordError({
205
221
  message: "Model access denied",
206
222
  requestId: incomingExecutionId,
@@ -232,6 +248,12 @@ export function createRouterApp(options: RouterAppOptions) {
232
248
  data: { returnedResponse: !!hookResponse },
233
249
  });
234
250
  if (hookResponse) {
251
+ usage.setResult(
252
+ responseResult(
253
+ hookResponse.status,
254
+ hookResponse.status >= 400 ? "Request rejected" : null,
255
+ ),
256
+ );
235
257
  if (hookResponse.status >= 400) {
236
258
  telemetry.recordError({
237
259
  message: `Request hook returned ${hookResponse.status}`,
@@ -245,6 +267,7 @@ export function createRouterApp(options: RouterAppOptions) {
245
267
  }
246
268
 
247
269
  if (style === "responses" && (await hasPreviousResponseId(request))) {
270
+ usage.setResult(responseResult(400, "Unsupported response state"));
248
271
  finalizeTrace();
249
272
  return errorResponse(
250
273
  new RouterError(
@@ -270,6 +293,10 @@ export function createRouterApp(options: RouterAppOptions) {
270
293
  ...(options.transforms ? { transforms: options.transforms } : {}),
271
294
  observe,
272
295
  incomingExecutionId,
296
+ userUsage: { inputTokens: usage.inputTokens() },
297
+ onUserResponse(result) {
298
+ usage.setResult(result);
299
+ },
273
300
  ...(options.upstreamTimeoutMs !== undefined
274
301
  ? { upstreamTimeoutMs: options.upstreamTimeoutMs }
275
302
  : {}),
@@ -296,6 +323,7 @@ export function createRouterApp(options: RouterAppOptions) {
296
323
  }
297
324
  return finalizeWhenStreamCloses(response, finalizeTrace);
298
325
  } catch (error) {
326
+ usage.setResult(responseResult(500, "Request execution failed"));
299
327
  telemetry.recordError({
300
328
  message:
301
329
  error instanceof Error ? error.message : "Request execution failed",
@@ -54,8 +54,8 @@ export function finalizeWhenStreamCloses(
54
54
  }
55
55
  },
56
56
  async cancel(reason) {
57
- finalize();
58
57
  await reader.cancel(reason).catch(() => {});
58
+ finalize();
59
59
  },
60
60
  });
61
61
  return new Response(body, {
package/src/app/types.ts CHANGED
@@ -7,6 +7,7 @@ import type { ExecutionSummary } from "../telemetry/index.ts";
7
7
  import type { RouterConfig } from "../router/config.ts";
8
8
  import type { ExecutionRuntimeOptions } from "../router/execution-types.ts";
9
9
  import type { RemoteMcpClientFactory } from "../router/mcp.ts";
10
+ import type { UserUsageReport } from "../usage/user-usage.ts";
10
11
 
11
12
  export type RouterAppAuth = RuntimeAuthenticator;
12
13
 
@@ -21,6 +22,10 @@ export type RouterAppHooks = {
21
22
  summary: ExecutionSummary,
22
23
  env: Record<string, unknown>,
23
24
  ): void | Promise<void>;
25
+ onUserUsage?(
26
+ report: UserUsageReport,
27
+ env: Record<string, unknown>,
28
+ ): void | Promise<void>;
24
29
  };
25
30
 
26
31
  export type RouterAppOptions = {
@@ -0,0 +1,68 @@
1
+ import type { Program, ProviderStyle } from "@neutrome/lil-engine";
2
+ import type { ExecutionSummary } from "../telemetry/index.ts";
3
+ import {
4
+ countUserProgramTokens,
5
+ toolUsageFromEvents,
6
+ type UserUsageReport,
7
+ } from "../usage/user-usage.ts";
8
+
9
+ export type UserResponseResult = Pick<
10
+ UserUsageReport,
11
+ "outputTokens" | "outcome" | "httpStatus" | "error"
12
+ >;
13
+
14
+ export function createUserUsageCollector(input: {
15
+ requestId: string;
16
+ protocol: ProviderStyle;
17
+ startedAtMs: number;
18
+ }) {
19
+ let inputTokens = 0;
20
+ let requestedModel: string | null = null;
21
+ let result: UserResponseResult | null = null;
22
+ return {
23
+ setRequest(program: Program, model: string | null) {
24
+ inputTokens = countUserProgramTokens(program);
25
+ requestedModel = model;
26
+ },
27
+ setResult(next: UserResponseResult) {
28
+ result = next;
29
+ },
30
+ inputTokens() {
31
+ return inputTokens;
32
+ },
33
+ report(summary: ExecutionSummary): UserUsageReport {
34
+ const finishedAt = new Date().toISOString();
35
+ return {
36
+ requestId: input.requestId,
37
+ protocol: input.protocol,
38
+ requestedModel,
39
+ inputTokens,
40
+ ...(result ??
41
+ responseResult(null, "Request did not produce a response")),
42
+ tools: toolUsageFromEvents(
43
+ summary.spans
44
+ .filter((span) => span.kind === "tool" && span.toolName)
45
+ .map((span) => ({
46
+ name: span.toolName!,
47
+ error: span.status === "error",
48
+ })),
49
+ ),
50
+ startedAt: new Date(input.startedAtMs).toISOString(),
51
+ finishedAt,
52
+ durationMs: Math.max(0, Date.parse(finishedAt) - input.startedAtMs),
53
+ };
54
+ },
55
+ };
56
+ }
57
+
58
+ export function responseResult(
59
+ status: number | null,
60
+ error: string | null,
61
+ ): UserResponseResult {
62
+ return {
63
+ outputTokens: 0,
64
+ outcome: status !== null && status < 400 ? "ok" : "error",
65
+ httpStatus: status,
66
+ error,
67
+ };
68
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  PosthogErrorTelemetry,
10
10
  PosthogTraceTelemetry,
11
11
  } from "./telemetry/index.ts";
12
+ export type { UserUsageReport, UserToolUsage } from "./usage/user-usage.ts";
12
13
  export type {
13
14
  ErrorTelemetryDriver,
14
15
  ExecutionSummary,
@@ -2,7 +2,7 @@ import {
2
2
  appendErrorBlock,
3
3
  createProgram,
4
4
  emitProviderError,
5
- firstError,
5
+ getFirstErrorBlock,
6
6
  parseProviderError,
7
7
  type Program,
8
8
  type ProviderStyle,
@@ -21,7 +21,7 @@ export async function toProviderError(
21
21
  contentType: response.headers.get("content-type"),
22
22
  });
23
23
  const summary =
24
- firstError(program)?.message ||
24
+ getFirstErrorBlock(program)?.message ||
25
25
  `Provider ${provider.name} returned ${response.status}`;
26
26
  return new ProviderHttpError(
27
27
  summary,
@@ -1,4 +1,4 @@
1
- import { appendAssistantMessage } from "@neutrome/lilsdk/synthetic";
1
+ import { appendAssistantMessage, createProgram } from "@neutrome/lil-engine";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import type { RequestContext } from "../upstream-auth/types.ts";
4
4
  import { handleChatCompletions } from "./execute.ts";
@@ -88,6 +88,7 @@ describe("router execution", () => {
88
88
  object: "chat.completion",
89
89
  id: "resp_1",
90
90
  model: "gpt-4o",
91
+ usage: { prompt_tokens: 0, completion_tokens: 27, total_tokens: 27 },
91
92
  choices: [
92
93
  {
93
94
  index: 0,
@@ -334,6 +335,7 @@ describe("router execution", () => {
334
335
  object: "chat.completion",
335
336
  id: "resp_1",
336
337
  model: "gpt-5",
338
+ usage: { prompt_tokens: 0, completion_tokens: 27, total_tokens: 27 },
337
339
  choices: [
338
340
  {
339
341
  index: 0,
@@ -411,9 +413,9 @@ describe("router execution", () => {
411
413
  id: "msg_1",
412
414
  model: "claude-sonnet-4",
413
415
  usage: {
414
- prompt_tokens: 10,
415
- completion_tokens: 5,
416
- total_tokens: 15,
416
+ prompt_tokens: 0,
417
+ completion_tokens: 27,
418
+ total_tokens: 27,
417
419
  },
418
420
  choices: [
419
421
  {
@@ -494,6 +496,7 @@ describe("router execution", () => {
494
496
  expect(await response.json()).toEqual({
495
497
  object: "chat.completion",
496
498
  model: "gemini-2.5-flash",
499
+ usage: { prompt_tokens: 0, completion_tokens: 27, total_tokens: 27 },
497
500
  choices: [
498
501
  {
499
502
  index: 0,
@@ -543,10 +546,10 @@ describe("router execution", () => {
543
546
  executorImplementations: {
544
547
  "enei-1": {
545
548
  async execute(request) {
546
- return appendAssistantMessage(request, "done");
549
+ return appendAssistantMessage(createProgram(), "done");
547
550
  },
548
551
  async *stream(request) {
549
- yield appendAssistantMessage(request, "done");
552
+ yield appendAssistantMessage(createProgram(), "done");
550
553
  },
551
554
  },
552
555
  },
@@ -556,6 +559,7 @@ describe("router execution", () => {
556
559
  expect(response.headers.get("x-openairouter-executor-id")).toBe("enei-1");
557
560
  expect(await response.json()).toEqual({
558
561
  object: "chat.completion",
562
+ usage: { prompt_tokens: 0, completion_tokens: 27, total_tokens: 27 },
559
563
  choices: [
560
564
  {
561
565
  index: 0,
@@ -1,20 +1,19 @@
1
1
  import {
2
- emitChatCompletionsResponse,
3
2
  emitProviderError,
4
3
  emitProviderRequest,
5
4
  emitProviderResponse,
6
5
  emitProviderStreamChunk,
7
6
  getModel,
7
+ getUsage,
8
8
  isStreaming,
9
- parseChatCompletionsRequest,
10
9
  parseProviderRequest,
11
10
  parseProviderResponse,
12
11
  parseProviderStreamChunk,
13
- setModel,
14
- setStreaming,
15
12
  type Program,
16
13
  type ProviderStyle,
17
- usageObject,
14
+ setModel,
15
+ setStreaming,
16
+ replaceUsage,
18
17
  } from "@neutrome/lil-engine";
19
18
  import { createExecutionRuntime } from "./execution-runtime.ts";
20
19
  import { createFetchProviderInvoker } from "./provider-invoker.ts";
@@ -63,6 +62,7 @@ import {
63
62
  import type { RemoteMcpClientFactory } from "./mcp.ts";
64
63
  import type { ProviderRuntime, RouterRuntime } from "./runtime.ts";
65
64
  import { observeTimedExecution } from "./execution-events.ts";
65
+ import { countUserProgramTokens, protocolUsage } from "../usage/user-usage.ts";
66
66
 
67
67
  const encoder = new TextEncoder();
68
68
  const decoder = new TextDecoder();
@@ -81,6 +81,13 @@ export type RouterExecutionOptions = Pick<
81
81
  upstreamTimeoutMs?: number;
82
82
  remoteMcpClientFactory?: RemoteMcpClientFactory;
83
83
  incomingExecutionId?: string;
84
+ userUsage?: { inputTokens: number };
85
+ onUserResponse?: (result: {
86
+ outputTokens: number;
87
+ outcome: "ok" | "error" | "cancelled";
88
+ httpStatus: number | null;
89
+ error: string | null;
90
+ }) => void | Promise<void>;
84
91
  };
85
92
 
86
93
  export type HandleChatCompletionsOptions = RouterExecutionOptions & {
@@ -165,10 +172,8 @@ export function createRouterExecutionRuntime(
165
172
  export async function handleChatCompletions(
166
173
  options: HandleChatCompletionsOptions,
167
174
  ): Promise<Response> {
168
- return handleRequestStyle(
169
- "chat-completions",
170
- options,
171
- emitChatCompletionsResponse,
175
+ return handleRequestStyle("chat-completions", options, (program) =>
176
+ emitProviderResponse("chat-completions", program),
172
177
  );
173
178
  }
174
179
 
@@ -244,6 +249,10 @@ async function handleRequestStyle(
244
249
  requestId: incomingExecutionId,
245
250
  executionId: incomingExecutionId,
246
251
  startedAtMs: responseStartedMs,
252
+ inputTokens: options.userUsage?.inputTokens ?? 0,
253
+ onFinish: async (result) => {
254
+ await options.onUserResponse?.({ ...result, httpStatus: 200 });
255
+ },
247
256
  },
248
257
  remoteMcp.close,
249
258
  );
@@ -259,12 +268,38 @@ async function handleRequestStyle(
259
268
  target: remoteMcp.target,
260
269
  });
261
270
  const userTime = Date.now() - startTime;
262
- return jsonExecutionResponse(emitResponse(response), resolved, userTime);
271
+ const outputTokens = countUserProgramTokens(response);
272
+ await options.onUserResponse?.({
273
+ outputTokens,
274
+ outcome: "ok",
275
+ httpStatus: 200,
276
+ error: null,
277
+ });
278
+ return jsonExecutionResponse(
279
+ emitResponse(
280
+ replaceUsage(
281
+ response,
282
+ protocolUsage(
283
+ style,
284
+ options.userUsage?.inputTokens ?? 0,
285
+ outputTokens,
286
+ ),
287
+ ),
288
+ ),
289
+ resolved,
290
+ userTime,
291
+ );
263
292
  } finally {
264
293
  await remoteMcp.close();
265
294
  }
266
295
  } catch (error) {
267
296
  const response = errorResponse(error, style);
297
+ await options.onUserResponse?.({
298
+ outputTokens: 0,
299
+ outcome: "error",
300
+ httpStatus: response.status,
301
+ error: "Request execution failed",
302
+ });
268
303
  return resolved ? applyRoutingHeaders(response, resolved) : response;
269
304
  }
270
305
  }
@@ -1,4 +1,4 @@
1
- import { firstError, type Program } from "@neutrome/lil-engine";
1
+ import { getFirstErrorBlock, type Program } from "@neutrome/lil-engine";
2
2
  import {
3
3
  ExecutionError,
4
4
  isExecutionError,
@@ -100,7 +100,7 @@ export function normalizeExecutionError(
100
100
  ): ExecutionError {
101
101
  if (isExecutionError(error)) return error;
102
102
  if (error instanceof ProviderHttpError) {
103
- const upstream = firstError(error.program);
103
+ const upstream = getFirstErrorBlock(error.program);
104
104
  return new ExecutionError("provider", error.message, {
105
105
  stage: fallbackStage,
106
106
  details: {
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  emitProviderRequest,
3
+ getUsage,
3
4
  isStreaming,
4
5
  parseProviderStreamChunk,
5
- setModel,
6
6
  type Program,
7
- usageObject,
7
+ setModel,
8
8
  } from "@neutrome/lil-engine";
9
9
  import { RouterError } from "./errors.ts";
10
10
  import { toProviderError } from "./error-codec.ts";
@@ -155,7 +155,7 @@ export async function* streamProvider(
155
155
  encoder.encode(data),
156
156
  );
157
157
  responseChunks.push(chunk);
158
- if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
158
+ if (getUsage(chunk) !== undefined) lastChunkWithUsage = chunk;
159
159
  yield chunk;
160
160
  }
161
161
  }
@@ -181,7 +181,7 @@ export async function* streamProvider(
181
181
  encoder.encode(data),
182
182
  );
183
183
  responseChunks.push(chunk);
184
- if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
184
+ if (getUsage(chunk) !== undefined) lastChunkWithUsage = chunk;
185
185
  yield chunk;
186
186
  }
187
187
  }
@@ -1,4 +1,4 @@
1
- import { usageObject, type Program } from "@neutrome/lil-engine";
1
+ import { getUsage, type Program } from "@neutrome/lil-engine";
2
2
  import { createExecutionEvent } from "@neutrome/lilsdk";
3
3
  import { z } from "zod";
4
4
  import {
@@ -21,7 +21,7 @@ export function emitProviderUsage(
21
21
  responseBytes: number,
22
22
  streamChunks?: readonly Program[],
23
23
  ): void {
24
- const usage = response ? usageObject(response) : undefined;
24
+ const usage = response ? getUsage(response) : undefined;
25
25
  const u = (usage && typeof usage === "object" ? usage : {}) as Record<
26
26
  string,
27
27
  unknown
@@ -1,4 +1,4 @@
1
- import { connectTools } from "@neutrome/lilsdk/tools";
1
+ import { createToolsExecutor } from "@neutrome/lilsdk/tools";
2
2
  import type {
3
3
  ExecutionTarget,
4
4
  Executor,
@@ -49,7 +49,10 @@ export function prepareRemoteMcpExecution(
49
49
  ...options,
50
50
  executorImplementations: {
51
51
  ...(options.executorImplementations ?? {}),
52
- [executorName]: connectTools(remoteMcp.tools, resolved.requestedModel),
52
+ [executorName]: createToolsExecutor(
53
+ resolved.requestedModel,
54
+ remoteMcp.tools,
55
+ ),
53
56
  },
54
57
  },
55
58
  close: remoteMcp.close,
@@ -1,5 +1,4 @@
1
1
  import {
2
- parseChatCompletionsRequest,
3
2
  parseProviderRequest,
4
3
  type Program,
5
4
  type ProviderStyle,
@@ -11,6 +10,6 @@ export async function parseRequestProgram(
11
10
  ): Promise<Program> {
12
11
  const body = new Uint8Array(await request.arrayBuffer());
13
12
  return style === "chat-completions"
14
- ? parseChatCompletionsRequest(body)
13
+ ? parseProviderRequest("chat-completions", body)
15
14
  : parseProviderRequest(style, body);
16
15
  }