@stigg/agent-sdk-js 0.0.0-stage → 0.1.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 +1296 -2
- package/dist/bindings/client/core/registry.d.ts +12 -0
- package/dist/bindings/client/core/registry.js +22 -0
- package/dist/bindings/client/core/types.d.ts +117 -0
- package/dist/bindings/client/core/types.js +13 -0
- package/dist/bindings/client/core/wrap-client.d.ts +6 -0
- package/dist/bindings/client/core/wrap-client.js +258 -0
- package/dist/bindings/client/gateways/bedrock/bedrock-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/bedrock/bedrock-recipe.js +134 -0
- package/dist/bindings/client/gateways/bedrock/types.d.ts +13 -0
- package/dist/bindings/client/gateways/bedrock/types.js +2 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.js +171 -0
- package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
- package/dist/bindings/client/gateways/litellm/types.js +2 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +20 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +244 -0
- package/dist/bindings/client/gateways/openrouter/sdk-usage.d.ts +37 -0
- package/dist/bindings/client/gateways/openrouter/sdk-usage.js +63 -0
- package/dist/bindings/client/gateways/openrouter/types.d.ts +78 -0
- package/dist/bindings/client/gateways/openrouter/types.js +2 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +34 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +169 -0
- package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
- package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
- package/dist/bindings/client/providers/groq/groq-recipe.d.ts +20 -0
- package/dist/bindings/client/providers/groq/groq-recipe.js +101 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +22 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.js +114 -0
- package/dist/bindings/client/providers/mistral/types.d.ts +38 -0
- package/dist/bindings/client/providers/mistral/types.js +2 -0
- package/dist/bindings/client/providers/ollama/ollama-recipe.d.ts +31 -0
- package/dist/bindings/client/providers/ollama/ollama-recipe.js +107 -0
- package/dist/bindings/client/providers/ollama/types.d.ts +24 -0
- package/dist/bindings/client/providers/ollama/types.js +2 -0
- package/dist/bindings/client/providers/openai/openai-recipe.d.ts +71 -0
- package/dist/bindings/client/providers/openai/openai-recipe.js +122 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +44 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +83 -0
- package/dist/bindings/proxy/headers.d.ts +6 -0
- package/dist/bindings/proxy/headers.js +12 -0
- package/dist/bindings/proxy/index.d.ts +21 -0
- package/dist/bindings/proxy/index.js +18 -0
- package/dist/bindings/proxy/types.d.ts +8 -0
- package/dist/bindings/proxy/types.js +2 -0
- package/dist/bindings/shared/estimate.d.ts +27 -0
- package/dist/bindings/shared/estimate.js +38 -0
- package/dist/bindings/shared/meter-stream.d.ts +18 -0
- package/dist/bindings/shared/meter-stream.js +151 -0
- package/dist/bindings/shared/token-usage.d.ts +240 -0
- package/dist/bindings/shared/token-usage.js +574 -0
- package/dist/bindings/shared/types.d.ts +129 -0
- package/dist/bindings/shared/types.js +2 -0
- package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
- package/dist/bindings/shared/weighted-metrics.js +47 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.js +99 -0
- package/dist/core/context.d.ts +160 -0
- package/dist/core/context.js +221 -0
- package/dist/core/dimensions.d.ts +22 -0
- package/dist/core/dimensions.js +47 -0
- package/dist/core/hooks.d.ts +21 -0
- package/dist/core/hooks.js +47 -0
- package/dist/core/session.d.ts +190 -0
- package/dist/core/session.js +448 -0
- package/dist/decision-text.d.ts +15 -0
- package/dist/decision-text.js +54 -0
- package/dist/engine/composite-engine.d.ts +72 -0
- package/dist/engine/composite-engine.js +135 -0
- package/dist/engine/create-engine.d.ts +16 -0
- package/dist/engine/create-engine.js +93 -0
- package/dist/engine/decision.d.ts +42 -0
- package/dist/engine/decision.js +135 -0
- package/dist/engine/engine.d.ts +166 -0
- package/dist/engine/engine.js +11 -0
- package/dist/engine/node-sdk-engine.d.ts +125 -0
- package/dist/engine/node-sdk-engine.js +262 -0
- package/dist/engine/report.d.ts +106 -0
- package/dist/engine/report.js +188 -0
- package/dist/engine/rest-governance-engine.d.ts +47 -0
- package/dist/engine/rest-governance-engine.js +220 -0
- package/dist/errors.d.ts +86 -0
- package/dist/errors.js +161 -0
- package/dist/headers.d.ts +40 -0
- package/dist/headers.js +82 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +106 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +87 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +20 -0
- package/dist/sdk-identity.d.ts +36 -0
- package/dist/sdk-identity.js +43 -0
- package/dist/stigg-agent-sdk.d.ts +300 -0
- package/dist/stigg-agent-sdk.js +656 -0
- package/dist/types.d.ts +642 -0
- package/dist/types.js +147 -0
- package/package.json +89 -3
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizedMetrics = exports.addMetrics = exports.METRIC_PATHS = exports.metricNamesMatchPaths = exports.COMPUTED_TOTALS = exports.metricName = exports.BEDROCK_METRICS = exports.BEDROCK_PATHS = exports.GATEWAY_METRICS = exports.OPENAI_METRICS = exports.OLLAMA_METRICS = exports.OLLAMA_COMPONENT_METRICS = exports.TYPESAFE_METRICS = exports.TYPESAFE_COMPONENT_METRICS = exports.MISTRAL_METRICS = exports.MISTRAL_METRIC_NAME_MAP = exports.OPENAI_RESPONSES_METRICS = exports.OPENAI_CHAT_METRICS = exports.GEMINI_COMPONENT_METRICS = exports.GEMINI_METRICS = exports.ANTHROPIC_METRICS = exports.ANTHROPIC_COMPONENT_METRICS = exports.TOTAL_TOKENS = exports.addTokenUsage = void 0;
|
|
4
|
+
exports.fromFlatTokenUsage = fromFlatTokenUsage;
|
|
5
|
+
exports.fromOpenAIUsage = fromOpenAIUsage;
|
|
6
|
+
exports.fromResponsesUsage = fromResponsesUsage;
|
|
7
|
+
exports.fromBedrockUsage = fromBedrockUsage;
|
|
8
|
+
exports.fromLangChainUsage = fromLangChainUsage;
|
|
9
|
+
exports.fromGeminiUsage = fromGeminiUsage;
|
|
10
|
+
exports.fromMistralUsage = fromMistralUsage;
|
|
11
|
+
exports.fromOllamaUsage = fromOllamaUsage;
|
|
12
|
+
exports.normalizeUsage = normalizeUsage;
|
|
13
|
+
exports.withComputedTotal = withComputedTotal;
|
|
14
|
+
exports.pickMetrics = pickMetrics;
|
|
15
|
+
exports.pickRenamedMetrics = pickRenamedMetrics;
|
|
16
|
+
exports.observedMeasurement = observedMeasurement;
|
|
17
|
+
/**
|
|
18
|
+
* Provider usage objects -> the SDK's neutral `TokenUsage`.
|
|
19
|
+
*
|
|
20
|
+
* The inputs are typed structurally rather than against each vendor's SDK, so this handful of
|
|
21
|
+
* readers covers a direct client, a gateway response and an orchestrator callback - and adding a
|
|
22
|
+
* provider whose response happens to match costs nothing. Hence the shape-named reader below:
|
|
23
|
+
* `fromFlatTokenUsage` is read by every vendor that counts that way, not by one of them.
|
|
24
|
+
*/
|
|
25
|
+
const types_1 = require("../../types");
|
|
26
|
+
const n = (value) => (typeof value === 'number' && value > 0 ? value : 0);
|
|
27
|
+
function fromFlatTokenUsage(usage) {
|
|
28
|
+
if (!usage)
|
|
29
|
+
return (0, types_1.emptyTokenUsage)();
|
|
30
|
+
const counted = {
|
|
31
|
+
input: n(usage.input_tokens),
|
|
32
|
+
output: n(usage.output_tokens),
|
|
33
|
+
cacheRead: n(usage.cache_read_input_tokens),
|
|
34
|
+
cacheCreate: n(usage.cache_creation_input_tokens),
|
|
35
|
+
};
|
|
36
|
+
// No vendor on this shape publishes a total, and these four are disjoint - cache reads and
|
|
37
|
+
// writes sit beside the input count rather than inside it - so their sum is the whole call.
|
|
38
|
+
return { ...counted, ...withTotal(counted.input + counted.output + counted.cacheRead + counted.cacheCreate) };
|
|
39
|
+
}
|
|
40
|
+
/** `{ total }` when there is one to state, and nothing at all when there is not. */
|
|
41
|
+
const withTotal = (total) => (total > 0 ? { total } : {});
|
|
42
|
+
function fromOpenAIUsage(usage) {
|
|
43
|
+
if (!usage)
|
|
44
|
+
return (0, types_1.emptyTokenUsage)();
|
|
45
|
+
const cacheRead = n(usage.prompt_tokens_details?.cached_tokens);
|
|
46
|
+
return {
|
|
47
|
+
// OpenAI states its own, and a stated total beats one reassembled from the parts.
|
|
48
|
+
...withTotal(n(usage.total_tokens)),
|
|
49
|
+
// OpenAI counts cached prompt tokens inside prompt_tokens, so subtract to avoid double-count.
|
|
50
|
+
input: Math.max(0, n(usage.prompt_tokens) - cacheRead),
|
|
51
|
+
output: n(usage.completion_tokens),
|
|
52
|
+
cacheRead,
|
|
53
|
+
cacheCreate: n(usage.prompt_tokens_details?.cache_write_tokens),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* OpenAI's Responses API, which counts the same tokens under different names - `input_tokens`
|
|
58
|
+
* rather than `prompt_tokens`, and the cache detail one level along. Reading it with the Chat
|
|
59
|
+
* Completions extractor finds none of them and meters the call at zero.
|
|
60
|
+
*/
|
|
61
|
+
function fromResponsesUsage(usage) {
|
|
62
|
+
if (!usage)
|
|
63
|
+
return (0, types_1.emptyTokenUsage)();
|
|
64
|
+
const cacheRead = n(usage.input_tokens_details?.cached_tokens);
|
|
65
|
+
return {
|
|
66
|
+
...withTotal(n(usage.total_tokens)),
|
|
67
|
+
// As with chat completions, cached tokens are counted inside the input total.
|
|
68
|
+
input: Math.max(0, n(usage.input_tokens) - cacheRead),
|
|
69
|
+
output: n(usage.output_tokens),
|
|
70
|
+
cacheRead,
|
|
71
|
+
// OpenAI's own Responses API reports no cache writes, so this is 0 for it either way. A
|
|
72
|
+
// gateway mirroring the surface may report them - OpenRouter does - and reading the field
|
|
73
|
+
// costs nothing where it is absent, where the alternative was dropping a counter the customer
|
|
74
|
+
// was charged for.
|
|
75
|
+
cacheCreate: n(usage.input_tokens_details?.cache_write_tokens),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Bedrock's Converse usage, whose four counters are disjoint.
|
|
80
|
+
*
|
|
81
|
+
* Nothing is subtracted, unlike the OpenAI and Gemini readers: AWS states that `inputTokens`
|
|
82
|
+
* already excludes whatever was read from or written to the cache, so taking the cache back out
|
|
83
|
+
* would charge the call for less than it used.
|
|
84
|
+
*
|
|
85
|
+
* The total is read, not recomputed, like every other vendor that publishes one - but it is worth
|
|
86
|
+
* knowing what it covers. AWS documents `totalTokens` as the input and output of the call, and
|
|
87
|
+
* documents `inputTokens` as cache-exclusive, so on a cached call the stated total can be smaller
|
|
88
|
+
* than the four counters add up to. Both are reported: price the breakdown if the cache matters to
|
|
89
|
+
* you, the total if it does not, and see the README's overlap table before pricing both.
|
|
90
|
+
*/
|
|
91
|
+
function fromBedrockUsage(usage) {
|
|
92
|
+
if (!usage)
|
|
93
|
+
return (0, types_1.emptyTokenUsage)();
|
|
94
|
+
const counted = {
|
|
95
|
+
input: n(usage.inputTokens),
|
|
96
|
+
output: n(usage.outputTokens),
|
|
97
|
+
cacheRead: n(usage.cacheReadInputTokens),
|
|
98
|
+
cacheCreate: n(usage.cacheWriteInputTokens),
|
|
99
|
+
};
|
|
100
|
+
const stated = n(usage.totalTokens);
|
|
101
|
+
return {
|
|
102
|
+
...counted,
|
|
103
|
+
// The vendor's figure when it gave one; otherwise the sum, which is sound precisely because
|
|
104
|
+
// these four do not overlap.
|
|
105
|
+
...withTotal(stated || counted.input + counted.output + counted.cacheRead + counted.cacheCreate),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function fromLangChainUsage(usage) {
|
|
109
|
+
if (!usage)
|
|
110
|
+
return (0, types_1.emptyTokenUsage)();
|
|
111
|
+
const cacheRead = n(usage.input_token_details?.cache_read);
|
|
112
|
+
const cacheCreate = n(usage.input_token_details?.cache_creation);
|
|
113
|
+
return {
|
|
114
|
+
input: Math.max(0, n(usage.input_tokens) - cacheRead - cacheCreate),
|
|
115
|
+
output: n(usage.output_tokens),
|
|
116
|
+
cacheRead,
|
|
117
|
+
cacheCreate,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Gemini's `usageMetadata`, whose two overlaps run in opposite directions - see `GeminiLikeUsage`
|
|
122
|
+
* for the vendor's own wording on each.
|
|
123
|
+
*
|
|
124
|
+
* The arithmetic is checked by the one invariant the vendor publishes: the four counters below
|
|
125
|
+
* add back up to `totalTokenCount`, which is why neither overlap may be read the other way.
|
|
126
|
+
*/
|
|
127
|
+
function fromGeminiUsage(usage) {
|
|
128
|
+
if (!usage)
|
|
129
|
+
return (0, types_1.emptyTokenUsage)();
|
|
130
|
+
const cacheRead = n(usage.cachedContentTokenCount);
|
|
131
|
+
return {
|
|
132
|
+
// Gemini's own, under its own name. Never recomputed from the counters below: they overlap,
|
|
133
|
+
// and the vendor's figure is the one its invoice will show.
|
|
134
|
+
...withTotal(n(usage.totalTokenCount)),
|
|
135
|
+
// Cached content is counted inside the prompt total, so subtract it rather than bill it
|
|
136
|
+
// twice. Tool-use prompt tokens are not inside it - `totalTokenCount` adds them alongside -
|
|
137
|
+
// so they are input this would otherwise drop entirely.
|
|
138
|
+
input: Math.max(0, n(usage.promptTokenCount) - cacheRead) + n(usage.toolUsePromptTokenCount),
|
|
139
|
+
// Thinking tokens are billed as output and counted beside the candidates, not within them.
|
|
140
|
+
output: n(usage.candidatesTokenCount) + n(usage.thoughtsTokenCount),
|
|
141
|
+
cacheRead,
|
|
142
|
+
// Nothing to read: an explicit cache is written by `caches.create`, a separate call whose own
|
|
143
|
+
// response carries the token count for it. A generation reports only what it read.
|
|
144
|
+
cacheCreate: 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Mistral's `UsageInfo` - the OpenAI three, camelCased by a generated SDK.
|
|
149
|
+
*
|
|
150
|
+
* Counts the same way OpenAI's chat usage does, so the arithmetic is the same: the vendor states
|
|
151
|
+
* its own total and it is read rather than recomputed. There is no cache counter to subtract -
|
|
152
|
+
* `UsageInfo` carries none, so unlike `fromOpenAIUsage` nothing is nested inside the prompt count.
|
|
153
|
+
*/
|
|
154
|
+
function fromMistralUsage(usage) {
|
|
155
|
+
if (!usage)
|
|
156
|
+
return (0, types_1.emptyTokenUsage)();
|
|
157
|
+
return {
|
|
158
|
+
...withTotal(n(usage.totalTokens)),
|
|
159
|
+
input: n(usage.promptTokens),
|
|
160
|
+
output: n(usage.completionTokens),
|
|
161
|
+
cacheRead: 0,
|
|
162
|
+
cacheCreate: 0,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Ollama's two counters, read off the response root rather than out of a `usage` block.
|
|
167
|
+
*
|
|
168
|
+
* Disjoint - the prompt evaluation and the generation are separate work - so their sum is the
|
|
169
|
+
* whole call and computing a total from them is the vendor's own arithmetic rather than ours.
|
|
170
|
+
* Ollama publishes no total of its own, which is why there is one to compute.
|
|
171
|
+
*/
|
|
172
|
+
function fromOllamaUsage(usage) {
|
|
173
|
+
if (!usage)
|
|
174
|
+
return (0, types_1.emptyTokenUsage)();
|
|
175
|
+
const counted = { input: n(usage.prompt_eval_count), output: n(usage.eval_count), cacheRead: 0, cacheCreate: 0 };
|
|
176
|
+
return { ...counted, ...withTotal(counted.input + counted.output) };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Any of the three shapes above, recognised by its own fields. Vendors disagree on names but not
|
|
180
|
+
* enough to be ambiguous, so the caller can pass whatever the response carried.
|
|
181
|
+
*/
|
|
182
|
+
function normalizeUsage(usage) {
|
|
183
|
+
if (!usage || typeof usage !== 'object')
|
|
184
|
+
return (0, types_1.emptyTokenUsage)();
|
|
185
|
+
const raw = usage;
|
|
186
|
+
// Already normalized - a caller doing its own accounting. Recognised by any of the four, not
|
|
187
|
+
// only the cache pair: an application that counts input and output and has no cache to report
|
|
188
|
+
// used to fall past this to the flat vendor reader, which looks for `input_tokens` and finds
|
|
189
|
+
// none - so `{ input: 600, output: 400 }` normalized to zeroes and metered the turn as nothing.
|
|
190
|
+
//
|
|
191
|
+
// Rebuilt from all four either way: a partial object leaves the rest undefined, and
|
|
192
|
+
// `undefined + number` is NaN, which the mapper would pass on as a measurement of nothing.
|
|
193
|
+
if (typeof raw.cacheRead === 'number' ||
|
|
194
|
+
typeof raw.cacheCreate === 'number' ||
|
|
195
|
+
typeof raw.input === 'number' ||
|
|
196
|
+
typeof raw.output === 'number') {
|
|
197
|
+
return { input: n(raw.input), output: n(raw.output), cacheRead: n(raw.cacheRead), cacheCreate: n(raw.cacheCreate) };
|
|
198
|
+
}
|
|
199
|
+
if ('prompt_tokens' in raw || 'completion_tokens' in raw)
|
|
200
|
+
return fromOpenAIUsage(raw);
|
|
201
|
+
// Mistral's camelCase, claimed before the flat reader below. Nothing else spells its counters
|
|
202
|
+
// this way, so one numeric field identifies it - and without this the object falls through to
|
|
203
|
+
// `fromFlatTokenUsage`, which looks for `input_tokens`, finds none, and meters the turn at zero.
|
|
204
|
+
if (typeof raw.promptTokens === 'number' || typeof raw.completionTokens === 'number') {
|
|
205
|
+
return fromMistralUsage(raw);
|
|
206
|
+
}
|
|
207
|
+
// Any one of Gemini's counters identifies it - they share no name with the shapes above, and
|
|
208
|
+
// one is all a caller may send. Read off the component list so the two cannot drift, and keyed
|
|
209
|
+
// on a *numeric* value: `in` is true for a key whose value is `undefined`, which a payload
|
|
210
|
+
// built by spreading optional fields carries, and that read someone else's counts as zero.
|
|
211
|
+
if (exports.GEMINI_COMPONENT_METRICS.some((key) => typeof raw[key] === 'number'))
|
|
212
|
+
return fromGeminiUsage(raw);
|
|
213
|
+
// Ollama's two, read the same way and for the same reason as Gemini's. The recipe never arrives
|
|
214
|
+
// here - it calls `fromOllamaUsage` on the response root directly - but this function is public
|
|
215
|
+
// and the examples print what it returns, so without this branch an Ollama turn falls through to
|
|
216
|
+
// `fromFlatTokenUsage`, finds no `input_tokens`, and prints zeroes beside counters that metered
|
|
217
|
+
// correctly.
|
|
218
|
+
if (exports.OLLAMA_COMPONENT_METRICS.some((key) => typeof raw[key] === 'number'))
|
|
219
|
+
return fromOllamaUsage(raw);
|
|
220
|
+
if ('input_token_details' in raw)
|
|
221
|
+
return fromLangChainUsage(raw);
|
|
222
|
+
// Three shapes count `input_tokens`/`output_tokens` - the flat one, the Responses API and
|
|
223
|
+
// LangChain - so the fallback reads any of them happily. Both nested shapes are claimed above
|
|
224
|
+
// by the detail key they carry, because falling through to the flat reader would silently drop
|
|
225
|
+
// the cache detail and overstate the input it was hiding inside.
|
|
226
|
+
if ('input_tokens_details' in raw)
|
|
227
|
+
return fromResponsesUsage(raw);
|
|
228
|
+
return fromFlatTokenUsage(raw);
|
|
229
|
+
}
|
|
230
|
+
/** Nothing counted and nothing stated: an accumulator before its first round, not a round. */
|
|
231
|
+
const isNeutral = (usage) => usage.total === undefined && !usage.input && !usage.output && !usage.cacheRead && !usage.cacheCreate;
|
|
232
|
+
const addTokenUsage = (into, add) => {
|
|
233
|
+
// Summed across the rounds of one call, but only while every round states one. A round that
|
|
234
|
+
// stated none makes the call's total unstateable, and passing the other round's on as if it
|
|
235
|
+
// were the whole understates the call silently - the failure this file exists to avoid. An
|
|
236
|
+
// empty accumulator is not a round, so it does not count as one that said nothing.
|
|
237
|
+
const total = isNeutral(into)
|
|
238
|
+
? add.total
|
|
239
|
+
: isNeutral(add)
|
|
240
|
+
? into.total
|
|
241
|
+
: into.total !== undefined && add.total !== undefined
|
|
242
|
+
? into.total + add.total
|
|
243
|
+
: undefined;
|
|
244
|
+
return {
|
|
245
|
+
input: into.input + add.input,
|
|
246
|
+
output: into.output + add.output,
|
|
247
|
+
cacheRead: into.cacheRead + add.cacheRead,
|
|
248
|
+
cacheCreate: into.cacheCreate + add.cacheCreate,
|
|
249
|
+
...(total !== undefined ? { total } : {}),
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
exports.addTokenUsage = addTokenUsage;
|
|
253
|
+
// ---------------------------------------------------------------------------------------------
|
|
254
|
+
// The vendor's own names, for the reports a run sends by default.
|
|
255
|
+
//
|
|
256
|
+
// Explicit key lists, never a walk over the response. A blind walk would report `cost` (a float
|
|
257
|
+
// Stigg rejects) or `service_tier` (a string) - and worse, the day a vendor adds a counter the
|
|
258
|
+
// SDK would start reporting a feature the customer's catalog has never heard of, failing every
|
|
259
|
+
// call from then on. A vendor's changelog must not be able to inject feature ids into a bill.
|
|
260
|
+
//
|
|
261
|
+
// Every vendor reports a **total** as well as its breakdown, and the two deliberately overlap.
|
|
262
|
+
// The breakdown answers "what did this call spend it on" and the total answers "what did it
|
|
263
|
+
// cost"; only the customer's catalog knows which of those it bills, so both are sent and neither
|
|
264
|
+
// is chosen on their behalf. Pricing a total *and* a component underneath it bills those tokens
|
|
265
|
+
// twice - `normalizedMetrics` is the disjoint alternative for anyone who would rather not think
|
|
266
|
+
// about it, and the README's overlap table says which counters nest inside which.
|
|
267
|
+
//
|
|
268
|
+
// Where the vendor publishes a total it is read, never recomputed. Where it publishes none -
|
|
269
|
+
// Anthropic and TypeSafe - it is summed from counters that are disjoint, which is the only case
|
|
270
|
+
// where adding them up is the vendor's own arithmetic rather than ours. See `COMPUTED_TOTALS`.
|
|
271
|
+
// ---------------------------------------------------------------------------------------------
|
|
272
|
+
// Where a counter is read and what it ends up called are the same string for Anthropic and are
|
|
273
|
+
// not for OpenAI, which nests its cache detail. One list cannot be both, so the paths stay
|
|
274
|
+
// private to the surface that reads them and only the reported names are exported - those are
|
|
275
|
+
// what a mapper receives, and what Stigg meters. `metricNamesMatchPaths` holds the two in step.
|
|
276
|
+
/** Where to read each counter off an Anthropic response. Flat, so the paths are the names. */
|
|
277
|
+
const ANTHROPIC_PATHS = [
|
|
278
|
+
'input_tokens',
|
|
279
|
+
'output_tokens',
|
|
280
|
+
'cache_read_input_tokens',
|
|
281
|
+
'cache_creation_input_tokens',
|
|
282
|
+
];
|
|
283
|
+
/** TypeSafe reports two flat counters and nothing else, so the paths are the names again. */
|
|
284
|
+
const TYPESAFE_PATHS = ['input_tokens', 'output_tokens'];
|
|
285
|
+
/**
|
|
286
|
+
* The counters Gemini breaks a call down into. Flat, so the paths are the names.
|
|
287
|
+
*
|
|
288
|
+
* Kept apart from the reported list because these are also what *identifies* a Gemini usage
|
|
289
|
+
* object to `normalizeUsage`. A total is not identifying - more than one vendor has one - so
|
|
290
|
+
* recognising a payload by `totalTokenCount` would route an object that counted its tokens some
|
|
291
|
+
* other way into the Gemini reader, which finds none of these and meters it at zero.
|
|
292
|
+
*/
|
|
293
|
+
const GEMINI_COMPONENT_PATHS = [
|
|
294
|
+
'promptTokenCount',
|
|
295
|
+
'candidatesTokenCount',
|
|
296
|
+
'cachedContentTokenCount',
|
|
297
|
+
'thoughtsTokenCount',
|
|
298
|
+
'toolUsePromptTokenCount',
|
|
299
|
+
];
|
|
300
|
+
/** Those, plus the vendor's own total. */
|
|
301
|
+
const GEMINI_PATHS = [...GEMINI_COMPONENT_PATHS, 'totalTokenCount'];
|
|
302
|
+
/** Chat Completions keeps its cache detail one level down; the leaf is what gets reported. */
|
|
303
|
+
const OPENAI_CHAT_PATHS = [
|
|
304
|
+
'prompt_tokens',
|
|
305
|
+
'completion_tokens',
|
|
306
|
+
'prompt_tokens_details.cached_tokens',
|
|
307
|
+
'total_tokens',
|
|
308
|
+
];
|
|
309
|
+
/** The Responses API counts the same tokens under different names, nested the same way. */
|
|
310
|
+
const OPENAI_RESPONSES_PATHS = [
|
|
311
|
+
'input_tokens',
|
|
312
|
+
'output_tokens',
|
|
313
|
+
'input_tokens_details.cached_tokens',
|
|
314
|
+
'total_tokens',
|
|
315
|
+
];
|
|
316
|
+
/**
|
|
317
|
+
* The name a computed total is reported under, for the vendors that publish none of their own.
|
|
318
|
+
*
|
|
319
|
+
* `total_tokens` rather than a name of ours, because it is what OpenAI calls the same number and
|
|
320
|
+
* what a reader expects to find - but it is worth knowing which vendors state it and which get it
|
|
321
|
+
* from {@link COMPUTED_TOTALS}: Anthropic and TypeSafe publish no total at all, so theirs is the
|
|
322
|
+
* SDK's arithmetic over counters the vendor *did* state, not a figure off their dashboard.
|
|
323
|
+
*/
|
|
324
|
+
exports.TOTAL_TOKENS = 'total_tokens';
|
|
325
|
+
/** Anthropic's four counters. Disjoint - cache reads and writes are not inside `input_tokens`. */
|
|
326
|
+
exports.ANTHROPIC_COMPONENT_METRICS = [
|
|
327
|
+
'input_tokens',
|
|
328
|
+
'output_tokens',
|
|
329
|
+
'cache_read_input_tokens',
|
|
330
|
+
'cache_creation_input_tokens',
|
|
331
|
+
];
|
|
332
|
+
/** Those four, plus the total this SDK adds up for them. */
|
|
333
|
+
exports.ANTHROPIC_METRICS = [...exports.ANTHROPIC_COMPONENT_METRICS, exports.TOTAL_TOKENS];
|
|
334
|
+
/**
|
|
335
|
+
* Gemini's five. Two of them overlap in opposite directions and both are reported verbatim, the
|
|
336
|
+
* way every vendor's are, because a number published under a vendor's field name has to reconcile
|
|
337
|
+
* against that vendor's own dashboard: `cachedContentTokenCount` is already *inside*
|
|
338
|
+
* `promptTokenCount`, while `thoughtsTokenCount` and `toolUsePromptTokenCount` sit *outside* the
|
|
339
|
+
* two headline counters. Price accordingly - or price `normalizedMetrics` instead, which is
|
|
340
|
+
* disjoint by construction.
|
|
341
|
+
*/
|
|
342
|
+
exports.GEMINI_METRICS = GEMINI_PATHS;
|
|
343
|
+
/** What identifies a Gemini usage object: its breakdown, never its total. */
|
|
344
|
+
exports.GEMINI_COMPONENT_METRICS = GEMINI_COMPONENT_PATHS;
|
|
345
|
+
/**
|
|
346
|
+
* Chat Completions. `prompt_tokens` **includes** `cached_tokens`, so these two overlap - reported
|
|
347
|
+
* as the vendor states them, which is what reconciles against its invoice. Whoever prices
|
|
348
|
+
* `prompt_tokens` must know the cached ones are already inside it.
|
|
349
|
+
*/
|
|
350
|
+
exports.OPENAI_CHAT_METRICS = ['prompt_tokens', 'completion_tokens', 'cached_tokens', 'total_tokens'];
|
|
351
|
+
/** The Responses API, which overlaps the same way under its own names. */
|
|
352
|
+
exports.OPENAI_RESPONSES_METRICS = ['input_tokens', 'output_tokens', 'cached_tokens', 'total_tokens'];
|
|
353
|
+
/**
|
|
354
|
+
* Where Mistral's counters are **read** and what each is **reported** as - the only vocabulary
|
|
355
|
+
* here where those are different strings.
|
|
356
|
+
*
|
|
357
|
+
* Every other vendor is reported under the key it was read from, which is why `METRIC_PATHS` is a
|
|
358
|
+
* list and `metricName` just takes the last segment. Mistral's generated SDK renames the wire on
|
|
359
|
+
* the way in - the API sends `prompt_tokens`, the client hands you `promptTokens` - so following
|
|
360
|
+
* that convention would report a camelCase feature id that matches nothing on Mistral's own
|
|
361
|
+
* dashboard, and would differ from what the same account reports through an `OpenAI` client
|
|
362
|
+
* pointed at `api.mistral.ai/v1`. One account, one invoice, one vocabulary: the wire's.
|
|
363
|
+
*
|
|
364
|
+
* So the map runs read-key -> reported-name, and `pickRenamedMetrics` is what applies it.
|
|
365
|
+
*/
|
|
366
|
+
exports.MISTRAL_METRIC_NAME_MAP = {
|
|
367
|
+
promptTokens: 'prompt_tokens',
|
|
368
|
+
completionTokens: 'completion_tokens',
|
|
369
|
+
totalTokens: 'total_tokens',
|
|
370
|
+
};
|
|
371
|
+
/**
|
|
372
|
+
* Mistral's three, under the wire's own names.
|
|
373
|
+
*
|
|
374
|
+
* No cache counter: `UsageInfo` carries none. The cache and reasoning detail the v2 SDK models
|
|
375
|
+
* lives on a different type that chat responses do not use, so declaring it here would type a
|
|
376
|
+
* pricing table against a number that never arrives.
|
|
377
|
+
*/
|
|
378
|
+
exports.MISTRAL_METRICS = ['prompt_tokens', 'completion_tokens', 'total_tokens'];
|
|
379
|
+
/**
|
|
380
|
+
* TypeSafe's two. No cache counters and no cost: the API reports what it charged for and stops
|
|
381
|
+
* there, which was confirmed against the wire rather than inferred from the SDK's types.
|
|
382
|
+
*/
|
|
383
|
+
exports.TYPESAFE_COMPONENT_METRICS = ['input_tokens', 'output_tokens'];
|
|
384
|
+
/** Those two, plus the total this SDK adds up for them. */
|
|
385
|
+
exports.TYPESAFE_METRICS = [...exports.TYPESAFE_COMPONENT_METRICS, exports.TOTAL_TOKENS];
|
|
386
|
+
/**
|
|
387
|
+
* Ollama's two. Flat on the response root, so the paths are the names.
|
|
388
|
+
*
|
|
389
|
+
* No cache counter and no cost: Ollama is self-hosted, so there is no invoice behind these at all
|
|
390
|
+
* - they measure the customer's own capacity rather than a vendor's charge. The durations beside
|
|
391
|
+
* them on the response are nanoseconds, not tokens, and are deliberately absent from this list.
|
|
392
|
+
*/
|
|
393
|
+
exports.OLLAMA_COMPONENT_METRICS = ['prompt_eval_count', 'eval_count'];
|
|
394
|
+
/** Those two, plus the total this SDK adds up for them. */
|
|
395
|
+
exports.OLLAMA_METRICS = [...exports.OLLAMA_COMPONENT_METRICS, exports.TOTAL_TOKENS];
|
|
396
|
+
/** Both OpenAI surfaces, which is what one client can report across its two APIs. */
|
|
397
|
+
exports.OPENAI_METRICS = [
|
|
398
|
+
'prompt_tokens',
|
|
399
|
+
'completion_tokens',
|
|
400
|
+
'input_tokens',
|
|
401
|
+
'output_tokens',
|
|
402
|
+
'cached_tokens',
|
|
403
|
+
'total_tokens',
|
|
404
|
+
];
|
|
405
|
+
/** A gateway speaks the OpenAI shape and adds the price it charged, under its own field name. */
|
|
406
|
+
exports.GATEWAY_METRICS = [...exports.OPENAI_METRICS, 'cost'];
|
|
407
|
+
/**
|
|
408
|
+
* Bedrock's five, under its own names and with no price among them.
|
|
409
|
+
*
|
|
410
|
+
* Reported verbatim rather than translated into the OpenAI spelling, which is the rule everywhere
|
|
411
|
+
* except where one service is reachable through two different clients - see `sdk-usage.ts` for
|
|
412
|
+
* that exception. Bedrock is reached one way, so its own names are the honest ones, and a reader
|
|
413
|
+
* comparing a Stigg meter against an AWS bill is comparing like with like.
|
|
414
|
+
*
|
|
415
|
+
* No `cost`: Bedrock states no price on the response, which is the thing that most separates it
|
|
416
|
+
* from the other two gateways here. See the recipe for what that costs a run.
|
|
417
|
+
*/
|
|
418
|
+
exports.BEDROCK_PATHS = [
|
|
419
|
+
'inputTokens',
|
|
420
|
+
'outputTokens',
|
|
421
|
+
'cacheReadInputTokens',
|
|
422
|
+
'cacheWriteInputTokens',
|
|
423
|
+
'totalTokens',
|
|
424
|
+
];
|
|
425
|
+
/** Those five. Flat, so the paths are the names. */
|
|
426
|
+
exports.BEDROCK_METRICS = exports.BEDROCK_PATHS;
|
|
427
|
+
/** The leaf a dotted path reports under - `prompt_tokens_details.cached_tokens` -> `cached_tokens`. */
|
|
428
|
+
const metricName = (path) => path.split('.').pop();
|
|
429
|
+
exports.metricName = metricName;
|
|
430
|
+
/**
|
|
431
|
+
* Which counters are summed to produce a vendor's total, for the vendors that publish none.
|
|
432
|
+
*
|
|
433
|
+
* Only ever disjoint sets: Anthropic's four do not overlap and neither do TypeSafe's two, so the
|
|
434
|
+
* sum is the whole call and nothing is counted twice. A vendor whose counters overlap - Gemini's
|
|
435
|
+
* cached tokens sit inside its prompt count, OpenAI's inside theirs - is not in this table,
|
|
436
|
+
* because adding those up would be arithmetic the vendor never agreed to. Those vendors state a
|
|
437
|
+
* total themselves and it is read, not computed.
|
|
438
|
+
*/
|
|
439
|
+
exports.COMPUTED_TOTALS = {
|
|
440
|
+
anthropic: exports.ANTHROPIC_COMPONENT_METRICS,
|
|
441
|
+
typesafe: exports.TYPESAFE_COMPONENT_METRICS,
|
|
442
|
+
ollama: exports.OLLAMA_COMPONENT_METRICS,
|
|
443
|
+
};
|
|
444
|
+
/**
|
|
445
|
+
* Adds the computed total to a metrics object, for a vendor that states none of its own.
|
|
446
|
+
*
|
|
447
|
+
* Absent rather than zero when nothing was counted, like every other metric here: a call that
|
|
448
|
+
* measured nothing reports nothing, instead of a zero somebody has to interpret.
|
|
449
|
+
*/
|
|
450
|
+
function withComputedTotal(metrics, keys) {
|
|
451
|
+
const total = keys.reduce((sum, key) => sum + (metrics[key] ?? 0), 0);
|
|
452
|
+
return total > 0 ? { ...metrics, [exports.TOTAL_TOKENS]: total } : metrics;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Whether a path list and a name list agree, for the test that keeps them from drifting.
|
|
456
|
+
*
|
|
457
|
+
* Two lists describing one thing is the cost of separating where a counter lives from what it is
|
|
458
|
+
* called. This is what makes that cost a compile-and-test problem rather than a billing one.
|
|
459
|
+
*/
|
|
460
|
+
const metricNamesMatchPaths = (paths, names) => {
|
|
461
|
+
const fromPaths = [...new Set(paths.map(exports.metricName))];
|
|
462
|
+
return fromPaths.length === names.length && fromPaths.every((name) => names.includes(name));
|
|
463
|
+
};
|
|
464
|
+
exports.metricNamesMatchPaths = metricNamesMatchPaths;
|
|
465
|
+
/** The read paths, for the tests that hold them in step with the names above. */
|
|
466
|
+
exports.METRIC_PATHS = {
|
|
467
|
+
anthropic: ANTHROPIC_PATHS,
|
|
468
|
+
openaiChat: OPENAI_CHAT_PATHS,
|
|
469
|
+
openaiResponses: OPENAI_RESPONSES_PATHS,
|
|
470
|
+
typesafe: TYPESAFE_PATHS,
|
|
471
|
+
gemini: GEMINI_PATHS,
|
|
472
|
+
bedrock: exports.BEDROCK_PATHS,
|
|
473
|
+
ollama: exports.OLLAMA_COMPONENT_METRICS,
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* Reads the named keys off a vendor's usage object, dotted paths included.
|
|
477
|
+
*
|
|
478
|
+
* A nested key reports under its leaf - `prompt_tokens_details.cached_tokens` becomes
|
|
479
|
+
* `cached_tokens` - because the leaf is the vendor's own name for it and a feature id with a dot
|
|
480
|
+
* in it reads badly everywhere. Anything absent, fractional or not a number is skipped: a metric
|
|
481
|
+
* the vendor did not report is not a zero, and Stigg rejects fractions.
|
|
482
|
+
*/
|
|
483
|
+
function pickMetrics(usage, keys) {
|
|
484
|
+
if (!usage || typeof usage !== 'object')
|
|
485
|
+
return {};
|
|
486
|
+
const metrics = {};
|
|
487
|
+
for (const key of keys) {
|
|
488
|
+
const value = key.split('.').reduce((node, part) => (node == null ? node : node[part]), usage);
|
|
489
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
|
|
490
|
+
metrics[key.split('.').pop()] = value;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return metrics;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Reads each key of a read -> reported map off a usage object, reporting under the mapped name.
|
|
497
|
+
*
|
|
498
|
+
* The counterpart to {@link pickMetrics} for a vendor whose SDK spells its counters differently
|
|
499
|
+
* from its own wire. Same guards, and for the same reasons: anything absent, fractional or not a
|
|
500
|
+
* number is skipped, because a metric the vendor did not report is not a zero and Stigg rejects
|
|
501
|
+
* fractions. Flat keys only - no vendor needing this nests its counters, and a dotted read here
|
|
502
|
+
* would make the map ambiguous about which half of the arrow the dot belongs to.
|
|
503
|
+
*/
|
|
504
|
+
function pickRenamedMetrics(usage, map) {
|
|
505
|
+
if (!usage || typeof usage !== 'object')
|
|
506
|
+
return {};
|
|
507
|
+
const metrics = {};
|
|
508
|
+
for (const [key, name] of Object.entries(map)) {
|
|
509
|
+
const value = usage[key];
|
|
510
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0)
|
|
511
|
+
metrics[name] = value;
|
|
512
|
+
}
|
|
513
|
+
return metrics;
|
|
514
|
+
}
|
|
515
|
+
/** Accumulates metrics across a stream, which reports its counters in more than one chunk. */
|
|
516
|
+
const addMetrics = (into, add) => {
|
|
517
|
+
const summed = { ...into };
|
|
518
|
+
for (const [key, value] of Object.entries(add))
|
|
519
|
+
summed[key] = (summed[key] ?? 0) + value;
|
|
520
|
+
return summed;
|
|
521
|
+
};
|
|
522
|
+
exports.addMetrics = addMetrics;
|
|
523
|
+
/**
|
|
524
|
+
* The SDK's own four counters, under SDK-owned names, from the normalized measurement.
|
|
525
|
+
*
|
|
526
|
+
* The opt-out from vendor names, for an application running more than one provider: these are
|
|
527
|
+
* disjoint and mean the same thing whoever served the call, so one set of Stigg features covers
|
|
528
|
+
* every vendor. Pass it as a run's `metrics`.
|
|
529
|
+
*/
|
|
530
|
+
const normalizedMetrics = ({ usage }) => {
|
|
531
|
+
if (!usage)
|
|
532
|
+
return {};
|
|
533
|
+
const metrics = {};
|
|
534
|
+
if (usage.input > 0)
|
|
535
|
+
metrics.stigg_input_tokens = usage.input;
|
|
536
|
+
if (usage.output > 0)
|
|
537
|
+
metrics.stigg_output_tokens = usage.output;
|
|
538
|
+
if (usage.cacheRead > 0)
|
|
539
|
+
metrics.stigg_cache_read_tokens = usage.cacheRead;
|
|
540
|
+
if (usage.cacheCreate > 0)
|
|
541
|
+
metrics.stigg_cache_creation_tokens = usage.cacheCreate;
|
|
542
|
+
return metrics;
|
|
543
|
+
};
|
|
544
|
+
exports.normalizedMetrics = normalizedMetrics;
|
|
545
|
+
// Read paths, not reported names: OpenAI keeps its cache detail nested, so the name alone finds
|
|
546
|
+
// nothing there. Anthropic and TypeSafe share the flat shape and differ only in how much of it
|
|
547
|
+
// they fill in, so a proxy fronting either reads the same paths - but it still has to be able to
|
|
548
|
+
// say which, rather than being told to pass the other vendor's name and know why.
|
|
549
|
+
const METRIC_KEYS = {
|
|
550
|
+
anthropic: exports.METRIC_PATHS.anthropic,
|
|
551
|
+
typesafe: exports.METRIC_PATHS.typesafe,
|
|
552
|
+
openai: exports.METRIC_PATHS.openaiChat,
|
|
553
|
+
'openai-responses': exports.METRIC_PATHS.openaiResponses,
|
|
554
|
+
gemini: exports.METRIC_PATHS.gemini,
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Both views of a usage object a proxy observed, for the ingest path that has no recipe.
|
|
558
|
+
*
|
|
559
|
+
* The vendor is named rather than sniffed. `normalizeUsage` guesses from the object's own fields
|
|
560
|
+
* and says itself that Anthropic and the Responses API are ambiguous - both count `input_tokens`.
|
|
561
|
+
* A wrong guess would report to another meter entirely, which nothing downstream can undo - so
|
|
562
|
+
* an unnamed vendor reports nothing raw and leaves the normalized counters to a mapper.
|
|
563
|
+
*/
|
|
564
|
+
function observedMeasurement(usage, vendor) {
|
|
565
|
+
return {
|
|
566
|
+
metrics: vendor ? pickMetrics(usage, METRIC_KEYS[vendor]) : {},
|
|
567
|
+
usage: normalizeUsage(usage),
|
|
568
|
+
// Kept whole for an event to carry, and it does not depend on naming the vendor: a dimension
|
|
569
|
+
// is something a meter reads deliberately, not a feature id a wrong guess could invent.
|
|
570
|
+
// An array is an object and is not this: `Object.entries` would flatten it to `raw.0`,
|
|
571
|
+
// `raw.1`, ... - dimensions named after positions in a list nobody meant to send.
|
|
572
|
+
raw: usage && typeof usage === 'object' && !Array.isArray(usage) ? usage : undefined,
|
|
573
|
+
};
|
|
574
|
+
}
|