@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.
- package/README.md +8 -4
- package/package.json +3 -2
- package/src/app/factory.test.ts +3 -3
- package/src/app/factory.ts +4 -4
- package/src/app/handlers.ts +3 -7
- package/src/example.test.ts +13 -13
- package/src/example.ts +2 -2
- package/src/index.ts +26 -4
- package/src/model.example.ts +61 -0
- package/src/observer.test.ts +9 -2
- package/src/observer.ts +16 -2
- package/src/router/audit.ts +21 -0
- package/src/router/config.ts +5 -5
- package/src/router/errors.ts +60 -0
- package/src/router/execute.test.ts +11 -11
- package/src/router/execute.ts +27 -24
- package/src/router/execution-runtime.test.ts +327 -0
- package/src/router/execution-runtime.ts +632 -0
- package/src/router/execution-types.ts +80 -0
- package/src/router/index.ts +26 -5
- package/src/router/mcp.ts +2 -2
- package/src/router/resolve.test.ts +10 -10
- package/src/router/resolve.ts +15 -15
- package/src/router/runtime.ts +24 -24
- package/src/router/targets.ts +17 -0
- package/src/worker.ts +1 -1
package/src/router/execute.ts
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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.
|
|
116
|
-
runtimeOptions.
|
|
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
|
|
140
|
+
return resolveModelNamespaceExecutor(runtime, name);
|
|
138
141
|
};
|
|
139
142
|
|
|
140
143
|
return createExecutionRuntime(runtimeOptions);
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
function
|
|
146
|
+
function resolveModelNamespaceExecutor(
|
|
144
147
|
runtime: RouterRuntime,
|
|
145
148
|
name: string,
|
|
146
149
|
): Executor | null {
|
|
147
|
-
const
|
|
148
|
-
if (!
|
|
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
|
-
|
|
562
|
+
executorId: executorName,
|
|
560
563
|
alias: resolved.requestedModel,
|
|
561
564
|
},
|
|
562
565
|
options: {
|
|
563
566
|
...options,
|
|
564
|
-
|
|
565
|
-
...(options.
|
|
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.
|
|
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:
|
|
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
|
+
});
|