@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.
- package/README.md +1411 -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 +126 -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 +261 -0
- package/dist/bindings/client/gateways/bedrock/bedrock-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/bedrock/bedrock-recipe.js +134 -0
- package/dist/bindings/client/gateways/bedrock/types.d.ts +13 -0
- package/dist/bindings/client/gateways/bedrock/types.js +2 -0
- package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.d.ts +10 -0
- package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.js +254 -0
- package/dist/bindings/client/gateways/cloudflare/types.d.ts +35 -0
- package/dist/bindings/client/gateways/cloudflare/types.js +2 -0
- package/dist/bindings/client/gateways/databricks/databricks-recipe.d.ts +32 -0
- package/dist/bindings/client/gateways/databricks/databricks-recipe.js +324 -0
- package/dist/bindings/client/gateways/databricks/types.d.ts +30 -0
- package/dist/bindings/client/gateways/databricks/types.js +2 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.js +158 -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 +20 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +233 -0
- package/dist/bindings/client/gateways/openrouter/sdk-usage.d.ts +37 -0
- package/dist/bindings/client/gateways/openrouter/sdk-usage.js +63 -0
- package/dist/bindings/client/gateways/openrouter/types.d.ts +78 -0
- package/dist/bindings/client/gateways/openrouter/types.js +2 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +34 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +169 -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/groq/groq-recipe.d.ts +20 -0
- package/dist/bindings/client/providers/groq/groq-recipe.js +84 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +22 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.js +114 -0
- package/dist/bindings/client/providers/mistral/types.d.ts +38 -0
- package/dist/bindings/client/providers/mistral/types.js +2 -0
- package/dist/bindings/client/providers/ollama/ollama-recipe.d.ts +31 -0
- package/dist/bindings/client/providers/ollama/ollama-recipe.js +107 -0
- package/dist/bindings/client/providers/ollama/types.d.ts +24 -0
- package/dist/bindings/client/providers/ollama/types.js +2 -0
- package/dist/bindings/client/providers/openai/openai-recipe.d.ts +71 -0
- package/dist/bindings/client/providers/openai/openai-recipe.js +122 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +44 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +83 -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/base-url.d.ts +44 -0
- package/dist/bindings/shared/base-url.js +90 -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 +345 -0
- package/dist/bindings/shared/token-usage.js +735 -0
- package/dist/bindings/shared/types.d.ts +170 -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 +47 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.js +99 -0
- package/dist/core/context.d.ts +160 -0
- package/dist/core/context.js +221 -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 +190 -0
- package/dist/core/session.js +448 -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 +42 -0
- package/dist/engine/decision.js +135 -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 +188 -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 +86 -0
- package/dist/errors.js +161 -0
- package/dist/headers.d.ts +40 -0
- package/dist/headers.js +82 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +124 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +91 -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 +300 -0
- package/dist/stigg-agent-sdk.js +669 -0
- package/dist/types.d.ts +676 -0
- package/dist/types.js +147 -0
- 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,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>;
|