@stigg/agent-sdk-js 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1147 -2
- package/dist/bindings/client/core/registry.d.ts +12 -0
- package/dist/bindings/client/core/registry.js +22 -0
- package/dist/bindings/client/core/types.d.ts +117 -0
- package/dist/bindings/client/core/types.js +13 -0
- package/dist/bindings/client/core/wrap-client.d.ts +6 -0
- package/dist/bindings/client/core/wrap-client.js +258 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/litellm/litellm-recipe.js +171 -0
- package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
- package/dist/bindings/client/gateways/litellm/types.js +2 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +15 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +141 -0
- package/dist/bindings/client/gateways/openrouter/types.d.ts +29 -0
- package/dist/bindings/client/gateways/openrouter/types.js +2 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +30 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +79 -0
- package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
- package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
- package/dist/bindings/client/providers/openai/openai-recipe.d.ts +43 -0
- package/dist/bindings/client/providers/openai/openai-recipe.js +80 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +33 -0
- package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +73 -0
- package/dist/bindings/proxy/headers.d.ts +6 -0
- package/dist/bindings/proxy/headers.js +12 -0
- package/dist/bindings/proxy/index.d.ts +21 -0
- package/dist/bindings/proxy/index.js +18 -0
- package/dist/bindings/proxy/types.d.ts +8 -0
- package/dist/bindings/proxy/types.js +2 -0
- package/dist/bindings/shared/estimate.d.ts +27 -0
- package/dist/bindings/shared/estimate.js +38 -0
- package/dist/bindings/shared/meter-stream.d.ts +18 -0
- package/dist/bindings/shared/meter-stream.js +151 -0
- package/dist/bindings/shared/token-usage.d.ts +148 -0
- package/dist/bindings/shared/token-usage.js +414 -0
- package/dist/bindings/shared/types.d.ts +79 -0
- package/dist/bindings/shared/types.js +2 -0
- package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
- package/dist/bindings/shared/weighted-metrics.js +44 -0
- package/dist/config.d.ts +136 -0
- package/dist/config.js +97 -0
- package/dist/core/context.d.ts +151 -0
- package/dist/core/context.js +154 -0
- package/dist/core/dimensions.d.ts +22 -0
- package/dist/core/dimensions.js +47 -0
- package/dist/core/hooks.d.ts +21 -0
- package/dist/core/hooks.js +47 -0
- package/dist/core/session.d.ts +161 -0
- package/dist/core/session.js +403 -0
- package/dist/decision-text.d.ts +15 -0
- package/dist/decision-text.js +54 -0
- package/dist/engine/composite-engine.d.ts +72 -0
- package/dist/engine/composite-engine.js +135 -0
- package/dist/engine/create-engine.d.ts +16 -0
- package/dist/engine/create-engine.js +93 -0
- package/dist/engine/decision.d.ts +37 -0
- package/dist/engine/decision.js +128 -0
- package/dist/engine/engine.d.ts +166 -0
- package/dist/engine/engine.js +11 -0
- package/dist/engine/node-sdk-engine.d.ts +125 -0
- package/dist/engine/node-sdk-engine.js +262 -0
- package/dist/engine/report.d.ts +106 -0
- package/dist/engine/report.js +185 -0
- package/dist/engine/rest-governance-engine.d.ts +47 -0
- package/dist/engine/rest-governance-engine.js +220 -0
- package/dist/errors.d.ts +78 -0
- package/dist/errors.js +151 -0
- package/dist/headers.d.ts +27 -0
- package/dist/headers.js +63 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +85 -0
- package/dist/internal.d.ts +33 -0
- package/dist/internal.js +81 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +20 -0
- package/dist/sdk-identity.d.ts +36 -0
- package/dist/sdk-identity.js +43 -0
- package/dist/stigg-agent-sdk.d.ts +242 -0
- package/dist/stigg-agent-sdk.js +594 -0
- package/dist/types.d.ts +531 -0
- package/dist/types.js +97 -0
- package/package.json +80 -3
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openAIRecipe = exports.openAISurface = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The OpenAI recipe - the second vendor, and the proof that the generic wrapper carries its
|
|
6
|
+
* weight: everything below is response-shape knowledge, with no gate logic repeated.
|
|
7
|
+
*
|
|
8
|
+
* Works against any OpenAI-compatible endpoint, a gateway included. Note that a gateway usually
|
|
9
|
+
* wants the ingest-only gateway binding instead: it sees requests, not runs.
|
|
10
|
+
*/
|
|
11
|
+
const token_usage_1 = require("../../../shared/token-usage");
|
|
12
|
+
const meter_stream_1 = require("../../../shared/meter-stream");
|
|
13
|
+
const types_1 = require("../../../../types");
|
|
14
|
+
const registry_1 = require("../../core/registry");
|
|
15
|
+
/**
|
|
16
|
+
* The two surfaces, and where each keeps its usage.
|
|
17
|
+
*
|
|
18
|
+
* Chat Completions and Responses are the same API family with different field names, so the only
|
|
19
|
+
* thing that varies per surface is which extractor reads the counts and where a stream hides them.
|
|
20
|
+
*/
|
|
21
|
+
const SURFACES = [
|
|
22
|
+
{
|
|
23
|
+
path: ['chat', 'completions', 'create'],
|
|
24
|
+
parse: token_usage_1.fromOpenAIUsage,
|
|
25
|
+
keys: token_usage_1.METRIC_PATHS.openaiChat,
|
|
26
|
+
metrics: token_usage_1.OPENAI_CHAT_METRICS,
|
|
27
|
+
// Reported once, on the final chunk.
|
|
28
|
+
streamed: (chunk) => chunk?.usage,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
path: ['responses', 'create'],
|
|
32
|
+
parse: token_usage_1.fromResponsesUsage,
|
|
33
|
+
keys: token_usage_1.METRIC_PATHS.openaiResponses,
|
|
34
|
+
metrics: token_usage_1.OPENAI_RESPONSES_METRICS,
|
|
35
|
+
// Reported on the completion event, which carries the whole response rather than a delta.
|
|
36
|
+
streamed: (chunk) => chunk?.response?.usage ?? chunk?.usage,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
/** The reader for a surface, so a recipe built on top of this one stays in step with it. */
|
|
40
|
+
const openAISurface = (path) => SURFACES.find((surface) => surface.path.join('.') === path.join('.')) ?? SURFACES[0];
|
|
41
|
+
exports.openAISurface = openAISurface;
|
|
42
|
+
exports.openAIRecipe = {
|
|
43
|
+
vendor: 'openai',
|
|
44
|
+
metrics: token_usage_1.OPENAI_METRICS,
|
|
45
|
+
totalTokensMetric: token_usage_1.TOTAL_TOKENS,
|
|
46
|
+
// Either surface is enough: the recipe governs both, and a client built only for the Responses
|
|
47
|
+
// API has no `chat.completions` to find.
|
|
48
|
+
matches: (client) => (0, registry_1.hasMethod)(client, ['chat', 'completions', 'create']) || (0, registry_1.hasMethod)(client, ['responses', 'create']),
|
|
49
|
+
methods: SURFACES.map(({ path, parse, keys, streamed }) => ({
|
|
50
|
+
path: [...path],
|
|
51
|
+
// What OpenAI called it, which is what gets reported, beside the normalized counters. Note
|
|
52
|
+
// `prompt_tokens` includes the cached tokens reported next to it - the overlap is the
|
|
53
|
+
// vendor's, kept so the numbers reconcile against its own dashboard.
|
|
54
|
+
extractUsage: (result) => result?.usage
|
|
55
|
+
? { metrics: (0, token_usage_1.pickMetrics)(result.usage, keys), usage: parse(result.usage), raw: result.usage }
|
|
56
|
+
: undefined,
|
|
57
|
+
attachStream: (stream, ingest) => {
|
|
58
|
+
let total = (0, types_1.emptyTokenUsage)();
|
|
59
|
+
let metrics = {};
|
|
60
|
+
// As on the non-streamed path, so a streamed turn states its usage just as fully - the
|
|
61
|
+
// counters this recipe does not name (`total_tokens`, the reasoning-token detail) reach a
|
|
62
|
+
// meter either way rather than only when the caller happened not to stream.
|
|
63
|
+
let raw;
|
|
64
|
+
(0, meter_stream_1.meterStream)(stream, (chunk) => {
|
|
65
|
+
const usage = streamed(chunk);
|
|
66
|
+
// Reported once and whole rather than as deltas, so the last one wins outright - which
|
|
67
|
+
// is also what makes the block safe to keep as it arrived.
|
|
68
|
+
if (usage) {
|
|
69
|
+
total = parse(usage);
|
|
70
|
+
metrics = (0, token_usage_1.pickMetrics)(usage, keys);
|
|
71
|
+
// Copied, not referenced - it is held until the stream ends, and the chunk it came
|
|
72
|
+
// from belongs to the consumer. See the Gemini recipe for the full reasoning.
|
|
73
|
+
raw = { ...usage };
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
// Absent rather than `undefined` when no chunk carried usage - see the Gemini recipe.
|
|
77
|
+
() => ingest({ metrics, usage: total, ...(raw ? { raw } : {}) }));
|
|
78
|
+
},
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MetricsFor, MetricsOf } from '../../core/types';
|
|
2
|
+
export declare const typesafeRecipe: {
|
|
3
|
+
vendor: string;
|
|
4
|
+
metrics: readonly ["input_tokens", "output_tokens", "total_tokens"];
|
|
5
|
+
totalTokensMetric: "total_tokens";
|
|
6
|
+
matches: (client: unknown) => boolean;
|
|
7
|
+
methods: {
|
|
8
|
+
path: string[];
|
|
9
|
+
extractUsage: (result: any) => {
|
|
10
|
+
metrics: import("../../../..").Metrics;
|
|
11
|
+
usage: import("../../../..").TokenUsage;
|
|
12
|
+
raw: any;
|
|
13
|
+
} | undefined;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A pre-call estimate from the size of the document being asked about.
|
|
18
|
+
*
|
|
19
|
+
* Opt-in, like every estimate here, but note what makes this one different from a chat vendor's:
|
|
20
|
+
* there is no `max_tokens` to read, and the output is not where the money goes. A batch of
|
|
21
|
+
* questions against one document runs input-heavy by a wide margin - the document is sent once
|
|
22
|
+
* and each question adds almost nothing - so the state is the only part worth estimating, and
|
|
23
|
+
* estimating it is unusually reliable because it is already in hand before the call.
|
|
24
|
+
*
|
|
25
|
+
* `charsPerToken` is the usual rough divisor for English prose. Deliberately a floor: an estimate
|
|
26
|
+
* that overshoots denies calls the budget could have afforded, and the ingest that follows
|
|
27
|
+
* corrects the balance either way.
|
|
28
|
+
*/
|
|
29
|
+
export declare const estimateFromState: (tokensPerUnit?: number, charsPerToken?: number) => (params: any) => number;
|
|
30
|
+
/** One of the metric names TypeSafe reports. */
|
|
31
|
+
export type TypeSafeMetric = MetricsOf<typeof typesafeRecipe>;
|
|
32
|
+
/** What a mapper reads off a TypeSafe call: those names, each possibly absent. */
|
|
33
|
+
export type TypeSafeMetrics = MetricsFor<typeof typesafeRecipe>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.estimateFromState = exports.typesafeRecipe = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The TypeSafe recipe - the smallest one here, and worth reading for what it leaves out.
|
|
6
|
+
*
|
|
7
|
+
* TypeSafe is not chat-shaped. One `systemOne` call carries a document and a whole set of typed
|
|
8
|
+
* questions and comes back, once, with every answer and a single usage record. So there is one
|
|
9
|
+
* method to gate, no second call style reaching the wire through it, and no `attachStream`:
|
|
10
|
+
* nothing streams, so there is no later report to accumulate.
|
|
11
|
+
*
|
|
12
|
+
* The metrics are the two the API reports. That was checked against the wire, not inferred from
|
|
13
|
+
* the SDK's types - the response body carries `model`, `answers` and `usage`, and `usage` carries
|
|
14
|
+
* `input_tokens` and `output_tokens` and nothing else. No cache counters, because there is no
|
|
15
|
+
* prompt to cache across turns; no `cost`, because TypeSafe is not a gateway reselling someone
|
|
16
|
+
* else's model.
|
|
17
|
+
*/
|
|
18
|
+
const token_usage_1 = require("../../../shared/token-usage");
|
|
19
|
+
const estimate_1 = require("../../../shared/estimate");
|
|
20
|
+
const registry_1 = require("../../core/registry");
|
|
21
|
+
exports.typesafeRecipe = {
|
|
22
|
+
vendor: 'typesafe',
|
|
23
|
+
metrics: token_usage_1.TYPESAFE_METRICS,
|
|
24
|
+
totalTokensMetric: token_usage_1.TOTAL_TOKENS,
|
|
25
|
+
// One distinctive method is the whole surface. Unlike Anthropic there is no second call style
|
|
26
|
+
// to prove, because `systemOne` is the only thing that reaches the wire carrying usage.
|
|
27
|
+
matches: (client) => (0, registry_1.hasMethod)(client, ['systemOne']),
|
|
28
|
+
methods: [
|
|
29
|
+
{
|
|
30
|
+
path: ['systemOne'],
|
|
31
|
+
// Both views of the call: TypeSafe's own names, which is what gets reported, and the
|
|
32
|
+
// normalized counters. The flat reader covers this shape exactly - the two cache counters
|
|
33
|
+
// it also looks for are absent here, which reads as zero, which is true.
|
|
34
|
+
extractUsage: (result) => result?.usage
|
|
35
|
+
? {
|
|
36
|
+
// No total is published here either, so one is added up from the two counters
|
|
37
|
+
// that are - disjoint, so the sum is the whole call.
|
|
38
|
+
metrics: (0, token_usage_1.withComputedTotal)((0, token_usage_1.pickMetrics)(result.usage, token_usage_1.METRIC_PATHS.typesafe), token_usage_1.COMPUTED_TOTALS.typesafe),
|
|
39
|
+
usage: (0, token_usage_1.fromFlatTokenUsage)(result.usage),
|
|
40
|
+
// Kept whole for an event to carry, the same as every other recipe.
|
|
41
|
+
raw: result.usage,
|
|
42
|
+
}
|
|
43
|
+
: undefined,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A pre-call estimate from the size of the document being asked about.
|
|
49
|
+
*
|
|
50
|
+
* Opt-in, like every estimate here, but note what makes this one different from a chat vendor's:
|
|
51
|
+
* there is no `max_tokens` to read, and the output is not where the money goes. A batch of
|
|
52
|
+
* questions against one document runs input-heavy by a wide margin - the document is sent once
|
|
53
|
+
* and each question adds almost nothing - so the state is the only part worth estimating, and
|
|
54
|
+
* estimating it is unusually reliable because it is already in hand before the call.
|
|
55
|
+
*
|
|
56
|
+
* `charsPerToken` is the usual rough divisor for English prose. Deliberately a floor: an estimate
|
|
57
|
+
* that overshoots denies calls the budget could have afforded, and the ingest that follows
|
|
58
|
+
* corrects the balance either way.
|
|
59
|
+
*/
|
|
60
|
+
const estimateFromState = (tokensPerUnit = 1, charsPerToken = 4) => {
|
|
61
|
+
// Its own divisor, refused on the same terms as the rate: a zero here divides inside the
|
|
62
|
+
// reading, which `requestedUnits` would clamp back to one unit just as quietly.
|
|
63
|
+
(0, estimate_1.requirePositiveRate)('charsPerToken', charsPerToken);
|
|
64
|
+
return (0, estimate_1.estimateUnits)((params) => {
|
|
65
|
+
const state = params?.state;
|
|
66
|
+
// Nothing to measure lands on the floor of one unit, like a call that named no ceiling.
|
|
67
|
+
if (state == null)
|
|
68
|
+
return 0;
|
|
69
|
+
const characters = typeof state === 'string' ? state.length : JSON.stringify(state).length;
|
|
70
|
+
return characters / charsPerToken;
|
|
71
|
+
}, tokensPerUnit);
|
|
72
|
+
};
|
|
73
|
+
exports.estimateFromState = estimateFromState;
|
|
@@ -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
|
+
}
|