@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.
Files changed (98) hide show
  1. package/README.md +1296 -2
  2. package/dist/bindings/client/core/registry.d.ts +12 -0
  3. package/dist/bindings/client/core/registry.js +22 -0
  4. package/dist/bindings/client/core/types.d.ts +117 -0
  5. package/dist/bindings/client/core/types.js +13 -0
  6. package/dist/bindings/client/core/wrap-client.d.ts +6 -0
  7. package/dist/bindings/client/core/wrap-client.js +258 -0
  8. package/dist/bindings/client/gateways/bedrock/bedrock-recipe.d.ts +17 -0
  9. package/dist/bindings/client/gateways/bedrock/bedrock-recipe.js +134 -0
  10. package/dist/bindings/client/gateways/bedrock/types.d.ts +13 -0
  11. package/dist/bindings/client/gateways/bedrock/types.js +2 -0
  12. package/dist/bindings/client/gateways/litellm/litellm-recipe.d.ts +17 -0
  13. package/dist/bindings/client/gateways/litellm/litellm-recipe.js +171 -0
  14. package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
  15. package/dist/bindings/client/gateways/litellm/types.js +2 -0
  16. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +20 -0
  17. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +244 -0
  18. package/dist/bindings/client/gateways/openrouter/sdk-usage.d.ts +37 -0
  19. package/dist/bindings/client/gateways/openrouter/sdk-usage.js +63 -0
  20. package/dist/bindings/client/gateways/openrouter/types.d.ts +78 -0
  21. package/dist/bindings/client/gateways/openrouter/types.js +2 -0
  22. package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +34 -0
  23. package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +169 -0
  24. package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
  25. package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
  26. package/dist/bindings/client/providers/groq/groq-recipe.d.ts +20 -0
  27. package/dist/bindings/client/providers/groq/groq-recipe.js +101 -0
  28. package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +22 -0
  29. package/dist/bindings/client/providers/mistral/mistral-recipe.js +114 -0
  30. package/dist/bindings/client/providers/mistral/types.d.ts +38 -0
  31. package/dist/bindings/client/providers/mistral/types.js +2 -0
  32. package/dist/bindings/client/providers/ollama/ollama-recipe.d.ts +31 -0
  33. package/dist/bindings/client/providers/ollama/ollama-recipe.js +107 -0
  34. package/dist/bindings/client/providers/ollama/types.d.ts +24 -0
  35. package/dist/bindings/client/providers/ollama/types.js +2 -0
  36. package/dist/bindings/client/providers/openai/openai-recipe.d.ts +71 -0
  37. package/dist/bindings/client/providers/openai/openai-recipe.js +122 -0
  38. package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +44 -0
  39. package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +83 -0
  40. package/dist/bindings/proxy/headers.d.ts +6 -0
  41. package/dist/bindings/proxy/headers.js +12 -0
  42. package/dist/bindings/proxy/index.d.ts +21 -0
  43. package/dist/bindings/proxy/index.js +18 -0
  44. package/dist/bindings/proxy/types.d.ts +8 -0
  45. package/dist/bindings/proxy/types.js +2 -0
  46. package/dist/bindings/shared/estimate.d.ts +27 -0
  47. package/dist/bindings/shared/estimate.js +38 -0
  48. package/dist/bindings/shared/meter-stream.d.ts +18 -0
  49. package/dist/bindings/shared/meter-stream.js +151 -0
  50. package/dist/bindings/shared/token-usage.d.ts +240 -0
  51. package/dist/bindings/shared/token-usage.js +574 -0
  52. package/dist/bindings/shared/types.d.ts +129 -0
  53. package/dist/bindings/shared/types.js +2 -0
  54. package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
  55. package/dist/bindings/shared/weighted-metrics.js +47 -0
  56. package/dist/config.d.ts +145 -0
  57. package/dist/config.js +99 -0
  58. package/dist/core/context.d.ts +160 -0
  59. package/dist/core/context.js +221 -0
  60. package/dist/core/dimensions.d.ts +22 -0
  61. package/dist/core/dimensions.js +47 -0
  62. package/dist/core/hooks.d.ts +21 -0
  63. package/dist/core/hooks.js +47 -0
  64. package/dist/core/session.d.ts +190 -0
  65. package/dist/core/session.js +448 -0
  66. package/dist/decision-text.d.ts +15 -0
  67. package/dist/decision-text.js +54 -0
  68. package/dist/engine/composite-engine.d.ts +72 -0
  69. package/dist/engine/composite-engine.js +135 -0
  70. package/dist/engine/create-engine.d.ts +16 -0
  71. package/dist/engine/create-engine.js +93 -0
  72. package/dist/engine/decision.d.ts +42 -0
  73. package/dist/engine/decision.js +135 -0
  74. package/dist/engine/engine.d.ts +166 -0
  75. package/dist/engine/engine.js +11 -0
  76. package/dist/engine/node-sdk-engine.d.ts +125 -0
  77. package/dist/engine/node-sdk-engine.js +262 -0
  78. package/dist/engine/report.d.ts +106 -0
  79. package/dist/engine/report.js +188 -0
  80. package/dist/engine/rest-governance-engine.d.ts +47 -0
  81. package/dist/engine/rest-governance-engine.js +220 -0
  82. package/dist/errors.d.ts +86 -0
  83. package/dist/errors.js +161 -0
  84. package/dist/headers.d.ts +40 -0
  85. package/dist/headers.js +82 -0
  86. package/dist/index.d.ts +57 -0
  87. package/dist/index.js +106 -0
  88. package/dist/internal.d.ts +33 -0
  89. package/dist/internal.js +87 -0
  90. package/dist/logger.d.ts +15 -0
  91. package/dist/logger.js +20 -0
  92. package/dist/sdk-identity.d.ts +36 -0
  93. package/dist/sdk-identity.js +43 -0
  94. package/dist/stigg-agent-sdk.d.ts +300 -0
  95. package/dist/stigg-agent-sdk.js +656 -0
  96. package/dist/types.d.ts +642 -0
  97. package/dist/types.js +147 -0
  98. 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
+ }