@stigg/agent-sdk-js 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1147 -2
- package/dist/bindings/client/core/registry.d.ts +12 -0
- package/dist/bindings/client/core/registry.js +22 -0
- package/dist/bindings/client/core/types.d.ts +117 -0
- package/dist/bindings/client/core/types.js +13 -0
- package/dist/bindings/client/core/wrap-client.d.ts +6 -0
- package/dist/bindings/client/core/wrap-client.js +258 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.js +171 -0
- package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
- package/dist/bindings/client/gateways/litellm/types.js +2 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +15 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +141 -0
- package/dist/bindings/client/gateways/openrouter/types.d.ts +29 -0
- package/dist/bindings/client/gateways/openrouter/types.js +2 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +30 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +79 -0
- package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
- package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
- package/dist/bindings/client/providers/openai/openai-recipe.d.ts +43 -0
- package/dist/bindings/client/providers/openai/openai-recipe.js +80 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +33 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +73 -0
- package/dist/bindings/proxy/headers.d.ts +6 -0
- package/dist/bindings/proxy/headers.js +12 -0
- package/dist/bindings/proxy/index.d.ts +21 -0
- package/dist/bindings/proxy/index.js +18 -0
- package/dist/bindings/proxy/types.d.ts +8 -0
- package/dist/bindings/proxy/types.js +2 -0
- package/dist/bindings/shared/estimate.d.ts +27 -0
- package/dist/bindings/shared/estimate.js +38 -0
- package/dist/bindings/shared/meter-stream.d.ts +18 -0
- package/dist/bindings/shared/meter-stream.js +151 -0
- package/dist/bindings/shared/token-usage.d.ts +148 -0
- package/dist/bindings/shared/token-usage.js +414 -0
- package/dist/bindings/shared/types.d.ts +79 -0
- package/dist/bindings/shared/types.js +2 -0
- package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
- package/dist/bindings/shared/weighted-metrics.js +44 -0
- package/dist/config.d.ts +136 -0
- package/dist/config.js +97 -0
- package/dist/core/context.d.ts +151 -0
- package/dist/core/context.js +154 -0
- package/dist/core/dimensions.d.ts +22 -0
- package/dist/core/dimensions.js +47 -0
- package/dist/core/hooks.d.ts +21 -0
- package/dist/core/hooks.js +47 -0
- package/dist/core/session.d.ts +161 -0
- package/dist/core/session.js +403 -0
- package/dist/decision-text.d.ts +15 -0
- package/dist/decision-text.js +54 -0
- package/dist/engine/composite-engine.d.ts +72 -0
- package/dist/engine/composite-engine.js +135 -0
- package/dist/engine/create-engine.d.ts +16 -0
- package/dist/engine/create-engine.js +93 -0
- package/dist/engine/decision.d.ts +37 -0
- package/dist/engine/decision.js +128 -0
- package/dist/engine/engine.d.ts +166 -0
- package/dist/engine/engine.js +11 -0
- package/dist/engine/node-sdk-engine.d.ts +125 -0
- package/dist/engine/node-sdk-engine.js +262 -0
- package/dist/engine/report.d.ts +106 -0
- package/dist/engine/report.js +185 -0
- package/dist/engine/rest-governance-engine.d.ts +47 -0
- package/dist/engine/rest-governance-engine.js +220 -0
- package/dist/errors.d.ts +78 -0
- package/dist/errors.js +151 -0
- package/dist/headers.d.ts +27 -0
- package/dist/headers.js +63 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +85 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +81 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +20 -0
- package/dist/sdk-identity.d.ts +36 -0
- package/dist/sdk-identity.js +43 -0
- package/dist/stigg-agent-sdk.d.ts +242 -0
- package/dist/stigg-agent-sdk.js +594 -0
- package/dist/types.d.ts +531 -0
- package/dist/types.js +97 -0
- 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
|
+
};
|