@neutrome/open-ai-router 0.8.1 → 0.9.2
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/package.json +2 -2
- package/src/app/factory.test.ts +8 -2
- package/src/app/factory.ts +3 -0
- package/src/app/user-usage-collector.ts +35 -5
- package/src/router/execute.ts +27 -1
- package/src/router/execution-invocation.ts +42 -0
- package/src/router/execution-runtime.test.ts +61 -0
- package/src/router/execution-runtime.ts +38 -2
- package/src/router/execution-types.ts +18 -0
- package/src/usage/user-usage.ts +15 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neutrome/open-ai-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"posthog-node": "^5.41.0",
|
|
18
18
|
"zod": "^4.2.1",
|
|
19
19
|
"@neutrome/lil-engine": "0.6.1",
|
|
20
|
-
"@neutrome/lilsdk": "0.6.
|
|
20
|
+
"@neutrome/lilsdk": "0.6.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^25.9.3",
|
package/src/app/factory.test.ts
CHANGED
|
@@ -150,8 +150,14 @@ describe("createRouterApp trace finalization", () => {
|
|
|
150
150
|
expect.objectContaining({
|
|
151
151
|
requestedModel: "gpt-4o",
|
|
152
152
|
outcome: "ok",
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
invocations: expect.arrayContaining([
|
|
154
|
+
expect.objectContaining({
|
|
155
|
+
kind: "incoming",
|
|
156
|
+
model: "gpt-4o",
|
|
157
|
+
inputTokens: expect.any(Number),
|
|
158
|
+
outputTokens: expect.any(Number),
|
|
159
|
+
}),
|
|
160
|
+
]),
|
|
155
161
|
}),
|
|
156
162
|
{},
|
|
157
163
|
);
|
package/src/app/factory.ts
CHANGED
|
@@ -297,6 +297,9 @@ export function createRouterApp(options: RouterAppOptions) {
|
|
|
297
297
|
onUserResponse(result) {
|
|
298
298
|
usage.setResult(result);
|
|
299
299
|
},
|
|
300
|
+
onNamedInvocation(result) {
|
|
301
|
+
usage.recordInvocation(result);
|
|
302
|
+
},
|
|
300
303
|
...(options.upstreamTimeoutMs !== undefined
|
|
301
304
|
? { upstreamTimeoutMs: options.upstreamTimeoutMs }
|
|
302
305
|
: {}),
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { Program, ProviderStyle } from "@neutrome/lil-engine";
|
|
2
2
|
import type { ExecutionSummary } from "../telemetry/index.ts";
|
|
3
|
+
import type { NamedInvocationResult } from "../router/execution-types.ts";
|
|
3
4
|
import {
|
|
4
5
|
countUserProgramTokens,
|
|
6
|
+
createStreamUsageAccumulator,
|
|
5
7
|
toolUsageFromEvents,
|
|
6
8
|
type UserUsageReport,
|
|
7
9
|
} from "../usage/user-usage.ts";
|
|
8
10
|
|
|
9
11
|
export type UserResponseResult = Pick<
|
|
10
12
|
UserUsageReport,
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
+
"outcome" | "httpStatus" | "error"
|
|
14
|
+
> & { outputTokens: number };
|
|
13
15
|
|
|
14
16
|
export function createUserUsageCollector(input: {
|
|
15
17
|
requestId: string;
|
|
@@ -19,6 +21,7 @@ export function createUserUsageCollector(input: {
|
|
|
19
21
|
let inputTokens = 0;
|
|
20
22
|
let requestedModel: string | null = null;
|
|
21
23
|
let result: UserResponseResult | null = null;
|
|
24
|
+
const invocations: UserUsageReport["invocations"] = [];
|
|
22
25
|
return {
|
|
23
26
|
setRequest(program: Program, model: string | null) {
|
|
24
27
|
inputTokens = countUserProgramTokens(program);
|
|
@@ -30,15 +33,42 @@ export function createUserUsageCollector(input: {
|
|
|
30
33
|
inputTokens() {
|
|
31
34
|
return inputTokens;
|
|
32
35
|
},
|
|
36
|
+
recordInvocation(invocation: NamedInvocationResult) {
|
|
37
|
+
const accumulator = createStreamUsageAccumulator();
|
|
38
|
+
for (const chunk of invocation.stream ?? []) accumulator.add(chunk);
|
|
39
|
+
invocations.push({
|
|
40
|
+
executionId: invocation.executionId,
|
|
41
|
+
parentExecutionId: invocation.parentExecutionId ?? null,
|
|
42
|
+
kind: invocation.kind,
|
|
43
|
+
model: invocation.model,
|
|
44
|
+
inputTokens: countUserProgramTokens(invocation.input),
|
|
45
|
+
outputTokens: invocation.stream
|
|
46
|
+
? accumulator.count()
|
|
47
|
+
: invocation.output
|
|
48
|
+
? countUserProgramTokens(invocation.output)
|
|
49
|
+
: 0,
|
|
50
|
+
startedAt: new Date(invocation.startedAtMs).toISOString(),
|
|
51
|
+
finishedAt: new Date(invocation.finishedAtMs).toISOString(),
|
|
52
|
+
durationMs: Math.max(
|
|
53
|
+
0,
|
|
54
|
+
invocation.finishedAtMs - invocation.startedAtMs,
|
|
55
|
+
),
|
|
56
|
+
outcome: invocation.outcome,
|
|
57
|
+
error: invocation.error ?? null,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
33
60
|
report(summary: ExecutionSummary): UserUsageReport {
|
|
34
61
|
const finishedAt = new Date().toISOString();
|
|
62
|
+
const response =
|
|
63
|
+
result ?? responseResult(null, "Request did not produce a response");
|
|
35
64
|
return {
|
|
36
65
|
requestId: input.requestId,
|
|
37
66
|
protocol: input.protocol,
|
|
38
67
|
requestedModel,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
invocations: [...invocations],
|
|
69
|
+
outcome: response.outcome,
|
|
70
|
+
httpStatus: response.httpStatus,
|
|
71
|
+
error: response.error,
|
|
42
72
|
tools: toolUsageFromEvents(
|
|
43
73
|
summary.spans
|
|
44
74
|
.filter((span) => span.kind === "tool" && span.toolName)
|
package/src/router/execute.ts
CHANGED
|
@@ -82,6 +82,7 @@ export type RouterExecutionOptions = Pick<
|
|
|
82
82
|
remoteMcpClientFactory?: RemoteMcpClientFactory;
|
|
83
83
|
incomingExecutionId?: string;
|
|
84
84
|
userUsage?: { inputTokens: number };
|
|
85
|
+
onNamedInvocation?: ExecutionRuntimeOptions["onNamedInvocation"];
|
|
85
86
|
onUserResponse?: (result: {
|
|
86
87
|
outputTokens: number;
|
|
87
88
|
outcome: "ok" | "error" | "cancelled";
|
|
@@ -156,6 +157,18 @@ export function createRouterExecutionRuntime(
|
|
|
156
157
|
if (options.executionIdFactory) {
|
|
157
158
|
runtimeOptions.executionIdFactory = options.executionIdFactory;
|
|
158
159
|
}
|
|
160
|
+
runtimeOptions.isBillableModel = (model) => {
|
|
161
|
+
try {
|
|
162
|
+
return runtime.modelNamespaces.has(
|
|
163
|
+
resolveNestedInvocationTarget(runtime, model).namespace,
|
|
164
|
+
);
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (options.onNamedInvocation) {
|
|
170
|
+
runtimeOptions.onNamedInvocation = options.onNamedInvocation;
|
|
171
|
+
}
|
|
159
172
|
|
|
160
173
|
const userResolveExecutor = options.resolveExecutor;
|
|
161
174
|
runtimeOptions.resolveExecutor = (name: string) => {
|
|
@@ -239,7 +252,15 @@ async function handleRequestStyle(
|
|
|
239
252
|
if (isStreaming(request)) {
|
|
240
253
|
try {
|
|
241
254
|
const responseStartedMs = Date.now();
|
|
242
|
-
const
|
|
255
|
+
const rootBillable = options.runtime.modelNamespaces.has(
|
|
256
|
+
resolved.namespace,
|
|
257
|
+
);
|
|
258
|
+
const chunks = execution.stream(request, {
|
|
259
|
+
target: remoteMcp.target,
|
|
260
|
+
requestId: incomingExecutionId,
|
|
261
|
+
executionId: incomingExecutionId,
|
|
262
|
+
...(rootBillable ? { namedModel: resolved.requestedModel } : {}),
|
|
263
|
+
});
|
|
243
264
|
return await streamExecutionResponse(
|
|
244
265
|
style,
|
|
245
266
|
chunks,
|
|
@@ -266,6 +287,11 @@ async function handleRequestStyle(
|
|
|
266
287
|
try {
|
|
267
288
|
const response = await execution.execute(request, {
|
|
268
289
|
target: remoteMcp.target,
|
|
290
|
+
requestId: incomingExecutionId,
|
|
291
|
+
executionId: incomingExecutionId,
|
|
292
|
+
...(options.runtime.modelNamespaces.has(resolved.namespace)
|
|
293
|
+
? { namedModel: resolved.requestedModel }
|
|
294
|
+
: {}),
|
|
269
295
|
});
|
|
270
296
|
const userTime = Date.now() - startTime;
|
|
271
297
|
const outputTokens = countUserProgramTokens(response);
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ProviderInvocationContext,
|
|
14
14
|
ProviderInvoker,
|
|
15
15
|
} from "./execution-types.ts";
|
|
16
|
+
import type { NamedInvocationResult } from "./execution-types.ts";
|
|
16
17
|
|
|
17
18
|
type InvocationIdentity = {
|
|
18
19
|
requestId: string;
|
|
@@ -39,6 +40,11 @@ export type InvocationOptions = {
|
|
|
39
40
|
stage: "provider_result" | "executor_result" | "stream_chunk",
|
|
40
41
|
mode: ValidationMode,
|
|
41
42
|
): void;
|
|
43
|
+
namedInvocation?: Omit<
|
|
44
|
+
NamedInvocationResult,
|
|
45
|
+
"output" | "stream" | "finishedAtMs" | "outcome" | "error"
|
|
46
|
+
>;
|
|
47
|
+
onNamedInvocation?(result: NamedInvocationResult): void;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
export async function invokeExecution(
|
|
@@ -55,8 +61,10 @@ export async function invokeExecution(
|
|
|
55
61
|
const result = await primitive.execute(options.program);
|
|
56
62
|
options.validate(result, primitive.resultStage, "program");
|
|
57
63
|
finishInvocation(options.identity, timing, false, options.observe);
|
|
64
|
+
finishNamed(options, { output: result, outcome: "ok" });
|
|
58
65
|
return result;
|
|
59
66
|
} catch (error) {
|
|
67
|
+
finishNamed(options, failureResult(error));
|
|
60
68
|
throw observedFailure(
|
|
61
69
|
options,
|
|
62
70
|
error,
|
|
@@ -76,17 +84,51 @@ export async function* streamExecution(
|
|
|
76
84
|
true,
|
|
77
85
|
options.observe,
|
|
78
86
|
);
|
|
87
|
+
const chunks: Program[] = [];
|
|
79
88
|
try {
|
|
80
89
|
for await (const chunk of primitive.stream(options.program)) {
|
|
81
90
|
options.validate(chunk, "stream_chunk", "stream_chunk");
|
|
91
|
+
if (options.namedInvocation) chunks.push(chunk);
|
|
82
92
|
yield chunk;
|
|
83
93
|
}
|
|
84
94
|
finishInvocation(options.identity, timing, true, options.observe);
|
|
95
|
+
finishNamed(options, { stream: chunks, outcome: "ok" });
|
|
85
96
|
} catch (error) {
|
|
97
|
+
finishNamed(options, { ...failureResult(error), stream: chunks });
|
|
86
98
|
throw observedFailure(options, error, "stream", "stream_chunk");
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
101
|
|
|
102
|
+
function finishNamed(
|
|
103
|
+
options: InvocationOptions,
|
|
104
|
+
result: Pick<NamedInvocationResult, "outcome"> &
|
|
105
|
+
Partial<Pick<NamedInvocationResult, "output" | "stream" | "error">>,
|
|
106
|
+
): void {
|
|
107
|
+
if (!options.namedInvocation || !options.onNamedInvocation) return;
|
|
108
|
+
options.onNamedInvocation({
|
|
109
|
+
...options.namedInvocation,
|
|
110
|
+
...result,
|
|
111
|
+
finishedAtMs: Date.now(),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function failureResult(
|
|
116
|
+
error: unknown,
|
|
117
|
+
): Pick<NamedInvocationResult, "outcome" | "error"> {
|
|
118
|
+
const cancelled =
|
|
119
|
+
error instanceof DOMException
|
|
120
|
+
? error.name === "AbortError"
|
|
121
|
+
: error instanceof Error &&
|
|
122
|
+
(error.name === "AbortError" || error.message === "Execution aborted");
|
|
123
|
+
return {
|
|
124
|
+
outcome: cancelled ? "cancelled" : "error",
|
|
125
|
+
error: (error instanceof Error ? error.message : String(error)).slice(
|
|
126
|
+
0,
|
|
127
|
+
500,
|
|
128
|
+
),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
90
132
|
function invocationPrimitive(options: InvocationOptions): {
|
|
91
133
|
execute(program: Program): Promise<Program>;
|
|
92
134
|
stream(program: Program): AsyncIterable<Program>;
|
|
@@ -20,6 +20,67 @@ import {
|
|
|
20
20
|
import { observeExecutionStream } from "@neutrome/lilsdk/stream";
|
|
21
21
|
|
|
22
22
|
describe("router execution runtime", () => {
|
|
23
|
+
it("records only named boundaries through anonymous executor fan-out", async () => {
|
|
24
|
+
const named: Array<{
|
|
25
|
+
executionId: string;
|
|
26
|
+
parentExecutionId?: string;
|
|
27
|
+
kind: "incoming" | "subrequest";
|
|
28
|
+
model: string;
|
|
29
|
+
}> = [];
|
|
30
|
+
const leaf = {
|
|
31
|
+
async execute(program: Program) {
|
|
32
|
+
return program;
|
|
33
|
+
},
|
|
34
|
+
async *stream() {},
|
|
35
|
+
};
|
|
36
|
+
const anonymousInner = {
|
|
37
|
+
async execute(program: Program, ctx: ExecutorContext) {
|
|
38
|
+
return ctx.invoke("namespace/model-b", program);
|
|
39
|
+
},
|
|
40
|
+
async *stream() {},
|
|
41
|
+
};
|
|
42
|
+
const anonymousOuter = {
|
|
43
|
+
async execute(program: Program, ctx: ExecutorContext) {
|
|
44
|
+
return ctx.invoke(anonymousInner, program);
|
|
45
|
+
},
|
|
46
|
+
async *stream() {},
|
|
47
|
+
};
|
|
48
|
+
const runtime = createExecutionRuntime({
|
|
49
|
+
executorImplementations: {
|
|
50
|
+
root: {
|
|
51
|
+
async execute(program, ctx) {
|
|
52
|
+
return ctx.invoke(anonymousOuter, program);
|
|
53
|
+
},
|
|
54
|
+
async *stream() {},
|
|
55
|
+
},
|
|
56
|
+
leaf,
|
|
57
|
+
},
|
|
58
|
+
resolveTarget(request) {
|
|
59
|
+
return getModel(request) === "namespace/model-b"
|
|
60
|
+
? { kind: "executor", executorId: "leaf", alias: "model-b" }
|
|
61
|
+
: { kind: "executor", executorId: "root", alias: "model-a" };
|
|
62
|
+
},
|
|
63
|
+
onNamedInvocation(result) {
|
|
64
|
+
named.push(result);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await runtime.execute(createProgram(), {
|
|
69
|
+
executionId: "model-a-execution",
|
|
70
|
+
namedModel: "namespace/model-a",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(named).toHaveLength(2);
|
|
74
|
+
expect(named.find((item) => item.kind === "incoming")).toMatchObject({
|
|
75
|
+
executionId: "model-a-execution",
|
|
76
|
+
model: "namespace/model-a",
|
|
77
|
+
});
|
|
78
|
+
expect(named.find((item) => item.kind === "subrequest")).toMatchObject({
|
|
79
|
+
parentExecutionId: "model-a-execution",
|
|
80
|
+
model: "namespace/model-b",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
23
84
|
it("shares one cache with transforms and executors", async () => {
|
|
24
85
|
const values = new Map<string, string>();
|
|
25
86
|
const cache = {
|
|
@@ -146,6 +146,10 @@ export function createExecutionRuntime(
|
|
|
146
146
|
depth: number,
|
|
147
147
|
): Promise<InvocationOptions> {
|
|
148
148
|
const prepared = await prepareExecution(request, invokeOptions, depth);
|
|
149
|
+
const namedModel = invokeOptions.namedModel;
|
|
150
|
+
const billable =
|
|
151
|
+
namedModel !== undefined &&
|
|
152
|
+
(options.isBillableModel?.(namedModel) ?? true);
|
|
149
153
|
const executor =
|
|
150
154
|
prepared.target.kind === "executor"
|
|
151
155
|
? await resolveRuntimeExecutor(
|
|
@@ -172,6 +176,9 @@ export function createExecutionRuntime(
|
|
|
172
176
|
prepared.parentExecutionId,
|
|
173
177
|
prepared.signal,
|
|
174
178
|
depth,
|
|
179
|
+
billable
|
|
180
|
+
? prepared.executionId
|
|
181
|
+
: invokeOptions.namedParentExecutionId,
|
|
175
182
|
),
|
|
176
183
|
createProviderContext: (target) =>
|
|
177
184
|
createProviderInvocationContext(
|
|
@@ -192,6 +199,22 @@ export function createExecutionRuntime(
|
|
|
192
199
|
depth,
|
|
193
200
|
emitSuccess: stage !== "stream_chunk",
|
|
194
201
|
}),
|
|
202
|
+
...(billable
|
|
203
|
+
? {
|
|
204
|
+
namedInvocation: {
|
|
205
|
+
executionId: prepared.executionId,
|
|
206
|
+
...(invokeOptions.namedParentExecutionId
|
|
207
|
+
? { parentExecutionId: invokeOptions.namedParentExecutionId }
|
|
208
|
+
: {}),
|
|
209
|
+
kind:
|
|
210
|
+
depth === 0 ? ("incoming" as const) : ("subrequest" as const),
|
|
211
|
+
model: namedModel,
|
|
212
|
+
input: cloneProgram(request),
|
|
213
|
+
startedAtMs: Date.now(),
|
|
214
|
+
},
|
|
215
|
+
onNamedInvocation: options.onNamedInvocation,
|
|
216
|
+
}
|
|
217
|
+
: {}),
|
|
195
218
|
};
|
|
196
219
|
}
|
|
197
220
|
|
|
@@ -201,6 +224,7 @@ export function createExecutionRuntime(
|
|
|
201
224
|
parentExecutionId: string | undefined,
|
|
202
225
|
signal: AbortSignal,
|
|
203
226
|
depth: number,
|
|
227
|
+
namedParentExecutionId?: string,
|
|
204
228
|
): ExecutorContext {
|
|
205
229
|
return {
|
|
206
230
|
cache: options.cache ?? unavailableCache,
|
|
@@ -218,12 +242,18 @@ export function createExecutionRuntime(
|
|
|
218
242
|
executionId,
|
|
219
243
|
signal,
|
|
220
244
|
depth + 1,
|
|
245
|
+
namedParentExecutionId,
|
|
221
246
|
),
|
|
222
247
|
);
|
|
223
248
|
}
|
|
224
249
|
return executeTarget(
|
|
225
250
|
setModel(cloneProgram(request), executor),
|
|
226
|
-
{
|
|
251
|
+
{
|
|
252
|
+
requestId,
|
|
253
|
+
parentExecutionId: executionId,
|
|
254
|
+
namedModel: executor,
|
|
255
|
+
...(namedParentExecutionId ? { namedParentExecutionId } : {}),
|
|
256
|
+
},
|
|
227
257
|
depth + 1,
|
|
228
258
|
);
|
|
229
259
|
},
|
|
@@ -238,13 +268,19 @@ export function createExecutionRuntime(
|
|
|
238
268
|
executionId,
|
|
239
269
|
signal,
|
|
240
270
|
depth + 1,
|
|
271
|
+
namedParentExecutionId,
|
|
241
272
|
),
|
|
242
273
|
);
|
|
243
274
|
return;
|
|
244
275
|
}
|
|
245
276
|
yield* streamTarget(
|
|
246
277
|
setModel(cloneProgram(request), executor),
|
|
247
|
-
{
|
|
278
|
+
{
|
|
279
|
+
requestId,
|
|
280
|
+
parentExecutionId: executionId,
|
|
281
|
+
namedModel: executor,
|
|
282
|
+
...(namedParentExecutionId ? { namedParentExecutionId } : {}),
|
|
283
|
+
},
|
|
248
284
|
depth + 1,
|
|
249
285
|
);
|
|
250
286
|
},
|
|
@@ -30,6 +30,22 @@ export type InvokeOptions = {
|
|
|
30
30
|
requestId?: string;
|
|
31
31
|
executionId?: string;
|
|
32
32
|
parentExecutionId?: string;
|
|
33
|
+
namedModel?: string;
|
|
34
|
+
namedParentExecutionId?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type NamedInvocationResult = {
|
|
38
|
+
executionId: string;
|
|
39
|
+
parentExecutionId?: string;
|
|
40
|
+
kind: "incoming" | "subrequest";
|
|
41
|
+
model: string;
|
|
42
|
+
input: Program;
|
|
43
|
+
output?: Program;
|
|
44
|
+
stream?: readonly Program[];
|
|
45
|
+
startedAtMs: number;
|
|
46
|
+
finishedAtMs: number;
|
|
47
|
+
outcome: "ok" | "error" | "cancelled";
|
|
48
|
+
error?: string;
|
|
33
49
|
};
|
|
34
50
|
|
|
35
51
|
export type ProviderInvocationContext = {
|
|
@@ -71,6 +87,8 @@ export type ExecutionRuntimeOptions = {
|
|
|
71
87
|
executionIdFactory?: () => string;
|
|
72
88
|
maxDepth?: number;
|
|
73
89
|
validatePrograms?: boolean;
|
|
90
|
+
isBillableModel?(model: string): boolean;
|
|
91
|
+
onNamedInvocation?(result: NamedInvocationResult): void;
|
|
74
92
|
};
|
|
75
93
|
|
|
76
94
|
export type ExecutionRuntime = {
|
package/src/usage/user-usage.ts
CHANGED
|
@@ -16,8 +16,7 @@ export type UserUsageReport = {
|
|
|
16
16
|
requestId: string;
|
|
17
17
|
protocol: ProviderStyle;
|
|
18
18
|
requestedModel: string | null;
|
|
19
|
-
|
|
20
|
-
outputTokens: number;
|
|
19
|
+
invocations: UserModelInvocation[];
|
|
21
20
|
tools: UserToolUsage[];
|
|
22
21
|
startedAt: string;
|
|
23
22
|
finishedAt: string;
|
|
@@ -27,6 +26,20 @@ export type UserUsageReport = {
|
|
|
27
26
|
error: string | null;
|
|
28
27
|
};
|
|
29
28
|
|
|
29
|
+
export type UserModelInvocation = {
|
|
30
|
+
executionId: string;
|
|
31
|
+
parentExecutionId: string | null;
|
|
32
|
+
kind: "incoming" | "subrequest";
|
|
33
|
+
model: string;
|
|
34
|
+
inputTokens: number;
|
|
35
|
+
outputTokens: number;
|
|
36
|
+
startedAt: string;
|
|
37
|
+
finishedAt: string;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
outcome: UserUsageOutcome;
|
|
40
|
+
error: string | null;
|
|
41
|
+
};
|
|
42
|
+
|
|
30
43
|
/** Counts semantic LIL content, never transport JSON, settings, model ids, or usage. */
|
|
31
44
|
export function countUserProgramTokens(program: Program): number {
|
|
32
45
|
return encode(semanticProgramText(program), { disallowedSpecial: "all" })
|