@neutrome/open-ai-router 0.1.17 → 0.2.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.
@@ -1,5 +1,4 @@
1
1
  import {
2
- createAuditEvent,
3
2
  emitChatCompletionsResponse,
4
3
  emitProviderRequest,
5
4
  emitProviderResponse,
@@ -14,19 +13,23 @@ import {
14
13
  setStreaming,
15
14
  type Program,
16
15
  type ProviderStyle,
17
- withTools,
18
16
  usageObject,
19
- type ExecutionTarget,
20
17
  } from "@neutrome/lil-engine";
18
+ import { withTools } from "@neutrome/lilsdk-ts/tools";
21
19
  import {
22
20
  createExecutionRuntime,
23
- ExecutionError,
24
- type ExecutionRuntime,
25
- type ExecutionRuntimeOptions,
26
- type Executor,
27
- type ProgramTransform,
28
- type ProviderInvoker,
29
- } from "@neutrome/lil-engine";
21
+ } from "./execution-runtime.ts";
22
+ import { ExecutionError } from "./errors.ts";
23
+ import type {
24
+ ExecutionTarget,
25
+ ExecutionRuntime,
26
+ ExecutionRuntimeOptions,
27
+ Executor,
28
+ ProgramTransform,
29
+ ProviderInvocationContext,
30
+ ProviderInvoker,
31
+ } from "./execution-types.ts";
32
+ import { createAuditEvent } from "./audit.ts";
30
33
  import type { RequestContext, TargetAuthContext } from "../auth/types.ts";
31
34
  import { cloneForwardHeaders, isSse } from "../util/headers.ts";
32
35
  import { eventDataLines, splitSseEvents } from "../util/sse.ts";
@@ -46,7 +49,7 @@ export type RouterExecutionOptions = Pick<
46
49
  ExecutionRuntimeOptions,
47
50
  "observe" | "requestIdFactory" | "executionIdFactory" | "resolveExecutor"
48
51
  > & {
49
- executors?: Readonly<Record<string, Executor>>;
52
+ executorImplementations?: Readonly<Record<string, Executor>>;
50
53
  transforms?: Readonly<Record<string, ProgramTransform>>;
51
54
  upstreamTimeoutMs?: number;
52
55
  remoteMcpClientFactory?: RemoteMcpClientFactory;
@@ -102,7 +105,7 @@ export function createRouterExecutionRuntime(
102
105
  ): ExecutionRuntime {
103
106
  const runtimeOptions: ExecutionRuntimeOptions = {
104
107
  signal: ctx.request.signal,
105
- resolveTarget(request) {
108
+ resolveTarget(request: Program) {
106
109
  return resolveRequestTarget(runtime, request).target;
107
110
  },
108
111
  providerInvoker: createFetchProviderInvoker(
@@ -112,8 +115,8 @@ export function createRouterExecutionRuntime(
112
115
  ),
113
116
  };
114
117
 
115
- if (options.executors) {
116
- runtimeOptions.executors = options.executors;
118
+ if (options.executorImplementations) {
119
+ runtimeOptions.executorImplementations = options.executorImplementations;
117
120
  }
118
121
  if (options.transforms) {
119
122
  runtimeOptions.transforms = options.transforms;
@@ -129,23 +132,23 @@ export function createRouterExecutionRuntime(
129
132
  }
130
133
 
131
134
  const userResolveExecutor = options.resolveExecutor;
132
- runtimeOptions.resolveExecutor = (name) => {
135
+ runtimeOptions.resolveExecutor = (name: string) => {
133
136
  if (userResolveExecutor) {
134
137
  const result = userResolveExecutor(name);
135
138
  if (result) return result;
136
139
  }
137
- return resolveNamespaceExecutor(runtime, name);
140
+ return resolveModelNamespaceExecutor(runtime, name);
138
141
  };
139
142
 
140
143
  return createExecutionRuntime(runtimeOptions);
141
144
  }
142
145
 
143
- function resolveNamespaceExecutor(
146
+ function resolveModelNamespaceExecutor(
144
147
  runtime: RouterRuntime,
145
148
  name: string,
146
149
  ): Executor | null {
147
- const executor = runtime.executors.get(name);
148
- if (!executor) return null;
150
+ const modelNamespace = runtime.modelNamespaces.get(name);
151
+ if (!modelNamespace) return null;
149
152
 
150
153
  return {
151
154
  async execute(request, ctx) {
@@ -556,13 +559,13 @@ function prepareRemoteMcpExecution(
556
559
  return {
557
560
  target: {
558
561
  kind: "executor",
559
- executor: executorName,
562
+ executorId: executorName,
560
563
  alias: resolved.requestedModel,
561
564
  },
562
565
  options: {
563
566
  ...options,
564
- executors: {
565
- ...(options.executors ?? {}),
567
+ executorImplementations: {
568
+ ...(options.executorImplementations ?? {}),
566
569
  [executorName]: withTools(remoteMcp.tools, resolved.target),
567
570
  },
568
571
  },
@@ -598,7 +601,7 @@ function routingHeaders(resolved: ResolvedTarget): Record<string, string> {
598
601
  "x-openairouter-engine": "open-ai-router",
599
602
  "x-openairouter-source": resolved.source,
600
603
  "x-openairouter-namespace": resolved.namespace,
601
- "x-openairouter-executor-id": resolved.target.executor,
604
+ "x-openairouter-executor-id": resolved.target.executorId,
602
605
  "x-openairouter-model-id": resolved.target.alias,
603
606
  };
604
607
  }
@@ -819,7 +822,7 @@ function createUpstreamSignal(
819
822
  }
820
823
 
821
824
  function emitProviderUsage(
822
- providerCtx: import("@neutrome/lil-engine").ProviderInvocationContext,
825
+ providerCtx: ProviderInvocationContext,
823
826
  response: Program | null,
824
827
  provider: string,
825
828
  requestBytes: number,
@@ -0,0 +1,327 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ cloneProgram,
4
+ createProgram,
5
+ Opcode,
6
+ } from "@neutrome/lil-engine";
7
+ import {
8
+ createAuditEvent,
9
+ createExecutionRuntime,
10
+ createInMemoryAuditSink,
11
+ ExecutionError,
12
+ executionTargetAuditRef,
13
+ executionTargetId,
14
+ } from "./index.ts";
15
+ import { observeExecutionStream } from "@neutrome/lilsdk-ts/stream";
16
+
17
+ describe("router execution runtime", () => {
18
+ it("creates audit events with a stable envelope", () => {
19
+ const event = createAuditEvent({
20
+ kind: "executor.started",
21
+ requestId: "req_1",
22
+ executionId: "exec_1",
23
+ target: { kind: "executor", id: "enei-1" },
24
+ });
25
+
26
+ expect(event.kind).toBe("executor.started");
27
+ expect(event.requestId).toBe("req_1");
28
+ expect(event.executionId).toBe("exec_1");
29
+ expect(event.target).toEqual({ kind: "executor", id: "enei-1" });
30
+ expect(typeof event.timestamp).toBe("string");
31
+ expect(event.timestamp.length).toBeGreaterThan(0);
32
+ });
33
+
34
+ it("collects audit events in memory", () => {
35
+ const sink = createInMemoryAuditSink();
36
+ sink.observe(createAuditEvent({
37
+ kind: "program.validated",
38
+ requestId: "req_2",
39
+ executionId: "exec_2",
40
+ data: { issues: 0 },
41
+ }));
42
+
43
+ expect(sink.events).toHaveLength(1);
44
+ expect(sink.events[0]?.kind).toBe("program.validated");
45
+ expect(sink.events[0]?.data).toEqual({ issues: 0 });
46
+ });
47
+
48
+ it("normalizes execution target identifiers", () => {
49
+ expect(executionTargetId({
50
+ kind: "provider",
51
+ provider: "openrouter",
52
+ model: "google/gemma-4-31b-it",
53
+ })).toBe("openrouter/google/gemma-4-31b-it");
54
+
55
+ expect(executionTargetAuditRef({
56
+ kind: "executor",
57
+ executorId: "enei",
58
+ alias: "enei-1",
59
+ })).toEqual({
60
+ kind: "executor",
61
+ id: "enei/enei-1",
62
+ });
63
+ });
64
+
65
+ it("invokes nested executors without router re-entry and preserves request identity", async () => {
66
+ const sink = createInMemoryAuditSink();
67
+ const runtime = createExecutionRuntime({
68
+ observe: sink.observe,
69
+ executorImplementations: {
70
+ parent: {
71
+ async execute(request, ctx) {
72
+ const childResult = await ctx.invoke(request, {
73
+ target: { kind: "executor", executorId: "child", alias: "child" },
74
+ });
75
+ return childResult;
76
+ },
77
+ async *stream() {
78
+ return;
79
+ },
80
+ },
81
+ child: {
82
+ async execute(request) {
83
+ const result = cloneProgram(request);
84
+ result.code.push({
85
+ opcode: Opcode.RESP_DONE,
86
+ value: { kind: "string", value: "stop" },
87
+ });
88
+ return result;
89
+ },
90
+ async *stream() {
91
+ return;
92
+ },
93
+ },
94
+ },
95
+ });
96
+
97
+ const request = createProgram({
98
+ code: [
99
+ { opcode: Opcode.SET_MODEL, value: { kind: "string", value: "enei-1" } },
100
+ ],
101
+ });
102
+
103
+ const result = await runtime.execute(request, {
104
+ target: { kind: "executor", executorId: "parent", alias: "parent" },
105
+ requestId: "req_nested",
106
+ executionId: "exec_root",
107
+ });
108
+
109
+ expect(result.code.at(-1)).toEqual({
110
+ opcode: Opcode.RESP_DONE,
111
+ value: { kind: "string", value: "stop" },
112
+ });
113
+
114
+ const started = sink.events.filter((event) => event.kind === "executor.started");
115
+ expect(started).toHaveLength(2);
116
+ expect(started[0]?.requestId).toBe("req_nested");
117
+ expect(started[1]?.requestId).toBe("req_nested");
118
+ expect(started[1]?.parentExecutionId).toBe("exec_root");
119
+ expect(started[0]?.data?.depth).toBe(0);
120
+ expect(started[1]?.data?.depth).toBe(1);
121
+ });
122
+
123
+ it("applies target transforms before provider invocation", async () => {
124
+ const providerCalls: string[] = [];
125
+ const runtime = createExecutionRuntime({
126
+ providerInvoker: {
127
+ async execute(request) {
128
+ const marker = request.code.find((instruction) =>
129
+ instruction.opcode === Opcode.SET_META &&
130
+ instruction.value.kind === "key_string" &&
131
+ instruction.value.key === "transform",
132
+ );
133
+ providerCalls.push(
134
+ marker?.value.kind === "key_string" ? marker.value.value : "missing",
135
+ );
136
+ return cloneProgram(request);
137
+ },
138
+ async *stream() {
139
+ return;
140
+ },
141
+ },
142
+ transforms: {
143
+ slwin: {
144
+ name: "slwin",
145
+ capabilities: ["write_messages"],
146
+ apply(program) {
147
+ const result = cloneProgram(program);
148
+ result.code.push({
149
+ opcode: Opcode.SET_META,
150
+ value: { kind: "key_string", key: "transform", value: "slwin" },
151
+ });
152
+ return result;
153
+ },
154
+ },
155
+ },
156
+ });
157
+
158
+ await runtime.execute(createProgram(), {
159
+ target: {
160
+ kind: "provider",
161
+ provider: "openrouter",
162
+ model: "gpt-4o",
163
+ transforms: ["slwin"],
164
+ },
165
+ });
166
+
167
+ expect(providerCalls).toEqual(["slwin"]);
168
+ });
169
+
170
+ it("streams through the provider invoker", async () => {
171
+ const runtime = createExecutionRuntime({
172
+ providerInvoker: {
173
+ async execute() {
174
+ throw new Error("not used");
175
+ },
176
+ async *stream(request) {
177
+ yield cloneProgram(request);
178
+ yield createProgram({
179
+ code: [
180
+ { opcode: Opcode.STREAM_DELTA, value: { kind: "string", value: "hello" } },
181
+ ],
182
+ });
183
+ },
184
+ },
185
+ });
186
+
187
+ const outputs = [];
188
+ for await (const chunk of runtime.stream(createProgram(), {
189
+ target: { kind: "provider", provider: "openrouter", model: "gpt-4o" },
190
+ })) {
191
+ outputs.push(chunk);
192
+ }
193
+
194
+ expect(outputs).toHaveLength(2);
195
+ expect(outputs[1]?.code[0]).toEqual({
196
+ opcode: Opcode.STREAM_DELTA,
197
+ value: { kind: "string", value: "hello" },
198
+ });
199
+ });
200
+
201
+ it("enforces recursion limits with typed errors", async () => {
202
+ const sink = createInMemoryAuditSink();
203
+ const runtime = createExecutionRuntime({
204
+ observe: sink.observe,
205
+ maxDepth: 1,
206
+ executorImplementations: {
207
+ parent: {
208
+ async execute(request, ctx) {
209
+ return ctx.invoke(request, {
210
+ target: { kind: "executor", executorId: "child", alias: "child" },
211
+ });
212
+ },
213
+ async *stream() {
214
+ return;
215
+ },
216
+ },
217
+ child: {
218
+ async execute(request, ctx) {
219
+ return ctx.invoke(request, {
220
+ target: { kind: "executor", executorId: "leaf", alias: "leaf" },
221
+ });
222
+ },
223
+ async *stream() {
224
+ return;
225
+ },
226
+ },
227
+ leaf: {
228
+ async execute(request) {
229
+ return request;
230
+ },
231
+ async *stream() {
232
+ return;
233
+ },
234
+ },
235
+ },
236
+ });
237
+
238
+ await expect(runtime.execute(createProgram(), {
239
+ target: { kind: "executor", executorId: "parent", alias: "parent" },
240
+ })).rejects.toMatchObject({
241
+ kind: "recursion_limit",
242
+ stage: "target_resolution",
243
+ } satisfies Partial<ExecutionError>);
244
+
245
+ const failed = sink.events.find((event) => event.kind === "execution.failed");
246
+ expect(failed?.errorKind).toBe("recursion_limit");
247
+ expect(failed?.data?.depth).toBe(2);
248
+ });
249
+
250
+ it("validates request and stream chunk shapes with typed validation errors", async () => {
251
+ const runtime = createExecutionRuntime({
252
+ providerInvoker: {
253
+ async execute(request) {
254
+ return request;
255
+ },
256
+ async *stream() {
257
+ yield createProgram({
258
+ code: [
259
+ { opcode: Opcode.MSG_START, value: { kind: "none" } },
260
+ ],
261
+ });
262
+ },
263
+ },
264
+ });
265
+
266
+ await expect(runtime.execute(createProgram({
267
+ code: [
268
+ { opcode: Opcode.EXT_DATA, value: { kind: "key_json", key: "provider", value: new Uint8Array([123, 125]) } },
269
+ ],
270
+ }), {
271
+ target: { kind: "provider", provider: "openrouter", model: "gpt-4o" },
272
+ })).rejects.toMatchObject({
273
+ kind: "validation",
274
+ stage: "request",
275
+ } satisfies Partial<ExecutionError>);
276
+
277
+ const iterator = runtime.stream(createProgram(), {
278
+ target: { kind: "provider", provider: "openrouter", model: "gpt-4o" },
279
+ })[Symbol.asyncIterator]();
280
+ await expect(iterator.next()).rejects.toMatchObject({
281
+ kind: "validation",
282
+ stage: "stream_chunk",
283
+ } satisfies Partial<ExecutionError>);
284
+ });
285
+
286
+ it("observes text-first streams and tool-call streams", async () => {
287
+ const textObserved = await observeExecutionStream((async function* () {
288
+ yield createProgram({
289
+ code: [
290
+ { opcode: Opcode.STREAM_DELTA, value: { kind: "string", value: "hello" } },
291
+ ],
292
+ });
293
+ yield createProgram({
294
+ code: [
295
+ { opcode: Opcode.STREAM_DELTA, value: { kind: "string", value: " world" } },
296
+ { opcode: Opcode.RESP_DONE, value: { kind: "string", value: "stop" } },
297
+ ],
298
+ });
299
+ })());
300
+
301
+ expect(textObserved).toEqual({
302
+ mode: "text",
303
+ transcript: "hello world",
304
+ emittedText: true,
305
+ });
306
+
307
+ const toolObserved = await observeExecutionStream((async function* () {
308
+ yield createProgram({
309
+ code: [
310
+ { opcode: Opcode.STREAM_TOOL_DELTA, value: { kind: "json", value: new Uint8Array([123, 125]) } },
311
+ ],
312
+ });
313
+ yield createProgram({
314
+ code: [
315
+ { opcode: Opcode.RESP_DONE, value: { kind: "string", value: "tool_calls" } },
316
+ ],
317
+ });
318
+ })());
319
+
320
+ expect(toolObserved.mode).toBe("tool");
321
+ if (toolObserved.mode === "tool") {
322
+ expect(toolObserved.chunks).toHaveLength(2);
323
+ expect(toolObserved.emittedText).toBe(false);
324
+ expect(toolObserved.transcript).toBe("");
325
+ }
326
+ });
327
+ });