@stigg/agent-sdk-js 0.7.3 → 0.7.4
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.
|
@@ -62,9 +62,11 @@ function stiggVercelMiddleware({ stigg, stiggContext, hooks, }) {
|
|
|
62
62
|
governance.abandon(runId);
|
|
63
63
|
throw err;
|
|
64
64
|
}
|
|
65
|
-
// Awaited, so the agent's next step is checked against this one's spend. Never throws
|
|
66
|
-
// usage
|
|
67
|
-
|
|
65
|
+
// Awaited, so the agent's next step is checked against this one's spend. Never throws. No
|
|
66
|
+
// usage stated: estimated from the reply, as a stream is, and counted as a metering failure.
|
|
67
|
+
const responseId = result?.response?.id;
|
|
68
|
+
const chars = (0, usage_1.contentChars)(result?.content);
|
|
69
|
+
await governance.endModel(runId, (0, usage_1.measurementOf)(result?.usage, responseId) ?? (chars ? (0, usage_1.estimated)(chars, usage_1.NO_USAGE, responseId) : undefined));
|
|
68
70
|
return result;
|
|
69
71
|
},
|
|
70
72
|
async wrapStream({ doStream, params }) {
|
|
@@ -95,6 +97,24 @@ function stiggVercelMiddleware({ stigg, stiggContext, hooks, }) {
|
|
|
95
97
|
},
|
|
96
98
|
};
|
|
97
99
|
}
|
|
100
|
+
/** What `incomplete` says for a stream that ended without its usage, by how it ended. */
|
|
101
|
+
const STREAM_GAPS = {
|
|
102
|
+
ended: usage_1.NO_USAGE,
|
|
103
|
+
failed: 'the stream failed before the model stated its usage, so its output was estimated',
|
|
104
|
+
timedOut: 'the call timed out before the model stated its usage, so its output was estimated',
|
|
105
|
+
stopped: 'the call was stopped before any output, so its input was not measured',
|
|
106
|
+
};
|
|
107
|
+
/** A timeout's abort is not the caller stopping. */
|
|
108
|
+
const isTimeout = (reason) => reason?.name === 'TimeoutError';
|
|
109
|
+
/**
|
|
110
|
+
* Why a reader cancelled: the AI SDK cancels an attempt it retries with the provider's error, which
|
|
111
|
+
* is a failure; any other reason is the caller stopping.
|
|
112
|
+
*/
|
|
113
|
+
function cancelEnd(reason) {
|
|
114
|
+
if (isTimeout(reason))
|
|
115
|
+
return 'timedOut';
|
|
116
|
+
return reason instanceof Error && reason.name !== 'AbortError' ? 'failed' : 'stopped';
|
|
117
|
+
}
|
|
98
118
|
/** What a stream has told us so far. */
|
|
99
119
|
function streamTally() {
|
|
100
120
|
let usage;
|
|
@@ -110,14 +130,19 @@ function streamTally() {
|
|
|
110
130
|
else if (typeof part?.delta === 'string')
|
|
111
131
|
chars += part.delta.length;
|
|
112
132
|
},
|
|
113
|
-
/**
|
|
114
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The usage the model stated; else an estimate; else nothing, for an attempt that produced
|
|
135
|
+
* nothing. A stream the caller stopped is estimated without counting as a metering failure -
|
|
136
|
+
* unless there was nothing to estimate, while the provider still counted its input.
|
|
137
|
+
*/
|
|
138
|
+
measurement(end) {
|
|
115
139
|
const measured = (0, usage_1.measurementOf)(usage, responseId);
|
|
116
140
|
if (measured)
|
|
117
141
|
return measured;
|
|
118
142
|
if (chars === 0 && responseId === undefined)
|
|
119
143
|
return undefined;
|
|
120
|
-
|
|
144
|
+
const gap = end === 'stopped' && chars > 0 ? undefined : STREAM_GAPS[end];
|
|
145
|
+
return (0, usage_1.estimated)(chars, gap, responseId);
|
|
121
146
|
},
|
|
122
147
|
};
|
|
123
148
|
}
|
|
@@ -134,27 +159,28 @@ function observeStream(result, signal, done) {
|
|
|
134
159
|
}
|
|
135
160
|
const tally = streamTally();
|
|
136
161
|
let reported = false;
|
|
137
|
-
const report = () => {
|
|
162
|
+
const report = (end) => {
|
|
138
163
|
if (reported)
|
|
139
164
|
return;
|
|
140
165
|
reported = true;
|
|
141
166
|
// Every step of a loop shares the signal, which may live as long as the process.
|
|
142
|
-
signal?.removeEventListener('abort',
|
|
143
|
-
done(tally.measurement());
|
|
167
|
+
signal?.removeEventListener('abort', onAbort);
|
|
168
|
+
done(tally.measurement(end));
|
|
144
169
|
};
|
|
145
|
-
signal?.
|
|
170
|
+
const onAbort = () => report(isTimeout(signal?.reason) ? 'timedOut' : 'stopped');
|
|
171
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
146
172
|
if (signal?.aborted)
|
|
147
|
-
|
|
173
|
+
onAbort();
|
|
148
174
|
// A source of its own rather than a TransformStream, whose flush never runs on a cancel.
|
|
149
175
|
const reader = source.getReader();
|
|
150
176
|
result.stream = new ReadableStream({
|
|
151
177
|
async pull(controller) {
|
|
152
178
|
const next = await reader.read().catch((err) => {
|
|
153
|
-
report();
|
|
179
|
+
report('failed');
|
|
154
180
|
throw err;
|
|
155
181
|
});
|
|
156
182
|
if (next.done) {
|
|
157
|
-
report();
|
|
183
|
+
report('ended');
|
|
158
184
|
controller.close();
|
|
159
185
|
return;
|
|
160
186
|
}
|
|
@@ -162,7 +188,7 @@ function observeStream(result, signal, done) {
|
|
|
162
188
|
controller.enqueue(next.value);
|
|
163
189
|
},
|
|
164
190
|
async cancel(reason) {
|
|
165
|
-
report();
|
|
191
|
+
report(cancelEnd(reason));
|
|
166
192
|
await reader.cancel(reason);
|
|
167
193
|
},
|
|
168
194
|
});
|
|
@@ -25,5 +25,12 @@ export type ModelUsage = {
|
|
|
25
25
|
};
|
|
26
26
|
/** One call's usage, or undefined when the model stated no counter - not a call that cost nothing. */
|
|
27
27
|
export declare function measurementOf(usage: ModelUsage | undefined, responseId?: string): Measurement | undefined;
|
|
28
|
-
/**
|
|
29
|
-
export declare
|
|
28
|
+
/** What `incomplete` says when the model never stated its usage: a gap in metering, not a stop. */
|
|
29
|
+
export declare const NO_USAGE = "the model stated no usage, so its output was estimated";
|
|
30
|
+
/**
|
|
31
|
+
* A call with no usage stated: its output, estimated from the text. Its input is unknown.
|
|
32
|
+
* `incomplete` is set only for a metering gap - a call the caller stopped is not one.
|
|
33
|
+
*/
|
|
34
|
+
export declare function estimated(chars: number, incomplete?: string, responseId?: string): Measurement;
|
|
35
|
+
/** The characters a generated reply holds: its text, its reasoning and its tool calls' input. */
|
|
36
|
+
export declare function contentChars(content: unknown): number;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VERCEL_AI_METRICS = void 0;
|
|
3
|
+
exports.NO_USAGE = exports.VERCEL_AI_METRICS = void 0;
|
|
4
4
|
exports.measurementOf = measurementOf;
|
|
5
5
|
exports.estimated = estimated;
|
|
6
|
+
exports.contentChars = contentChars;
|
|
6
7
|
exports.VERCEL_AI_METRICS = [
|
|
7
8
|
'input_tokens',
|
|
8
9
|
'output_tokens',
|
|
@@ -43,16 +44,35 @@ function measurementOf(usage, responseId) {
|
|
|
43
44
|
...(responseId ? { idempotencyKey: responseId } : {}),
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
47
|
+
/** What `incomplete` says when the model never stated its usage: a gap in metering, not a stop. */
|
|
48
|
+
exports.NO_USAGE = 'the model stated no usage, so its output was estimated';
|
|
49
|
+
/**
|
|
50
|
+
* A call with no usage stated: its output, estimated from the text. Its input is unknown.
|
|
51
|
+
* `incomplete` is set only for a metering gap - a call the caller stopped is not one.
|
|
52
|
+
*/
|
|
53
|
+
function estimated(chars, incomplete, responseId) {
|
|
48
54
|
const output = chars > 0 ? Math.max(1, Math.ceil(chars / CHARS_PER_OUTPUT_TOKEN)) : 0;
|
|
49
55
|
return {
|
|
50
56
|
metrics: output ? { output_tokens: output, total_tokens: output } : {},
|
|
51
57
|
usage: { input: 0, output, cacheRead: 0, cacheCreate: 0 },
|
|
52
58
|
...(output ? { estimated: true } : {}),
|
|
53
|
-
incomplete
|
|
59
|
+
...(incomplete ? { incomplete } : {}),
|
|
60
|
+
// Keyed by the response, as a stated usage is: a replayed reply is still one report.
|
|
61
|
+
...(responseId ? { idempotencyKey: responseId } : {}),
|
|
54
62
|
};
|
|
55
63
|
}
|
|
64
|
+
/** The characters a generated reply holds: its text, its reasoning and its tool calls' input. */
|
|
65
|
+
function contentChars(content) {
|
|
66
|
+
if (!Array.isArray(content))
|
|
67
|
+
return 0;
|
|
68
|
+
let chars = 0;
|
|
69
|
+
for (const part of content) {
|
|
70
|
+
const text = part?.type === 'tool-call' ? part.input : part?.text;
|
|
71
|
+
if (typeof text === 'string')
|
|
72
|
+
chars += text.length;
|
|
73
|
+
}
|
|
74
|
+
return chars;
|
|
75
|
+
}
|
|
56
76
|
function numbersOnly(stated) {
|
|
57
77
|
const metrics = {};
|
|
58
78
|
for (const [name, value] of Object.entries(stated)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigg/agent-sdk-js",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Stigg agent SDK (JS) - run-lifecycle governance for agentic stacks, with examples per layer",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -123,6 +123,7 @@
|
|
|
123
123
|
"@opentelemetry/sdk-trace-node": "^2.0.1",
|
|
124
124
|
"@typesafe-ai/sdk": "^0.6.0",
|
|
125
125
|
"ai": "^7.0.153",
|
|
126
|
+
"ai-v6": "npm:ai@^6.0.320",
|
|
126
127
|
"cloudflare": "^7.3.0",
|
|
127
128
|
"dotenv": "^17.4.2",
|
|
128
129
|
"groq-sdk": "^1.6.0",
|