@stigg/agent-sdk-js 0.0.0-stage → 1.0.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 (82) hide show
  1. package/README.md +1147 -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/litellm/litellm-recipe.d.ts +17 -0
  9. package/dist/bindings/client/gateways/litellm/litellm-recipe.js +171 -0
  10. package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
  11. package/dist/bindings/client/gateways/litellm/types.js +2 -0
  12. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +15 -0
  13. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +141 -0
  14. package/dist/bindings/client/gateways/openrouter/types.d.ts +29 -0
  15. package/dist/bindings/client/gateways/openrouter/types.js +2 -0
  16. package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +34 -0
  17. package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +91 -0
  18. package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
  19. package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
  20. package/dist/bindings/client/providers/openai/openai-recipe.d.ts +43 -0
  21. package/dist/bindings/client/providers/openai/openai-recipe.js +80 -0
  22. package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +33 -0
  23. package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +73 -0
  24. package/dist/bindings/proxy/headers.d.ts +6 -0
  25. package/dist/bindings/proxy/headers.js +12 -0
  26. package/dist/bindings/proxy/index.d.ts +21 -0
  27. package/dist/bindings/proxy/index.js +18 -0
  28. package/dist/bindings/proxy/types.d.ts +8 -0
  29. package/dist/bindings/proxy/types.js +2 -0
  30. package/dist/bindings/shared/estimate.d.ts +27 -0
  31. package/dist/bindings/shared/estimate.js +38 -0
  32. package/dist/bindings/shared/meter-stream.d.ts +18 -0
  33. package/dist/bindings/shared/meter-stream.js +151 -0
  34. package/dist/bindings/shared/token-usage.d.ts +148 -0
  35. package/dist/bindings/shared/token-usage.js +414 -0
  36. package/dist/bindings/shared/types.d.ts +79 -0
  37. package/dist/bindings/shared/types.js +2 -0
  38. package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
  39. package/dist/bindings/shared/weighted-metrics.js +44 -0
  40. package/dist/config.d.ts +136 -0
  41. package/dist/config.js +97 -0
  42. package/dist/core/context.d.ts +151 -0
  43. package/dist/core/context.js +154 -0
  44. package/dist/core/dimensions.d.ts +22 -0
  45. package/dist/core/dimensions.js +47 -0
  46. package/dist/core/hooks.d.ts +21 -0
  47. package/dist/core/hooks.js +47 -0
  48. package/dist/core/session.d.ts +161 -0
  49. package/dist/core/session.js +403 -0
  50. package/dist/decision-text.d.ts +15 -0
  51. package/dist/decision-text.js +54 -0
  52. package/dist/engine/composite-engine.d.ts +72 -0
  53. package/dist/engine/composite-engine.js +135 -0
  54. package/dist/engine/create-engine.d.ts +16 -0
  55. package/dist/engine/create-engine.js +93 -0
  56. package/dist/engine/decision.d.ts +37 -0
  57. package/dist/engine/decision.js +128 -0
  58. package/dist/engine/engine.d.ts +166 -0
  59. package/dist/engine/engine.js +11 -0
  60. package/dist/engine/node-sdk-engine.d.ts +125 -0
  61. package/dist/engine/node-sdk-engine.js +262 -0
  62. package/dist/engine/report.d.ts +106 -0
  63. package/dist/engine/report.js +185 -0
  64. package/dist/engine/rest-governance-engine.d.ts +47 -0
  65. package/dist/engine/rest-governance-engine.js +220 -0
  66. package/dist/errors.d.ts +78 -0
  67. package/dist/errors.js +151 -0
  68. package/dist/headers.d.ts +27 -0
  69. package/dist/headers.js +63 -0
  70. package/dist/index.d.ts +48 -0
  71. package/dist/index.js +85 -0
  72. package/dist/internal.d.ts +33 -0
  73. package/dist/internal.js +81 -0
  74. package/dist/logger.d.ts +15 -0
  75. package/dist/logger.js +20 -0
  76. package/dist/sdk-identity.d.ts +36 -0
  77. package/dist/sdk-identity.js +43 -0
  78. package/dist/stigg-agent-sdk.d.ts +242 -0
  79. package/dist/stigg-agent-sdk.js +594 -0
  80. package/dist/types.d.ts +531 -0
  81. package/dist/types.js +97 -0
  82. package/package.json +80 -3
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Provider usage objects -> the SDK's neutral `TokenUsage`.
3
+ *
4
+ * The inputs are typed structurally rather than against each vendor's SDK, so this handful of
5
+ * readers covers a direct client, a gateway response and an orchestrator callback - and adding a
6
+ * provider whose response happens to match costs nothing. Hence the shape-named reader below:
7
+ * `fromFlatTokenUsage` is read by every vendor that counts that way, not by one of them.
8
+ */
9
+ import { type Measurement, type Metrics, type MetricsMapper, type TokenUsage } from '../../types';
10
+ import type { FlatTokenUsage, GeminiLikeUsage, LangChainLikeUsage, OpenAILikeUsage, ResponsesLikeUsage } from './types';
11
+ export declare function fromFlatTokenUsage(usage: FlatTokenUsage | null | undefined): TokenUsage;
12
+ export declare function fromOpenAIUsage(usage: OpenAILikeUsage | null | undefined): TokenUsage;
13
+ /**
14
+ * OpenAI's Responses API, which counts the same tokens under different names - `input_tokens`
15
+ * rather than `prompt_tokens`, and the cache detail one level along. Reading it with the Chat
16
+ * Completions extractor finds none of them and meters the call at zero.
17
+ */
18
+ export declare function fromResponsesUsage(usage: ResponsesLikeUsage | null | undefined): TokenUsage;
19
+ export declare function fromLangChainUsage(usage: LangChainLikeUsage | null | undefined): TokenUsage;
20
+ /**
21
+ * Gemini's `usageMetadata`, whose two overlaps run in opposite directions - see `GeminiLikeUsage`
22
+ * for the vendor's own wording on each.
23
+ *
24
+ * The arithmetic is checked by the one invariant the vendor publishes: the four counters below
25
+ * add back up to `totalTokenCount`, which is why neither overlap may be read the other way.
26
+ */
27
+ export declare function fromGeminiUsage(usage: GeminiLikeUsage | null | undefined): TokenUsage;
28
+ /**
29
+ * Any of the three shapes above, recognised by its own fields. Vendors disagree on names but not
30
+ * enough to be ambiguous, so the caller can pass whatever the response carried.
31
+ */
32
+ export declare function normalizeUsage(usage: unknown): TokenUsage;
33
+ export declare const addTokenUsage: (into: TokenUsage, add: TokenUsage) => TokenUsage;
34
+ /**
35
+ * The name a computed total is reported under, for the vendors that publish none of their own.
36
+ *
37
+ * `total_tokens` rather than a name of ours, because it is what OpenAI calls the same number and
38
+ * what a reader expects to find - but it is worth knowing which vendors state it and which get it
39
+ * from {@link COMPUTED_TOTALS}: Anthropic and TypeSafe publish no total at all, so theirs is the
40
+ * SDK's arithmetic over counters the vendor *did* state, not a figure off their dashboard.
41
+ */
42
+ export declare const TOTAL_TOKENS = "total_tokens";
43
+ /** Anthropic's four counters. Disjoint - cache reads and writes are not inside `input_tokens`. */
44
+ export declare const ANTHROPIC_COMPONENT_METRICS: readonly ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"];
45
+ /** Those four, plus the total this SDK adds up for them. */
46
+ export declare const ANTHROPIC_METRICS: readonly ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "total_tokens"];
47
+ /**
48
+ * Gemini's five. Two of them overlap in opposite directions and both are reported verbatim, the
49
+ * way every vendor's are, because a number published under a vendor's field name has to reconcile
50
+ * against that vendor's own dashboard: `cachedContentTokenCount` is already *inside*
51
+ * `promptTokenCount`, while `thoughtsTokenCount` and `toolUsePromptTokenCount` sit *outside* the
52
+ * two headline counters. Price accordingly - or price `normalizedMetrics` instead, which is
53
+ * disjoint by construction.
54
+ */
55
+ export declare const GEMINI_METRICS: readonly ["promptTokenCount", "candidatesTokenCount", "cachedContentTokenCount", "thoughtsTokenCount", "toolUsePromptTokenCount", "totalTokenCount"];
56
+ /** What identifies a Gemini usage object: its breakdown, never its total. */
57
+ export declare const GEMINI_COMPONENT_METRICS: readonly ["promptTokenCount", "candidatesTokenCount", "cachedContentTokenCount", "thoughtsTokenCount", "toolUsePromptTokenCount"];
58
+ /**
59
+ * Chat Completions. `prompt_tokens` **includes** `cached_tokens`, so these two overlap - reported
60
+ * as the vendor states them, which is what reconciles against its invoice. Whoever prices
61
+ * `prompt_tokens` must know the cached ones are already inside it.
62
+ */
63
+ export declare const OPENAI_CHAT_METRICS: readonly ["prompt_tokens", "completion_tokens", "cached_tokens", "total_tokens"];
64
+ /** The Responses API, which overlaps the same way under its own names. */
65
+ export declare const OPENAI_RESPONSES_METRICS: readonly ["input_tokens", "output_tokens", "cached_tokens", "total_tokens"];
66
+ /**
67
+ * TypeSafe's two. No cache counters and no cost: the API reports what it charged for and stops
68
+ * there, which was confirmed against the wire rather than inferred from the SDK's types.
69
+ */
70
+ export declare const TYPESAFE_COMPONENT_METRICS: readonly ["input_tokens", "output_tokens"];
71
+ /** Those two, plus the total this SDK adds up for them. */
72
+ export declare const TYPESAFE_METRICS: readonly ["input_tokens", "output_tokens", "total_tokens"];
73
+ /** Both OpenAI surfaces, which is what one client can report across its two APIs. */
74
+ export declare const OPENAI_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens"];
75
+ /** A gateway speaks the OpenAI shape and adds the price it charged, under its own field name. */
76
+ export declare const GATEWAY_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens", "cost"];
77
+ /** The leaf a dotted path reports under - `prompt_tokens_details.cached_tokens` -> `cached_tokens`. */
78
+ export declare const metricName: (path: string) => string;
79
+ /**
80
+ * Which counters are summed to produce a vendor's total, for the vendors that publish none.
81
+ *
82
+ * Only ever disjoint sets: Anthropic's four do not overlap and neither do TypeSafe's two, so the
83
+ * sum is the whole call and nothing is counted twice. A vendor whose counters overlap - Gemini's
84
+ * cached tokens sit inside its prompt count, OpenAI's inside theirs - is not in this table,
85
+ * because adding those up would be arithmetic the vendor never agreed to. Those vendors state a
86
+ * total themselves and it is read, not computed.
87
+ */
88
+ export declare const COMPUTED_TOTALS: Record<string, readonly string[]>;
89
+ /**
90
+ * Adds the computed total to a metrics object, for a vendor that states none of its own.
91
+ *
92
+ * Absent rather than zero when nothing was counted, like every other metric here: a call that
93
+ * measured nothing reports nothing, instead of a zero somebody has to interpret.
94
+ */
95
+ export declare function withComputedTotal(metrics: Metrics, keys: readonly string[]): Metrics;
96
+ /**
97
+ * Whether a path list and a name list agree, for the test that keeps them from drifting.
98
+ *
99
+ * Two lists describing one thing is the cost of separating where a counter lives from what it is
100
+ * called. This is what makes that cost a compile-and-test problem rather than a billing one.
101
+ */
102
+ export declare const metricNamesMatchPaths: (paths: readonly string[], names: readonly string[]) => boolean;
103
+ /** The read paths, for the tests that hold them in step with the names above. */
104
+ export declare const METRIC_PATHS: {
105
+ readonly anthropic: readonly ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"];
106
+ readonly openaiChat: readonly ["prompt_tokens", "completion_tokens", "prompt_tokens_details.cached_tokens", "total_tokens"];
107
+ readonly openaiResponses: readonly ["input_tokens", "output_tokens", "input_tokens_details.cached_tokens", "total_tokens"];
108
+ readonly typesafe: readonly ["input_tokens", "output_tokens"];
109
+ readonly gemini: readonly ["promptTokenCount", "candidatesTokenCount", "cachedContentTokenCount", "thoughtsTokenCount", "toolUsePromptTokenCount", "totalTokenCount"];
110
+ };
111
+ /**
112
+ * Reads the named keys off a vendor's usage object, dotted paths included.
113
+ *
114
+ * A nested key reports under its leaf - `prompt_tokens_details.cached_tokens` becomes
115
+ * `cached_tokens` - because the leaf is the vendor's own name for it and a feature id with a dot
116
+ * in it reads badly everywhere. Anything absent, fractional or not a number is skipped: a metric
117
+ * the vendor did not report is not a zero, and Stigg rejects fractions.
118
+ */
119
+ export declare function pickMetrics(usage: unknown, keys: readonly string[]): Metrics;
120
+ /** Accumulates metrics across a stream, which reports its counters in more than one chunk. */
121
+ export declare const addMetrics: (into: Metrics, add: Metrics) => Metrics;
122
+ /**
123
+ * The SDK's own four counters, under SDK-owned names, from the normalized measurement.
124
+ *
125
+ * The opt-out from vendor names, for an application running more than one provider: these are
126
+ * disjoint and mean the same thing whoever served the call, so one set of Stigg features covers
127
+ * every vendor. Pass it as a run's `metrics`.
128
+ */
129
+ export declare const normalizedMetrics: MetricsMapper;
130
+ /** The shapes a gateway can be told it observed, since guessing reports to the wrong features. */
131
+ /**
132
+ * The shapes a proxy can be told it observed.
133
+ *
134
+ * Named for the vendor rather than for the shape, because a proxy operator knows what is behind
135
+ * them and not what its usage object looks like - but it is the shape that is being selected,
136
+ * which is why OpenAI appears twice. Its two APIs spell the same counters differently, and
137
+ * `openai-responses` is that surface rather than a vendor of its own.
138
+ */
139
+ export type ObservedVendor = 'anthropic' | 'typesafe' | 'openai' | 'openai-responses' | 'gemini';
140
+ /**
141
+ * Both views of a usage object a proxy observed, for the ingest path that has no recipe.
142
+ *
143
+ * The vendor is named rather than sniffed. `normalizeUsage` guesses from the object's own fields
144
+ * and says itself that Anthropic and the Responses API are ambiguous - both count `input_tokens`.
145
+ * A wrong guess would report to another meter entirely, which nothing downstream can undo - so
146
+ * an unnamed vendor reports nothing raw and leaves the normalized counters to a mapper.
147
+ */
148
+ export declare function observedMeasurement(usage: unknown, vendor?: ObservedVendor): Measurement;
@@ -0,0 +1,414 @@
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.GATEWAY_METRICS = exports.OPENAI_METRICS = exports.TYPESAFE_METRICS = exports.TYPESAFE_COMPONENT_METRICS = 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.fromLangChainUsage = fromLangChainUsage;
8
+ exports.fromGeminiUsage = fromGeminiUsage;
9
+ exports.normalizeUsage = normalizeUsage;
10
+ exports.withComputedTotal = withComputedTotal;
11
+ exports.pickMetrics = pickMetrics;
12
+ exports.observedMeasurement = observedMeasurement;
13
+ /**
14
+ * Provider usage objects -> the SDK's neutral `TokenUsage`.
15
+ *
16
+ * The inputs are typed structurally rather than against each vendor's SDK, so this handful of
17
+ * readers covers a direct client, a gateway response and an orchestrator callback - and adding a
18
+ * provider whose response happens to match costs nothing. Hence the shape-named reader below:
19
+ * `fromFlatTokenUsage` is read by every vendor that counts that way, not by one of them.
20
+ */
21
+ const types_1 = require("../../types");
22
+ const n = (value) => (typeof value === 'number' && value > 0 ? value : 0);
23
+ function fromFlatTokenUsage(usage) {
24
+ if (!usage)
25
+ return (0, types_1.emptyTokenUsage)();
26
+ const counted = {
27
+ input: n(usage.input_tokens),
28
+ output: n(usage.output_tokens),
29
+ cacheRead: n(usage.cache_read_input_tokens),
30
+ cacheCreate: n(usage.cache_creation_input_tokens),
31
+ };
32
+ // No vendor on this shape publishes a total, and these four are disjoint - cache reads and
33
+ // writes sit beside the input count rather than inside it - so their sum is the whole call.
34
+ return { ...counted, ...withTotal(counted.input + counted.output + counted.cacheRead + counted.cacheCreate) };
35
+ }
36
+ /** `{ total }` when there is one to state, and nothing at all when there is not. */
37
+ const withTotal = (total) => (total > 0 ? { total } : {});
38
+ function fromOpenAIUsage(usage) {
39
+ if (!usage)
40
+ return (0, types_1.emptyTokenUsage)();
41
+ const cacheRead = n(usage.prompt_tokens_details?.cached_tokens);
42
+ return {
43
+ // OpenAI states its own, and a stated total beats one reassembled from the parts.
44
+ ...withTotal(n(usage.total_tokens)),
45
+ // OpenAI counts cached prompt tokens inside prompt_tokens, so subtract to avoid double-count.
46
+ input: Math.max(0, n(usage.prompt_tokens) - cacheRead),
47
+ output: n(usage.completion_tokens),
48
+ cacheRead,
49
+ cacheCreate: n(usage.prompt_tokens_details?.cache_write_tokens),
50
+ };
51
+ }
52
+ /**
53
+ * OpenAI's Responses API, which counts the same tokens under different names - `input_tokens`
54
+ * rather than `prompt_tokens`, and the cache detail one level along. Reading it with the Chat
55
+ * Completions extractor finds none of them and meters the call at zero.
56
+ */
57
+ function fromResponsesUsage(usage) {
58
+ if (!usage)
59
+ return (0, types_1.emptyTokenUsage)();
60
+ const cacheRead = n(usage.input_tokens_details?.cached_tokens);
61
+ return {
62
+ ...withTotal(n(usage.total_tokens)),
63
+ // As with chat completions, cached tokens are counted inside the input total.
64
+ input: Math.max(0, n(usage.input_tokens) - cacheRead),
65
+ output: n(usage.output_tokens),
66
+ cacheRead,
67
+ // The surface reports no cache writes of its own.
68
+ cacheCreate: 0,
69
+ };
70
+ }
71
+ function fromLangChainUsage(usage) {
72
+ if (!usage)
73
+ return (0, types_1.emptyTokenUsage)();
74
+ const cacheRead = n(usage.input_token_details?.cache_read);
75
+ const cacheCreate = n(usage.input_token_details?.cache_creation);
76
+ return {
77
+ input: Math.max(0, n(usage.input_tokens) - cacheRead - cacheCreate),
78
+ output: n(usage.output_tokens),
79
+ cacheRead,
80
+ cacheCreate,
81
+ };
82
+ }
83
+ /**
84
+ * Gemini's `usageMetadata`, whose two overlaps run in opposite directions - see `GeminiLikeUsage`
85
+ * for the vendor's own wording on each.
86
+ *
87
+ * The arithmetic is checked by the one invariant the vendor publishes: the four counters below
88
+ * add back up to `totalTokenCount`, which is why neither overlap may be read the other way.
89
+ */
90
+ function fromGeminiUsage(usage) {
91
+ if (!usage)
92
+ return (0, types_1.emptyTokenUsage)();
93
+ const cacheRead = n(usage.cachedContentTokenCount);
94
+ return {
95
+ // Gemini's own, under its own name. Never recomputed from the counters below: they overlap,
96
+ // and the vendor's figure is the one its invoice will show.
97
+ ...withTotal(n(usage.totalTokenCount)),
98
+ // Cached content is counted inside the prompt total, so subtract it rather than bill it
99
+ // twice. Tool-use prompt tokens are not inside it - `totalTokenCount` adds them alongside -
100
+ // so they are input this would otherwise drop entirely.
101
+ input: Math.max(0, n(usage.promptTokenCount) - cacheRead) + n(usage.toolUsePromptTokenCount),
102
+ // Thinking tokens are billed as output and counted beside the candidates, not within them.
103
+ output: n(usage.candidatesTokenCount) + n(usage.thoughtsTokenCount),
104
+ cacheRead,
105
+ // Nothing to read: an explicit cache is written by `caches.create`, a separate call whose own
106
+ // response carries the token count for it. A generation reports only what it read.
107
+ cacheCreate: 0,
108
+ };
109
+ }
110
+ /**
111
+ * Any of the three shapes above, recognised by its own fields. Vendors disagree on names but not
112
+ * enough to be ambiguous, so the caller can pass whatever the response carried.
113
+ */
114
+ function normalizeUsage(usage) {
115
+ if (!usage || typeof usage !== 'object')
116
+ return (0, types_1.emptyTokenUsage)();
117
+ const raw = usage;
118
+ // Already normalized - a caller doing its own accounting. Recognised by any of the four, not
119
+ // only the cache pair: an application that counts input and output and has no cache to report
120
+ // used to fall past this to the flat vendor reader, which looks for `input_tokens` and finds
121
+ // none - so `{ input: 600, output: 400 }` normalized to zeroes and metered the turn as nothing.
122
+ //
123
+ // Rebuilt from all four either way: a partial object leaves the rest undefined, and
124
+ // `undefined + number` is NaN, which the mapper would pass on as a measurement of nothing.
125
+ if (typeof raw.cacheRead === 'number' ||
126
+ typeof raw.cacheCreate === 'number' ||
127
+ typeof raw.input === 'number' ||
128
+ typeof raw.output === 'number') {
129
+ return { input: n(raw.input), output: n(raw.output), cacheRead: n(raw.cacheRead), cacheCreate: n(raw.cacheCreate) };
130
+ }
131
+ if ('prompt_tokens' in raw || 'completion_tokens' in raw)
132
+ return fromOpenAIUsage(raw);
133
+ // Any one of Gemini's counters identifies it - they share no name with the shapes above, and
134
+ // one is all a caller may send. Read off the component list so the two cannot drift, and keyed
135
+ // on a *numeric* value: `in` is true for a key whose value is `undefined`, which a payload
136
+ // built by spreading optional fields carries, and that read someone else's counts as zero.
137
+ if (exports.GEMINI_COMPONENT_METRICS.some((key) => typeof raw[key] === 'number'))
138
+ return fromGeminiUsage(raw);
139
+ if ('input_token_details' in raw)
140
+ return fromLangChainUsage(raw);
141
+ // Three shapes count `input_tokens`/`output_tokens` - the flat one, the Responses API and
142
+ // LangChain - so the fallback reads any of them happily. Both nested shapes are claimed above
143
+ // by the detail key they carry, because falling through to the flat reader would silently drop
144
+ // the cache detail and overstate the input it was hiding inside.
145
+ if ('input_tokens_details' in raw)
146
+ return fromResponsesUsage(raw);
147
+ return fromFlatTokenUsage(raw);
148
+ }
149
+ /** Nothing counted and nothing stated: an accumulator before its first round, not a round. */
150
+ const isNeutral = (usage) => usage.total === undefined && !usage.input && !usage.output && !usage.cacheRead && !usage.cacheCreate;
151
+ const addTokenUsage = (into, add) => {
152
+ // Summed across the rounds of one call, but only while every round states one. A round that
153
+ // stated none makes the call's total unstateable, and passing the other round's on as if it
154
+ // were the whole understates the call silently - the failure this file exists to avoid. An
155
+ // empty accumulator is not a round, so it does not count as one that said nothing.
156
+ const total = isNeutral(into)
157
+ ? add.total
158
+ : isNeutral(add)
159
+ ? into.total
160
+ : into.total !== undefined && add.total !== undefined
161
+ ? into.total + add.total
162
+ : undefined;
163
+ return {
164
+ input: into.input + add.input,
165
+ output: into.output + add.output,
166
+ cacheRead: into.cacheRead + add.cacheRead,
167
+ cacheCreate: into.cacheCreate + add.cacheCreate,
168
+ ...(total !== undefined ? { total } : {}),
169
+ };
170
+ };
171
+ exports.addTokenUsage = addTokenUsage;
172
+ // ---------------------------------------------------------------------------------------------
173
+ // The vendor's own names, for the reports a run sends by default.
174
+ //
175
+ // Explicit key lists, never a walk over the response. A blind walk would report `cost` (a float
176
+ // Stigg rejects) or `service_tier` (a string) - and worse, the day a vendor adds a counter the
177
+ // SDK would start reporting a feature the customer's catalog has never heard of, failing every
178
+ // call from then on. A vendor's changelog must not be able to inject feature ids into a bill.
179
+ //
180
+ // Every vendor reports a **total** as well as its breakdown, and the two deliberately overlap.
181
+ // The breakdown answers "what did this call spend it on" and the total answers "what did it
182
+ // cost"; only the customer's catalog knows which of those it bills, so both are sent and neither
183
+ // is chosen on their behalf. Pricing a total *and* a component underneath it bills those tokens
184
+ // twice - `normalizedMetrics` is the disjoint alternative for anyone who would rather not think
185
+ // about it, and the README's overlap table says which counters nest inside which.
186
+ //
187
+ // Where the vendor publishes a total it is read, never recomputed. Where it publishes none -
188
+ // Anthropic and TypeSafe - it is summed from counters that are disjoint, which is the only case
189
+ // where adding them up is the vendor's own arithmetic rather than ours. See `COMPUTED_TOTALS`.
190
+ // ---------------------------------------------------------------------------------------------
191
+ // Where a counter is read and what it ends up called are the same string for Anthropic and are
192
+ // not for OpenAI, which nests its cache detail. One list cannot be both, so the paths stay
193
+ // private to the surface that reads them and only the reported names are exported - those are
194
+ // what a mapper receives, and what Stigg meters. `metricNamesMatchPaths` holds the two in step.
195
+ /** Where to read each counter off an Anthropic response. Flat, so the paths are the names. */
196
+ const ANTHROPIC_PATHS = [
197
+ 'input_tokens',
198
+ 'output_tokens',
199
+ 'cache_read_input_tokens',
200
+ 'cache_creation_input_tokens',
201
+ ];
202
+ /** TypeSafe reports two flat counters and nothing else, so the paths are the names again. */
203
+ const TYPESAFE_PATHS = ['input_tokens', 'output_tokens'];
204
+ /**
205
+ * The counters Gemini breaks a call down into. Flat, so the paths are the names.
206
+ *
207
+ * Kept apart from the reported list because these are also what *identifies* a Gemini usage
208
+ * object to `normalizeUsage`. A total is not identifying - more than one vendor has one - so
209
+ * recognising a payload by `totalTokenCount` would route an object that counted its tokens some
210
+ * other way into the Gemini reader, which finds none of these and meters it at zero.
211
+ */
212
+ const GEMINI_COMPONENT_PATHS = [
213
+ 'promptTokenCount',
214
+ 'candidatesTokenCount',
215
+ 'cachedContentTokenCount',
216
+ 'thoughtsTokenCount',
217
+ 'toolUsePromptTokenCount',
218
+ ];
219
+ /** Those, plus the vendor's own total. */
220
+ const GEMINI_PATHS = [...GEMINI_COMPONENT_PATHS, 'totalTokenCount'];
221
+ /** Chat Completions keeps its cache detail one level down; the leaf is what gets reported. */
222
+ const OPENAI_CHAT_PATHS = [
223
+ 'prompt_tokens',
224
+ 'completion_tokens',
225
+ 'prompt_tokens_details.cached_tokens',
226
+ 'total_tokens',
227
+ ];
228
+ /** The Responses API counts the same tokens under different names, nested the same way. */
229
+ const OPENAI_RESPONSES_PATHS = [
230
+ 'input_tokens',
231
+ 'output_tokens',
232
+ 'input_tokens_details.cached_tokens',
233
+ 'total_tokens',
234
+ ];
235
+ /**
236
+ * The name a computed total is reported under, for the vendors that publish none of their own.
237
+ *
238
+ * `total_tokens` rather than a name of ours, because it is what OpenAI calls the same number and
239
+ * what a reader expects to find - but it is worth knowing which vendors state it and which get it
240
+ * from {@link COMPUTED_TOTALS}: Anthropic and TypeSafe publish no total at all, so theirs is the
241
+ * SDK's arithmetic over counters the vendor *did* state, not a figure off their dashboard.
242
+ */
243
+ exports.TOTAL_TOKENS = 'total_tokens';
244
+ /** Anthropic's four counters. Disjoint - cache reads and writes are not inside `input_tokens`. */
245
+ exports.ANTHROPIC_COMPONENT_METRICS = [
246
+ 'input_tokens',
247
+ 'output_tokens',
248
+ 'cache_read_input_tokens',
249
+ 'cache_creation_input_tokens',
250
+ ];
251
+ /** Those four, plus the total this SDK adds up for them. */
252
+ exports.ANTHROPIC_METRICS = [...exports.ANTHROPIC_COMPONENT_METRICS, exports.TOTAL_TOKENS];
253
+ /**
254
+ * Gemini's five. Two of them overlap in opposite directions and both are reported verbatim, the
255
+ * way every vendor's are, because a number published under a vendor's field name has to reconcile
256
+ * against that vendor's own dashboard: `cachedContentTokenCount` is already *inside*
257
+ * `promptTokenCount`, while `thoughtsTokenCount` and `toolUsePromptTokenCount` sit *outside* the
258
+ * two headline counters. Price accordingly - or price `normalizedMetrics` instead, which is
259
+ * disjoint by construction.
260
+ */
261
+ exports.GEMINI_METRICS = GEMINI_PATHS;
262
+ /** What identifies a Gemini usage object: its breakdown, never its total. */
263
+ exports.GEMINI_COMPONENT_METRICS = GEMINI_COMPONENT_PATHS;
264
+ /**
265
+ * Chat Completions. `prompt_tokens` **includes** `cached_tokens`, so these two overlap - reported
266
+ * as the vendor states them, which is what reconciles against its invoice. Whoever prices
267
+ * `prompt_tokens` must know the cached ones are already inside it.
268
+ */
269
+ exports.OPENAI_CHAT_METRICS = ['prompt_tokens', 'completion_tokens', 'cached_tokens', 'total_tokens'];
270
+ /** The Responses API, which overlaps the same way under its own names. */
271
+ exports.OPENAI_RESPONSES_METRICS = ['input_tokens', 'output_tokens', 'cached_tokens', 'total_tokens'];
272
+ /**
273
+ * TypeSafe's two. No cache counters and no cost: the API reports what it charged for and stops
274
+ * there, which was confirmed against the wire rather than inferred from the SDK's types.
275
+ */
276
+ exports.TYPESAFE_COMPONENT_METRICS = ['input_tokens', 'output_tokens'];
277
+ /** Those two, plus the total this SDK adds up for them. */
278
+ exports.TYPESAFE_METRICS = [...exports.TYPESAFE_COMPONENT_METRICS, exports.TOTAL_TOKENS];
279
+ /** Both OpenAI surfaces, which is what one client can report across its two APIs. */
280
+ exports.OPENAI_METRICS = [
281
+ 'prompt_tokens',
282
+ 'completion_tokens',
283
+ 'input_tokens',
284
+ 'output_tokens',
285
+ 'cached_tokens',
286
+ 'total_tokens',
287
+ ];
288
+ /** A gateway speaks the OpenAI shape and adds the price it charged, under its own field name. */
289
+ exports.GATEWAY_METRICS = [...exports.OPENAI_METRICS, 'cost'];
290
+ /** The leaf a dotted path reports under - `prompt_tokens_details.cached_tokens` -> `cached_tokens`. */
291
+ const metricName = (path) => path.split('.').pop();
292
+ exports.metricName = metricName;
293
+ /**
294
+ * Which counters are summed to produce a vendor's total, for the vendors that publish none.
295
+ *
296
+ * Only ever disjoint sets: Anthropic's four do not overlap and neither do TypeSafe's two, so the
297
+ * sum is the whole call and nothing is counted twice. A vendor whose counters overlap - Gemini's
298
+ * cached tokens sit inside its prompt count, OpenAI's inside theirs - is not in this table,
299
+ * because adding those up would be arithmetic the vendor never agreed to. Those vendors state a
300
+ * total themselves and it is read, not computed.
301
+ */
302
+ exports.COMPUTED_TOTALS = {
303
+ anthropic: exports.ANTHROPIC_COMPONENT_METRICS,
304
+ typesafe: exports.TYPESAFE_COMPONENT_METRICS,
305
+ };
306
+ /**
307
+ * Adds the computed total to a metrics object, for a vendor that states none of its own.
308
+ *
309
+ * Absent rather than zero when nothing was counted, like every other metric here: a call that
310
+ * measured nothing reports nothing, instead of a zero somebody has to interpret.
311
+ */
312
+ function withComputedTotal(metrics, keys) {
313
+ const total = keys.reduce((sum, key) => sum + (metrics[key] ?? 0), 0);
314
+ return total > 0 ? { ...metrics, [exports.TOTAL_TOKENS]: total } : metrics;
315
+ }
316
+ /**
317
+ * Whether a path list and a name list agree, for the test that keeps them from drifting.
318
+ *
319
+ * Two lists describing one thing is the cost of separating where a counter lives from what it is
320
+ * called. This is what makes that cost a compile-and-test problem rather than a billing one.
321
+ */
322
+ const metricNamesMatchPaths = (paths, names) => {
323
+ const fromPaths = [...new Set(paths.map(exports.metricName))];
324
+ return fromPaths.length === names.length && fromPaths.every((name) => names.includes(name));
325
+ };
326
+ exports.metricNamesMatchPaths = metricNamesMatchPaths;
327
+ /** The read paths, for the tests that hold them in step with the names above. */
328
+ exports.METRIC_PATHS = {
329
+ anthropic: ANTHROPIC_PATHS,
330
+ openaiChat: OPENAI_CHAT_PATHS,
331
+ openaiResponses: OPENAI_RESPONSES_PATHS,
332
+ typesafe: TYPESAFE_PATHS,
333
+ gemini: GEMINI_PATHS,
334
+ };
335
+ /**
336
+ * Reads the named keys off a vendor's usage object, dotted paths included.
337
+ *
338
+ * A nested key reports under its leaf - `prompt_tokens_details.cached_tokens` becomes
339
+ * `cached_tokens` - because the leaf is the vendor's own name for it and a feature id with a dot
340
+ * in it reads badly everywhere. Anything absent, fractional or not a number is skipped: a metric
341
+ * the vendor did not report is not a zero, and Stigg rejects fractions.
342
+ */
343
+ function pickMetrics(usage, keys) {
344
+ if (!usage || typeof usage !== 'object')
345
+ return {};
346
+ const metrics = {};
347
+ for (const key of keys) {
348
+ const value = key.split('.').reduce((node, part) => (node == null ? node : node[part]), usage);
349
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
350
+ metrics[key.split('.').pop()] = value;
351
+ }
352
+ }
353
+ return metrics;
354
+ }
355
+ /** Accumulates metrics across a stream, which reports its counters in more than one chunk. */
356
+ const addMetrics = (into, add) => {
357
+ const summed = { ...into };
358
+ for (const [key, value] of Object.entries(add))
359
+ summed[key] = (summed[key] ?? 0) + value;
360
+ return summed;
361
+ };
362
+ exports.addMetrics = addMetrics;
363
+ /**
364
+ * The SDK's own four counters, under SDK-owned names, from the normalized measurement.
365
+ *
366
+ * The opt-out from vendor names, for an application running more than one provider: these are
367
+ * disjoint and mean the same thing whoever served the call, so one set of Stigg features covers
368
+ * every vendor. Pass it as a run's `metrics`.
369
+ */
370
+ const normalizedMetrics = ({ usage }) => {
371
+ if (!usage)
372
+ return {};
373
+ const metrics = {};
374
+ if (usage.input > 0)
375
+ metrics.stigg_input_tokens = usage.input;
376
+ if (usage.output > 0)
377
+ metrics.stigg_output_tokens = usage.output;
378
+ if (usage.cacheRead > 0)
379
+ metrics.stigg_cache_read_tokens = usage.cacheRead;
380
+ if (usage.cacheCreate > 0)
381
+ metrics.stigg_cache_creation_tokens = usage.cacheCreate;
382
+ return metrics;
383
+ };
384
+ exports.normalizedMetrics = normalizedMetrics;
385
+ // Read paths, not reported names: OpenAI keeps its cache detail nested, so the name alone finds
386
+ // nothing there. Anthropic and TypeSafe share the flat shape and differ only in how much of it
387
+ // they fill in, so a proxy fronting either reads the same paths - but it still has to be able to
388
+ // say which, rather than being told to pass the other vendor's name and know why.
389
+ const METRIC_KEYS = {
390
+ anthropic: exports.METRIC_PATHS.anthropic,
391
+ typesafe: exports.METRIC_PATHS.typesafe,
392
+ openai: exports.METRIC_PATHS.openaiChat,
393
+ 'openai-responses': exports.METRIC_PATHS.openaiResponses,
394
+ gemini: exports.METRIC_PATHS.gemini,
395
+ };
396
+ /**
397
+ * Both views of a usage object a proxy observed, for the ingest path that has no recipe.
398
+ *
399
+ * The vendor is named rather than sniffed. `normalizeUsage` guesses from the object's own fields
400
+ * and says itself that Anthropic and the Responses API are ambiguous - both count `input_tokens`.
401
+ * A wrong guess would report to another meter entirely, which nothing downstream can undo - so
402
+ * an unnamed vendor reports nothing raw and leaves the normalized counters to a mapper.
403
+ */
404
+ function observedMeasurement(usage, vendor) {
405
+ return {
406
+ metrics: vendor ? pickMetrics(usage, METRIC_KEYS[vendor]) : {},
407
+ usage: normalizeUsage(usage),
408
+ // Kept whole for an event to carry, and it does not depend on naming the vendor: a dimension
409
+ // is something a meter reads deliberately, not a feature id a wrong guess could invent.
410
+ // An array is an object and is not this: `Object.entries` would flatten it to `raw.0`,
411
+ // `raw.1`, ... - dimensions named after positions in a list nobody meant to send.
412
+ raw: usage && typeof usage === 'object' && !Array.isArray(usage) ? usage : undefined,
413
+ };
414
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The vendor response shapes these converters read.
3
+ *
4
+ * Typed structurally rather than against each vendor's SDK, so the same handful of functions cover
5
+ * a direct client, a gateway response and an orchestrator callback - and adding a provider whose
6
+ * response happens to match costs nothing.
7
+ */
8
+ /**
9
+ * A flat `usage` object whose cache counters sit BESIDE the input count rather than inside it.
10
+ *
11
+ * Named for the shape, not for a vendor, because more than one reports it: Anthropic's Messages
12
+ * API and TypeSafe's System One, which carries only the first two. The distinction from the two
13
+ * shapes below is the whole reason this is a separate reader - they spell the same two counters
14
+ * the same way but count their cached tokens *within* the input total, so reading one as the
15
+ * other double-counts in one direction or overstates in the other.
16
+ */
17
+ export type FlatTokenUsage = {
18
+ input_tokens?: number | null;
19
+ output_tokens?: number | null;
20
+ cache_read_input_tokens?: number | null;
21
+ cache_creation_input_tokens?: number | null;
22
+ };
23
+ /** OpenAI-compatible `usage` - also what OpenRouter and most gateways return. */
24
+ export type OpenAILikeUsage = {
25
+ prompt_tokens?: number | null;
26
+ completion_tokens?: number | null;
27
+ /** The vendor's own total. `prompt_tokens + completion_tokens`; cached tokens are inside the first. */
28
+ total_tokens?: number | null;
29
+ prompt_tokens_details?: {
30
+ cached_tokens?: number | null;
31
+ cache_write_tokens?: number | null;
32
+ } | null;
33
+ };
34
+ /** OpenAI's Responses API `usage` - the same counts as above under different names. */
35
+ export type ResponsesLikeUsage = {
36
+ input_tokens?: number | null;
37
+ output_tokens?: number | null;
38
+ /** The vendor's own total, under the same name the Chat Completions shape uses. */
39
+ total_tokens?: number | null;
40
+ input_tokens_details?: {
41
+ cached_tokens?: number | null;
42
+ } | null;
43
+ };
44
+ /** LangChain's `usage_metadata`, as carried on an AIMessage. */
45
+ export type LangChainLikeUsage = {
46
+ input_tokens?: number | null;
47
+ output_tokens?: number | null;
48
+ input_token_details?: {
49
+ cache_read?: number | null;
50
+ cache_creation?: number | null;
51
+ } | null;
52
+ };
53
+ /**
54
+ * Gemini's `usageMetadata`, which counts in its own names and overlaps in two directions at once.
55
+ *
56
+ * Both overlaps are quoted from the vendor's own field docs, because each one is a doubled or
57
+ * halved bill if read the other way round:
58
+ *
59
+ * - `promptTokenCount` - "when `cached_content` is set, this **also includes** the number of
60
+ * tokens in the cached content", so `cachedContentTokenCount` is counted *inside* it, the way
61
+ * OpenAI nests `cached_tokens` inside `prompt_tokens`.
62
+ * - `totalTokenCount` - "the sum of `prompt_token_count`, `candidates_token_count`,
63
+ * `tool_use_prompt_token_count`, and `thoughts_token_count`", so the last two sit *beside* the
64
+ * first two rather than within them. Thinking tokens in particular are billed as output and
65
+ * are **not** inside `candidatesTokenCount`: a reader that ignores them undercounts the output
66
+ * of every reasoning model this vendor sells.
67
+ */
68
+ export type GeminiLikeUsage = {
69
+ /**
70
+ * The vendor's own total: `promptTokenCount + candidatesTokenCount + toolUsePromptTokenCount +
71
+ * thoughtsTokenCount`, with the cached count already inside the first. Read, never recomputed.
72
+ */
73
+ totalTokenCount?: number | null;
74
+ promptTokenCount?: number | null;
75
+ candidatesTokenCount?: number | null;
76
+ cachedContentTokenCount?: number | null;
77
+ thoughtsTokenCount?: number | null;
78
+ toolUsePromptTokenCount?: number | null;
79
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });