@stigg/agent-sdk-js 0.0.0-stage → 0.1.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 +1296 -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/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/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 +20 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +244 -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 +101 -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/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 +240 -0
- package/dist/bindings/shared/token-usage.js +574 -0
- package/dist/bindings/shared/types.d.ts +129 -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 +57 -0
- package/dist/index.js +106 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +87 -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 +656 -0
- package/dist/types.d.ts +642 -0
- package/dist/types.js +147 -0
- package/package.json +89 -3
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway binding is generic on purpose: a gateway sees HTTP requests, not SDK calls, so
|
|
3
|
+
* there is nothing vendor-specific to adapt. What travels is attribution.
|
|
4
|
+
*/
|
|
5
|
+
export { GOVERNANCE_HEADERS, parseGovernanceHeaders } from '../../headers';
|
|
6
|
+
export type { GovernanceHeaders, ParsedGovernanceHeaders } from '../../headers';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseGovernanceHeaders = exports.GOVERNANCE_HEADERS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The gateway binding is generic on purpose: a gateway sees HTTP requests, not SDK calls, so
|
|
6
|
+
* there is nothing vendor-specific to adapt. What travels is attribution.
|
|
7
|
+
*/
|
|
8
|
+
var headers_1 = require("../../headers");
|
|
9
|
+
Object.defineProperty(exports, "GOVERNANCE_HEADERS", { enumerable: true, get: function () { return headers_1.GOVERNANCE_HEADERS; } });
|
|
10
|
+
Object.defineProperty(exports, "parseGovernanceHeaders", { enumerable: true, get: function () { return headers_1.parseGovernanceHeaders; } });
|
|
11
|
+
// Attribution to send with a request is `run.headers()` on the handle itself - the run owns the
|
|
12
|
+
// three values, so a free function here could only copy them back out of it.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest-only, by design.
|
|
3
|
+
*
|
|
4
|
+
* A gateway is out-of-process and sees requests, not runs: it cannot provision, cannot check
|
|
5
|
+
* against run-scoped state, and cannot archive. What works is splitting the lifecycle across the
|
|
6
|
+
* boundary - the application provisions and checks in-process, the attribution travels as
|
|
7
|
+
* headers, and the gateway ingests against it. Governance never leaves the process.
|
|
8
|
+
*
|
|
9
|
+
* Both halves of that are methods, not free functions: `run.headers()` writes the attribution and
|
|
10
|
+
* `stigg.ingestFromHeaders(...)` reads it back. This module is the header contract and the input
|
|
11
|
+
* shape they agree on.
|
|
12
|
+
*/
|
|
13
|
+
export { GOVERNANCE_HEADERS, parseGovernanceHeaders } from './headers';
|
|
14
|
+
export type { GovernanceHeaders, ParsedGovernanceHeaders } from './headers';
|
|
15
|
+
export type * from './types';
|
|
16
|
+
/**
|
|
17
|
+
* The proxy's one vendor-shaped decision, re-exported from where it is implemented so it sits
|
|
18
|
+
* beside the call that takes it. A proxy has no response object to adapt - there is no recipe out
|
|
19
|
+
* here - so naming the shape it observed is the whole of what it can say about the vendor.
|
|
20
|
+
*/
|
|
21
|
+
export type { ObservedVendor } from '../shared/token-usage';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseGovernanceHeaders = exports.GOVERNANCE_HEADERS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Ingest-only, by design.
|
|
6
|
+
*
|
|
7
|
+
* A gateway is out-of-process and sees requests, not runs: it cannot provision, cannot check
|
|
8
|
+
* against run-scoped state, and cannot archive. What works is splitting the lifecycle across the
|
|
9
|
+
* boundary - the application provisions and checks in-process, the attribution travels as
|
|
10
|
+
* headers, and the gateway ingests against it. Governance never leaves the process.
|
|
11
|
+
*
|
|
12
|
+
* Both halves of that are methods, not free functions: `run.headers()` writes the attribution and
|
|
13
|
+
* `stigg.ingestFromHeaders(...)` reads it back. This module is the header contract and the input
|
|
14
|
+
* shape they agree on.
|
|
15
|
+
*/
|
|
16
|
+
var headers_1 = require("./headers");
|
|
17
|
+
Object.defineProperty(exports, "GOVERNANCE_HEADERS", { enumerable: true, get: function () { return headers_1.GOVERNANCE_HEADERS; } });
|
|
18
|
+
Object.defineProperty(exports, "parseGovernanceHeaders", { enumerable: true, get: function () { return headers_1.parseGovernanceHeaders; } });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** What this binding accepts: the run's context, plus what only the request itself can say. */
|
|
2
|
+
import type { IngestContext } from '../../types';
|
|
3
|
+
export type ProxyIngestInput = IngestContext & {
|
|
4
|
+
/** Whatever the vendor reported; the shape is recognised on the way in. */
|
|
5
|
+
usage: unknown;
|
|
6
|
+
/** The gateway's own request id is the natural key - it is stable across its retries. */
|
|
7
|
+
idempotencyKey: string;
|
|
8
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a vendor's own request parameters into "how many units should this call ask for".
|
|
3
|
+
*
|
|
4
|
+
* Every vendor states a ceiling for the answer it is about to generate, under its own name -
|
|
5
|
+
* `max_tokens`, `config.maxOutputTokens`, a document whose length stands in for one - and what
|
|
6
|
+
* happens to that number afterwards is identical: divide by the rate, round up, never ask for
|
|
7
|
+
* less than one. Only the reading differs, so only the reading stays with the recipe.
|
|
8
|
+
*
|
|
9
|
+
* All of these are opt-in. The default asks for a single unit, because an estimate that
|
|
10
|
+
* overshoots denies calls the budget could have afforded, and the ingest that follows corrects
|
|
11
|
+
* the balance anyway.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Refuses a rate that cannot divide, where it is configured rather than where it is used.
|
|
15
|
+
*
|
|
16
|
+
* Zero divides to `Infinity` and a negative to less than one; `requestedUnits` in the wrapper
|
|
17
|
+
* clamps both back to asking for a single unit, so a misconfigured rate would gate on nothing and
|
|
18
|
+
* never say so. Failing here makes that a startup error instead of a quiet one, per call, forever.
|
|
19
|
+
*/
|
|
20
|
+
export declare function requirePositiveRate(name: string, value: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* The shared half: a rate, a reading, and the arithmetic between them.
|
|
23
|
+
*
|
|
24
|
+
* `read` returns the vendor's ceiling in tokens, or 0 where the call named none - which lands on
|
|
25
|
+
* the same floor of one unit rather than needing a case of its own.
|
|
26
|
+
*/
|
|
27
|
+
export declare function estimateUnits(read: (params: any) => number, tokensPerUnit: number): (params: any) => number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Turning a vendor's own request parameters into "how many units should this call ask for".
|
|
4
|
+
*
|
|
5
|
+
* Every vendor states a ceiling for the answer it is about to generate, under its own name -
|
|
6
|
+
* `max_tokens`, `config.maxOutputTokens`, a document whose length stands in for one - and what
|
|
7
|
+
* happens to that number afterwards is identical: divide by the rate, round up, never ask for
|
|
8
|
+
* less than one. Only the reading differs, so only the reading stays with the recipe.
|
|
9
|
+
*
|
|
10
|
+
* All of these are opt-in. The default asks for a single unit, because an estimate that
|
|
11
|
+
* overshoots denies calls the budget could have afforded, and the ingest that follows corrects
|
|
12
|
+
* the balance anyway.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.requirePositiveRate = requirePositiveRate;
|
|
16
|
+
exports.estimateUnits = estimateUnits;
|
|
17
|
+
/**
|
|
18
|
+
* Refuses a rate that cannot divide, where it is configured rather than where it is used.
|
|
19
|
+
*
|
|
20
|
+
* Zero divides to `Infinity` and a negative to less than one; `requestedUnits` in the wrapper
|
|
21
|
+
* clamps both back to asking for a single unit, so a misconfigured rate would gate on nothing and
|
|
22
|
+
* never say so. Failing here makes that a startup error instead of a quiet one, per call, forever.
|
|
23
|
+
*/
|
|
24
|
+
function requirePositiveRate(name, value) {
|
|
25
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
26
|
+
throw new Error(`${name} must be a finite positive number - got ${value}, which estimates every call wrongly.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The shared half: a rate, a reading, and the arithmetic between them.
|
|
31
|
+
*
|
|
32
|
+
* `read` returns the vendor's ceiling in tokens, or 0 where the call named none - which lands on
|
|
33
|
+
* the same floor of one unit rather than needing a case of its own.
|
|
34
|
+
*/
|
|
35
|
+
function estimateUnits(read, tokensPerUnit) {
|
|
36
|
+
requirePositiveRate('tokensPerUnit', tokensPerUnit);
|
|
37
|
+
return (params) => Math.max(1, Math.ceil(read(params) / tokensPerUnit));
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapping a stream so it can be metered, which every vendor needs and none of them differ on.
|
|
3
|
+
*
|
|
4
|
+
* What they *do* differ on is where the usage is: Anthropic accumulates it across two event types,
|
|
5
|
+
* Chat Completions puts it on the last chunk, the Responses API nests it under a completion event,
|
|
6
|
+
* and a gateway adds a cost and the model that served. So the mechanism is shared and the reading
|
|
7
|
+
* stays with the recipe that knows the shape.
|
|
8
|
+
*
|
|
9
|
+
* Two properties this owes every recipe, both of which it once got wrong:
|
|
10
|
+
*
|
|
11
|
+
* - **One completed stream meters once.** `onComplete` fires on the first ending and never
|
|
12
|
+
* again, however many times the object is iterated.
|
|
13
|
+
* - **Every way out of the stream is metered.** `for await` is the usual one, but an async
|
|
14
|
+
* generator - which is exactly what Gemini's `generateContentStream` resolves to - also
|
|
15
|
+
* exposes `next()` as public API, and a caller driving it by hand used to be metered at
|
|
16
|
+
* nothing at all.
|
|
17
|
+
*/
|
|
18
|
+
export declare function meterStream(stream: any, collect: (chunk: any) => void, onComplete: () => void): void;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.meterStream = meterStream;
|
|
4
|
+
/**
|
|
5
|
+
* Wrapping a stream so it can be metered, which every vendor needs and none of them differ on.
|
|
6
|
+
*
|
|
7
|
+
* What they *do* differ on is where the usage is: Anthropic accumulates it across two event types,
|
|
8
|
+
* Chat Completions puts it on the last chunk, the Responses API nests it under a completion event,
|
|
9
|
+
* and a gateway adds a cost and the model that served. So the mechanism is shared and the reading
|
|
10
|
+
* stays with the recipe that knows the shape.
|
|
11
|
+
*
|
|
12
|
+
* Two properties this owes every recipe, both of which it once got wrong:
|
|
13
|
+
*
|
|
14
|
+
* - **One completed stream meters once.** `onComplete` fires on the first ending and never
|
|
15
|
+
* again, however many times the object is iterated.
|
|
16
|
+
* - **Every way out of the stream is metered.** `for await` is the usual one, but an async
|
|
17
|
+
* generator - which is exactly what Gemini's `generateContentStream` resolves to - also
|
|
18
|
+
* exposes `next()` as public API, and a caller driving it by hand used to be metered at
|
|
19
|
+
* nothing at all.
|
|
20
|
+
*/
|
|
21
|
+
function meterStream(stream, collect, onComplete) {
|
|
22
|
+
if (!stream)
|
|
23
|
+
return;
|
|
24
|
+
// Captured before anything is patched, so the patched methods below can drive the vendor's own
|
|
25
|
+
// iterator without re-entering themselves. A bare generator's `[Symbol.asyncIterator]()`
|
|
26
|
+
// returns *itself*, so without this the loop would call whatever `next` currently holds - by
|
|
27
|
+
// then the patched one - and either recurse or count every chunk twice.
|
|
28
|
+
const iterate = typeof stream[Symbol.asyncIterator] === 'function' ? stream[Symbol.asyncIterator].bind(stream) : undefined;
|
|
29
|
+
const advance = typeof stream.next === 'function' ? stream.next.bind(stream) : undefined;
|
|
30
|
+
// Closing by hand ends a stream as surely as running it out, so these are captured too - and
|
|
31
|
+
// for the same reason: `source()` drives them, and binding them after the patch would have it
|
|
32
|
+
// calling itself.
|
|
33
|
+
const closers = ['return', 'throw']
|
|
34
|
+
.map((key) => ({ key, original: typeof stream[key] === 'function' ? stream[key].bind(stream) : undefined }))
|
|
35
|
+
.filter((entry) => !!entry.original);
|
|
36
|
+
if (!iterate && !advance)
|
|
37
|
+
return;
|
|
38
|
+
/** Fires once. A stream iterated twice is still one completed stream, and one measurement. */
|
|
39
|
+
let completed = false;
|
|
40
|
+
const finish = () => {
|
|
41
|
+
if (completed)
|
|
42
|
+
return;
|
|
43
|
+
completed = true;
|
|
44
|
+
onComplete();
|
|
45
|
+
};
|
|
46
|
+
// Reading a chunk is instrumentation: a vendor that changes shape must not break the loop the
|
|
47
|
+
// caller is standing in. The recipe reports what it could not read.
|
|
48
|
+
const observe = (value) => {
|
|
49
|
+
try {
|
|
50
|
+
collect(value);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Deliberately swallowed - `onComplete` still runs, and meters whatever was collected.
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The vendor's own iterator, resolved once and shared by both patched paths - so a caller that
|
|
58
|
+
* takes a few chunks by hand and then falls into a `for await` keeps reading one stream rather
|
|
59
|
+
* than opening a second.
|
|
60
|
+
*
|
|
61
|
+
* Lazy on purpose: for the vendors whose `[Symbol.asyncIterator]()` builds a reader, calling it
|
|
62
|
+
* eagerly would open a connection for a stream the caller may never consume.
|
|
63
|
+
*/
|
|
64
|
+
let inner;
|
|
65
|
+
/** Whether reading ever began. Closing a stream nobody read meters nothing, not a zero. */
|
|
66
|
+
const started = () => inner !== undefined;
|
|
67
|
+
const source = () => {
|
|
68
|
+
if (!inner) {
|
|
69
|
+
const it = iterate ? iterate() : stream;
|
|
70
|
+
inner =
|
|
71
|
+
// A generator hands back itself, and its `next` is patched below - so drive the original.
|
|
72
|
+
it === stream
|
|
73
|
+
? { next: advance, return: closers.find((entry) => entry.key === 'return')?.original }
|
|
74
|
+
: { next: it.next.bind(it), return: typeof it.return === 'function' ? it.return.bind(it) : undefined };
|
|
75
|
+
}
|
|
76
|
+
return inner;
|
|
77
|
+
};
|
|
78
|
+
if (advance) {
|
|
79
|
+
// The hand-driven path. Public API on any generator, and metered at nothing until now.
|
|
80
|
+
stream.next = async (...args) => {
|
|
81
|
+
try {
|
|
82
|
+
const result = await source().next(...args);
|
|
83
|
+
if (result?.done)
|
|
84
|
+
finish();
|
|
85
|
+
else
|
|
86
|
+
observe(result?.value);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// The stream ended, badly - but what it produced before it failed was still produced,
|
|
91
|
+
// and still cost the customer. `for await` meters that through its own `finally`; this
|
|
92
|
+
// path had no equivalent and dropped the whole measurement on the floor.
|
|
93
|
+
finish();
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// `return()` and `throw()` are how a hand-driven consumer stops early, and either one is the
|
|
99
|
+
// end of the stream. Without this the usage read up to that point was never reported.
|
|
100
|
+
for (const { key, original } of closers) {
|
|
101
|
+
stream[key] = async (...args) => {
|
|
102
|
+
try {
|
|
103
|
+
return await original(...args);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (started())
|
|
107
|
+
finish();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (iterate) {
|
|
112
|
+
stream[Symbol.asyncIterator] = async function* metered() {
|
|
113
|
+
const it = source();
|
|
114
|
+
// Whether the source ran itself out, as opposed to the consumer walking away from it. Only
|
|
115
|
+
// the second case owes it a `return()` - see the `finally`.
|
|
116
|
+
let exhausted = false;
|
|
117
|
+
try {
|
|
118
|
+
while (true) {
|
|
119
|
+
const { value, done } = await it.next();
|
|
120
|
+
if (done) {
|
|
121
|
+
exhausted = true;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
observe(value);
|
|
125
|
+
yield value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
// Closes the vendor's stream when the consumer broke out early, which is what `for await`
|
|
130
|
+
// did on the way out. Without it an abandoned stream holds its connection open.
|
|
131
|
+
//
|
|
132
|
+
// Only then, though. The iteration protocol closes an iterator on *abrupt* completion and
|
|
133
|
+
// deliberately not when it ran itself out, so a source that already finished is never
|
|
134
|
+
// asked to close - driving `next()` by hand means saying that explicitly rather than
|
|
135
|
+
// inheriting it. Calling `return()` on a spent iterator is a no-op for a plain generator,
|
|
136
|
+
// but a vendor is free to implement it as "abort the request", and aborting a stream that
|
|
137
|
+
// has already delivered its last chunk is a cancellation nobody asked for.
|
|
138
|
+
if (!exhausted) {
|
|
139
|
+
try {
|
|
140
|
+
await it.return?.();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// A source that objects to being closed must not mask the caller's own exit path.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Meter what was actually produced, even if the consumer broke out of the loop early.
|
|
147
|
+
finish();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
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 { BedrockLikeUsage, FlatTokenUsage, GeminiLikeUsage, LangChainLikeUsage, MistralLikeUsage, OllamaLikeUsage, 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
|
+
/**
|
|
20
|
+
* Bedrock's Converse usage, whose four counters are disjoint.
|
|
21
|
+
*
|
|
22
|
+
* Nothing is subtracted, unlike the OpenAI and Gemini readers: AWS states that `inputTokens`
|
|
23
|
+
* already excludes whatever was read from or written to the cache, so taking the cache back out
|
|
24
|
+
* would charge the call for less than it used.
|
|
25
|
+
*
|
|
26
|
+
* The total is read, not recomputed, like every other vendor that publishes one - but it is worth
|
|
27
|
+
* knowing what it covers. AWS documents `totalTokens` as the input and output of the call, and
|
|
28
|
+
* documents `inputTokens` as cache-exclusive, so on a cached call the stated total can be smaller
|
|
29
|
+
* than the four counters add up to. Both are reported: price the breakdown if the cache matters to
|
|
30
|
+
* you, the total if it does not, and see the README's overlap table before pricing both.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fromBedrockUsage(usage: BedrockLikeUsage | null | undefined): TokenUsage;
|
|
33
|
+
export declare function fromLangChainUsage(usage: LangChainLikeUsage | null | undefined): TokenUsage;
|
|
34
|
+
/**
|
|
35
|
+
* Gemini's `usageMetadata`, whose two overlaps run in opposite directions - see `GeminiLikeUsage`
|
|
36
|
+
* for the vendor's own wording on each.
|
|
37
|
+
*
|
|
38
|
+
* The arithmetic is checked by the one invariant the vendor publishes: the four counters below
|
|
39
|
+
* add back up to `totalTokenCount`, which is why neither overlap may be read the other way.
|
|
40
|
+
*/
|
|
41
|
+
export declare function fromGeminiUsage(usage: GeminiLikeUsage | null | undefined): TokenUsage;
|
|
42
|
+
/**
|
|
43
|
+
* Mistral's `UsageInfo` - the OpenAI three, camelCased by a generated SDK.
|
|
44
|
+
*
|
|
45
|
+
* Counts the same way OpenAI's chat usage does, so the arithmetic is the same: the vendor states
|
|
46
|
+
* its own total and it is read rather than recomputed. There is no cache counter to subtract -
|
|
47
|
+
* `UsageInfo` carries none, so unlike `fromOpenAIUsage` nothing is nested inside the prompt count.
|
|
48
|
+
*/
|
|
49
|
+
export declare function fromMistralUsage(usage: MistralLikeUsage | null | undefined): TokenUsage;
|
|
50
|
+
/**
|
|
51
|
+
* Ollama's two counters, read off the response root rather than out of a `usage` block.
|
|
52
|
+
*
|
|
53
|
+
* Disjoint - the prompt evaluation and the generation are separate work - so their sum is the
|
|
54
|
+
* whole call and computing a total from them is the vendor's own arithmetic rather than ours.
|
|
55
|
+
* Ollama publishes no total of its own, which is why there is one to compute.
|
|
56
|
+
*/
|
|
57
|
+
export declare function fromOllamaUsage(usage: OllamaLikeUsage | null | undefined): TokenUsage;
|
|
58
|
+
/**
|
|
59
|
+
* Any of the three shapes above, recognised by its own fields. Vendors disagree on names but not
|
|
60
|
+
* enough to be ambiguous, so the caller can pass whatever the response carried.
|
|
61
|
+
*/
|
|
62
|
+
export declare function normalizeUsage(usage: unknown): TokenUsage;
|
|
63
|
+
export declare const addTokenUsage: (into: TokenUsage, add: TokenUsage) => TokenUsage;
|
|
64
|
+
/**
|
|
65
|
+
* The name a computed total is reported under, for the vendors that publish none of their own.
|
|
66
|
+
*
|
|
67
|
+
* `total_tokens` rather than a name of ours, because it is what OpenAI calls the same number and
|
|
68
|
+
* what a reader expects to find - but it is worth knowing which vendors state it and which get it
|
|
69
|
+
* from {@link COMPUTED_TOTALS}: Anthropic and TypeSafe publish no total at all, so theirs is the
|
|
70
|
+
* SDK's arithmetic over counters the vendor *did* state, not a figure off their dashboard.
|
|
71
|
+
*/
|
|
72
|
+
export declare const TOTAL_TOKENS = "total_tokens";
|
|
73
|
+
/** Anthropic's four counters. Disjoint - cache reads and writes are not inside `input_tokens`. */
|
|
74
|
+
export declare const ANTHROPIC_COMPONENT_METRICS: readonly ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"];
|
|
75
|
+
/** Those four, plus the total this SDK adds up for them. */
|
|
76
|
+
export declare const ANTHROPIC_METRICS: readonly ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "total_tokens"];
|
|
77
|
+
/**
|
|
78
|
+
* Gemini's five. Two of them overlap in opposite directions and both are reported verbatim, the
|
|
79
|
+
* way every vendor's are, because a number published under a vendor's field name has to reconcile
|
|
80
|
+
* against that vendor's own dashboard: `cachedContentTokenCount` is already *inside*
|
|
81
|
+
* `promptTokenCount`, while `thoughtsTokenCount` and `toolUsePromptTokenCount` sit *outside* the
|
|
82
|
+
* two headline counters. Price accordingly - or price `normalizedMetrics` instead, which is
|
|
83
|
+
* disjoint by construction.
|
|
84
|
+
*/
|
|
85
|
+
export declare const GEMINI_METRICS: readonly ["promptTokenCount", "candidatesTokenCount", "cachedContentTokenCount", "thoughtsTokenCount", "toolUsePromptTokenCount", "totalTokenCount"];
|
|
86
|
+
/** What identifies a Gemini usage object: its breakdown, never its total. */
|
|
87
|
+
export declare const GEMINI_COMPONENT_METRICS: readonly ["promptTokenCount", "candidatesTokenCount", "cachedContentTokenCount", "thoughtsTokenCount", "toolUsePromptTokenCount"];
|
|
88
|
+
/**
|
|
89
|
+
* Chat Completions. `prompt_tokens` **includes** `cached_tokens`, so these two overlap - reported
|
|
90
|
+
* as the vendor states them, which is what reconciles against its invoice. Whoever prices
|
|
91
|
+
* `prompt_tokens` must know the cached ones are already inside it.
|
|
92
|
+
*/
|
|
93
|
+
export declare const OPENAI_CHAT_METRICS: readonly ["prompt_tokens", "completion_tokens", "cached_tokens", "total_tokens"];
|
|
94
|
+
/** The Responses API, which overlaps the same way under its own names. */
|
|
95
|
+
export declare const OPENAI_RESPONSES_METRICS: readonly ["input_tokens", "output_tokens", "cached_tokens", "total_tokens"];
|
|
96
|
+
/**
|
|
97
|
+
* Where Mistral's counters are **read** and what each is **reported** as - the only vocabulary
|
|
98
|
+
* here where those are different strings.
|
|
99
|
+
*
|
|
100
|
+
* Every other vendor is reported under the key it was read from, which is why `METRIC_PATHS` is a
|
|
101
|
+
* list and `metricName` just takes the last segment. Mistral's generated SDK renames the wire on
|
|
102
|
+
* the way in - the API sends `prompt_tokens`, the client hands you `promptTokens` - so following
|
|
103
|
+
* that convention would report a camelCase feature id that matches nothing on Mistral's own
|
|
104
|
+
* dashboard, and would differ from what the same account reports through an `OpenAI` client
|
|
105
|
+
* pointed at `api.mistral.ai/v1`. One account, one invoice, one vocabulary: the wire's.
|
|
106
|
+
*
|
|
107
|
+
* So the map runs read-key -> reported-name, and `pickRenamedMetrics` is what applies it.
|
|
108
|
+
*/
|
|
109
|
+
export declare const MISTRAL_METRIC_NAME_MAP: {
|
|
110
|
+
readonly promptTokens: "prompt_tokens";
|
|
111
|
+
readonly completionTokens: "completion_tokens";
|
|
112
|
+
readonly totalTokens: "total_tokens";
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Mistral's three, under the wire's own names.
|
|
116
|
+
*
|
|
117
|
+
* No cache counter: `UsageInfo` carries none. The cache and reasoning detail the v2 SDK models
|
|
118
|
+
* lives on a different type that chat responses do not use, so declaring it here would type a
|
|
119
|
+
* pricing table against a number that never arrives.
|
|
120
|
+
*/
|
|
121
|
+
export declare const MISTRAL_METRICS: readonly ["prompt_tokens", "completion_tokens", "total_tokens"];
|
|
122
|
+
/**
|
|
123
|
+
* TypeSafe's two. No cache counters and no cost: the API reports what it charged for and stops
|
|
124
|
+
* there, which was confirmed against the wire rather than inferred from the SDK's types.
|
|
125
|
+
*/
|
|
126
|
+
export declare const TYPESAFE_COMPONENT_METRICS: readonly ["input_tokens", "output_tokens"];
|
|
127
|
+
/** Those two, plus the total this SDK adds up for them. */
|
|
128
|
+
export declare const TYPESAFE_METRICS: readonly ["input_tokens", "output_tokens", "total_tokens"];
|
|
129
|
+
/**
|
|
130
|
+
* Ollama's two. Flat on the response root, so the paths are the names.
|
|
131
|
+
*
|
|
132
|
+
* No cache counter and no cost: Ollama is self-hosted, so there is no invoice behind these at all
|
|
133
|
+
* - they measure the customer's own capacity rather than a vendor's charge. The durations beside
|
|
134
|
+
* them on the response are nanoseconds, not tokens, and are deliberately absent from this list.
|
|
135
|
+
*/
|
|
136
|
+
export declare const OLLAMA_COMPONENT_METRICS: readonly ["prompt_eval_count", "eval_count"];
|
|
137
|
+
/** Those two, plus the total this SDK adds up for them. */
|
|
138
|
+
export declare const OLLAMA_METRICS: readonly ["prompt_eval_count", "eval_count", "total_tokens"];
|
|
139
|
+
/** Both OpenAI surfaces, which is what one client can report across its two APIs. */
|
|
140
|
+
export declare const OPENAI_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens"];
|
|
141
|
+
/** A gateway speaks the OpenAI shape and adds the price it charged, under its own field name. */
|
|
142
|
+
export declare const GATEWAY_METRICS: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens", "cost"];
|
|
143
|
+
/**
|
|
144
|
+
* Bedrock's five, under its own names and with no price among them.
|
|
145
|
+
*
|
|
146
|
+
* Reported verbatim rather than translated into the OpenAI spelling, which is the rule everywhere
|
|
147
|
+
* except where one service is reachable through two different clients - see `sdk-usage.ts` for
|
|
148
|
+
* that exception. Bedrock is reached one way, so its own names are the honest ones, and a reader
|
|
149
|
+
* comparing a Stigg meter against an AWS bill is comparing like with like.
|
|
150
|
+
*
|
|
151
|
+
* No `cost`: Bedrock states no price on the response, which is the thing that most separates it
|
|
152
|
+
* from the other two gateways here. See the recipe for what that costs a run.
|
|
153
|
+
*/
|
|
154
|
+
export declare const BEDROCK_PATHS: readonly ["inputTokens", "outputTokens", "cacheReadInputTokens", "cacheWriteInputTokens", "totalTokens"];
|
|
155
|
+
/** Those five. Flat, so the paths are the names. */
|
|
156
|
+
export declare const BEDROCK_METRICS: readonly ["inputTokens", "outputTokens", "cacheReadInputTokens", "cacheWriteInputTokens", "totalTokens"];
|
|
157
|
+
/** The leaf a dotted path reports under - `prompt_tokens_details.cached_tokens` -> `cached_tokens`. */
|
|
158
|
+
export declare const metricName: (path: string) => string;
|
|
159
|
+
/**
|
|
160
|
+
* Which counters are summed to produce a vendor's total, for the vendors that publish none.
|
|
161
|
+
*
|
|
162
|
+
* Only ever disjoint sets: Anthropic's four do not overlap and neither do TypeSafe's two, so the
|
|
163
|
+
* sum is the whole call and nothing is counted twice. A vendor whose counters overlap - Gemini's
|
|
164
|
+
* cached tokens sit inside its prompt count, OpenAI's inside theirs - is not in this table,
|
|
165
|
+
* because adding those up would be arithmetic the vendor never agreed to. Those vendors state a
|
|
166
|
+
* total themselves and it is read, not computed.
|
|
167
|
+
*/
|
|
168
|
+
export declare const COMPUTED_TOTALS: Record<string, readonly string[]>;
|
|
169
|
+
/**
|
|
170
|
+
* Adds the computed total to a metrics object, for a vendor that states none of its own.
|
|
171
|
+
*
|
|
172
|
+
* Absent rather than zero when nothing was counted, like every other metric here: a call that
|
|
173
|
+
* measured nothing reports nothing, instead of a zero somebody has to interpret.
|
|
174
|
+
*/
|
|
175
|
+
export declare function withComputedTotal(metrics: Metrics, keys: readonly string[]): Metrics;
|
|
176
|
+
/**
|
|
177
|
+
* Whether a path list and a name list agree, for the test that keeps them from drifting.
|
|
178
|
+
*
|
|
179
|
+
* Two lists describing one thing is the cost of separating where a counter lives from what it is
|
|
180
|
+
* called. This is what makes that cost a compile-and-test problem rather than a billing one.
|
|
181
|
+
*/
|
|
182
|
+
export declare const metricNamesMatchPaths: (paths: readonly string[], names: readonly string[]) => boolean;
|
|
183
|
+
/** The read paths, for the tests that hold them in step with the names above. */
|
|
184
|
+
export declare const METRIC_PATHS: {
|
|
185
|
+
readonly anthropic: readonly ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"];
|
|
186
|
+
readonly openaiChat: readonly ["prompt_tokens", "completion_tokens", "prompt_tokens_details.cached_tokens", "total_tokens"];
|
|
187
|
+
readonly openaiResponses: readonly ["input_tokens", "output_tokens", "input_tokens_details.cached_tokens", "total_tokens"];
|
|
188
|
+
readonly typesafe: readonly ["input_tokens", "output_tokens"];
|
|
189
|
+
readonly gemini: readonly ["promptTokenCount", "candidatesTokenCount", "cachedContentTokenCount", "thoughtsTokenCount", "toolUsePromptTokenCount", "totalTokenCount"];
|
|
190
|
+
readonly bedrock: readonly ["inputTokens", "outputTokens", "cacheReadInputTokens", "cacheWriteInputTokens", "totalTokens"];
|
|
191
|
+
readonly ollama: readonly ["prompt_eval_count", "eval_count"];
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Reads the named keys off a vendor's usage object, dotted paths included.
|
|
195
|
+
*
|
|
196
|
+
* A nested key reports under its leaf - `prompt_tokens_details.cached_tokens` becomes
|
|
197
|
+
* `cached_tokens` - because the leaf is the vendor's own name for it and a feature id with a dot
|
|
198
|
+
* in it reads badly everywhere. Anything absent, fractional or not a number is skipped: a metric
|
|
199
|
+
* the vendor did not report is not a zero, and Stigg rejects fractions.
|
|
200
|
+
*/
|
|
201
|
+
export declare function pickMetrics(usage: unknown, keys: readonly string[]): Metrics;
|
|
202
|
+
/**
|
|
203
|
+
* Reads each key of a read -> reported map off a usage object, reporting under the mapped name.
|
|
204
|
+
*
|
|
205
|
+
* The counterpart to {@link pickMetrics} for a vendor whose SDK spells its counters differently
|
|
206
|
+
* from its own wire. Same guards, and for the same reasons: anything absent, fractional or not a
|
|
207
|
+
* number is skipped, because a metric the vendor did not report is not a zero and Stigg rejects
|
|
208
|
+
* fractions. Flat keys only - no vendor needing this nests its counters, and a dotted read here
|
|
209
|
+
* would make the map ambiguous about which half of the arrow the dot belongs to.
|
|
210
|
+
*/
|
|
211
|
+
export declare function pickRenamedMetrics(usage: unknown, map: Record<string, string>): Metrics;
|
|
212
|
+
/** Accumulates metrics across a stream, which reports its counters in more than one chunk. */
|
|
213
|
+
export declare const addMetrics: (into: Metrics, add: Metrics) => Metrics;
|
|
214
|
+
/**
|
|
215
|
+
* The SDK's own four counters, under SDK-owned names, from the normalized measurement.
|
|
216
|
+
*
|
|
217
|
+
* The opt-out from vendor names, for an application running more than one provider: these are
|
|
218
|
+
* disjoint and mean the same thing whoever served the call, so one set of Stigg features covers
|
|
219
|
+
* every vendor. Pass it as a run's `metrics`.
|
|
220
|
+
*/
|
|
221
|
+
export declare const normalizedMetrics: MetricsMapper;
|
|
222
|
+
/** The shapes a gateway can be told it observed, since guessing reports to the wrong features. */
|
|
223
|
+
/**
|
|
224
|
+
* The shapes a proxy can be told it observed.
|
|
225
|
+
*
|
|
226
|
+
* Named for the vendor rather than for the shape, because a proxy operator knows what is behind
|
|
227
|
+
* them and not what its usage object looks like - but it is the shape that is being selected,
|
|
228
|
+
* which is why OpenAI appears twice. Its two APIs spell the same counters differently, and
|
|
229
|
+
* `openai-responses` is that surface rather than a vendor of its own.
|
|
230
|
+
*/
|
|
231
|
+
export type ObservedVendor = 'anthropic' | 'typesafe' | 'openai' | 'openai-responses' | 'gemini';
|
|
232
|
+
/**
|
|
233
|
+
* Both views of a usage object a proxy observed, for the ingest path that has no recipe.
|
|
234
|
+
*
|
|
235
|
+
* The vendor is named rather than sniffed. `normalizeUsage` guesses from the object's own fields
|
|
236
|
+
* and says itself that Anthropic and the Responses API are ambiguous - both count `input_tokens`.
|
|
237
|
+
* A wrong guess would report to another meter entirely, which nothing downstream can undo - so
|
|
238
|
+
* an unnamed vendor reports nothing raw and leaves the normalized counters to a mapper.
|
|
239
|
+
*/
|
|
240
|
+
export declare function observedMeasurement(usage: unknown, vendor?: ObservedVendor): Measurement;
|