@stigg/agent-sdk-js 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +1411 -2
  2. package/dist/bindings/client/core/registry.d.ts +12 -0
  3. package/dist/bindings/client/core/registry.js +22 -0
  4. package/dist/bindings/client/core/types.d.ts +126 -0
  5. package/dist/bindings/client/core/types.js +13 -0
  6. package/dist/bindings/client/core/wrap-client.d.ts +6 -0
  7. package/dist/bindings/client/core/wrap-client.js +261 -0
  8. package/dist/bindings/client/gateways/bedrock/bedrock-recipe.d.ts +17 -0
  9. package/dist/bindings/client/gateways/bedrock/bedrock-recipe.js +134 -0
  10. package/dist/bindings/client/gateways/bedrock/types.d.ts +13 -0
  11. package/dist/bindings/client/gateways/bedrock/types.js +2 -0
  12. package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.d.ts +10 -0
  13. package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.js +254 -0
  14. package/dist/bindings/client/gateways/cloudflare/types.d.ts +35 -0
  15. package/dist/bindings/client/gateways/cloudflare/types.js +2 -0
  16. package/dist/bindings/client/gateways/databricks/databricks-recipe.d.ts +32 -0
  17. package/dist/bindings/client/gateways/databricks/databricks-recipe.js +324 -0
  18. package/dist/bindings/client/gateways/databricks/types.d.ts +30 -0
  19. package/dist/bindings/client/gateways/databricks/types.js +2 -0
  20. package/dist/bindings/client/gateways/litellm/litellm-recipe.d.ts +17 -0
  21. package/dist/bindings/client/gateways/litellm/litellm-recipe.js +158 -0
  22. package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
  23. package/dist/bindings/client/gateways/litellm/types.js +2 -0
  24. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +20 -0
  25. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +233 -0
  26. package/dist/bindings/client/gateways/openrouter/sdk-usage.d.ts +37 -0
  27. package/dist/bindings/client/gateways/openrouter/sdk-usage.js +63 -0
  28. package/dist/bindings/client/gateways/openrouter/types.d.ts +78 -0
  29. package/dist/bindings/client/gateways/openrouter/types.js +2 -0
  30. package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +34 -0
  31. package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +169 -0
  32. package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
  33. package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
  34. package/dist/bindings/client/providers/groq/groq-recipe.d.ts +20 -0
  35. package/dist/bindings/client/providers/groq/groq-recipe.js +84 -0
  36. package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +22 -0
  37. package/dist/bindings/client/providers/mistral/mistral-recipe.js +114 -0
  38. package/dist/bindings/client/providers/mistral/types.d.ts +38 -0
  39. package/dist/bindings/client/providers/mistral/types.js +2 -0
  40. package/dist/bindings/client/providers/ollama/ollama-recipe.d.ts +31 -0
  41. package/dist/bindings/client/providers/ollama/ollama-recipe.js +107 -0
  42. package/dist/bindings/client/providers/ollama/types.d.ts +24 -0
  43. package/dist/bindings/client/providers/ollama/types.js +2 -0
  44. package/dist/bindings/client/providers/openai/openai-recipe.d.ts +71 -0
  45. package/dist/bindings/client/providers/openai/openai-recipe.js +122 -0
  46. package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +44 -0
  47. package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +83 -0
  48. package/dist/bindings/proxy/headers.d.ts +6 -0
  49. package/dist/bindings/proxy/headers.js +12 -0
  50. package/dist/bindings/proxy/index.d.ts +21 -0
  51. package/dist/bindings/proxy/index.js +18 -0
  52. package/dist/bindings/proxy/types.d.ts +8 -0
  53. package/dist/bindings/proxy/types.js +2 -0
  54. package/dist/bindings/shared/base-url.d.ts +44 -0
  55. package/dist/bindings/shared/base-url.js +90 -0
  56. package/dist/bindings/shared/estimate.d.ts +27 -0
  57. package/dist/bindings/shared/estimate.js +38 -0
  58. package/dist/bindings/shared/meter-stream.d.ts +18 -0
  59. package/dist/bindings/shared/meter-stream.js +151 -0
  60. package/dist/bindings/shared/token-usage.d.ts +345 -0
  61. package/dist/bindings/shared/token-usage.js +735 -0
  62. package/dist/bindings/shared/types.d.ts +170 -0
  63. package/dist/bindings/shared/types.js +2 -0
  64. package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
  65. package/dist/bindings/shared/weighted-metrics.js +47 -0
  66. package/dist/config.d.ts +145 -0
  67. package/dist/config.js +99 -0
  68. package/dist/core/context.d.ts +160 -0
  69. package/dist/core/context.js +221 -0
  70. package/dist/core/dimensions.d.ts +22 -0
  71. package/dist/core/dimensions.js +47 -0
  72. package/dist/core/hooks.d.ts +21 -0
  73. package/dist/core/hooks.js +47 -0
  74. package/dist/core/session.d.ts +190 -0
  75. package/dist/core/session.js +448 -0
  76. package/dist/decision-text.d.ts +15 -0
  77. package/dist/decision-text.js +54 -0
  78. package/dist/engine/composite-engine.d.ts +72 -0
  79. package/dist/engine/composite-engine.js +135 -0
  80. package/dist/engine/create-engine.d.ts +16 -0
  81. package/dist/engine/create-engine.js +93 -0
  82. package/dist/engine/decision.d.ts +42 -0
  83. package/dist/engine/decision.js +135 -0
  84. package/dist/engine/engine.d.ts +166 -0
  85. package/dist/engine/engine.js +11 -0
  86. package/dist/engine/node-sdk-engine.d.ts +125 -0
  87. package/dist/engine/node-sdk-engine.js +262 -0
  88. package/dist/engine/report.d.ts +106 -0
  89. package/dist/engine/report.js +188 -0
  90. package/dist/engine/rest-governance-engine.d.ts +47 -0
  91. package/dist/engine/rest-governance-engine.js +220 -0
  92. package/dist/errors.d.ts +86 -0
  93. package/dist/errors.js +161 -0
  94. package/dist/headers.d.ts +40 -0
  95. package/dist/headers.js +82 -0
  96. package/dist/index.d.ts +63 -0
  97. package/dist/index.js +124 -0
  98. package/dist/internal.d.ts +33 -0
  99. package/dist/internal.js +91 -0
  100. package/dist/logger.d.ts +15 -0
  101. package/dist/logger.js +20 -0
  102. package/dist/sdk-identity.d.ts +36 -0
  103. package/dist/sdk-identity.js +43 -0
  104. package/dist/stigg-agent-sdk.d.ts +300 -0
  105. package/dist/stigg-agent-sdk.js +669 -0
  106. package/dist/types.d.ts +676 -0
  107. package/dist/types.js +147 -0
  108. package/package.json +94 -3
@@ -0,0 +1,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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }