@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,44 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* `systemOne` is the whole surface - unlike Anthropic there is no second call style to prove,
|
|
8
|
+
* because it is the only thing that reaches the wire carrying usage.
|
|
9
|
+
*
|
|
10
|
+
* The exclusion names the one client that collides: `ollama` ships a `systemone` method whose
|
|
11
|
+
* request and response are the *same shape* as this one, so casing would otherwise be the only
|
|
12
|
+
* thing between them. Excluding that client by its own three-marker guard - rather than
|
|
13
|
+
* demanding extra methods here - keeps this recipe claiming anything that exposes `systemOne`,
|
|
14
|
+
* a thin adapter or a test double included, while making the two guards mutually exclusive
|
|
15
|
+
* whatever order a registry consults them in.
|
|
16
|
+
*/
|
|
17
|
+
matches: (client: unknown) => boolean;
|
|
18
|
+
methods: {
|
|
19
|
+
path: string[];
|
|
20
|
+
extractUsage: (result: any) => {
|
|
21
|
+
metrics: import("../../../..").Metrics;
|
|
22
|
+
usage: import("../../../..").TokenUsage;
|
|
23
|
+
raw: any;
|
|
24
|
+
} | undefined;
|
|
25
|
+
}[];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A pre-call estimate from the size of the document being asked about.
|
|
29
|
+
*
|
|
30
|
+
* Opt-in, like every estimate here, but note what makes this one different from a chat vendor's:
|
|
31
|
+
* there is no `max_tokens` to read, and the output is not where the money goes. A batch of
|
|
32
|
+
* questions against one document runs input-heavy by a wide margin - the document is sent once
|
|
33
|
+
* and each question adds almost nothing - so the state is the only part worth estimating, and
|
|
34
|
+
* estimating it is unusually reliable because it is already in hand before the call.
|
|
35
|
+
*
|
|
36
|
+
* `charsPerToken` is the usual rough divisor for English prose. Deliberately a floor: an estimate
|
|
37
|
+
* that overshoots denies calls the budget could have afforded, and the ingest that follows
|
|
38
|
+
* corrects the balance either way.
|
|
39
|
+
*/
|
|
40
|
+
export declare const estimateFromState: (tokensPerUnit?: number, charsPerToken?: number) => (params: any) => number;
|
|
41
|
+
/** One of the metric names TypeSafe reports. */
|
|
42
|
+
export type TypeSafeMetric = MetricsOf<typeof typesafeRecipe>;
|
|
43
|
+
/** What a mapper reads off a TypeSafe call: those names, each possibly absent. */
|
|
44
|
+
export type TypeSafeMetrics = MetricsFor<typeof typesafeRecipe>;
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
const ollama_recipe_1 = require("../ollama/ollama-recipe");
|
|
22
|
+
exports.typesafeRecipe = {
|
|
23
|
+
vendor: 'typesafe',
|
|
24
|
+
metrics: token_usage_1.TYPESAFE_METRICS,
|
|
25
|
+
totalTokensMetric: token_usage_1.TOTAL_TOKENS,
|
|
26
|
+
/**
|
|
27
|
+
* `systemOne` is the whole surface - unlike Anthropic there is no second call style to prove,
|
|
28
|
+
* because it is the only thing that reaches the wire carrying usage.
|
|
29
|
+
*
|
|
30
|
+
* The exclusion names the one client that collides: `ollama` ships a `systemone` method whose
|
|
31
|
+
* request and response are the *same shape* as this one, so casing would otherwise be the only
|
|
32
|
+
* thing between them. Excluding that client by its own three-marker guard - rather than
|
|
33
|
+
* demanding extra methods here - keeps this recipe claiming anything that exposes `systemOne`,
|
|
34
|
+
* a thin adapter or a test double included, while making the two guards mutually exclusive
|
|
35
|
+
* whatever order a registry consults them in.
|
|
36
|
+
*/
|
|
37
|
+
matches: (client) => (0, registry_1.hasMethod)(client, ['systemOne']) && !ollama_recipe_1.ollamaRecipe.matches(client),
|
|
38
|
+
methods: [
|
|
39
|
+
{
|
|
40
|
+
path: ['systemOne'],
|
|
41
|
+
// Both views of the call: TypeSafe's own names, which is what gets reported, and the
|
|
42
|
+
// normalized counters. The flat reader covers this shape exactly - the two cache counters
|
|
43
|
+
// it also looks for are absent here, which reads as zero, which is true.
|
|
44
|
+
extractUsage: (result) => result?.usage
|
|
45
|
+
? {
|
|
46
|
+
// No total is published here either, so one is added up from the two counters
|
|
47
|
+
// that are - disjoint, so the sum is the whole call.
|
|
48
|
+
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),
|
|
49
|
+
usage: (0, token_usage_1.fromFlatTokenUsage)(result.usage),
|
|
50
|
+
// Kept whole for an event to carry, the same as every other recipe.
|
|
51
|
+
raw: result.usage,
|
|
52
|
+
}
|
|
53
|
+
: undefined,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A pre-call estimate from the size of the document being asked about.
|
|
59
|
+
*
|
|
60
|
+
* Opt-in, like every estimate here, but note what makes this one different from a chat vendor's:
|
|
61
|
+
* there is no `max_tokens` to read, and the output is not where the money goes. A batch of
|
|
62
|
+
* questions against one document runs input-heavy by a wide margin - the document is sent once
|
|
63
|
+
* and each question adds almost nothing - so the state is the only part worth estimating, and
|
|
64
|
+
* estimating it is unusually reliable because it is already in hand before the call.
|
|
65
|
+
*
|
|
66
|
+
* `charsPerToken` is the usual rough divisor for English prose. Deliberately a floor: an estimate
|
|
67
|
+
* that overshoots denies calls the budget could have afforded, and the ingest that follows
|
|
68
|
+
* corrects the balance either way.
|
|
69
|
+
*/
|
|
70
|
+
const estimateFromState = (tokensPerUnit = 1, charsPerToken = 4) => {
|
|
71
|
+
// Its own divisor, refused on the same terms as the rate: a zero here divides inside the
|
|
72
|
+
// reading, which `requestedUnits` would clamp back to one unit just as quietly.
|
|
73
|
+
(0, estimate_1.requirePositiveRate)('charsPerToken', charsPerToken);
|
|
74
|
+
return (0, estimate_1.estimateUnits)((params) => {
|
|
75
|
+
const state = params?.state;
|
|
76
|
+
// Nothing to measure lands on the floor of one unit, like a call that named no ceiling.
|
|
77
|
+
if (state == null)
|
|
78
|
+
return 0;
|
|
79
|
+
const characters = typeof state === 'string' ? state.length : JSON.stringify(state).length;
|
|
80
|
+
return characters / charsPerToken;
|
|
81
|
+
}, tokensPerUnit);
|
|
82
|
+
};
|
|
83
|
+
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,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a client's `baseURL`, which is the only thing that says which vendor is on the far end.
|
|
3
|
+
*
|
|
4
|
+
* Shape cannot answer that question: every OpenAI-compatible endpoint has the same methods, so
|
|
5
|
+
* `matches` falls back to the URL, and a wrong answer there does not throw - it silently meters
|
|
6
|
+
* one vendor's traffic under another vendor's name and bills the wrong party. That is what makes
|
|
7
|
+
* these few lines the most consequential ones in a binding, and why they live in one place rather
|
|
8
|
+
* than being written out per recipe.
|
|
9
|
+
*
|
|
10
|
+
* The subdomain rule below is the part worth getting exactly right, and the part a hand-written
|
|
11
|
+
* copy tends to get wrong. Five recipes match on a host - Groq, OpenRouter, LiteLLM, Databricks
|
|
12
|
+
* and Cloudflare - and two of them need a *set* of host suffixes rather than one, with Databricks
|
|
13
|
+
* needing the path as well, because its host families cover every Databricks API and most of them
|
|
14
|
+
* are not inference.
|
|
15
|
+
*
|
|
16
|
+
* This is deliberately not in `token-usage.ts`. That file is the vocabulary of what a response
|
|
17
|
+
* counts, and a hostname is not a counter.
|
|
18
|
+
*/
|
|
19
|
+
/** The hostname, lowercased, or '' when the URL is not one this can reason about. */
|
|
20
|
+
export declare function hostOf(url: string): string;
|
|
21
|
+
/** The path, lowercased and without trailing slashes, or '' when the URL will not parse. */
|
|
22
|
+
export declare function pathOf(url: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether a URL is served by `expected` or a subdomain of it.
|
|
25
|
+
*
|
|
26
|
+
* The dot is the whole point. A substring test lets `api.vendor.com.attacker.net` through, and an
|
|
27
|
+
* `endsWith` without it lets `notvendor.com` match `vendor.com` - both are somebody else's domain,
|
|
28
|
+
* and claiming either attributes their traffic to this vendor.
|
|
29
|
+
*/
|
|
30
|
+
export declare function matchesHost(url: string, expected: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The same test against several hosts, for a platform that serves one product from more than one
|
|
33
|
+
* domain - Databricks has a hostname per cloud, and none of them is more canonical than the rest.
|
|
34
|
+
*/
|
|
35
|
+
export declare function matchesAnyHost(url: string, expected: readonly string[]): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether `candidate` is the configured endpoint, or something underneath it.
|
|
38
|
+
*
|
|
39
|
+
* A prefix match on the raw string lets `.../v1` claim `.../v10`, which is a different deployment
|
|
40
|
+
* with a different price list. Compare the origin, then the path one whole segment at a time.
|
|
41
|
+
*/
|
|
42
|
+
export declare function withinBaseUrl(configured: string, candidate: string): boolean;
|
|
43
|
+
/** A client's configured base URL, when it has one this can read. */
|
|
44
|
+
export declare function baseUrlOf(client: unknown): string | undefined;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reading a client's `baseURL`, which is the only thing that says which vendor is on the far end.
|
|
4
|
+
*
|
|
5
|
+
* Shape cannot answer that question: every OpenAI-compatible endpoint has the same methods, so
|
|
6
|
+
* `matches` falls back to the URL, and a wrong answer there does not throw - it silently meters
|
|
7
|
+
* one vendor's traffic under another vendor's name and bills the wrong party. That is what makes
|
|
8
|
+
* these few lines the most consequential ones in a binding, and why they live in one place rather
|
|
9
|
+
* than being written out per recipe.
|
|
10
|
+
*
|
|
11
|
+
* The subdomain rule below is the part worth getting exactly right, and the part a hand-written
|
|
12
|
+
* copy tends to get wrong. Five recipes match on a host - Groq, OpenRouter, LiteLLM, Databricks
|
|
13
|
+
* and Cloudflare - and two of them need a *set* of host suffixes rather than one, with Databricks
|
|
14
|
+
* needing the path as well, because its host families cover every Databricks API and most of them
|
|
15
|
+
* are not inference.
|
|
16
|
+
*
|
|
17
|
+
* This is deliberately not in `token-usage.ts`. That file is the vocabulary of what a response
|
|
18
|
+
* counts, and a hostname is not a counter.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.hostOf = hostOf;
|
|
22
|
+
exports.pathOf = pathOf;
|
|
23
|
+
exports.matchesHost = matchesHost;
|
|
24
|
+
exports.matchesAnyHost = matchesAnyHost;
|
|
25
|
+
exports.withinBaseUrl = withinBaseUrl;
|
|
26
|
+
exports.baseUrlOf = baseUrlOf;
|
|
27
|
+
/** The hostname, lowercased, or '' when the URL is not one this can reason about. */
|
|
28
|
+
function hostOf(url) {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(url).hostname.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The path, lowercased and without trailing slashes, or '' when the URL will not parse. */
|
|
37
|
+
function pathOf(url) {
|
|
38
|
+
try {
|
|
39
|
+
return new URL(url).pathname.toLowerCase().replace(/\/+$/, '');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a URL is served by `expected` or a subdomain of it.
|
|
47
|
+
*
|
|
48
|
+
* The dot is the whole point. A substring test lets `api.vendor.com.attacker.net` through, and an
|
|
49
|
+
* `endsWith` without it lets `notvendor.com` match `vendor.com` - both are somebody else's domain,
|
|
50
|
+
* and claiming either attributes their traffic to this vendor.
|
|
51
|
+
*/
|
|
52
|
+
function matchesHost(url, expected) {
|
|
53
|
+
const host = hostOf(url);
|
|
54
|
+
const want = expected.toLowerCase();
|
|
55
|
+
if (!host || !want)
|
|
56
|
+
return false;
|
|
57
|
+
return host === want || host.endsWith(`.${want}`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The same test against several hosts, for a platform that serves one product from more than one
|
|
61
|
+
* domain - Databricks has a hostname per cloud, and none of them is more canonical than the rest.
|
|
62
|
+
*/
|
|
63
|
+
function matchesAnyHost(url, expected) {
|
|
64
|
+
return expected.some((host) => matchesHost(url, host));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether `candidate` is the configured endpoint, or something underneath it.
|
|
68
|
+
*
|
|
69
|
+
* A prefix match on the raw string lets `.../v1` claim `.../v10`, which is a different deployment
|
|
70
|
+
* with a different price list. Compare the origin, then the path one whole segment at a time.
|
|
71
|
+
*/
|
|
72
|
+
function withinBaseUrl(configured, candidate) {
|
|
73
|
+
try {
|
|
74
|
+
const want = new URL(configured);
|
|
75
|
+
const got = new URL(candidate);
|
|
76
|
+
if (want.origin !== got.origin)
|
|
77
|
+
return false;
|
|
78
|
+
const base = want.pathname.replace(/\/+$/, '');
|
|
79
|
+
const path = got.pathname.replace(/\/+$/, '');
|
|
80
|
+
return path === base || path.startsWith(`${base}/`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** A client's configured base URL, when it has one this can read. */
|
|
87
|
+
function baseUrlOf(client) {
|
|
88
|
+
const configured = client?.baseURL;
|
|
89
|
+
return typeof configured === 'string' ? configured : undefined;
|
|
90
|
+
}
|
|
@@ -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
|
+
}
|