@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,78 @@
1
+ import type { OpenAILikeUsage } from '../../../shared/types';
2
+ /** OpenRouter extends the OpenAI usage object with the credits it actually charged. */
3
+ export type OpenRouterUsage = OpenAILikeUsage & {
4
+ cost?: number | null;
5
+ /**
6
+ * The Responses-API counters, for the one client that can report either shape.
7
+ *
8
+ * `@openrouter/sdk` reaches both of OpenRouter's surfaces, and they count the same tokens under
9
+ * different names - so the object a reader hands back may carry one set or the other. Optional
10
+ * on both sides rather than a union, because the reader that produced it already knows which
11
+ * surface it read, and the metric list it is picked with is the surface's own.
12
+ */
13
+ input_tokens?: number | null;
14
+ output_tokens?: number | null;
15
+ input_tokens_details?: {
16
+ cached_tokens?: number | null;
17
+ cache_write_tokens?: number | null;
18
+ } | null;
19
+ };
20
+ /**
21
+ * What `@openrouter/sdk` hands back for `chat.send`, in the SDK's own spelling.
22
+ *
23
+ * Declared structurally rather than imported from `@openrouter/sdk`: the package is a devDependency
24
+ * of the examples, and a recipe that imported its types would make it a dependency of the SDK
25
+ * itself - every consumer installing a client they may not use. Only the counters are named here,
26
+ * which is all a meter reads; see `sdk-usage.ts` for why the spelling is undone rather than kept.
27
+ */
28
+ export type OpenRouterSdkChatUsage = {
29
+ promptTokens?: number | null;
30
+ completionTokens?: number | null;
31
+ totalTokens?: number | null;
32
+ cost?: number | null;
33
+ promptTokensDetails?: {
34
+ cachedTokens?: number | null;
35
+ cacheWriteTokens?: number | null;
36
+ } | null;
37
+ completionTokensDetails?: {
38
+ reasoningTokens?: number | null;
39
+ } | null;
40
+ };
41
+ /** The same, for `responses.send` - the Responses API's names, camelCased the same way. */
42
+ export type OpenRouterSdkResponsesUsage = {
43
+ inputTokens?: number | null;
44
+ outputTokens?: number | null;
45
+ totalTokens?: number | null;
46
+ cost?: number | null;
47
+ inputTokensDetails?: {
48
+ cachedTokens?: number | null;
49
+ cacheWriteTokens?: number | null;
50
+ } | null;
51
+ outputTokensDetails?: {
52
+ reasoningTokens?: number | null;
53
+ } | null;
54
+ };
55
+ export type OpenRouterRecipeOptions = {
56
+ /**
57
+ * How much to ask permission for before the call.
58
+ *
59
+ * The default asks for 1 - "is there any budget left at all?" - and lets the ingest afterwards
60
+ * settle the balance. Billing tokens, the gap that leaves is about one call's worth of units.
61
+ * Billing `cost` it is not: a call priced in micro-credits is thousands of units, so a run with
62
+ * one unit left passes the gate and then spends far past it.
63
+ *
64
+ * Nothing here can price a call the gateway has not priced yet, so closing that gap needs a
65
+ * ceiling only the caller knows - `max_tokens` against a rate they are willing to assume. Give
66
+ * one and the gate asks for it; leave it and the overshoot is bounded by a single call, which
67
+ * is the trade the default makes everywhere else.
68
+ */
69
+ estimate?: (params: any) => number;
70
+ /**
71
+ * Units per credit. OpenRouter charges fractional credits and Stigg ingests whole units, so the
72
+ * meter has to be denominated in something small enough not to round every call up to 1. The
73
+ * default is micro-credits; set it to match however the Stigg currency is defined.
74
+ */
75
+ unitsPerCredit?: number;
76
+ /** Dimension the served model is attributed to, so routing drift is visible in Stigg. */
77
+ modelDimension?: string;
78
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,34 @@
1
+ import type { Measurement } from '../../../../types';
2
+ import type { MetricsFor, MetricsOf } from '../../core/types';
3
+ /**
4
+ * Accumulates usage across raw stream events: Anthropic sends the input counts on
5
+ * `message_start` and restates them, with the output count, on `message_delta` - so one completed
6
+ * stream produces one measurement rather than one per event.
7
+ */
8
+ declare function observeStream(stream: any, ingest: (measurement: Measurement) => void): void;
9
+ export declare const anthropicRecipe: {
10
+ vendor: string;
11
+ metrics: readonly ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "total_tokens"];
12
+ totalTokensMetric: "total_tokens";
13
+ matches: (client: unknown) => boolean;
14
+ methods: {
15
+ path: string[];
16
+ extractUsage: (result: any) => {
17
+ metrics: import("../../../../types").Metrics;
18
+ usage: import("../../../../types").TokenUsage;
19
+ raw: any;
20
+ } | undefined;
21
+ attachStream: typeof observeStream;
22
+ }[];
23
+ };
24
+ /**
25
+ * A pre-call estimate based on the requested output ceiling. Opt-in: the default asks for a
26
+ * single unit, because an estimate that overshoots denies calls the budget could have afforded,
27
+ * and the ingest that follows corrects the balance anyway.
28
+ */
29
+ export declare const estimateFromMaxTokens: (tokensPerUnit?: number) => (params: any) => number;
30
+ /** One of the metric names Anthropic reports. */
31
+ export type AnthropicMetric = MetricsOf<typeof anthropicRecipe>;
32
+ /** What a mapper reads off an Anthropic call: those names, each possibly absent. */
33
+ export type AnthropicMetrics = MetricsFor<typeof anthropicRecipe>;
34
+ export {};
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.estimateFromMaxTokens = exports.anthropicRecipe = void 0;
4
+ /**
5
+ * The Anthropic recipe.
6
+ *
7
+ * Only `messages.create` is gated, because `messages.stream()` routes through it - so one patch
8
+ * covers both call styles, and `stream()` keeps its synchronous signature (callers attach
9
+ * listeners to the returned object before awaiting anything). A denial raised inside the create
10
+ * leg surfaces as a rejection from `finalMessage()`/`done()`, which is where a stream caller
11
+ * already handles failure.
12
+ */
13
+ const token_usage_1 = require("../../../shared/token-usage");
14
+ const estimate_1 = require("../../../shared/estimate");
15
+ const registry_1 = require("../../core/registry");
16
+ /**
17
+ * Characters per output token, for the one case where the vendor states no count.
18
+ *
19
+ * Approximate on purpose and deliberately not tuned: it is only ever reached for a stream that
20
+ * was abandoned, where the alternative is reporting nothing for text the customer read and the
21
+ * vendor billed. Roughly four characters to a token holds well enough across English prose for
22
+ * the estimate to be the closer of the two answers. Anything reported from it is marked
23
+ * `estimated` so it is never mistaken for a counter Anthropic stated.
24
+ */
25
+ const CHARS_PER_OUTPUT_TOKEN = 4;
26
+ /** The characters a delta contributed, or 0 for the events that generate nothing. */
27
+ function generatedChars(delta) {
28
+ if (!delta)
29
+ return 0;
30
+ const produced = delta.type === 'text_delta'
31
+ ? delta.text
32
+ : delta.type === 'thinking_delta'
33
+ ? delta.thinking
34
+ : delta.type === 'input_json_delta'
35
+ ? delta.partial_json
36
+ : undefined;
37
+ return typeof produced === 'string' ? produced.length : 0;
38
+ }
39
+ /**
40
+ * Whether a wire value is a count at all.
41
+ *
42
+ * Whole and non-negative, because a counter that *replaces* another has to be checked before it
43
+ * is trusted: a negative or fractional one is not a token count, and letting it overwrite a real
44
+ * count reads back as zero.
45
+ */
46
+ const isCount = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0;
47
+ /**
48
+ * The counters a stream has most recently stated - each event replacing what it names, rather
49
+ * than adding to it.
50
+ *
51
+ * Anthropic restates cumulatively. `message_delta.usage.input_tokens` is "the *cumulative* number
52
+ * of input tokens which were used", in the vendor's own words, and the same holds for both cache
53
+ * counters and for `output_tokens`. None of them is a delta despite the event's name, so summing
54
+ * the two events bills every input counter twice.
55
+ *
56
+ * Per counter rather than by replacing the block wholesale, because the two events do not carry
57
+ * the same fields: on older API versions `message_start` is the only one that states the input
58
+ * counts, and a wholesale replace would make the opposite billing error.
59
+ *
60
+ * The key list is {@link ANTHROPIC_COMPONENT_METRICS} rather than one of its own: these are the
61
+ * same four counters under the same four names, and a second list would be free to disagree with
62
+ * the one that decides what gets reported.
63
+ */
64
+ function restate(into, stated) {
65
+ if (!stated)
66
+ return into;
67
+ const merged = { ...into };
68
+ for (const key of token_usage_1.ANTHROPIC_COMPONENT_METRICS) {
69
+ const value = stated[key];
70
+ // Only a whole, non-negative number counts as a statement. `null` is how this wire says
71
+ // "not this event", and a malformed or negative one is not a count - letting either through
72
+ // would erase a count an earlier event did state.
73
+ if (isCount(value))
74
+ merged[key] = value;
75
+ }
76
+ return merged;
77
+ }
78
+ /**
79
+ * Accumulates usage across raw stream events: Anthropic sends the input counts on
80
+ * `message_start` and restates them, with the output count, on `message_delta` - so one completed
81
+ * stream produces one measurement rather than one per event.
82
+ */
83
+ function observeStream(stream, ingest) {
84
+ if (!stream || typeof stream[Symbol.asyncIterator] !== 'function')
85
+ return;
86
+ const iterate = stream[Symbol.asyncIterator].bind(stream);
87
+ stream[Symbol.asyncIterator] = async function* gatedIterator() {
88
+ // What the stream has said so far, in the vendor's own shape. Kept flat rather than converted
89
+ // per event: both views of the call - the vendor's names and the normalized counters - are
90
+ // derived from it once at the end, so neither can be accumulated out of step with the other.
91
+ let stated = {};
92
+ // Whether the stream ever stated a final output count. `message_start` carries a placeholder
93
+ // `output_tokens: 1`, so the presence of the counter says nothing - only `message_delta` does.
94
+ let hasStatedOutputTokens = false;
95
+ // What the stream actually produced, for the case where it never gets to say.
96
+ let chars = 0;
97
+ try {
98
+ for await (const event of iterate()) {
99
+ if (event?.type === 'message_start')
100
+ stated = restate(stated, event.message?.usage);
101
+ if (event?.type === 'message_delta') {
102
+ stated = restate(stated, event.usage);
103
+ hasStatedOutputTokens ||= typeof event.usage?.output_tokens === 'number';
104
+ }
105
+ if (event?.type === 'content_block_delta')
106
+ chars += generatedChars(event.delta);
107
+ yield event;
108
+ }
109
+ }
110
+ finally {
111
+ // Meter what was actually produced, even if the consumer broke out of the loop early.
112
+ //
113
+ // No `raw` here, deliberately, and this is the one vendor that cannot have one: the counters
114
+ // arrive split across `message_start` and `message_delta`, so there is no single usage block
115
+ // on the wire to carry. The Gemini and OpenAI streams each report one whole block and do
116
+ // pass it on; `stated` is this SDK's reconstruction across two events, not an object the
117
+ // stream ever actually sent, so passing it would be a claim about the wire that is not true.
118
+ //
119
+ // A stream the consumer walked away from never reaches `message_delta`, the only event
120
+ // that states a final output count - so without this the text it did produce meters at the
121
+ // placeholder `output_tokens: 1` from `message_start`. The customer read that text and
122
+ // Anthropic billed for generating it, so one token is a hole rather than caution. What it
123
+ // produced is estimated instead, and the measurement says so.
124
+ const estimated = !hasStatedOutputTokens && chars > 0;
125
+ const final = estimated
126
+ ? restate(stated, { output_tokens: Math.max(1, Math.ceil(chars / CHARS_PER_OUTPUT_TOKEN)) })
127
+ : stated;
128
+ ingest({
129
+ metrics: (0, token_usage_1.withComputedTotal)((0, token_usage_1.pickMetrics)(final, token_usage_1.ANTHROPIC_COMPONENT_METRICS), token_usage_1.COMPUTED_TOTALS.anthropic),
130
+ usage: (0, token_usage_1.fromFlatTokenUsage)(final),
131
+ ...(estimated ? { estimated: true } : {}),
132
+ });
133
+ }
134
+ };
135
+ }
136
+ exports.anthropicRecipe = {
137
+ vendor: 'anthropic',
138
+ metrics: token_usage_1.ANTHROPIC_METRICS,
139
+ totalTokensMetric: token_usage_1.TOTAL_TOKENS,
140
+ matches: (client) => (0, registry_1.hasMethod)(client, ['messages', 'create']) && (0, registry_1.hasMethod)(client, ['messages', 'stream']),
141
+ methods: [
142
+ {
143
+ // `messages.stream()` reaches the wire through this same method, so gating it here covers
144
+ // both call styles - and leaves `stream()` synchronous, which its callers rely on.
145
+ path: ['messages', 'create'],
146
+ // Both views of the call: what Anthropic called it, which is what gets reported, and the
147
+ // normalized counters a mapper of your own would write against.
148
+ extractUsage: (result) => result?.usage
149
+ ? {
150
+ // Anthropic publishes no total of its own, so one is added up from the four it
151
+ // does publish - which are disjoint, so the sum is the whole call.
152
+ metrics: (0, token_usage_1.withComputedTotal)((0, token_usage_1.pickMetrics)(result.usage, token_usage_1.ANTHROPIC_COMPONENT_METRICS), token_usage_1.COMPUTED_TOTALS.anthropic),
153
+ usage: (0, token_usage_1.fromFlatTokenUsage)(result.usage),
154
+ // The block as it arrived, for an event to state in full - including the counters
155
+ // this recipe does not name, which a meter can then read without an SDK release.
156
+ raw: result.usage,
157
+ }
158
+ : undefined,
159
+ attachStream: observeStream,
160
+ },
161
+ ],
162
+ };
163
+ /**
164
+ * A pre-call estimate based on the requested output ceiling. Opt-in: the default asks for a
165
+ * single unit, because an estimate that overshoots denies calls the budget could have afforded,
166
+ * and the ingest that follows corrects the balance anyway.
167
+ */
168
+ const estimateFromMaxTokens = (tokensPerUnit = 1) => (0, estimate_1.estimateUnits)((params) => params?.max_tokens ?? 0, tokensPerUnit);
169
+ exports.estimateFromMaxTokens = estimateFromMaxTokens;
@@ -0,0 +1,54 @@
1
+ import { type Measurement } from '../../../../types';
2
+ import type { MetricsFor, MetricsOf } from '../../core/types';
3
+ /** The one-shot surface: `ai.models.generateContent` and its streaming twin. */
4
+ export declare const geminiRecipe: {
5
+ vendor: string;
6
+ metrics: readonly ["promptTokenCount", "candidatesTokenCount", "cachedContentTokenCount", "thoughtsTokenCount", "toolUsePromptTokenCount", "totalTokenCount"];
7
+ totalTokensMetric: "totalTokenCount";
8
+ matches: (client: unknown) => boolean;
9
+ methods: ({
10
+ path: readonly ["models", "generateContent"];
11
+ extractUsage: (result: any) => Measurement | undefined;
12
+ attachStream: (stream: any, ingest: (measurement: Measurement) => void) => void;
13
+ } | {
14
+ path: readonly ["models", "generateContentStream"];
15
+ extractUsage: (result: any) => Measurement | undefined;
16
+ attachStream: (stream: any, ingest: (measurement: Measurement) => void) => void;
17
+ })[];
18
+ };
19
+ /**
20
+ * The conversational surface: one `Chat`, as `ai.chats.create(...)` hands it back.
21
+ *
22
+ * Matched on all three of its own methods rather than on `sendMessage` alone - that name is far
23
+ * too common for a duck-typed registry to claim on sight, and a wrong match here would wrap
24
+ * somebody else's object and meter its calls as Gemini traffic.
25
+ */
26
+ export declare const geminiChatRecipe: {
27
+ vendor: string;
28
+ metrics: readonly ["promptTokenCount", "candidatesTokenCount", "cachedContentTokenCount", "thoughtsTokenCount", "toolUsePromptTokenCount", "totalTokenCount"];
29
+ totalTokensMetric: "totalTokenCount";
30
+ matches: (client: unknown) => boolean;
31
+ methods: ({
32
+ path: readonly ["sendMessage"];
33
+ event: string;
34
+ extractUsage: (result: any) => Measurement | undefined;
35
+ attachStream: (stream: any, ingest: (measurement: Measurement) => void) => void;
36
+ } | {
37
+ path: readonly ["sendMessageStream"];
38
+ event: string;
39
+ extractUsage: (result: any) => Measurement | undefined;
40
+ attachStream: (stream: any, ingest: (measurement: Measurement) => void) => void;
41
+ })[];
42
+ };
43
+ /**
44
+ * A pre-call estimate based on the requested output ceiling, which Gemini nests under `config`.
45
+ *
46
+ * Opt-in, on the same terms as the Anthropic one: the default asks for a single unit, because an
47
+ * estimate that overshoots denies calls the budget could have afforded, and the ingest that
48
+ * follows corrects the balance anyway.
49
+ */
50
+ export declare const estimateFromMaxOutputTokens: (tokensPerUnit?: number) => (params: any) => number;
51
+ /** One of the metric names Gemini reports. */
52
+ export type GeminiMetric = MetricsOf<typeof geminiRecipe>;
53
+ /** What a mapper reads off a Gemini call: those names, each possibly absent. */
54
+ export type GeminiMetrics = MetricsFor<typeof geminiRecipe>;
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.estimateFromMaxOutputTokens = exports.geminiChatRecipe = exports.geminiRecipe = void 0;
4
+ /**
5
+ * The Gemini recipe - `@google/genai`, the GA SDK that replaced `@google/generative-ai`.
6
+ *
7
+ * Two recipes, because the two call styles share no code path. `new GoogleGenAI()` builds its
8
+ * chat module as `new Chats(this.models, ...)` and each `Chat` keeps that reference privately, so
9
+ * a chat created from a wrapped client still calls the original, unpatched `models.generateContent`.
10
+ * Gating `models.*` alone would leave every conversation ungoverned while the client looked
11
+ * governed, so a chat is wrapped in its own right:
12
+ *
13
+ * const chat = stigg.wrap(ai.chats.create({ model }));
14
+ *
15
+ * Nothing meters twice: the chat reaches the unwrapped module by construction.
16
+ *
17
+ * Generation only. `models.embedContent`, `generateImages`, `generateVideos`, `countTokens`,
18
+ * `batches.*`, `caches.*` and `live.*` are billable and **not** gated here - each counts in a unit
19
+ * these metrics do not describe. Meter one by naming your own recipe at `wrap(client, { recipe })`.
20
+ */
21
+ const token_usage_1 = require("../../../shared/token-usage");
22
+ const meter_stream_1 = require("../../../shared/meter-stream");
23
+ const types_1 = require("../../../../types");
24
+ const estimate_1 = require("../../../shared/estimate");
25
+ const registry_1 = require("../../core/registry");
26
+ /**
27
+ * Both views of a completed response: what Gemini called the counters, which is what gets
28
+ * reported, and the normalized ones a mapper of your own would write against.
29
+ *
30
+ * Returns undefined for a streamed call - `generateContentStream` resolves to an async generator
31
+ * with no `usageMetadata` on it - which is what hands the result to `attachStream` below.
32
+ */
33
+ /**
34
+ * How many model calls one `generateContent` made.
35
+ *
36
+ * With a callable tool the SDK drives the tool loop itself - up to `maximumRemoteCalls`, 10 by
37
+ * default - and returns only the last response. Each round that called a tool leaves its reply in
38
+ * `automaticFunctionCallingHistory`, which is `Content` and carries no token counts: the rounds
39
+ * can be counted, not priced.
40
+ */
41
+ function afcRounds(result) {
42
+ const history = result?.automaticFunctionCallingHistory;
43
+ if (!Array.isArray(history))
44
+ return 1;
45
+ const toolReplies = history.filter((content) => Array.isArray(content?.parts) && content.parts.some((part) => part?.functionResponse)).length;
46
+ return toolReplies + 1;
47
+ }
48
+ /**
49
+ * What a call whose earlier rounds were never reported can honestly say about itself.
50
+ *
51
+ * `generateContent` reassigns `response` each iteration and returns the last, so the earlier
52
+ * rounds are gone before it returns and no arithmetic here recovers them. Measured: two rounds
53
+ * billed at 126 and 171, of which the response carries 171. The streamed surface has no such gap.
54
+ */
55
+ const partialAfcReason = (rounds, measured) => `Gemini ran ${rounds} model calls for one generateContent (automatic function calling), and the ` +
56
+ `response carries usage for the last one only - ${measured} tokens, out of a total this SDK ` +
57
+ `cannot see. The earlier rounds were billed by the vendor and are not in the response, so they ` +
58
+ `are not in this measurement. To meter every round: stream the call, or set ` +
59
+ `\`automaticFunctionCalling: { disable: true }\` and drive the tool loop yourself, which makes ` +
60
+ `each round a call this SDK gates and meters on its own.`;
61
+ const extractUsage = (result) => result?.usageMetadata
62
+ ? {
63
+ metrics: (0, token_usage_1.pickMetrics)(result.usageMetadata, token_usage_1.GEMINI_METRICS),
64
+ usage: (0, token_usage_1.fromGeminiUsage)(result.usageMetadata),
65
+ // The vendor's own id, so one turn reported twice is one turn. The fallback is a step
66
+ // counter off the run, which is stable only within a process.
67
+ ...(typeof result.responseId === 'string' ? { idempotencyKey: result.responseId } : {}),
68
+ ...afcShortfall(result),
69
+ // The block as it arrived, for an event to state in full - including `totalTokenCount`
70
+ // and the per-modality breakdowns this recipe does not name, which a meter can then read
71
+ // without waiting for an SDK release.
72
+ raw: result.usageMetadata,
73
+ }
74
+ : undefined;
75
+ /** The rounds this call made and what went unmeasured, for the calls where any did. */
76
+ function afcShortfall(result) {
77
+ const rounds = afcRounds(result);
78
+ if (rounds < 2)
79
+ return {};
80
+ return {
81
+ dimensions: { gemini_afc_rounds: String(rounds) },
82
+ incomplete: partialAfcReason(rounds, n(result?.usageMetadata?.totalTokenCount)),
83
+ };
84
+ }
85
+ const n = (value) => (typeof value === 'number' ? value : 0);
86
+ /**
87
+ * Meters a streamed call: the last block of each round, summed across rounds.
88
+ *
89
+ * **Within a round** `usageMetadata` repeats on every chunk as a running total, so the last block
90
+ * is the whole round and summing the chunks bills it many times over. **Across rounds** automatic
91
+ * function calling yields several rounds through one generator, each counting from zero, so
92
+ * keeping only the last block bills one round of several. Measured: 305 charged, 175 from the last
93
+ * block alone, 556 from summing every chunk.
94
+ *
95
+ * The boundary is `responseId`, shared by a round's chunks and changed by the next, and it is
96
+ * trusted on its own. Measured rather than assumed: `promptTokenCount` changing is **not** a
97
+ * boundary - it moves within a round too (92 -> 131 on the wire) - and neither is a counter
98
+ * *failing* to drop, since a round that ended on a function call is easily smaller than the first
99
+ * chunk of the one after it. Where no id is reported, a counter running backwards stands in,
100
+ * being the only evidence left that a round restarted.
101
+ */
102
+ const attachStream = (stream, ingest) => {
103
+ /** Rounds already finished, which never change again. */
104
+ let banked = (0, types_1.emptyTokenUsage)();
105
+ let bankedMetrics = {};
106
+ /** The round being read, replaced by each block it reports because they are running totals. */
107
+ let round;
108
+ let roundMetrics = {};
109
+ /** What the round being read calls itself, which is what makes the boundary exact. */
110
+ let roundId;
111
+ /** Undefined until a chunk states one. A counter nobody has reported cannot have gone down. */
112
+ let candidates;
113
+ let thoughts;
114
+ /**
115
+ * The block as it arrived, for an event to state in full - kept only while the stream turns
116
+ * out to be a single round. Copied rather than referenced, because it is held until the stream
117
+ * ends and the chunk it came from is the consumer's: a reference would let the dimensions
118
+ * reported disagree with the metrics computed beside them. (`extractUsage` needs no copy -
119
+ * there the block is flattened in the same tick it is read.)
120
+ */
121
+ let raw;
122
+ let lastId;
123
+ let rounds = 0;
124
+ (0, meter_stream_1.meterStream)(stream, (chunk) => {
125
+ const reported = chunk?.usageMetadata;
126
+ if (!reported)
127
+ return;
128
+ const id = typeof chunk.responseId === 'string' ? chunk.responseId : undefined;
129
+ const nextCandidates = typeof reported.candidatesTokenCount === 'number' ? reported.candidatesTokenCount : undefined;
130
+ const nextThoughts = typeof reported.thoughtsTokenCount === 'number' ? reported.thoughtsTokenCount : undefined;
131
+ // Both values must be present for a decrease to mean anything: an absent counter read as
132
+ // zero looks like a restart, and banks a round that never ended.
133
+ const dropped = (next, previous) => next !== undefined && previous !== undefined && next < previous;
134
+ // The id decides, on its own, wherever one is reported: a `responseId` identifies one
135
+ // response, and one response is one round. Requiring a counter to drop as well looked
136
+ // safer and was not - it misses a boundary whenever the new round's first usage chunk
137
+ // already carries more output than the whole of the previous round, which is the normal
138
+ // shape when a round ends on a function call. Measured: 223 billed as 147.
139
+ //
140
+ // The counters are the fallback for a stream that names nothing, where a counter running
141
+ // backwards is the only evidence a round restarted.
142
+ const restarted = dropped(nextCandidates, candidates) || dropped(nextThoughts, thoughts);
143
+ const endsRound = id !== undefined && roundId !== undefined ? id !== roundId : restarted;
144
+ // A round ended: bank what it finally reported before the new one starts from zero.
145
+ let ended = false;
146
+ if (round !== undefined && endsRound) {
147
+ ended = true;
148
+ banked = (0, token_usage_1.addTokenUsage)(banked, round);
149
+ bankedMetrics = (0, token_usage_1.addMetrics)(bankedMetrics, roundMetrics);
150
+ rounds += 1;
151
+ }
152
+ round = (0, token_usage_1.fromGeminiUsage)(reported);
153
+ roundMetrics = (0, token_usage_1.pickMetrics)(reported, token_usage_1.GEMINI_METRICS);
154
+ raw = { ...reported };
155
+ roundId = id;
156
+ // The last round's id names the call: one measurement goes out, and this is the id that is
157
+ // still the same if the stream is read again.
158
+ if (id !== undefined)
159
+ lastId = id;
160
+ // Within a round an omitted counter keeps its last value; across one the baselines are
161
+ // replaced, absence included. Carrying the old round's figure made the next chunk stating
162
+ // that counter look like a further restart, banking a round mid-round.
163
+ if (ended) {
164
+ candidates = nextCandidates;
165
+ thoughts = nextThoughts;
166
+ }
167
+ else {
168
+ if (nextCandidates !== undefined)
169
+ candidates = nextCandidates;
170
+ if (nextThoughts !== undefined)
171
+ thoughts = nextThoughts;
172
+ }
173
+ }, () => {
174
+ const usage = round ? (0, token_usage_1.addTokenUsage)(banked, round) : banked;
175
+ const metrics = round ? (0, token_usage_1.addMetrics)(bankedMetrics, roundMetrics) : bankedMetrics;
176
+ // One round, one block, and `raw` describes the whole measurement. Several rounds and no
177
+ // block does: each states only its own round, so passing the last would hand a meter
178
+ // dimensions that disagree with the metrics beside them. Reported instead is how many
179
+ // rounds there were, which is the thing a reader would otherwise have no way to see.
180
+ const single = rounds === 0 && raw;
181
+ ingest({
182
+ metrics,
183
+ usage,
184
+ ...(single ? { raw } : {}),
185
+ ...(lastId ? { idempotencyKey: lastId } : {}),
186
+ ...(rounds ? { dimensions: { gemini_afc_rounds: String(rounds + 1) } } : {}),
187
+ });
188
+ });
189
+ };
190
+ /** The one-shot surface: `ai.models.generateContent` and its streaming twin. */
191
+ exports.geminiRecipe = {
192
+ vendor: 'gemini',
193
+ metrics: token_usage_1.GEMINI_METRICS,
194
+ // Gemini's own name for it, which is the one its invoice shows.
195
+ totalTokensMetric: 'totalTokenCount',
196
+ // Either is enough on its own, the way the OpenAI recipe treats its two surfaces. Both methods
197
+ // sit on the same module today, so requiring both would pass anyway - and would start refusing
198
+ // clients the day the vendor splits them, which is a guard failing for a reason nobody intended.
199
+ matches: (client) => (0, registry_1.hasMethod)(client, ['models', 'generateContent']) || (0, registry_1.hasMethod)(client, ['models', 'generateContentStream']),
200
+ // `as const` on each path, which is what `PathsOf` reads: a recipe named explicitly at
201
+ // `wrap(client, { recipe })` then overlays the per-call context on exactly the methods it
202
+ // gates, rather than falling back to the shipped vendors' list. It matters most for the chat
203
+ // recipe below - see there - and both are written the same way so neither drifts.
204
+ methods: [
205
+ { path: ['models', 'generateContent'], extractUsage, attachStream },
206
+ { path: ['models', 'generateContentStream'], extractUsage, attachStream },
207
+ ],
208
+ };
209
+ /**
210
+ * The conversational surface: one `Chat`, as `ai.chats.create(...)` hands it back.
211
+ *
212
+ * Matched on all three of its own methods rather than on `sendMessage` alone - that name is far
213
+ * too common for a duck-typed registry to claim on sight, and a wrong match here would wrap
214
+ * somebody else's object and meter its calls as Gemini traffic.
215
+ */
216
+ exports.geminiChatRecipe = {
217
+ vendor: 'gemini',
218
+ metrics: token_usage_1.GEMINI_METRICS,
219
+ // Gemini's own name for it, which is the one its invoice shows.
220
+ totalTokensMetric: 'totalTokenCount',
221
+ matches: (client) => (0, registry_1.hasMethod)(client, ['sendMessage']) && (0, registry_1.hasMethod)(client, ['sendMessageStream']) && (0, registry_1.hasMethod)(client, ['getHistory']),
222
+ // Literal paths, so naming this recipe explicitly types the context onto these two methods
223
+ // whatever the client looks like. Without it the overlay fell back to `ChatOf<T>`, which keys
224
+ // off `getHistory` - right for detection, wrong here: an explicit recipe never consults
225
+ // `matches`, so `wrapClient` patches and strips the context on a two-method client the types
226
+ // were refusing it on.
227
+ methods: [
228
+ // Named for where the call comes from rather than for the object it was reached through: a
229
+ // bare `gemini.sendMessage` says nothing about which API it is, and the event name is what
230
+ // somebody has to recognise when configuring a meter to filter on it.
231
+ { path: ['sendMessage'], event: 'gemini.chats.sendMessage', extractUsage, attachStream },
232
+ { path: ['sendMessageStream'], event: 'gemini.chats.sendMessageStream', extractUsage, attachStream },
233
+ ],
234
+ };
235
+ /**
236
+ * A pre-call estimate based on the requested output ceiling, which Gemini nests under `config`.
237
+ *
238
+ * Opt-in, on the same terms as the Anthropic one: the default asks for a single unit, because an
239
+ * estimate that overshoots denies calls the budget could have afforded, and the ingest that
240
+ * follows corrects the balance anyway.
241
+ */
242
+ const estimateFromMaxOutputTokens = (tokensPerUnit = 1) => (0, estimate_1.estimateUnits)((params) => params?.config?.maxOutputTokens ?? 0, tokensPerUnit);
243
+ exports.estimateFromMaxOutputTokens = estimateFromMaxOutputTokens;
@@ -0,0 +1,20 @@
1
+ import type { MetricsFor, MetricsOf } from '../../core/types';
2
+ export declare const groqRecipe: {
3
+ vendor: string;
4
+ metrics: readonly ["prompt_tokens", "completion_tokens", "cached_tokens", "total_tokens"];
5
+ totalTokensMetric: "total_tokens";
6
+ matches: (client: unknown) => boolean;
7
+ methods: {
8
+ path: ("create" | "chat" | "completions")[];
9
+ extractUsage: (result: any) => {
10
+ metrics: import("../../../..").Metrics;
11
+ usage: import("../../../..").TokenUsage;
12
+ raw: any;
13
+ } | undefined;
14
+ attachStream: (stream: any, ingest: (measurement: import("../../../..").Measurement) => void) => void;
15
+ }[];
16
+ };
17
+ /** One of the metric names Groq reports. */
18
+ export type GroqMetric = MetricsOf<typeof groqRecipe>;
19
+ /** What a mapper reads off a Groq call: those names, each possibly absent. */
20
+ export type GroqMetrics = MetricsFor<typeof groqRecipe>;