@neutrome/open-ai-router 0.4.2 → 0.5.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/.dev.vars.example +2 -0
- package/README.md +5 -1
- package/package.json +5 -4
- package/src/admission/access.ts +0 -1
- package/src/admission/types.ts +0 -1
- package/src/app/factory.test.ts +40 -34
- package/src/app/factory.ts +120 -173
- package/src/app/google-genai-route.ts +48 -0
- package/src/app/http-utils.ts +14 -0
- package/src/app/model-listing.ts +29 -0
- package/src/app/response-lifecycle.ts +66 -0
- package/src/app/types.ts +38 -0
- package/src/index.ts +11 -43
- package/src/otlp.ts +148 -0
- package/src/router/audit.ts +5 -12
- package/src/router/config.ts +4 -4
- package/src/router/error-codec.ts +170 -0
- package/src/router/errors.ts +38 -1
- package/src/router/execute.test.ts +177 -107
- package/src/router/execute.ts +68 -924
- package/src/router/execution-events.ts +39 -0
- package/src/router/execution-invocation.ts +144 -0
- package/src/router/execution-observation.ts +161 -0
- package/src/router/execution-resolution.ts +120 -0
- package/src/router/execution-runtime.test.ts +156 -83
- package/src/router/execution-runtime.ts +144 -542
- package/src/router/execution-transforms.ts +82 -0
- package/src/router/execution-types.ts +8 -19
- package/src/router/index.ts +6 -5
- package/src/router/mcp.ts +12 -6
- package/src/router/provider-invoker.ts +134 -0
- package/src/router/provider-stream.ts +192 -0
- package/src/router/provider-telemetry.ts +59 -0
- package/src/router/remote-mcp-execution.ts +61 -0
- package/src/router/request-program.ts +16 -0
- package/src/router/request-target.ts +56 -0
- package/src/router/resolve.test.ts +43 -10
- package/src/router/resolve.ts +50 -28
- package/src/router/response-delivery.ts +141 -0
- package/src/router/runtime.test.ts +56 -0
- package/src/router/runtime.ts +79 -9
- package/src/router/targets.ts +3 -1
- package/src/router/upstream-client.ts +137 -0
- package/src/telemetry.test.ts +50 -0
- package/src/telemetry.ts +316 -0
- package/src/trace-labels.ts +29 -0
- package/src/upstream-auth/env.ts +15 -3
- package/src/upstream-auth/proxy.ts +6 -1
- package/src/upstream-auth/registry.ts +3 -1
- package/src/util/glob.ts +1 -1
- package/src/util/model-syntax.ts +3 -3
- package/src/worker.ts +12 -6
- package/worker-configuration.d.ts +195 -164
- package/pnpm-workspace.yaml +0 -4
- package/src/admission/jwt.test.ts +0 -96
- package/src/admission/jwt.ts +0 -221
- package/src/app/handlers.ts +0 -198
- package/src/example.test.ts +0 -377
- package/src/example.ts +0 -73
- package/src/observer.test.ts +0 -54
- package/src/observer.ts +0 -315
- package/src/timing.ts +0 -36
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { cloneProgram } from "@neutrome/lil-engine";
|
|
2
|
+
import type { Program } from "@neutrome/lil-engine";
|
|
3
|
+
import { createExecutionEvent } from "@neutrome/lilsdk-ts";
|
|
4
|
+
import type { ExecutorContext } from "@neutrome/lilsdk-ts";
|
|
5
|
+
import { ExecutionError } from "./errors.ts";
|
|
6
|
+
import { executionTargetAuditRef, executionTargetId } from "./targets.ts";
|
|
7
|
+
import type { ExecutionTarget, ProgramTransform } from "./execution-types.ts";
|
|
8
|
+
|
|
9
|
+
export async function applyExecutionTransforms(options: {
|
|
10
|
+
program: Program;
|
|
11
|
+
target: ExecutionTarget;
|
|
12
|
+
transforms: ReadonlyMap<string, ProgramTransform>;
|
|
13
|
+
context: ExecutorContext;
|
|
14
|
+
requestId: string;
|
|
15
|
+
executionId: string;
|
|
16
|
+
parentExecutionId: string | undefined;
|
|
17
|
+
depth: number;
|
|
18
|
+
maxDepth: number;
|
|
19
|
+
observe: ExecutorContext["observe"];
|
|
20
|
+
validate(program: Program): void;
|
|
21
|
+
}): Promise<Program> {
|
|
22
|
+
let current = cloneProgram(options.program);
|
|
23
|
+
for (const transformName of options.target.transforms ?? []) {
|
|
24
|
+
const transform = options.transforms.get(transformName);
|
|
25
|
+
if (!transform) {
|
|
26
|
+
throw new ExecutionError(
|
|
27
|
+
"transform",
|
|
28
|
+
`Unknown transform: ${transformName}`,
|
|
29
|
+
{
|
|
30
|
+
stage: "transform",
|
|
31
|
+
details: transformDetails(transformName, options),
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
current = await transform.apply(current, options.context);
|
|
37
|
+
} catch (cause) {
|
|
38
|
+
throw new ExecutionError(
|
|
39
|
+
"transform",
|
|
40
|
+
`Transform ${transform.name} failed`,
|
|
41
|
+
{
|
|
42
|
+
stage: "transform",
|
|
43
|
+
details: transformDetails(transform.name, options),
|
|
44
|
+
cause,
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
options.validate(current);
|
|
49
|
+
options.observe(
|
|
50
|
+
createExecutionEvent({
|
|
51
|
+
kind: "transform.applied",
|
|
52
|
+
requestId: options.requestId,
|
|
53
|
+
executionId: options.executionId,
|
|
54
|
+
target: executionTargetAuditRef(options.target),
|
|
55
|
+
data: transformDetails(transform.name, options),
|
|
56
|
+
...parent(options.parentExecutionId),
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return current;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function transformDetails(
|
|
64
|
+
transform: string,
|
|
65
|
+
options: Pick<
|
|
66
|
+
Parameters<typeof applyExecutionTransforms>[0],
|
|
67
|
+
"target" | "depth" | "maxDepth"
|
|
68
|
+
>,
|
|
69
|
+
): Record<string, string | number> {
|
|
70
|
+
return {
|
|
71
|
+
transform,
|
|
72
|
+
target: executionTargetId(options.target),
|
|
73
|
+
depth: options.depth,
|
|
74
|
+
maxDepth: options.maxDepth,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parent(parentExecutionId: string | undefined): {
|
|
79
|
+
parentExecutionId?: string;
|
|
80
|
+
} {
|
|
81
|
+
return parentExecutionId === undefined ? {} : { parentExecutionId };
|
|
82
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Program, ValidationIssue } from "@neutrome/lil-engine";
|
|
2
2
|
import type {
|
|
3
3
|
ExecutionTarget,
|
|
4
|
+
ExecutionEvent,
|
|
4
5
|
Executor,
|
|
5
6
|
InvokeOptions,
|
|
6
7
|
ProgramTransform,
|
|
@@ -11,33 +12,21 @@ import type {
|
|
|
11
12
|
ExecutionErrorStage,
|
|
12
13
|
} from "./errors.ts";
|
|
13
14
|
|
|
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
15
|
export type ProviderInvocationContext = {
|
|
30
16
|
requestId: string;
|
|
31
17
|
executionId: string;
|
|
32
18
|
parentExecutionId?: string;
|
|
33
19
|
target: Extract<ExecutionTarget, { kind: "provider" }>;
|
|
34
20
|
signal: AbortSignal;
|
|
35
|
-
observe(event:
|
|
21
|
+
observe(event: ExecutionEvent): void;
|
|
36
22
|
};
|
|
37
23
|
|
|
38
24
|
export type ProviderInvoker = {
|
|
39
25
|
execute(request: Program, ctx: ProviderInvocationContext): Promise<Program>;
|
|
40
|
-
stream(
|
|
26
|
+
stream(
|
|
27
|
+
request: Program,
|
|
28
|
+
ctx: ProviderInvocationContext,
|
|
29
|
+
): AsyncIterable<Program>;
|
|
41
30
|
};
|
|
42
31
|
|
|
43
32
|
export type ResolveTarget = (
|
|
@@ -55,7 +44,7 @@ export type ExecutionRuntimeOptions = {
|
|
|
55
44
|
transforms?: Readonly<Record<string, ProgramTransform>>;
|
|
56
45
|
providerInvoker?: ProviderInvoker;
|
|
57
46
|
resolveTarget?: ResolveTarget;
|
|
58
|
-
observe?: (event:
|
|
47
|
+
observe?: (event: ExecutionEvent) => void;
|
|
59
48
|
signal?: AbortSignal;
|
|
60
49
|
requestIdFactory?: () => string;
|
|
61
50
|
executionIdFactory?: () => string;
|
|
@@ -73,8 +62,8 @@ export type {
|
|
|
73
62
|
ExecutionErrorKind,
|
|
74
63
|
ExecutionErrorStage,
|
|
75
64
|
ExecutionTarget,
|
|
65
|
+
ExecutionEvent,
|
|
76
66
|
Executor,
|
|
77
67
|
InvokeOptions,
|
|
78
68
|
ProgramTransform,
|
|
79
|
-
ValidationIssue,
|
|
80
69
|
};
|
package/src/router/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { createInMemoryAuditSink } from "./audit.ts";
|
|
2
2
|
export { ExecutionError, isExecutionError } from "./errors.ts";
|
|
3
3
|
export { createExecutionRuntime } from "./execution-runtime.ts";
|
|
4
4
|
export { createRouterRuntime } from "./runtime.ts";
|
|
@@ -14,7 +14,11 @@ export {
|
|
|
14
14
|
resolveRequestTarget,
|
|
15
15
|
RouterError,
|
|
16
16
|
} from "./execute.ts";
|
|
17
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
listConfiguredModels,
|
|
19
|
+
resolveInvocationTarget,
|
|
20
|
+
resolveInvocationTargets,
|
|
21
|
+
} from "./resolve.ts";
|
|
18
22
|
export { executionTargetAuditRef, executionTargetId } from "./targets.ts";
|
|
19
23
|
export {
|
|
20
24
|
createRemoteMcpToolSet,
|
|
@@ -37,12 +41,9 @@ export type {
|
|
|
37
41
|
ModelNamespaceRuntime,
|
|
38
42
|
ModelRouteRuntime,
|
|
39
43
|
ProviderRuntime,
|
|
40
|
-
RemoteMcpServerRuntime,
|
|
41
44
|
RouterRuntime,
|
|
42
45
|
} from "./runtime.ts";
|
|
43
46
|
export type {
|
|
44
|
-
AuditEvent,
|
|
45
|
-
AuditEventInput,
|
|
46
47
|
ExecutionErrorDetails,
|
|
47
48
|
ExecutionErrorKind,
|
|
48
49
|
ExecutionErrorStage,
|
package/src/router/mcp.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
3
3
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
4
4
|
import type { ExecutorContext, Tool } from "@neutrome/lilsdk-ts";
|
|
5
5
|
import type { RemoteMcpServerRuntime, RouterRuntime } from "./runtime.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { observeTimedExecution } from "./execution-events.ts";
|
|
7
7
|
|
|
8
8
|
export type RemoteMcpToolCallClient = {
|
|
9
9
|
callTool(name: string, args: Record<string, unknown>): Promise<unknown>;
|
|
@@ -26,7 +26,9 @@ export function createRemoteMcpToolSet(
|
|
|
26
26
|
factory?: RemoteMcpClientFactory,
|
|
27
27
|
): RemoteMcpToolSet {
|
|
28
28
|
const pool = new RemoteMcpSessionPool(
|
|
29
|
-
factory ??
|
|
29
|
+
factory ??
|
|
30
|
+
((server, signal) =>
|
|
31
|
+
createStreamableHttpMcpClient(server, signal, runtime.fetchImpl)),
|
|
30
32
|
);
|
|
31
33
|
const tools: Tool[] = [];
|
|
32
34
|
const usedNames = new Map<string, number>();
|
|
@@ -43,12 +45,14 @@ export function createRemoteMcpToolSet(
|
|
|
43
45
|
|
|
44
46
|
tools.push({
|
|
45
47
|
name,
|
|
46
|
-
description:
|
|
48
|
+
description:
|
|
49
|
+
snapshot.description ||
|
|
50
|
+
`Remote MCP tool ${server.name}/${snapshot.name}`,
|
|
47
51
|
schema: normalizeSchema(snapshot.inputSchema),
|
|
48
52
|
async execute(args: Record<string, unknown>, ctx: ExecutorContext) {
|
|
49
53
|
const connectStartedMs = Date.now();
|
|
50
54
|
const client = await pool.get(server, ctx.signal);
|
|
51
|
-
|
|
55
|
+
observeTimedExecution({
|
|
52
56
|
observe: ctx.observe,
|
|
53
57
|
kind: "remote_mcp.connect",
|
|
54
58
|
requestId: ctx.requestId,
|
|
@@ -63,7 +67,7 @@ export function createRemoteMcpToolSet(
|
|
|
63
67
|
});
|
|
64
68
|
const callStartedMs = Date.now();
|
|
65
69
|
const result = await client.callTool(snapshot.name, args);
|
|
66
|
-
|
|
70
|
+
observeTimedExecution({
|
|
67
71
|
observe: ctx.observe,
|
|
68
72
|
kind: "remote_mcp.tool_call",
|
|
69
73
|
requestId: ctx.requestId,
|
|
@@ -126,7 +130,9 @@ export function stringifyMcpToolResult(result: unknown): string {
|
|
|
126
130
|
}
|
|
127
131
|
const body = chunks.filter(Boolean).join("\n");
|
|
128
132
|
if (result.isError === true) {
|
|
129
|
-
return body
|
|
133
|
+
return body
|
|
134
|
+
? `MCP tool returned an error:\n${body}`
|
|
135
|
+
: "MCP tool returned an error.";
|
|
130
136
|
}
|
|
131
137
|
return body || JSON.stringify(result);
|
|
132
138
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
emitProviderRequest,
|
|
3
|
+
isStreaming,
|
|
4
|
+
parseProviderResponse,
|
|
5
|
+
setModel,
|
|
6
|
+
} from "@neutrome/lil-engine";
|
|
7
|
+
import { toProviderError } from "./error-codec.ts";
|
|
8
|
+
import {
|
|
9
|
+
createUpstreamSignal,
|
|
10
|
+
fetchProvider,
|
|
11
|
+
getProviderOrThrow,
|
|
12
|
+
normalizeUpstreamAbort,
|
|
13
|
+
} from "./upstream-client.ts";
|
|
14
|
+
import { emitProviderUsage, injectStreamUsage } from "./provider-telemetry.ts";
|
|
15
|
+
import type { ProviderInvoker } from "./execution-types.ts";
|
|
16
|
+
import type { RequestContext } from "../upstream-auth/types.ts";
|
|
17
|
+
import { streamProvider } from "./provider-stream.ts";
|
|
18
|
+
import type { RouterRuntime } from "./runtime.ts";
|
|
19
|
+
import { observeTimedExecution } from "./execution-events.ts";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_UPSTREAM_TIMEOUT_MS = 120_000;
|
|
22
|
+
|
|
23
|
+
export function createFetchProviderInvoker(
|
|
24
|
+
runtime: RouterRuntime,
|
|
25
|
+
ctx: RequestContext,
|
|
26
|
+
upstreamTimeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS,
|
|
27
|
+
): ProviderInvoker {
|
|
28
|
+
return {
|
|
29
|
+
async execute(request, providerCtx) {
|
|
30
|
+
const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
|
|
31
|
+
const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const resolvedRequest = setModel(request, providerCtx.target.model);
|
|
35
|
+
const encodeStartedMs = Date.now();
|
|
36
|
+
let reqBody = emitProviderRequest(provider.style, resolvedRequest);
|
|
37
|
+
if (
|
|
38
|
+
provider.style === "chat-completions" &&
|
|
39
|
+
isStreaming(resolvedRequest)
|
|
40
|
+
) {
|
|
41
|
+
reqBody = injectStreamUsage(reqBody);
|
|
42
|
+
}
|
|
43
|
+
observeTimedExecution({
|
|
44
|
+
observe: providerCtx.observe,
|
|
45
|
+
kind: "provider.request_encoded",
|
|
46
|
+
requestId: providerCtx.requestId,
|
|
47
|
+
executionId: providerCtx.executionId,
|
|
48
|
+
...(providerCtx.parentExecutionId
|
|
49
|
+
? { parentExecutionId: providerCtx.parentExecutionId }
|
|
50
|
+
: {}),
|
|
51
|
+
startedAtMs: encodeStartedMs,
|
|
52
|
+
finishedAtMs: Date.now(),
|
|
53
|
+
data: {
|
|
54
|
+
provider: provider.name,
|
|
55
|
+
model: providerCtx.target.model,
|
|
56
|
+
bytes: reqBody.byteLength,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const reqBytes = reqBody.byteLength;
|
|
60
|
+
const fetchStartedMs = Date.now();
|
|
61
|
+
const upstream = await fetchProvider(
|
|
62
|
+
runtime,
|
|
63
|
+
ctx,
|
|
64
|
+
provider,
|
|
65
|
+
reqBody,
|
|
66
|
+
timed.signal,
|
|
67
|
+
providerCtx,
|
|
68
|
+
);
|
|
69
|
+
observeTimedExecution({
|
|
70
|
+
observe: providerCtx.observe,
|
|
71
|
+
kind: "provider.fetch_headers",
|
|
72
|
+
requestId: providerCtx.requestId,
|
|
73
|
+
executionId: providerCtx.executionId,
|
|
74
|
+
...(providerCtx.parentExecutionId
|
|
75
|
+
? { parentExecutionId: providerCtx.parentExecutionId }
|
|
76
|
+
: {}),
|
|
77
|
+
startedAtMs: fetchStartedMs,
|
|
78
|
+
finishedAtMs: Date.now(),
|
|
79
|
+
data: {
|
|
80
|
+
provider: provider.name,
|
|
81
|
+
model: providerCtx.target.model,
|
|
82
|
+
status: upstream.status,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (!upstream.ok) {
|
|
86
|
+
const providerError = await toProviderError(provider, upstream);
|
|
87
|
+
throw providerError;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const readStartedMs = Date.now();
|
|
91
|
+
const bytes = new Uint8Array(await upstream.arrayBuffer());
|
|
92
|
+
observeTimedExecution({
|
|
93
|
+
observe: providerCtx.observe,
|
|
94
|
+
kind: "provider.response_body_read",
|
|
95
|
+
requestId: providerCtx.requestId,
|
|
96
|
+
executionId: providerCtx.executionId,
|
|
97
|
+
...(providerCtx.parentExecutionId
|
|
98
|
+
? { parentExecutionId: providerCtx.parentExecutionId }
|
|
99
|
+
: {}),
|
|
100
|
+
startedAtMs: readStartedMs,
|
|
101
|
+
finishedAtMs: Date.now(),
|
|
102
|
+
data: {
|
|
103
|
+
provider: provider.name,
|
|
104
|
+
model: providerCtx.target.model,
|
|
105
|
+
bytes: bytes.byteLength,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const response = parseProviderResponse(provider.style, bytes);
|
|
109
|
+
emitProviderUsage(
|
|
110
|
+
providerCtx,
|
|
111
|
+
response,
|
|
112
|
+
provider.name,
|
|
113
|
+
reqBytes,
|
|
114
|
+
bytes.byteLength,
|
|
115
|
+
);
|
|
116
|
+
return response;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw normalizeUpstreamAbort(error, timed.signal);
|
|
119
|
+
} finally {
|
|
120
|
+
timed.cleanup();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
stream(request, providerCtx) {
|
|
125
|
+
return streamProvider(
|
|
126
|
+
runtime,
|
|
127
|
+
ctx,
|
|
128
|
+
upstreamTimeoutMs,
|
|
129
|
+
request,
|
|
130
|
+
providerCtx,
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
emitProviderRequest,
|
|
3
|
+
isStreaming,
|
|
4
|
+
parseProviderStreamChunk,
|
|
5
|
+
setModel,
|
|
6
|
+
type Program,
|
|
7
|
+
usageObject,
|
|
8
|
+
} from "@neutrome/lil-engine";
|
|
9
|
+
import { RouterError } from "./errors.ts";
|
|
10
|
+
import { toProviderError } from "./error-codec.ts";
|
|
11
|
+
import {
|
|
12
|
+
createUpstreamSignal,
|
|
13
|
+
fetchProvider,
|
|
14
|
+
getProviderOrThrow,
|
|
15
|
+
normalizeUpstreamAbort,
|
|
16
|
+
} from "./upstream-client.ts";
|
|
17
|
+
import { emitProviderUsage, injectStreamUsage } from "./provider-telemetry.ts";
|
|
18
|
+
import type { ProviderInvocationContext } from "./execution-types.ts";
|
|
19
|
+
import type { RequestContext } from "../upstream-auth/types.ts";
|
|
20
|
+
import { isSse } from "../util/headers.ts";
|
|
21
|
+
import { eventDataLines, splitSseEvents } from "../util/sse.ts";
|
|
22
|
+
import type { RouterRuntime } from "./runtime.ts";
|
|
23
|
+
import { observeTimedExecution } from "./execution-events.ts";
|
|
24
|
+
|
|
25
|
+
const encoder = new TextEncoder();
|
|
26
|
+
const decoder = new TextDecoder();
|
|
27
|
+
|
|
28
|
+
export async function* streamProvider(
|
|
29
|
+
runtime: RouterRuntime,
|
|
30
|
+
ctx: RequestContext,
|
|
31
|
+
upstreamTimeoutMs: number,
|
|
32
|
+
request: Program,
|
|
33
|
+
providerCtx: ProviderInvocationContext,
|
|
34
|
+
): AsyncGenerator<Program> {
|
|
35
|
+
const provider = getProviderOrThrow(runtime, providerCtx.target.provider);
|
|
36
|
+
const timed = createUpstreamSignal(providerCtx.signal, upstreamTimeoutMs);
|
|
37
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
38
|
+
let buffer = "";
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const resolvedRequest = setModel(request, providerCtx.target.model);
|
|
42
|
+
const encodeStartedMs = Date.now();
|
|
43
|
+
let reqBody = emitProviderRequest(provider.style, resolvedRequest);
|
|
44
|
+
if (provider.style === "chat-completions" && isStreaming(resolvedRequest)) {
|
|
45
|
+
reqBody = injectStreamUsage(reqBody);
|
|
46
|
+
}
|
|
47
|
+
observeTimedExecution({
|
|
48
|
+
observe: providerCtx.observe,
|
|
49
|
+
kind: "provider.request_encoded",
|
|
50
|
+
requestId: providerCtx.requestId,
|
|
51
|
+
executionId: providerCtx.executionId,
|
|
52
|
+
...(providerCtx.parentExecutionId
|
|
53
|
+
? { parentExecutionId: providerCtx.parentExecutionId }
|
|
54
|
+
: {}),
|
|
55
|
+
startedAtMs: encodeStartedMs,
|
|
56
|
+
finishedAtMs: Date.now(),
|
|
57
|
+
data: {
|
|
58
|
+
provider: provider.name,
|
|
59
|
+
model: providerCtx.target.model,
|
|
60
|
+
bytes: reqBody.byteLength,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const reqBytes = reqBody.byteLength;
|
|
64
|
+
const fetchStartedMs = Date.now();
|
|
65
|
+
const upstream = await fetchProvider(
|
|
66
|
+
runtime,
|
|
67
|
+
ctx,
|
|
68
|
+
provider,
|
|
69
|
+
reqBody,
|
|
70
|
+
timed.signal,
|
|
71
|
+
providerCtx,
|
|
72
|
+
);
|
|
73
|
+
observeTimedExecution({
|
|
74
|
+
observe: providerCtx.observe,
|
|
75
|
+
kind: "provider.fetch_headers",
|
|
76
|
+
requestId: providerCtx.requestId,
|
|
77
|
+
executionId: providerCtx.executionId,
|
|
78
|
+
...(providerCtx.parentExecutionId
|
|
79
|
+
? { parentExecutionId: providerCtx.parentExecutionId }
|
|
80
|
+
: {}),
|
|
81
|
+
startedAtMs: fetchStartedMs,
|
|
82
|
+
finishedAtMs: Date.now(),
|
|
83
|
+
data: {
|
|
84
|
+
provider: provider.name,
|
|
85
|
+
model: providerCtx.target.model,
|
|
86
|
+
status: upstream.status,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (!upstream.ok) {
|
|
90
|
+
const providerError = await toProviderError(provider, upstream);
|
|
91
|
+
throw providerError;
|
|
92
|
+
}
|
|
93
|
+
if (!isSse(upstream.headers) || !upstream.body) {
|
|
94
|
+
throw new RouterError(
|
|
95
|
+
`Provider ${provider.name} did not return an event stream`,
|
|
96
|
+
502,
|
|
97
|
+
"invalid_upstream_stream",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
reader = upstream.body.getReader();
|
|
102
|
+
let firstByteObserved = false;
|
|
103
|
+
const streamReadStartedMs = Date.now();
|
|
104
|
+
let lastChunkWithUsage: Program | null = null;
|
|
105
|
+
let respBytes = 0;
|
|
106
|
+
while (true) {
|
|
107
|
+
const { done, value } = await reader.read();
|
|
108
|
+
if (done) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (!firstByteObserved) {
|
|
112
|
+
firstByteObserved = true;
|
|
113
|
+
observeTimedExecution({
|
|
114
|
+
observe: providerCtx.observe,
|
|
115
|
+
kind: "provider.stream_first_byte",
|
|
116
|
+
requestId: providerCtx.requestId,
|
|
117
|
+
executionId: providerCtx.executionId,
|
|
118
|
+
...(providerCtx.parentExecutionId
|
|
119
|
+
? { parentExecutionId: providerCtx.parentExecutionId }
|
|
120
|
+
: {}),
|
|
121
|
+
startedAtMs: streamReadStartedMs,
|
|
122
|
+
finishedAtMs: Date.now(),
|
|
123
|
+
data: {
|
|
124
|
+
provider: provider.name,
|
|
125
|
+
model: providerCtx.target.model,
|
|
126
|
+
bytes: value.byteLength,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
respBytes += value.byteLength;
|
|
131
|
+
buffer += decoder.decode(value, { stream: true });
|
|
132
|
+
const split = splitSseEvents(buffer);
|
|
133
|
+
buffer = split.remainder;
|
|
134
|
+
for (const event of split.events) {
|
|
135
|
+
for (const data of eventDataLines(event)) {
|
|
136
|
+
if (data === "[DONE]") {
|
|
137
|
+
emitProviderUsage(
|
|
138
|
+
providerCtx,
|
|
139
|
+
lastChunkWithUsage,
|
|
140
|
+
provider.name,
|
|
141
|
+
reqBytes,
|
|
142
|
+
respBytes,
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (data[0] !== "{") continue;
|
|
147
|
+
const chunk = parseProviderStreamChunk(
|
|
148
|
+
provider.style,
|
|
149
|
+
encoder.encode(data),
|
|
150
|
+
);
|
|
151
|
+
if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
|
|
152
|
+
yield chunk;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (buffer.length > 0) {
|
|
158
|
+
for (const data of eventDataLines(buffer)) {
|
|
159
|
+
if (data === "[DONE]") {
|
|
160
|
+
emitProviderUsage(
|
|
161
|
+
providerCtx,
|
|
162
|
+
lastChunkWithUsage,
|
|
163
|
+
provider.name,
|
|
164
|
+
reqBytes,
|
|
165
|
+
respBytes,
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (data[0] !== "{") continue;
|
|
170
|
+
const chunk = parseProviderStreamChunk(
|
|
171
|
+
provider.style,
|
|
172
|
+
encoder.encode(data),
|
|
173
|
+
);
|
|
174
|
+
if (usageObject(chunk) !== undefined) lastChunkWithUsage = chunk;
|
|
175
|
+
yield chunk;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
emitProviderUsage(
|
|
179
|
+
providerCtx,
|
|
180
|
+
lastChunkWithUsage,
|
|
181
|
+
provider.name,
|
|
182
|
+
reqBytes,
|
|
183
|
+
respBytes,
|
|
184
|
+
);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw normalizeUpstreamAbort(error, timed.signal);
|
|
187
|
+
} finally {
|
|
188
|
+
await reader?.cancel().catch(() => {});
|
|
189
|
+
reader?.releaseLock();
|
|
190
|
+
timed.cleanup();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { usageObject, type Program } from "@neutrome/lil-engine";
|
|
2
|
+
import { createExecutionEvent } from "@neutrome/lilsdk-ts";
|
|
3
|
+
import type { ProviderInvocationContext } from "./execution-types.ts";
|
|
4
|
+
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
|
|
8
|
+
export function emitProviderUsage(
|
|
9
|
+
providerCtx: ProviderInvocationContext,
|
|
10
|
+
response: Program | null,
|
|
11
|
+
provider: string,
|
|
12
|
+
requestBytes: number,
|
|
13
|
+
responseBytes: number,
|
|
14
|
+
): void {
|
|
15
|
+
const usage = response ? usageObject(response) : undefined;
|
|
16
|
+
const u = (usage && typeof usage === "object" ? usage : {}) as Record<
|
|
17
|
+
string,
|
|
18
|
+
unknown
|
|
19
|
+
>;
|
|
20
|
+
const tokensInput = asNum(u.prompt_tokens) || asNum(u.input_tokens);
|
|
21
|
+
const tokensOutput = asNum(u.completion_tokens) || asNum(u.output_tokens);
|
|
22
|
+
const details = u.prompt_tokens_details as
|
|
23
|
+
| Record<string, unknown>
|
|
24
|
+
| undefined;
|
|
25
|
+
const cachedTokensInput = details ? asNum(details.cached_tokens) : 0;
|
|
26
|
+
providerCtx.observe(
|
|
27
|
+
createExecutionEvent({
|
|
28
|
+
kind: "provider.usage",
|
|
29
|
+
requestId: providerCtx.requestId,
|
|
30
|
+
executionId: providerCtx.executionId,
|
|
31
|
+
...(providerCtx.parentExecutionId
|
|
32
|
+
? { parentExecutionId: providerCtx.parentExecutionId }
|
|
33
|
+
: {}),
|
|
34
|
+
data: {
|
|
35
|
+
provider,
|
|
36
|
+
model: providerCtx.target.model,
|
|
37
|
+
tokensInput,
|
|
38
|
+
tokensOutput,
|
|
39
|
+
cachedTokensInput,
|
|
40
|
+
requestBytes,
|
|
41
|
+
responseBytes,
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asNum(v: unknown): number {
|
|
48
|
+
return typeof v === "number" && v >= 0 ? v : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function injectStreamUsage(body: Uint8Array): Uint8Array {
|
|
52
|
+
try {
|
|
53
|
+
const obj = JSON.parse(decoder.decode(body)) as Record<string, unknown>;
|
|
54
|
+
obj.stream_options = { include_usage: true };
|
|
55
|
+
return encoder.encode(JSON.stringify(obj));
|
|
56
|
+
} catch {
|
|
57
|
+
return body;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createTargetExecutor } from "@neutrome/lilsdk-ts/managed";
|
|
2
|
+
import { connectTools } from "@neutrome/lilsdk-ts/tools";
|
|
3
|
+
import type {
|
|
4
|
+
ExecutionTarget,
|
|
5
|
+
Executor,
|
|
6
|
+
ProgramTransform,
|
|
7
|
+
} from "./execution-types.ts";
|
|
8
|
+
import { createRemoteMcpToolSet, type RemoteMcpClientFactory } from "./mcp.ts";
|
|
9
|
+
import type { ResolvedTarget } from "./resolve.ts";
|
|
10
|
+
import type { RouterRuntime } from "./runtime.ts";
|
|
11
|
+
|
|
12
|
+
type RemoteMcpExecutionOptions = {
|
|
13
|
+
executorImplementations?: Readonly<Record<string, Executor>>;
|
|
14
|
+
transforms?: Readonly<Record<string, ProgramTransform>>;
|
|
15
|
+
upstreamTimeoutMs?: number;
|
|
16
|
+
remoteMcpClientFactory?: RemoteMcpClientFactory;
|
|
17
|
+
incomingExecutionId?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function prepareRemoteMcpExecution(
|
|
21
|
+
runtime: RouterRuntime,
|
|
22
|
+
resolved: ResolvedTarget,
|
|
23
|
+
options: RemoteMcpExecutionOptions,
|
|
24
|
+
): {
|
|
25
|
+
target: ExecutionTarget;
|
|
26
|
+
options: RemoteMcpExecutionOptions;
|
|
27
|
+
close: () => Promise<void>;
|
|
28
|
+
} {
|
|
29
|
+
if (resolved.mcps.length === 0) {
|
|
30
|
+
return { target: resolved.target, options, close: async () => {} };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const remoteMcp = createRemoteMcpToolSet(
|
|
34
|
+
runtime,
|
|
35
|
+
resolved.mcps,
|
|
36
|
+
options.remoteMcpClientFactory,
|
|
37
|
+
);
|
|
38
|
+
if (remoteMcp.tools.length === 0) {
|
|
39
|
+
return { target: resolved.target, options, close: remoteMcp.close };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const executorName = `__remote_mcp_${crypto.randomUUID()}`;
|
|
43
|
+
return {
|
|
44
|
+
target: {
|
|
45
|
+
kind: "executor",
|
|
46
|
+
executorId: executorName,
|
|
47
|
+
alias: resolved.requestedModel,
|
|
48
|
+
},
|
|
49
|
+
options: {
|
|
50
|
+
...options,
|
|
51
|
+
executorImplementations: {
|
|
52
|
+
...(options.executorImplementations ?? {}),
|
|
53
|
+
[executorName]: connectTools(
|
|
54
|
+
remoteMcp.tools,
|
|
55
|
+
createTargetExecutor(resolved.target),
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
close: remoteMcp.close,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseChatCompletionsRequest,
|
|
3
|
+
parseProviderRequest,
|
|
4
|
+
type Program,
|
|
5
|
+
type ProviderStyle,
|
|
6
|
+
} from "@neutrome/lil-engine";
|
|
7
|
+
|
|
8
|
+
export async function parseRequestProgram(
|
|
9
|
+
style: ProviderStyle,
|
|
10
|
+
request: Request,
|
|
11
|
+
): Promise<Program> {
|
|
12
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
13
|
+
return style === "chat-completions"
|
|
14
|
+
? parseChatCompletionsRequest(body)
|
|
15
|
+
: parseProviderRequest(style, body);
|
|
16
|
+
}
|