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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +1296 -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 +117 -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 +258 -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/litellm/litellm-recipe.d.ts +17 -0
  13. package/dist/bindings/client/gateways/litellm/litellm-recipe.js +171 -0
  14. package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
  15. package/dist/bindings/client/gateways/litellm/types.js +2 -0
  16. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +20 -0
  17. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +244 -0
  18. package/dist/bindings/client/gateways/openrouter/sdk-usage.d.ts +37 -0
  19. package/dist/bindings/client/gateways/openrouter/sdk-usage.js +63 -0
  20. package/dist/bindings/client/gateways/openrouter/types.d.ts +78 -0
  21. package/dist/bindings/client/gateways/openrouter/types.js +2 -0
  22. package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +34 -0
  23. package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +169 -0
  24. package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
  25. package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
  26. package/dist/bindings/client/providers/groq/groq-recipe.d.ts +20 -0
  27. package/dist/bindings/client/providers/groq/groq-recipe.js +101 -0
  28. package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +22 -0
  29. package/dist/bindings/client/providers/mistral/mistral-recipe.js +114 -0
  30. package/dist/bindings/client/providers/mistral/types.d.ts +38 -0
  31. package/dist/bindings/client/providers/mistral/types.js +2 -0
  32. package/dist/bindings/client/providers/ollama/ollama-recipe.d.ts +31 -0
  33. package/dist/bindings/client/providers/ollama/ollama-recipe.js +107 -0
  34. package/dist/bindings/client/providers/ollama/types.d.ts +24 -0
  35. package/dist/bindings/client/providers/ollama/types.js +2 -0
  36. package/dist/bindings/client/providers/openai/openai-recipe.d.ts +71 -0
  37. package/dist/bindings/client/providers/openai/openai-recipe.js +122 -0
  38. package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +44 -0
  39. package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +83 -0
  40. package/dist/bindings/proxy/headers.d.ts +6 -0
  41. package/dist/bindings/proxy/headers.js +12 -0
  42. package/dist/bindings/proxy/index.d.ts +21 -0
  43. package/dist/bindings/proxy/index.js +18 -0
  44. package/dist/bindings/proxy/types.d.ts +8 -0
  45. package/dist/bindings/proxy/types.js +2 -0
  46. package/dist/bindings/shared/estimate.d.ts +27 -0
  47. package/dist/bindings/shared/estimate.js +38 -0
  48. package/dist/bindings/shared/meter-stream.d.ts +18 -0
  49. package/dist/bindings/shared/meter-stream.js +151 -0
  50. package/dist/bindings/shared/token-usage.d.ts +240 -0
  51. package/dist/bindings/shared/token-usage.js +574 -0
  52. package/dist/bindings/shared/types.d.ts +129 -0
  53. package/dist/bindings/shared/types.js +2 -0
  54. package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
  55. package/dist/bindings/shared/weighted-metrics.js +47 -0
  56. package/dist/config.d.ts +145 -0
  57. package/dist/config.js +99 -0
  58. package/dist/core/context.d.ts +160 -0
  59. package/dist/core/context.js +221 -0
  60. package/dist/core/dimensions.d.ts +22 -0
  61. package/dist/core/dimensions.js +47 -0
  62. package/dist/core/hooks.d.ts +21 -0
  63. package/dist/core/hooks.js +47 -0
  64. package/dist/core/session.d.ts +190 -0
  65. package/dist/core/session.js +448 -0
  66. package/dist/decision-text.d.ts +15 -0
  67. package/dist/decision-text.js +54 -0
  68. package/dist/engine/composite-engine.d.ts +72 -0
  69. package/dist/engine/composite-engine.js +135 -0
  70. package/dist/engine/create-engine.d.ts +16 -0
  71. package/dist/engine/create-engine.js +93 -0
  72. package/dist/engine/decision.d.ts +42 -0
  73. package/dist/engine/decision.js +135 -0
  74. package/dist/engine/engine.d.ts +166 -0
  75. package/dist/engine/engine.js +11 -0
  76. package/dist/engine/node-sdk-engine.d.ts +125 -0
  77. package/dist/engine/node-sdk-engine.js +262 -0
  78. package/dist/engine/report.d.ts +106 -0
  79. package/dist/engine/report.js +188 -0
  80. package/dist/engine/rest-governance-engine.d.ts +47 -0
  81. package/dist/engine/rest-governance-engine.js +220 -0
  82. package/dist/errors.d.ts +86 -0
  83. package/dist/errors.js +161 -0
  84. package/dist/headers.d.ts +40 -0
  85. package/dist/headers.js +82 -0
  86. package/dist/index.d.ts +57 -0
  87. package/dist/index.js +106 -0
  88. package/dist/internal.d.ts +33 -0
  89. package/dist/internal.js +87 -0
  90. package/dist/logger.d.ts +15 -0
  91. package/dist/logger.js +20 -0
  92. package/dist/sdk-identity.d.ts +36 -0
  93. package/dist/sdk-identity.js +43 -0
  94. package/dist/stigg-agent-sdk.d.ts +300 -0
  95. package/dist/stigg-agent-sdk.js +656 -0
  96. package/dist/types.d.ts +642 -0
  97. package/dist/types.js +147 -0
  98. package/package.json +89 -3
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mistralRecipe = void 0;
4
+ /**
5
+ * The Mistral recipe - `@mistralai/mistralai`, the Speakeasy-generated client.
6
+ *
7
+ * ## What is covered
8
+ *
9
+ * Every surface that burns tokens and reports them the same way: `chat.complete`, `chat.stream`,
10
+ * `chat.parse`, `chat.parseStream`, `fim.complete`, `fim.stream`, `agents.complete` and
11
+ * `agents.stream`. Eight methods, one extractor - `FIMCompletionResponse` and
12
+ * `ChatCompletionResponse` both carry `usage: UsageInfo`, `ParsedChatCompletionResponse` is a
13
+ * `ChatCompletionResponse` with its choices retyped, and all four streamed methods resolve to the
14
+ * same `EventStream<CompletionEvent>`. That was read off the SDK's own types, not assumed.
15
+ *
16
+ * Each is its own event name, because a meter filtering on one sees nothing from the others.
17
+ * `chat.parse` in particular is **not** a wrapper over `chat.complete`: it reaches the wire itself,
18
+ * so leaving it out would ship a billable path that looks governed.
19
+ *
20
+ * ## What is deliberately not covered
21
+ *
22
+ * `beta.conversations.*` is declined by name. It reports `ConversationUsageInfo` - the three
23
+ * counters plus `connectorTokens` and a per-connector map - which is a second vocabulary, and an
24
+ * event name is permanent once a customer's meter filters on it. Gating it with this extractor
25
+ * would read the three it shares and silently drop the connector spend, which is the shape of
26
+ * error this file exists to avoid. It wants its own recipe and its own metric list.
27
+ *
28
+ * Also not covered, each counting in units these token metrics do not describe: `embeddings.create`
29
+ * (tokens, but of a different kind and against a different price list), `ocr.process` (whose
30
+ * `usageInfo` is `pagesProcessed`, a page count), `audio.transcriptions.*` (`promptAudioSeconds`),
31
+ * `classifiers.*`, `files.*`, `batch.*` and v2's `workflows.*`. Meter one by naming your own
32
+ * recipe at `wrap(client, { recipe })`.
33
+ *
34
+ * ## Two gaps a caller should know about
35
+ *
36
+ * **The standalone functions bypass this entirely.** Every method also exists as a tree-shakable
37
+ * function taking the client as its first argument - `chatComplete(client, request)` from
38
+ * `@mistralai/mistralai/funcs/chatComplete.js`. Those do not go through the patched method, so a
39
+ * call made that way is neither gated nor metered while `wrap()` reports success. Mistral ships an
40
+ * `SDKHooks` seam that would catch both forms, and it is deliberately not used: holding its
41
+ * `HookContext` means importing the vendor package into `src/`, which `module-boundaries.test.ts`
42
+ * forbids outright. A recipe here is data - method paths and duck-typed extractors - and that is
43
+ * what lets `src/index.ts` export every binding with no vendor SDK at runtime. So this is a real
44
+ * limit rather than an oversight: call the methods, not the funcs.
45
+ *
46
+ * **A stream read through `getReader()` is not metered.** `EventStream` extends `ReadableStream`
47
+ * *and* overrides `[Symbol.asyncIterator]`, so `meterStream` patches the iterator in place and the
48
+ * documented `for await` is covered. A consumer that takes a reader instead bypasses it, and the
49
+ * stream is the caller's own object - it cannot be swapped for a metered copy without breaking
50
+ * `instanceof`. Stated here rather than left to be discovered.
51
+ */
52
+ const token_usage_1 = require("../../../shared/token-usage");
53
+ const meter_stream_1 = require("../../../shared/meter-stream");
54
+ const types_1 = require("../../../../types");
55
+ const registry_1 = require("../../core/registry");
56
+ /**
57
+ * Both views of one usage block: Mistral's counters under the wire's names, and the normalized
58
+ * four a mapper of your own would read.
59
+ *
60
+ * `raw` is the renamed projection rather than the object the SDK handed over, so an event's
61
+ * `raw.*` dimensions agree with its metrics - and so the same account reports the same strings
62
+ * whether the application holds a `Mistral` client or an `OpenAI` one pointed at its base URL.
63
+ */
64
+ function measure(usage) {
65
+ if (!usage)
66
+ return undefined;
67
+ const metrics = (0, token_usage_1.pickRenamedMetrics)(usage, token_usage_1.MISTRAL_METRIC_NAME_MAP);
68
+ // Nothing readable in it - an absent counter is absent, not zero, so there is no measurement to
69
+ // report rather than one that says the call cost nothing.
70
+ if (Object.keys(metrics).length === 0)
71
+ return undefined;
72
+ return { metrics, usage: (0, token_usage_1.fromMistralUsage)(usage), raw: { ...metrics } };
73
+ }
74
+ /** Reads the buffered response every gated `complete`/`parse` surface returns. */
75
+ const extractUsage = (result) => measure(result?.usage);
76
+ /** The streamed counterpart, whose counters arrive once and a level down, under `data`. */
77
+ const attachStream = (stream, ingest) => {
78
+ let metrics = {};
79
+ let usage = (0, types_1.emptyTokenUsage)();
80
+ let raw;
81
+ (0, meter_stream_1.meterStream)(stream, (chunk) => {
82
+ // Stated whole on the terminating chunk rather than as deltas, so the last one wins.
83
+ const measured = measure(chunk?.data?.usage);
84
+ if (measured) {
85
+ metrics = measured.metrics ?? {};
86
+ usage = measured.usage ?? (0, types_1.emptyTokenUsage)();
87
+ raw = measured.raw;
88
+ }
89
+ },
90
+ // Absent rather than an empty object when no chunk carried usage, as the other recipes do.
91
+ () => ingest({ metrics, usage, ...(raw ? { raw } : {}) }));
92
+ };
93
+ /** The surfaces, and whether each one streams. */
94
+ const SURFACES = [
95
+ { path: ['chat', 'complete'], streams: false },
96
+ { path: ['chat', 'stream'], streams: true },
97
+ { path: ['chat', 'parse'], streams: false },
98
+ { path: ['chat', 'parseStream'], streams: true },
99
+ { path: ['fim', 'complete'], streams: false },
100
+ { path: ['fim', 'stream'], streams: true },
101
+ { path: ['agents', 'complete'], streams: false },
102
+ { path: ['agents', 'stream'], streams: true },
103
+ ];
104
+ exports.mistralRecipe = {
105
+ vendor: 'mistral',
106
+ metrics: token_usage_1.MISTRAL_METRICS,
107
+ // Mistral states its own total, so it is read rather than summed from the parts.
108
+ totalTokensMetric: token_usage_1.TOTAL_TOKENS,
109
+ // Two signals, because `complete` on its own is far too common a verb to claim. An `OpenAI`
110
+ // client pointed at `api.mistral.ai/v1` has neither of these - it speaks `chat.completions.
111
+ // create` and is governed by `openAIRecipe` as `openai.*`, which the README spells out.
112
+ matches: (client) => (0, registry_1.hasMethod)(client, ['chat', 'complete']) && (0, registry_1.hasMethod)(client, ['chat', 'stream']),
113
+ methods: SURFACES.map(({ path, streams }) => streams ? { path: [...path], attachStream } : { path: [...path], extractUsage }),
114
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * What a Mistral response carries, typed structurally.
3
+ *
4
+ * No import from `@mistralai/mistralai` - `module-boundaries.test.ts` would fail on one, and the
5
+ * package's whole claim is that importing a recipe pulls in no vendor SDK. These are the fields
6
+ * the recipe reads and nothing more.
7
+ */
8
+ import type { MistralLikeUsage } from '../../../shared/types';
9
+ /**
10
+ * The buffered response, shared by every surface this recipe gates.
11
+ *
12
+ * `chat.complete`, `fim.complete` and `agents.complete` return three differently-named types in
13
+ * the SDK - `ChatCompletionResponse`, `FIMCompletionResponse`, `ChatCompletionResponse` again -
14
+ * and all three carry the same `usage: UsageInfo`. `chat.parse` is `Omit<ChatCompletionResponse,
15
+ * 'choices'> & { choices? }`, so it keeps `usage` too. That was checked against the SDK's own
16
+ * types rather than assumed, because one extractor across four surfaces is only safe if the
17
+ * shapes really are one shape.
18
+ */
19
+ export type MistralCompletionResponse = {
20
+ id?: string;
21
+ model?: string;
22
+ usage?: MistralLikeUsage | null;
23
+ };
24
+ /**
25
+ * One streamed event.
26
+ *
27
+ * The counters sit a level down, under `data` - all four streamed surfaces resolve to the same
28
+ * `EventStream<CompletionEvent>`, and `CompletionEvent` is `{ data: CompletionChunk }`. Reading
29
+ * `chunk.usage` instead of `chunk.data.usage` finds nothing and meters the call at zero, which is
30
+ * why this is typed rather than reached for inline.
31
+ */
32
+ export type MistralCompletionEvent = {
33
+ data?: {
34
+ id?: string;
35
+ model?: string;
36
+ usage?: MistralLikeUsage | null;
37
+ } | null;
38
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,31 @@
1
+ import { type Measurement } from '../../../../types';
2
+ import type { OllamaResponse } from './types';
3
+ import type { MetricsFor, MetricsOf } from '../../core/types';
4
+ export declare const ollamaRecipe: {
5
+ vendor: string;
6
+ metrics: readonly ["prompt_eval_count", "eval_count", "total_tokens"];
7
+ totalTokensMetric: "total_tokens";
8
+ /**
9
+ * Three markers, not one.
10
+ *
11
+ * `chat` and `generate` are extremely common verbs, and this client has no namespace to make
12
+ * them distinctive - so a two-signal guard would still claim unrelated objects. `ps` is the
13
+ * third because it is specific to a model runner. Not matched on the host: Ollama is
14
+ * self-hosted so there is no fixed hostname, and `proxy: true` leaves `config.host` an empty
15
+ * string, which a truthiness check would miss.
16
+ */
17
+ matches: (client: unknown) => boolean;
18
+ methods: ({
19
+ path: readonly ["chat"];
20
+ extractUsage: (result: OllamaResponse | undefined) => Measurement | undefined;
21
+ attachStream: (stream: unknown, ingest: (measurement: Measurement) => void) => void;
22
+ } | {
23
+ path: readonly ["generate"];
24
+ extractUsage: (result: OllamaResponse | undefined) => Measurement | undefined;
25
+ attachStream: (stream: unknown, ingest: (measurement: Measurement) => void) => void;
26
+ })[];
27
+ };
28
+ /** One of the metric names Ollama reports. */
29
+ export type OllamaMetric = MetricsOf<typeof ollamaRecipe>;
30
+ /** What a mapper reads off an Ollama call: those names, each possibly absent. */
31
+ export type OllamaMetrics = MetricsFor<typeof ollamaRecipe>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ollamaRecipe = void 0;
4
+ /**
5
+ * The Ollama recipe - `ollama`, the self-hosted engine's own client.
6
+ *
7
+ * ## What these numbers mean, which is not what every other recipe's mean
8
+ *
9
+ * Ollama invoices nobody. It is not a provider in the sense the other recipes here are - it bills
10
+ * nothing, and it serves open-weight models it did not make - so there is no vendor dashboard to
11
+ * reconcile against and no `cost` to recover. What is being governed is the customer's **own**
12
+ * capacity: a per-agent or per-team budget over hardware they already pay for. The counters are
13
+ * reported under Ollama's own names anyway, for the same reason as everywhere else - so a meter
14
+ * reads one vocabulary - but nobody should expect them to match an invoice, because there is none.
15
+ *
16
+ * ## Shape
17
+ *
18
+ * The client is **flat**: `chat` and `generate` are methods on the client object itself, not
19
+ * namespaces. Both take `{ stream: true }` to switch mode, so each declares `extractUsage` *and*
20
+ * `attachStream` - a streamed call resolves to an `AbortableAsyncIterator` with no counters on it,
21
+ * `extractUsage` returns undefined, and the wrapper falls through to the stream path. That is the
22
+ * same dual-mode arrangement `openAIRecipe` uses for `chat.completions.create`.
23
+ *
24
+ * The counters sit flat on the response root rather than inside a `usage` object, which is why
25
+ * `raw` here is an explicit projection of the two counters rather than the response. Passing the
26
+ * response would flatten `message.content` - the model's own reply text - into event dimensions.
27
+ *
28
+ * ## What is deliberately not covered
29
+ *
30
+ * `embed` reports `prompt_eval_count` but no `eval_count`: it is a different unit against a
31
+ * different price list, and metering it with a chat recipe reports a number that is wrong rather
32
+ * than missing. `embeddings` (the legacy endpoint) reports no counters at all. `pull`, `push` and
33
+ * `create` move or quantize bytes and report progress, not tokens.
34
+ *
35
+ * `systemone` is declined by name even though it genuinely carries tokens. Its block is
36
+ * `{ input_tokens, output_tokens }` - the identical names `typesafe.systemOne` reports - so gating
37
+ * it here would emit `ollama.systemone` carrying metrics indistinguishable from TypeSafe's for
38
+ * anyone metering on metric names rather than event names. One vocabulary per vendor is worth more
39
+ * than one extra surface; name your own recipe at `wrap(client, { recipe })` if you need it.
40
+ *
41
+ * `webSearch` and `webFetch` are the ones worth knowing about: they are **billable**, they are
42
+ * charged against `OLLAMA_API_KEY` at `ollama.com`, and they ignore `config.host` entirely - so
43
+ * they are invisible to any host-based reasoning about where this client points. They report no
44
+ * token counters, so there is nothing honest for this recipe to meter them with.
45
+ */
46
+ const token_usage_1 = require("../../../shared/token-usage");
47
+ const meter_stream_1 = require("../../../shared/meter-stream");
48
+ const types_1 = require("../../../../types");
49
+ const registry_1 = require("../../core/registry");
50
+ /**
51
+ * Both views of one response's counters.
52
+ *
53
+ * `raw` is built from the picked metrics rather than from the response, deliberately: the counters
54
+ * are flat on a root that also carries the generated text, and `raw` becomes event dimensions.
55
+ */
56
+ function measure(result) {
57
+ // Absent, not zero. The declared type says these are always present and the wire disagrees -
58
+ // every intermediate stream chunk omits them - so a response with neither has nothing to report.
59
+ const metrics = (0, token_usage_1.pickMetrics)(result, token_usage_1.METRIC_PATHS.ollama);
60
+ if (Object.keys(metrics).length === 0)
61
+ return undefined;
62
+ return {
63
+ // Ollama publishes no total, so one is summed from two disjoint counters - the only case where
64
+ // adding them up is the vendor's own arithmetic rather than ours.
65
+ metrics: (0, token_usage_1.withComputedTotal)(metrics, token_usage_1.COMPUTED_TOTALS.ollama),
66
+ usage: (0, token_usage_1.fromOllamaUsage)(result),
67
+ raw: { ...metrics },
68
+ };
69
+ }
70
+ /** A buffered `chat`/`generate`. A streamed call resolves to an iterator and reports nothing here. */
71
+ const extractUsage = (result) => measure(result);
72
+ /** The streamed counterpart: the counters ride the final `done: true` chunk and nothing before it. */
73
+ const attachStream = (stream, ingest) => {
74
+ let metrics = {};
75
+ let usage = (0, types_1.emptyTokenUsage)();
76
+ let raw;
77
+ (0, meter_stream_1.meterStream)(stream, (chunk) => {
78
+ const measured = measure(chunk);
79
+ if (measured) {
80
+ metrics = measured.metrics ?? {};
81
+ usage = measured.usage ?? (0, types_1.emptyTokenUsage)();
82
+ raw = measured.raw;
83
+ }
84
+ },
85
+ // A stream that ended before its final chunk reports nothing rather than zeroes: it produced
86
+ // tokens the customer paid for in compute, and a zero would say it produced none.
87
+ () => ingest({ metrics, usage, ...(raw ? { raw } : {}) }));
88
+ };
89
+ exports.ollamaRecipe = {
90
+ vendor: 'ollama',
91
+ metrics: token_usage_1.OLLAMA_METRICS,
92
+ totalTokensMetric: token_usage_1.TOTAL_TOKENS,
93
+ /**
94
+ * Three markers, not one.
95
+ *
96
+ * `chat` and `generate` are extremely common verbs, and this client has no namespace to make
97
+ * them distinctive - so a two-signal guard would still claim unrelated objects. `ps` is the
98
+ * third because it is specific to a model runner. Not matched on the host: Ollama is
99
+ * self-hosted so there is no fixed hostname, and `proxy: true` leaves `config.host` an empty
100
+ * string, which a truthiness check would miss.
101
+ */
102
+ matches: (client) => (0, registry_1.hasMethod)(client, ['chat']) && (0, registry_1.hasMethod)(client, ['generate']) && (0, registry_1.hasMethod)(client, ['ps']),
103
+ methods: [
104
+ { path: ['chat'], extractUsage, attachStream },
105
+ { path: ['generate'], extractUsage, attachStream },
106
+ ],
107
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * What an Ollama response carries, typed structurally.
3
+ *
4
+ * No import from `ollama` - `module-boundaries.test.ts` would fail on one.
5
+ */
6
+ import type { OllamaLikeUsage } from '../../../shared/types';
7
+ /**
8
+ * A completed `chat` or `generate` response, or the final chunk of a streamed one.
9
+ *
10
+ * The counters are flat on the root - there is no `usage` object to reach into - and every one of
11
+ * them is optional here, against the vendor's own `.d.ts`, which declares them required. The
12
+ * server marks the whole `Metrics` struct `omitempty`, so an intermediate chunk carries none of
13
+ * them and the declared type is wrong about it. See `OllamaLikeUsage`.
14
+ *
15
+ * `done` is what distinguishes a final chunk from an intermediate one, and is likewise optional
16
+ * on the wire. The response also carries `message`, `response`, `context` and a set of
17
+ * nanosecond durations; none is read here, and `message` in particular must never reach `raw` -
18
+ * it holds the model's reply text, and `raw` becomes event dimensions.
19
+ */
20
+ export type OllamaResponse = OllamaLikeUsage & {
21
+ model?: string;
22
+ done?: boolean;
23
+ done_reason?: string;
24
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The OpenAI recipe - the second vendor, and the proof that the generic wrapper carries its
3
+ * weight: everything below is response-shape knowledge, with no gate logic repeated.
4
+ *
5
+ * Works against any OpenAI-compatible endpoint, a gateway included. Note that a gateway usually
6
+ * wants the ingest-only gateway binding instead: it sees requests, not runs.
7
+ *
8
+ * ## Why Groq's own field name is read here, when Groq has a recipe of its own
9
+ *
10
+ * `groqRecipe` exists and is matched ahead of this one, so most Groq traffic never reaches here.
11
+ * The `x_groq.usage` read below stays anyway, and deliberately, for two reasons.
12
+ *
13
+ * **It is written once and used twice.** `groqRecipe` builds its methods from these surfaces, the
14
+ * way the OpenRouter and LiteLLM recipes do, so it reads its own counters through this reader
15
+ * rather than carrying a copy. Moving the read into that recipe would duplicate it, not relocate
16
+ * it.
17
+ *
18
+ * **A Groq client the host matcher declines still lands here.** That matcher is narrow on purpose -
19
+ * it compares the hostname, because a wrong guess silently changes who gets billed - so a Groq
20
+ * deployment reached on any other base URL is not claimed by it: `GROQ_BASE_URL` pointed at a
21
+ * regional or staging endpoint, or a corporate proxy in front of `api.groq.com`. Those clients are
22
+ * governed by this recipe, their streamed chunks still carry **no top-level `usage` at all**, and
23
+ * without this fallback every one of them would be wrapped, stamped governed, and metered at
24
+ * nothing. That is the bug this read was added to fix, and it is reachable by configuration alone.
25
+ *
26
+ * Nothing else is affected: a response from any other OpenAI-compatible vendor has no `x_groq`, so
27
+ * the fallback yields `undefined` and the behaviour is exactly what it was.
28
+ *
29
+ * Mind the trap in that field. `x_groq.usage` means token totals on a **streamed** chunk and
30
+ * hardware cache statistics - `dram_cached_tokens`, `sram_cached_tokens` - on a buffered response.
31
+ * Two things keep them apart: only `streamed` consults it at all, and {@link asTokenUsage} refuses
32
+ * a block that names none of the token counters. Without that guard a buffered response would read
33
+ * as all-zero counters with cache statistics riding along as `raw` - a measurement of nothing that
34
+ * looks like data, which is the one failure shape nothing downstream can notice.
35
+ */
36
+ import { fromOpenAIUsage, fromResponsesUsage } from '../../../shared/token-usage';
37
+ import { type Measurement, type Metrics } from '../../../../types';
38
+ import type { MetricsFor, MetricsOf } from '../../core/types';
39
+ /** The reader for a surface, so a recipe built on top of this one stays in step with it. */
40
+ export declare const openAISurface: (path: readonly string[]) => {
41
+ readonly path: readonly ["chat", "completions", "create"];
42
+ readonly parse: typeof fromOpenAIUsage;
43
+ readonly keys: readonly ["prompt_tokens", "completion_tokens", "prompt_tokens_details.cached_tokens", "total_tokens"];
44
+ readonly metrics: readonly ["prompt_tokens", "completion_tokens", "cached_tokens", "total_tokens"];
45
+ readonly streamed: (chunk: any) => any;
46
+ } | {
47
+ readonly path: readonly ["responses", "create"];
48
+ readonly parse: typeof fromResponsesUsage;
49
+ readonly keys: readonly ["input_tokens", "output_tokens", "input_tokens_details.cached_tokens", "total_tokens"];
50
+ readonly metrics: readonly ["input_tokens", "output_tokens", "cached_tokens", "total_tokens"];
51
+ readonly streamed: (chunk: any) => any;
52
+ };
53
+ export declare const openAIRecipe: {
54
+ vendor: string;
55
+ metrics: readonly ["prompt_tokens", "completion_tokens", "input_tokens", "output_tokens", "cached_tokens", "total_tokens"];
56
+ totalTokensMetric: "total_tokens";
57
+ matches: (client: unknown) => boolean;
58
+ methods: {
59
+ path: ("create" | "chat" | "completions" | "responses")[];
60
+ extractUsage: (result: any) => {
61
+ metrics: Metrics;
62
+ usage: import("../../../../types").TokenUsage;
63
+ raw: any;
64
+ } | undefined;
65
+ attachStream: (stream: any, ingest: (measurement: Measurement) => void) => void;
66
+ }[];
67
+ };
68
+ /** One of the metric names OpenAI reports. */
69
+ export type OpenAIMetric = MetricsOf<typeof openAIRecipe>;
70
+ /** What a mapper reads off an OpenAI call: those names, each possibly absent. */
71
+ export type OpenAIMetrics = MetricsFor<typeof openAIRecipe>;
@@ -0,0 +1,122 @@
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
+ * ## Why Groq's own field name is read here, when Groq has a recipe of its own
12
+ *
13
+ * `groqRecipe` exists and is matched ahead of this one, so most Groq traffic never reaches here.
14
+ * The `x_groq.usage` read below stays anyway, and deliberately, for two reasons.
15
+ *
16
+ * **It is written once and used twice.** `groqRecipe` builds its methods from these surfaces, the
17
+ * way the OpenRouter and LiteLLM recipes do, so it reads its own counters through this reader
18
+ * rather than carrying a copy. Moving the read into that recipe would duplicate it, not relocate
19
+ * it.
20
+ *
21
+ * **A Groq client the host matcher declines still lands here.** That matcher is narrow on purpose -
22
+ * it compares the hostname, because a wrong guess silently changes who gets billed - so a Groq
23
+ * deployment reached on any other base URL is not claimed by it: `GROQ_BASE_URL` pointed at a
24
+ * regional or staging endpoint, or a corporate proxy in front of `api.groq.com`. Those clients are
25
+ * governed by this recipe, their streamed chunks still carry **no top-level `usage` at all**, and
26
+ * without this fallback every one of them would be wrapped, stamped governed, and metered at
27
+ * nothing. That is the bug this read was added to fix, and it is reachable by configuration alone.
28
+ *
29
+ * Nothing else is affected: a response from any other OpenAI-compatible vendor has no `x_groq`, so
30
+ * the fallback yields `undefined` and the behaviour is exactly what it was.
31
+ *
32
+ * Mind the trap in that field. `x_groq.usage` means token totals on a **streamed** chunk and
33
+ * hardware cache statistics - `dram_cached_tokens`, `sram_cached_tokens` - on a buffered response.
34
+ * Two things keep them apart: only `streamed` consults it at all, and {@link asTokenUsage} refuses
35
+ * a block that names none of the token counters. Without that guard a buffered response would read
36
+ * as all-zero counters with cache statistics riding along as `raw` - a measurement of nothing that
37
+ * looks like data, which is the one failure shape nothing downstream can notice.
38
+ */
39
+ const token_usage_1 = require("../../../shared/token-usage");
40
+ const meter_stream_1 = require("../../../shared/meter-stream");
41
+ const types_1 = require("../../../../types");
42
+ const registry_1 = require("../../core/registry");
43
+ /**
44
+ * A usage block, or nothing - for the one field that holds something else half the time.
45
+ *
46
+ * Groq's `x_groq.usage` carries the token counters while streaming and hardware cache statistics
47
+ * (`dram_cached_tokens`, `sram_cached_tokens`) when not. Recognised by the counters themselves
48
+ * rather than by which call produced it, so a block naming none of them is declined outright
49
+ * instead of being read as three zeroes.
50
+ */
51
+ const asTokenUsage = (usage) => typeof usage?.prompt_tokens === 'number' ||
52
+ typeof usage?.completion_tokens === 'number' ||
53
+ typeof usage?.total_tokens === 'number'
54
+ ? usage
55
+ : undefined;
56
+ /**
57
+ * The two surfaces, and where each keeps its usage.
58
+ *
59
+ * Chat Completions and Responses are the same API family with different field names, so the only
60
+ * thing that varies per surface is which extractor reads the counts and where a stream hides them.
61
+ */
62
+ const SURFACES = [
63
+ {
64
+ path: ['chat', 'completions', 'create'],
65
+ parse: token_usage_1.fromOpenAIUsage,
66
+ keys: token_usage_1.METRIC_PATHS.openaiChat,
67
+ metrics: token_usage_1.OPENAI_CHAT_METRICS,
68
+ // Reported once, on the final chunk - at the root for OpenAI itself, and under `x_groq` for
69
+ // Groq, whose chunks carry no top-level `usage` at all. See the header on that second read.
70
+ streamed: (chunk) => chunk?.usage ?? asTokenUsage(chunk?.x_groq?.usage),
71
+ },
72
+ {
73
+ path: ['responses', 'create'],
74
+ parse: token_usage_1.fromResponsesUsage,
75
+ keys: token_usage_1.METRIC_PATHS.openaiResponses,
76
+ metrics: token_usage_1.OPENAI_RESPONSES_METRICS,
77
+ // Reported on the completion event, which carries the whole response rather than a delta.
78
+ streamed: (chunk) => chunk?.response?.usage ?? chunk?.usage,
79
+ },
80
+ ];
81
+ /** The reader for a surface, so a recipe built on top of this one stays in step with it. */
82
+ const openAISurface = (path) => SURFACES.find((surface) => surface.path.join('.') === path.join('.')) ?? SURFACES[0];
83
+ exports.openAISurface = openAISurface;
84
+ exports.openAIRecipe = {
85
+ vendor: 'openai',
86
+ metrics: token_usage_1.OPENAI_METRICS,
87
+ totalTokensMetric: token_usage_1.TOTAL_TOKENS,
88
+ // Either surface is enough: the recipe governs both, and a client built only for the Responses
89
+ // API has no `chat.completions` to find.
90
+ matches: (client) => (0, registry_1.hasMethod)(client, ['chat', 'completions', 'create']) || (0, registry_1.hasMethod)(client, ['responses', 'create']),
91
+ methods: SURFACES.map(({ path, parse, keys, streamed }) => ({
92
+ path: [...path],
93
+ // What OpenAI called it, which is what gets reported, beside the normalized counters. Note
94
+ // `prompt_tokens` includes the cached tokens reported next to it - the overlap is the
95
+ // vendor's, kept so the numbers reconcile against its own dashboard.
96
+ extractUsage: (result) => result?.usage
97
+ ? { metrics: (0, token_usage_1.pickMetrics)(result.usage, keys), usage: parse(result.usage), raw: result.usage }
98
+ : undefined,
99
+ attachStream: (stream, ingest) => {
100
+ let total = (0, types_1.emptyTokenUsage)();
101
+ let metrics = {};
102
+ // As on the non-streamed path, so a streamed turn states its usage just as fully - the
103
+ // counters this recipe does not name (`total_tokens`, the reasoning-token detail) reach a
104
+ // meter either way rather than only when the caller happened not to stream.
105
+ let raw;
106
+ (0, meter_stream_1.meterStream)(stream, (chunk) => {
107
+ const usage = streamed(chunk);
108
+ // Reported once and whole rather than as deltas, so the last one wins outright - which
109
+ // is also what makes the block safe to keep as it arrived.
110
+ if (usage) {
111
+ total = parse(usage);
112
+ metrics = (0, token_usage_1.pickMetrics)(usage, keys);
113
+ // Copied, not referenced - it is held until the stream ends, and the chunk it came
114
+ // from belongs to the consumer. See the Gemini recipe for the full reasoning.
115
+ raw = { ...usage };
116
+ }
117
+ },
118
+ // Absent rather than `undefined` when no chunk carried usage - see the Gemini recipe.
119
+ () => ingest({ metrics, usage: total, ...(raw ? { raw } : {}) }));
120
+ },
121
+ })),
122
+ };
@@ -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;