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