@neutrome/open-ai-router 0.1.18 → 0.3.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/admission/access.ts +14 -0
- package/src/admission/jwt.test.ts +96 -0
- package/src/admission/jwt.ts +221 -0
- package/src/admission/types.ts +14 -0
- package/src/app/factory.test.ts +14 -11
- package/src/app/factory.ts +41 -31
- package/src/app/handlers.ts +5 -9
- package/src/example.test.ts +14 -14
- package/src/example.ts +5 -2
- package/src/index.ts +45 -12
- package/src/model.example.ts +61 -0
- package/src/observer.ts +2 -1
- package/src/router/audit.ts +21 -0
- package/src/router/config.ts +8 -8
- package/src/router/errors.ts +60 -0
- package/src/router/execute.test.ts +12 -12
- package/src/router/execute.ts +47 -32
- 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 +28 -6
- 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 +29 -29
- package/src/router/targets.ts +17 -0
- package/src/{auth → upstream-auth}/config.ts +1 -1
- package/src/{auth → upstream-auth}/env.ts +2 -2
- package/src/{auth → upstream-auth}/proxy.ts +2 -2
- package/src/{auth → upstream-auth}/registry.ts +4 -4
- package/src/{auth → upstream-auth}/types.ts +1 -1
- package/src/worker.ts +2 -2
- package/src/auth/apikey.ts +0 -51
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,20 +13,24 @@ 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
|
-
|
|
30
|
-
|
|
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";
|
|
33
|
+
import type { RequestContext, TargetAuthContext } from "../upstream-auth/types.ts";
|
|
31
34
|
import { cloneForwardHeaders, isSse } from "../util/headers.ts";
|
|
32
35
|
import { eventDataLines, splitSseEvents } from "../util/sse.ts";
|
|
33
36
|
import { renderProgramAsLil } from "../observer.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;
|
|
@@ -55,21 +58,25 @@ export type RouterExecutionOptions = Pick<
|
|
|
55
58
|
export type HandleChatCompletionsOptions = RouterExecutionOptions & {
|
|
56
59
|
runtime: RouterRuntime;
|
|
57
60
|
ctx: RequestContext;
|
|
61
|
+
program?: Program;
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
export type HandleResponsesOptions = RouterExecutionOptions & {
|
|
61
65
|
runtime: RouterRuntime;
|
|
62
66
|
ctx: RequestContext;
|
|
67
|
+
program?: Program;
|
|
63
68
|
};
|
|
64
69
|
|
|
65
70
|
export type HandleAnthropicMessagesOptions = RouterExecutionOptions & {
|
|
66
71
|
runtime: RouterRuntime;
|
|
67
72
|
ctx: RequestContext;
|
|
73
|
+
program?: Program;
|
|
68
74
|
};
|
|
69
75
|
|
|
70
76
|
export type HandleGoogleGenAIOptions = RouterExecutionOptions & {
|
|
71
77
|
runtime: RouterRuntime;
|
|
72
78
|
ctx: RequestContext;
|
|
79
|
+
program?: Program;
|
|
73
80
|
forceStreaming?: boolean;
|
|
74
81
|
};
|
|
75
82
|
|
|
@@ -102,7 +109,7 @@ export function createRouterExecutionRuntime(
|
|
|
102
109
|
): ExecutionRuntime {
|
|
103
110
|
const runtimeOptions: ExecutionRuntimeOptions = {
|
|
104
111
|
signal: ctx.request.signal,
|
|
105
|
-
resolveTarget(request) {
|
|
112
|
+
resolveTarget(request: Program) {
|
|
106
113
|
return resolveRequestTarget(runtime, request).target;
|
|
107
114
|
},
|
|
108
115
|
providerInvoker: createFetchProviderInvoker(
|
|
@@ -112,8 +119,8 @@ export function createRouterExecutionRuntime(
|
|
|
112
119
|
),
|
|
113
120
|
};
|
|
114
121
|
|
|
115
|
-
if (options.
|
|
116
|
-
runtimeOptions.
|
|
122
|
+
if (options.executorImplementations) {
|
|
123
|
+
runtimeOptions.executorImplementations = options.executorImplementations;
|
|
117
124
|
}
|
|
118
125
|
if (options.transforms) {
|
|
119
126
|
runtimeOptions.transforms = options.transforms;
|
|
@@ -129,23 +136,23 @@ export function createRouterExecutionRuntime(
|
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
const userResolveExecutor = options.resolveExecutor;
|
|
132
|
-
runtimeOptions.resolveExecutor = (name) => {
|
|
139
|
+
runtimeOptions.resolveExecutor = (name: string) => {
|
|
133
140
|
if (userResolveExecutor) {
|
|
134
141
|
const result = userResolveExecutor(name);
|
|
135
142
|
if (result) return result;
|
|
136
143
|
}
|
|
137
|
-
return
|
|
144
|
+
return resolveModelNamespaceExecutor(runtime, name);
|
|
138
145
|
};
|
|
139
146
|
|
|
140
147
|
return createExecutionRuntime(runtimeOptions);
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
function
|
|
150
|
+
function resolveModelNamespaceExecutor(
|
|
144
151
|
runtime: RouterRuntime,
|
|
145
152
|
name: string,
|
|
146
153
|
): Executor | null {
|
|
147
|
-
const
|
|
148
|
-
if (!
|
|
154
|
+
const modelNamespace = runtime.modelNamespaces.get(name);
|
|
155
|
+
if (!modelNamespace) return null;
|
|
149
156
|
|
|
150
157
|
return {
|
|
151
158
|
async execute(request, ctx) {
|
|
@@ -167,8 +174,7 @@ export async function handleChatCompletions(
|
|
|
167
174
|
options: HandleChatCompletionsOptions,
|
|
168
175
|
): Promise<Response> {
|
|
169
176
|
try {
|
|
170
|
-
const
|
|
171
|
-
const request = parseChatCompletionsRequest(requestBody);
|
|
177
|
+
const request = options.program ?? await parseRequestProgram("chat-completions", options.ctx.request);
|
|
172
178
|
const incomingExecutionId = `incoming_${crypto.randomUUID()}`;
|
|
173
179
|
emitObservedProgram(options.observe, "request.received", request, {
|
|
174
180
|
requestId: incomingExecutionId,
|
|
@@ -240,14 +246,13 @@ async function handleProviderStyle(
|
|
|
240
246
|
options: RouterExecutionOptions & {
|
|
241
247
|
runtime: RouterRuntime;
|
|
242
248
|
ctx: RequestContext;
|
|
249
|
+
program?: Program;
|
|
243
250
|
},
|
|
244
251
|
mutateRequest?: (request: Program) => Program,
|
|
245
252
|
): Promise<Response> {
|
|
246
253
|
try {
|
|
247
|
-
const
|
|
248
|
-
const request = mutateRequest
|
|
249
|
-
? mutateRequest(parseProviderRequest(style, requestBody))
|
|
250
|
-
: parseProviderRequest(style, requestBody);
|
|
254
|
+
const baseRequest = options.program ?? await parseRequestProgram(style, options.ctx.request);
|
|
255
|
+
const request = mutateRequest ? mutateRequest(baseRequest) : baseRequest;
|
|
251
256
|
const incomingExecutionId = `incoming_${crypto.randomUUID()}`;
|
|
252
257
|
emitObservedProgram(options.observe, "request.received", request, {
|
|
253
258
|
requestId: incomingExecutionId,
|
|
@@ -312,6 +317,16 @@ export function resolveRequestTarget(
|
|
|
312
317
|
}
|
|
313
318
|
}
|
|
314
319
|
|
|
320
|
+
export async function parseRequestProgram(
|
|
321
|
+
style: ProviderStyle,
|
|
322
|
+
request: Request,
|
|
323
|
+
): Promise<Program> {
|
|
324
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
325
|
+
return style === "chat-completions"
|
|
326
|
+
? parseChatCompletionsRequest(body)
|
|
327
|
+
: parseProviderRequest(style, body);
|
|
328
|
+
}
|
|
329
|
+
|
|
315
330
|
export function createFetchProviderInvoker(
|
|
316
331
|
runtime: RouterRuntime,
|
|
317
332
|
ctx: RequestContext,
|
|
@@ -556,13 +571,13 @@ function prepareRemoteMcpExecution(
|
|
|
556
571
|
return {
|
|
557
572
|
target: {
|
|
558
573
|
kind: "executor",
|
|
559
|
-
|
|
574
|
+
executorId: executorName,
|
|
560
575
|
alias: resolved.requestedModel,
|
|
561
576
|
},
|
|
562
577
|
options: {
|
|
563
578
|
...options,
|
|
564
|
-
|
|
565
|
-
...(options.
|
|
579
|
+
executorImplementations: {
|
|
580
|
+
...(options.executorImplementations ?? {}),
|
|
566
581
|
[executorName]: withTools(remoteMcp.tools, resolved.target),
|
|
567
582
|
},
|
|
568
583
|
},
|
|
@@ -598,7 +613,7 @@ function routingHeaders(resolved: ResolvedTarget): Record<string, string> {
|
|
|
598
613
|
"x-openairouter-engine": "open-ai-router",
|
|
599
614
|
"x-openairouter-source": resolved.source,
|
|
600
615
|
"x-openairouter-namespace": resolved.namespace,
|
|
601
|
-
"x-openairouter-executor-id": resolved.target.
|
|
616
|
+
"x-openairouter-executor-id": resolved.target.executorId,
|
|
602
617
|
"x-openairouter-model-id": resolved.target.alias,
|
|
603
618
|
};
|
|
604
619
|
}
|
|
@@ -622,7 +637,7 @@ async function fetchProvider(
|
|
|
622
637
|
headers.set(key, value);
|
|
623
638
|
}
|
|
624
639
|
|
|
625
|
-
for (const driver of runtime.
|
|
640
|
+
for (const driver of runtime.upstreamAuthChain) {
|
|
626
641
|
const targetCtx: TargetAuthContext = { ...ctx, providerName: provider.name };
|
|
627
642
|
const auth = await driver.collectTarget(targetCtx);
|
|
628
643
|
if (!auth) {
|
|
@@ -819,7 +834,7 @@ function createUpstreamSignal(
|
|
|
819
834
|
}
|
|
820
835
|
|
|
821
836
|
function emitProviderUsage(
|
|
822
|
-
providerCtx:
|
|
837
|
+
providerCtx: ProviderInvocationContext,
|
|
823
838
|
response: Program | null,
|
|
824
839
|
provider: string,
|
|
825
840
|
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
|
+
});
|