@neutrome/open-ai-router 0.1.18 → 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.
@@ -0,0 +1,632 @@
1
+ import { cloneProgram, validateProgram } from "@neutrome/lil-engine";
2
+ import type { Program, ValidationMode } from "@neutrome/lil-engine";
3
+ import type { ExecutorContext } from "@neutrome/lilsdk-ts";
4
+ import { createAuditEvent } from "./audit.ts";
5
+ import { ExecutionError, isExecutionError } from "./errors.ts";
6
+ import { executionTargetAuditRef, executionTargetId } from "./targets.ts";
7
+ import type {
8
+ AuditEvent,
9
+ ExecutionRuntime,
10
+ ExecutionRuntimeOptions,
11
+ ExecutionTarget,
12
+ Executor,
13
+ InvokeOptions,
14
+ ProgramTransform,
15
+ ProviderInvocationContext,
16
+ ProviderInvoker,
17
+ } from "./execution-types.ts";
18
+
19
+ const DEFAULT_MAX_DEPTH = 8;
20
+
21
+ export function createExecutionRuntime(options: ExecutionRuntimeOptions = {}): ExecutionRuntime {
22
+ let requestCounter = 0;
23
+ let executionCounter = 0;
24
+
25
+ const requestIdFactory = options.requestIdFactory ?? (() => `req_${++requestCounter}`);
26
+ const executionIdFactory = options.executionIdFactory ?? (() => `exec_${++executionCounter}`);
27
+ const observe = options.observe ?? (() => {});
28
+ const fallbackSignal = options.signal ?? new AbortController().signal;
29
+ const executorImplementations = new Map(Object.entries(options.executorImplementations ?? {}));
30
+ const transforms = new Map(Object.entries(options.transforms ?? {}));
31
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
32
+ const validatePrograms = options.validatePrograms ?? true;
33
+
34
+ return {
35
+ async execute(request, invokeOptions) {
36
+ return executeTarget(cloneProgram(request), invokeOptions, 0);
37
+ },
38
+ async *stream(request, invokeOptions) {
39
+ yield* streamTarget(cloneProgram(request), invokeOptions, 0);
40
+ },
41
+ };
42
+
43
+ async function executeTarget(
44
+ request: ReturnType<typeof cloneProgram>,
45
+ invokeOptions: InvokeOptions = {},
46
+ depth: number,
47
+ ) {
48
+ throwIfRecursionExceeded(depth);
49
+ const requestId = invokeOptions.requestId ?? requestIdFactory();
50
+ const executionId = invokeOptions.executionId ?? executionIdFactory();
51
+ const parentExecutionId = invokeOptions.parentExecutionId;
52
+ const signal = fallbackSignal;
53
+ const target = resolveTargetOrThrow(request, invokeOptions);
54
+
55
+ ensureActiveSignal(signal, "request", target, depth);
56
+ validateProgramOrThrow(request, {
57
+ stage: "request",
58
+ mode: "program",
59
+ requestId,
60
+ executionId,
61
+ parentExecutionId,
62
+ target,
63
+ depth,
64
+ emitSuccess: true,
65
+ });
66
+
67
+ const transformed = applyTransforms(
68
+ request,
69
+ target,
70
+ requestId,
71
+ executionId,
72
+ parentExecutionId,
73
+ signal,
74
+ depth,
75
+ );
76
+
77
+ if (target.kind === "executor") {
78
+ const executor = await getExecutorOrThrow(target.executorId);
79
+ observe(createAuditEvent({
80
+ kind: "executor.started",
81
+ requestId,
82
+ executionId,
83
+ target: executionTargetAuditRef(target),
84
+ data: {
85
+ executorId: target.executorId,
86
+ alias: target.alias,
87
+ transforms: target.transforms ?? [],
88
+ depth,
89
+ maxDepth,
90
+ },
91
+ ...withParentExecutionId(parentExecutionId),
92
+ }));
93
+
94
+ try {
95
+ const result = await executor.execute(
96
+ transformed,
97
+ createExecutionContext(requestId, executionId, parentExecutionId, signal, depth),
98
+ );
99
+ validateProgramOrThrow(result, {
100
+ stage: "executor_result",
101
+ mode: "program",
102
+ requestId,
103
+ executionId,
104
+ parentExecutionId,
105
+ target,
106
+ depth,
107
+ emitSuccess: true,
108
+ });
109
+ observe(createAuditEvent({
110
+ kind: "executor.finished",
111
+ requestId,
112
+ executionId,
113
+ target: executionTargetAuditRef(target),
114
+ ...withParentExecutionId(parentExecutionId),
115
+ }));
116
+ return result;
117
+ } catch (error) {
118
+ const normalized = normalizeExecutionError(error, "executor", "executor_invoke", target, depth);
119
+ observeExecutionError(normalized, requestId, executionId, parentExecutionId, target, depth);
120
+ throw normalized;
121
+ }
122
+ }
123
+
124
+ const providerInvoker = getProviderInvokerOrThrow();
125
+ const providerCtx = createProviderInvocationContext(
126
+ requestId,
127
+ executionId,
128
+ parentExecutionId,
129
+ target,
130
+ signal,
131
+ );
132
+
133
+ observe(createAuditEvent({
134
+ kind: "provider.started",
135
+ requestId,
136
+ executionId,
137
+ target: executionTargetAuditRef(target),
138
+ data: {
139
+ model: target.model,
140
+ provider: target.provider ?? "",
141
+ transforms: target.transforms ?? [],
142
+ depth,
143
+ maxDepth,
144
+ },
145
+ ...withParentExecutionId(parentExecutionId),
146
+ }));
147
+
148
+ try {
149
+ const result = await providerInvoker.execute(transformed, providerCtx);
150
+ validateProgramOrThrow(result, {
151
+ stage: "provider_result",
152
+ mode: "program",
153
+ requestId,
154
+ executionId,
155
+ parentExecutionId,
156
+ target,
157
+ depth,
158
+ emitSuccess: true,
159
+ });
160
+ observe(createAuditEvent({
161
+ kind: "provider.finished",
162
+ requestId,
163
+ executionId,
164
+ target: executionTargetAuditRef(target),
165
+ ...withParentExecutionId(parentExecutionId),
166
+ }));
167
+ return result;
168
+ } catch (error) {
169
+ const normalized = normalizeExecutionError(error, "provider", "provider_invoke", target, depth);
170
+ observeExecutionError(normalized, requestId, executionId, parentExecutionId, target, depth);
171
+ throw normalized;
172
+ }
173
+ }
174
+
175
+ async function* streamTarget(
176
+ request: ReturnType<typeof cloneProgram>,
177
+ invokeOptions: InvokeOptions = {},
178
+ depth: number,
179
+ ) {
180
+ throwIfRecursionExceeded(depth);
181
+ const requestId = invokeOptions.requestId ?? requestIdFactory();
182
+ const executionId = invokeOptions.executionId ?? executionIdFactory();
183
+ const parentExecutionId = invokeOptions.parentExecutionId;
184
+ const signal = fallbackSignal;
185
+ const target = resolveTargetOrThrow(request, invokeOptions);
186
+
187
+ ensureActiveSignal(signal, "request", target, depth);
188
+ validateProgramOrThrow(request, {
189
+ stage: "request",
190
+ mode: "program",
191
+ requestId,
192
+ executionId,
193
+ parentExecutionId,
194
+ target,
195
+ depth,
196
+ emitSuccess: true,
197
+ });
198
+
199
+ const transformed = applyTransforms(
200
+ request,
201
+ target,
202
+ requestId,
203
+ executionId,
204
+ parentExecutionId,
205
+ signal,
206
+ depth,
207
+ );
208
+
209
+ if (target.kind === "executor") {
210
+ const executor = await getExecutorOrThrow(target.executorId);
211
+ observe(createAuditEvent({
212
+ kind: "executor.stream_started",
213
+ requestId,
214
+ executionId,
215
+ target: executionTargetAuditRef(target),
216
+ data: {
217
+ executorId: target.executorId,
218
+ alias: target.alias,
219
+ transforms: target.transforms ?? [],
220
+ depth,
221
+ maxDepth,
222
+ },
223
+ ...withParentExecutionId(parentExecutionId),
224
+ }));
225
+
226
+ try {
227
+ for await (const chunk of executor.stream(
228
+ transformed,
229
+ createExecutionContext(requestId, executionId, parentExecutionId, signal, depth),
230
+ )) {
231
+ validateProgramOrThrow(chunk, {
232
+ stage: "stream_chunk",
233
+ mode: "stream_chunk",
234
+ requestId,
235
+ executionId,
236
+ parentExecutionId,
237
+ target,
238
+ depth,
239
+ emitSuccess: false,
240
+ });
241
+ yield chunk;
242
+ }
243
+ observe(createAuditEvent({
244
+ kind: "executor.stream_finished",
245
+ requestId,
246
+ executionId,
247
+ target: executionTargetAuditRef(target),
248
+ ...withParentExecutionId(parentExecutionId),
249
+ }));
250
+ return;
251
+ } catch (error) {
252
+ const normalized = normalizeExecutionError(error, "stream", "stream_chunk", target, depth);
253
+ observeExecutionError(normalized, requestId, executionId, parentExecutionId, target, depth);
254
+ throw normalized;
255
+ }
256
+ }
257
+
258
+ const providerInvoker = getProviderInvokerOrThrow();
259
+ const providerCtx = createProviderInvocationContext(
260
+ requestId,
261
+ executionId,
262
+ parentExecutionId,
263
+ target,
264
+ signal,
265
+ );
266
+
267
+ observe(createAuditEvent({
268
+ kind: "provider.stream_started",
269
+ requestId,
270
+ executionId,
271
+ target: executionTargetAuditRef(target),
272
+ data: {
273
+ model: target.model,
274
+ provider: target.provider ?? "",
275
+ transforms: target.transforms ?? [],
276
+ depth,
277
+ maxDepth,
278
+ },
279
+ ...withParentExecutionId(parentExecutionId),
280
+ }));
281
+
282
+ try {
283
+ for await (const chunk of providerInvoker.stream(transformed, providerCtx)) {
284
+ validateProgramOrThrow(chunk, {
285
+ stage: "stream_chunk",
286
+ mode: "stream_chunk",
287
+ requestId,
288
+ executionId,
289
+ parentExecutionId,
290
+ target,
291
+ depth,
292
+ emitSuccess: false,
293
+ });
294
+ yield chunk;
295
+ }
296
+ observe(createAuditEvent({
297
+ kind: "provider.stream_finished",
298
+ requestId,
299
+ executionId,
300
+ target: executionTargetAuditRef(target),
301
+ ...withParentExecutionId(parentExecutionId),
302
+ }));
303
+ } catch (error) {
304
+ const normalized = normalizeExecutionError(error, "stream", "stream_chunk", target, depth);
305
+ observeExecutionError(normalized, requestId, executionId, parentExecutionId, target, depth);
306
+ throw normalized;
307
+ }
308
+ }
309
+
310
+ function createExecutionContext(
311
+ requestId: string,
312
+ executionId: string,
313
+ parentExecutionId: string | undefined,
314
+ signal: AbortSignal,
315
+ depth: number,
316
+ ): ExecutorContext {
317
+ return {
318
+ requestId,
319
+ executionId,
320
+ ...(parentExecutionId ? { parentExecutionId } : {}),
321
+ async invoke(request, invokeOptions = {}) {
322
+ return executeTarget(cloneProgram(request), {
323
+ ...invokeOptions,
324
+ requestId,
325
+ parentExecutionId: executionId,
326
+ }, depth + 1);
327
+ },
328
+ async *invokeStream(request, invokeOptions = {}) {
329
+ yield* streamTarget(cloneProgram(request), {
330
+ ...invokeOptions,
331
+ requestId,
332
+ parentExecutionId: executionId,
333
+ }, depth + 1);
334
+ },
335
+ observe,
336
+ signal,
337
+ };
338
+ }
339
+
340
+ function applyTransforms(
341
+ program: ReturnType<typeof cloneProgram>,
342
+ target: ExecutionTarget,
343
+ requestId: string,
344
+ executionId: string,
345
+ parentExecutionId: string | undefined,
346
+ signal: AbortSignal,
347
+ depth: number,
348
+ ) {
349
+ let current = cloneProgram(program);
350
+ for (const transformName of target.transforms ?? []) {
351
+ const transform = transforms.get(transformName);
352
+ if (!transform) {
353
+ throw new ExecutionError(
354
+ "transform",
355
+ `Unknown transform: ${transformName}`,
356
+ {
357
+ stage: "transform",
358
+ details: { transform: transformName, target: executionTargetId(target), depth, maxDepth },
359
+ },
360
+ );
361
+ }
362
+ try {
363
+ current = transform.apply(current, { observe, signal });
364
+ } catch (error) {
365
+ throw new ExecutionError(
366
+ "transform",
367
+ `Transform ${transform.name} failed`,
368
+ {
369
+ stage: "transform",
370
+ details: { transform: transform.name, target: executionTargetId(target), depth, maxDepth },
371
+ cause: error,
372
+ },
373
+ );
374
+ }
375
+ validateProgramOrThrow(current, {
376
+ stage: "transform_output",
377
+ mode: "program",
378
+ requestId,
379
+ executionId,
380
+ parentExecutionId,
381
+ target,
382
+ depth,
383
+ emitSuccess: true,
384
+ });
385
+ observe(createAuditEvent({
386
+ kind: "transform.applied",
387
+ requestId,
388
+ executionId,
389
+ target: executionTargetAuditRef(target),
390
+ data: {
391
+ transform: transform.name,
392
+ target: executionTargetId(target),
393
+ depth,
394
+ maxDepth,
395
+ },
396
+ ...withParentExecutionId(parentExecutionId),
397
+ }));
398
+ }
399
+ return current;
400
+ }
401
+
402
+ function resolveTargetOrThrow(
403
+ request: ReturnType<typeof cloneProgram>,
404
+ invokeOptions: InvokeOptions,
405
+ ): ExecutionTarget {
406
+ if (invokeOptions.target) {
407
+ return invokeOptions.target;
408
+ }
409
+ if (options.resolveTarget) {
410
+ try {
411
+ return options.resolveTarget(request, invokeOptions);
412
+ } catch (error) {
413
+ throw new ExecutionError(
414
+ "resolution",
415
+ error instanceof Error ? error.message : "Execution target resolution failed",
416
+ {
417
+ stage: "target_resolution",
418
+ cause: error,
419
+ },
420
+ );
421
+ }
422
+ }
423
+ throw new ExecutionError("resolution", "No execution target specified", {
424
+ stage: "target_resolution",
425
+ });
426
+ }
427
+
428
+ async function getExecutorOrThrow(name: string): Promise<Executor> {
429
+ const cached = executorImplementations.get(name);
430
+ if (cached) {
431
+ return cached;
432
+ }
433
+
434
+ const resolved = await options.resolveExecutor?.(name);
435
+ if (resolved) {
436
+ executorImplementations.set(name, resolved);
437
+ return resolved;
438
+ }
439
+
440
+ const executor = executorImplementations.get(name);
441
+ if (!executor) {
442
+ throw new ExecutionError("resolution", `Unknown executor: ${name}`, {
443
+ stage: "target_resolution",
444
+ });
445
+ }
446
+ return executor;
447
+ }
448
+
449
+ function getProviderInvokerOrThrow(): ProviderInvoker {
450
+ if (!options.providerInvoker) {
451
+ throw new ExecutionError("provider", "No provider invoker configured", {
452
+ stage: "provider_invoke",
453
+ });
454
+ }
455
+ return options.providerInvoker;
456
+ }
457
+
458
+ function observeExecutionError(
459
+ error: ExecutionError,
460
+ requestId: string,
461
+ executionId: string,
462
+ parentExecutionId: string | undefined,
463
+ target: ExecutionTarget,
464
+ depth: number,
465
+ ): void {
466
+ observe(createAuditEvent({
467
+ kind: "execution.failed",
468
+ requestId,
469
+ executionId,
470
+ target: executionTargetAuditRef(target),
471
+ errorKind: error.kind,
472
+ data: {
473
+ message: error.message,
474
+ stage: error.stage,
475
+ depth,
476
+ maxDepth,
477
+ ...(error.details ?? {}),
478
+ },
479
+ ...withParentExecutionId(parentExecutionId),
480
+ }));
481
+ }
482
+
483
+ function createProviderInvocationContext(
484
+ requestId: string,
485
+ executionId: string,
486
+ parentExecutionId: string | undefined,
487
+ target: Extract<ExecutionTarget, { kind: "provider" }>,
488
+ signal: AbortSignal,
489
+ ): ProviderInvocationContext {
490
+ return {
491
+ requestId,
492
+ executionId,
493
+ target,
494
+ signal,
495
+ observe,
496
+ ...withParentExecutionId(parentExecutionId),
497
+ };
498
+ }
499
+
500
+ function withParentExecutionId(parentExecutionId: string | undefined): {
501
+ parentExecutionId?: string;
502
+ } {
503
+ if (parentExecutionId === undefined) {
504
+ return {};
505
+ }
506
+ return { parentExecutionId };
507
+ }
508
+
509
+ function throwIfRecursionExceeded(depth: number): void {
510
+ if (depth <= maxDepth) {
511
+ return;
512
+ }
513
+ throw new ExecutionError(
514
+ "recursion_limit",
515
+ `Execution recursion depth ${depth} exceeds maxDepth ${maxDepth}`,
516
+ {
517
+ stage: "target_resolution",
518
+ details: { depth, maxDepth },
519
+ },
520
+ );
521
+ }
522
+
523
+ function validateProgramOrThrow(
524
+ program: Program,
525
+ context: {
526
+ stage: "request" | "transform_output" | "provider_result" | "executor_result" | "stream_chunk";
527
+ mode: ValidationMode;
528
+ requestId: string;
529
+ executionId: string;
530
+ parentExecutionId: string | undefined;
531
+ target: ExecutionTarget;
532
+ depth: number;
533
+ emitSuccess: boolean;
534
+ },
535
+ ): void {
536
+ if (!validatePrograms) {
537
+ return;
538
+ }
539
+
540
+ const result = validateProgram(program, { mode: context.mode });
541
+ if (!result.ok) {
542
+ throw new ExecutionError(
543
+ "validation",
544
+ `Program validation failed during ${context.stage}`,
545
+ {
546
+ stage: context.stage,
547
+ details: {
548
+ issues: result.issues,
549
+ depth: context.depth,
550
+ maxDepth,
551
+ target: executionTargetId(context.target),
552
+ },
553
+ },
554
+ );
555
+ }
556
+
557
+ if (context.emitSuccess) {
558
+ observe(createAuditEvent({
559
+ kind: "program.validated",
560
+ requestId: context.requestId,
561
+ executionId: context.executionId,
562
+ target: executionTargetAuditRef(context.target),
563
+ data: {
564
+ stage: context.stage,
565
+ mode: context.mode,
566
+ issues: 0,
567
+ depth: context.depth,
568
+ maxDepth,
569
+ },
570
+ ...withParentExecutionId(context.parentExecutionId),
571
+ }));
572
+ }
573
+ }
574
+
575
+ function ensureActiveSignal(
576
+ signal: AbortSignal,
577
+ stage: "request",
578
+ target: ExecutionTarget,
579
+ depth: number,
580
+ ): void {
581
+ if (!signal.aborted) {
582
+ return;
583
+ }
584
+ throw new ExecutionError(
585
+ "cancellation",
586
+ "Execution aborted",
587
+ {
588
+ stage,
589
+ details: { depth, maxDepth, target: executionTargetId(target) },
590
+ cause: signal.reason,
591
+ },
592
+ );
593
+ }
594
+
595
+ function normalizeExecutionError(
596
+ error: unknown,
597
+ fallbackKind: "provider" | "executor" | "stream",
598
+ fallbackStage: "provider_invoke" | "executor_invoke" | "stream_chunk",
599
+ target: ExecutionTarget,
600
+ depth: number,
601
+ ): ExecutionError {
602
+ if (isExecutionError(error)) {
603
+ return error;
604
+ }
605
+ if (isAbortLike(error)) {
606
+ return new ExecutionError(
607
+ "cancellation",
608
+ "Execution aborted",
609
+ {
610
+ stage: fallbackStage,
611
+ details: { depth, maxDepth, target: executionTargetId(target) },
612
+ cause: error,
613
+ },
614
+ );
615
+ }
616
+ return new ExecutionError(
617
+ fallbackKind,
618
+ error instanceof Error ? error.message : String(error),
619
+ {
620
+ stage: fallbackStage,
621
+ details: { depth, maxDepth, target: executionTargetId(target) },
622
+ cause: error,
623
+ },
624
+ );
625
+ }
626
+
627
+ function isAbortLike(error: unknown): boolean {
628
+ return error instanceof DOMException
629
+ ? error.name === "AbortError"
630
+ : error instanceof Error && error.name === "AbortError";
631
+ }
632
+ }
@@ -0,0 +1,80 @@
1
+ import type { Program, ValidationIssue } from "@neutrome/lil-engine";
2
+ import type {
3
+ ExecutionTarget,
4
+ Executor,
5
+ InvokeOptions,
6
+ ProgramTransform,
7
+ } from "@neutrome/lilsdk-ts";
8
+ import type {
9
+ ExecutionErrorDetails,
10
+ ExecutionErrorKind,
11
+ ExecutionErrorStage,
12
+ } from "./errors.ts";
13
+
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
+ export type ProviderInvocationContext = {
30
+ requestId: string;
31
+ executionId: string;
32
+ parentExecutionId?: string;
33
+ target: Extract<ExecutionTarget, { kind: "provider" }>;
34
+ signal: AbortSignal;
35
+ observe(event: AuditEvent): void;
36
+ };
37
+
38
+ export type ProviderInvoker = {
39
+ execute(request: Program, ctx: ProviderInvocationContext): Promise<Program>;
40
+ stream(request: Program, ctx: ProviderInvocationContext): AsyncIterable<Program>;
41
+ };
42
+
43
+ export type ResolveTarget = (
44
+ request: Program,
45
+ options?: InvokeOptions,
46
+ ) => ExecutionTarget;
47
+
48
+ export type ResolveExecutor = (
49
+ name: string,
50
+ ) => Executor | Promise<Executor | null> | null;
51
+
52
+ export type ExecutionRuntimeOptions = {
53
+ executorImplementations?: Readonly<Record<string, Executor>>;
54
+ resolveExecutor?: ResolveExecutor;
55
+ transforms?: Readonly<Record<string, ProgramTransform>>;
56
+ providerInvoker?: ProviderInvoker;
57
+ resolveTarget?: ResolveTarget;
58
+ observe?: (event: AuditEvent) => void;
59
+ signal?: AbortSignal;
60
+ requestIdFactory?: () => string;
61
+ executionIdFactory?: () => string;
62
+ maxDepth?: number;
63
+ validatePrograms?: boolean;
64
+ };
65
+
66
+ export type ExecutionRuntime = {
67
+ execute(request: Program, options?: InvokeOptions): Promise<Program>;
68
+ stream(request: Program, options?: InvokeOptions): AsyncIterable<Program>;
69
+ };
70
+
71
+ export type {
72
+ ExecutionErrorDetails,
73
+ ExecutionErrorKind,
74
+ ExecutionErrorStage,
75
+ ExecutionTarget,
76
+ Executor,
77
+ InvokeOptions,
78
+ ProgramTransform,
79
+ ValidationIssue,
80
+ };