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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +1147 -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/litellm/litellm-recipe.d.ts +17 -0
  9. package/dist/bindings/client/gateways/litellm/litellm-recipe.js +171 -0
  10. package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
  11. package/dist/bindings/client/gateways/litellm/types.js +2 -0
  12. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +15 -0
  13. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +141 -0
  14. package/dist/bindings/client/gateways/openrouter/types.d.ts +29 -0
  15. package/dist/bindings/client/gateways/openrouter/types.js +2 -0
  16. package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +30 -0
  17. package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +79 -0
  18. package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
  19. package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
  20. package/dist/bindings/client/providers/openai/openai-recipe.d.ts +43 -0
  21. package/dist/bindings/client/providers/openai/openai-recipe.js +80 -0
  22. package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +33 -0
  23. package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +73 -0
  24. package/dist/bindings/proxy/headers.d.ts +6 -0
  25. package/dist/bindings/proxy/headers.js +12 -0
  26. package/dist/bindings/proxy/index.d.ts +21 -0
  27. package/dist/bindings/proxy/index.js +18 -0
  28. package/dist/bindings/proxy/types.d.ts +8 -0
  29. package/dist/bindings/proxy/types.js +2 -0
  30. package/dist/bindings/shared/estimate.d.ts +27 -0
  31. package/dist/bindings/shared/estimate.js +38 -0
  32. package/dist/bindings/shared/meter-stream.d.ts +18 -0
  33. package/dist/bindings/shared/meter-stream.js +151 -0
  34. package/dist/bindings/shared/token-usage.d.ts +148 -0
  35. package/dist/bindings/shared/token-usage.js +414 -0
  36. package/dist/bindings/shared/types.d.ts +79 -0
  37. package/dist/bindings/shared/types.js +2 -0
  38. package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
  39. package/dist/bindings/shared/weighted-metrics.js +44 -0
  40. package/dist/config.d.ts +136 -0
  41. package/dist/config.js +97 -0
  42. package/dist/core/context.d.ts +151 -0
  43. package/dist/core/context.js +154 -0
  44. package/dist/core/dimensions.d.ts +22 -0
  45. package/dist/core/dimensions.js +47 -0
  46. package/dist/core/hooks.d.ts +21 -0
  47. package/dist/core/hooks.js +47 -0
  48. package/dist/core/session.d.ts +161 -0
  49. package/dist/core/session.js +403 -0
  50. package/dist/decision-text.d.ts +15 -0
  51. package/dist/decision-text.js +54 -0
  52. package/dist/engine/composite-engine.d.ts +72 -0
  53. package/dist/engine/composite-engine.js +135 -0
  54. package/dist/engine/create-engine.d.ts +16 -0
  55. package/dist/engine/create-engine.js +93 -0
  56. package/dist/engine/decision.d.ts +37 -0
  57. package/dist/engine/decision.js +128 -0
  58. package/dist/engine/engine.d.ts +166 -0
  59. package/dist/engine/engine.js +11 -0
  60. package/dist/engine/node-sdk-engine.d.ts +125 -0
  61. package/dist/engine/node-sdk-engine.js +262 -0
  62. package/dist/engine/report.d.ts +106 -0
  63. package/dist/engine/report.js +185 -0
  64. package/dist/engine/rest-governance-engine.d.ts +47 -0
  65. package/dist/engine/rest-governance-engine.js +220 -0
  66. package/dist/errors.d.ts +78 -0
  67. package/dist/errors.js +151 -0
  68. package/dist/headers.d.ts +27 -0
  69. package/dist/headers.js +63 -0
  70. package/dist/index.d.ts +48 -0
  71. package/dist/index.js +85 -0
  72. package/dist/internal.d.ts +33 -0
  73. package/dist/internal.js +81 -0
  74. package/dist/logger.d.ts +15 -0
  75. package/dist/logger.js +20 -0
  76. package/dist/sdk-identity.d.ts +36 -0
  77. package/dist/sdk-identity.js +43 -0
  78. package/dist/stigg-agent-sdk.d.ts +242 -0
  79. package/dist/stigg-agent-sdk.js +594 -0
  80. package/dist/types.d.ts +531 -0
  81. package/dist/types.js +97 -0
  82. package/package.json +80 -3
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A price per metric, as the mapper a run reports through.
3
+ *
4
+ * Writing one by hand is a reduce over a record and three details that are easy to miss: Stigg
5
+ * rejects fractions, a turn that measured nothing must report nothing rather than a zero, and a
6
+ * counter the response omitted is absent rather than zero. This gets all three right once.
7
+ */
8
+ import type { MetricsMapper } from '../../types';
9
+ import type { ClientRecipe } from '../client/core/types';
10
+ export type WeightedMetricsOptions = {
11
+ /**
12
+ * How many weighted units make one of `target`. Defaults to 1, so a weight reads as the price
13
+ * of one unit of that metric: `output_tokens: 0.5` is half a credit per output token.
14
+ *
15
+ * Set it when the weights are easier to write per thousand than per one - `per: 1000` with
16
+ * `output_tokens: 5` prices five credits per thousand output tokens.
17
+ */
18
+ per?: number;
19
+ };
20
+ /**
21
+ * Prices what a vendor reported into one metric of your own.
22
+ *
23
+ * The weights are keyed to the recipe's own metric names, so a typo does not compile. Naming one
24
+ * is optional: a metric with no weight is free, and a table that prices nothing meters nothing.
25
+ *
26
+ * ```ts
27
+ * // Half a credit per output token, a whole one per input token, cache reads free.
28
+ * metrics: weightedMetrics(anthropicRecipe, 'data_credit', {
29
+ * input_tokens: 1,
30
+ * output_tokens: 0.5,
31
+ * })
32
+ * ```
33
+ *
34
+ * Prices what the vendor actually said, under its own names. For one vocabulary across several
35
+ * vendors, `normalizedMetrics` reports the four neutral counters instead.
36
+ */
37
+ export declare function weightedMetrics<M extends string>(recipe: ClientRecipe<M>, target: string, weights: Partial<Record<M, number>>, options?: WeightedMetricsOptions): MetricsMapper;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.weightedMetrics = weightedMetrics;
4
+ /**
5
+ * Prices what a vendor reported into one metric of your own.
6
+ *
7
+ * The weights are keyed to the recipe's own metric names, so a typo does not compile. Naming one
8
+ * is optional: a metric with no weight is free, and a table that prices nothing meters nothing.
9
+ *
10
+ * ```ts
11
+ * // Half a credit per output token, a whole one per input token, cache reads free.
12
+ * metrics: weightedMetrics(anthropicRecipe, 'data_credit', {
13
+ * input_tokens: 1,
14
+ * output_tokens: 0.5,
15
+ * })
16
+ * ```
17
+ *
18
+ * Prices what the vendor actually said, under its own names. For one vocabulary across several
19
+ * vendors, `normalizedMetrics` reports the four neutral counters instead.
20
+ */
21
+ function weightedMetrics(recipe, target, weights, options = {}) {
22
+ const per = options.per && options.per > 0 ? options.per : 1;
23
+ // The one thing this cannot report its way out of: an empty target is a feature id of `''`,
24
+ // which nothing downstream rejects - the value is validated, the key is not - so the turn
25
+ // reports to a feature that does not exist and the budget never moves.
26
+ if (!target.trim()) {
27
+ throw new Error('weightedMetrics needs a target to price into - an empty one reports to no feature.');
28
+ }
29
+ // Deduplicated, because the reducer walks the declaration: a recipe listing a name twice would
30
+ // charge for it twice, which is the one direction a meter must not be wrong in.
31
+ const priced = [...new Set(recipe.metrics)];
32
+ return ({ metrics }) => {
33
+ if (!metrics)
34
+ return {};
35
+ // An unpriced metric is free rather than an error, so a table names only what it charges for.
36
+ const weighted = priced.reduce((sum, name) => sum + (metrics[name] ?? 0) * (weights[name] ?? 0), 0);
37
+ // Nothing measured is not the same as nothing owed: report no metric at all, which the run
38
+ // counts as the no-op ingest it is rather than as a report of zero.
39
+ if (weighted <= 0)
40
+ return {};
41
+ // Stigg takes whole units, and rounding down would bill nothing for a turn that did happen.
42
+ return { [target]: Math.ceil(weighted / per) };
43
+ };
44
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Client configuration - everything that is true of one Stigg *environment*, and nothing else.
3
+ *
4
+ * A Stigg API key is environment-bound, so the key identifies the environment: one instance per
5
+ * environment, like the Node server SDK. That draws the line for what belongs here. Anything that
6
+ * varies within an environment - which customer, which agent, what it bills against, who wants to
7
+ * watch it happen - is context for the call that needs it, not configuration for the process.
8
+ *
9
+ * The data half (ids, urls, numbers, enums) goes through zod so a typo fails at construction with
10
+ * a readable message instead of as a 404 three calls later. The collaborator half (logger,
11
+ * onError) is injected as-is - those are substitution seams, not data to validate.
12
+ */
13
+ import { z } from 'zod';
14
+ import { type Logger } from './logger';
15
+ import type { CacheOptions, NodeSdkClient } from './engine/node-sdk-engine';
16
+ import type { CheckErrorAction, IngestMode } from './types';
17
+ import type { EngineError } from './errors';
18
+ /**
19
+ * Where the SDK sends its traffic, and how it identifies itself there.
20
+ *
21
+ * Either a server key - which names an environment on the hosted API - or a base URL that needs
22
+ * no key, which is a local stand-in, a self-hosted deployment, or the built-in mock. They are one
23
+ * positional parameter rather than two options fields precisely so they cannot both be given:
24
+ * "an API key or a URL" is the real rule, and a union says it without a runtime check to fail.
25
+ *
26
+ * A self-hosted deployment that *does* want a key passes the key here and the URL in options.
27
+ */
28
+ export type StiggTarget = string | {
29
+ baseUrl: string;
30
+ };
31
+ declare const dataSchema: z.ZodObject<{
32
+ apiKey: z.ZodString;
33
+ baseUrl: z.ZodOptional<z.ZodString>;
34
+ checkErrorAction: z.ZodDefault<z.ZodEnum<{
35
+ deny: "deny";
36
+ allow: "allow";
37
+ }>>;
38
+ ingest: z.ZodDefault<z.ZodEnum<{
39
+ sync: "sync";
40
+ async: "async";
41
+ }>>;
42
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
43
+ }, z.core.$strip>;
44
+ export type { CacheOptions, NodeSdkClient };
45
+ /** Reported to `onError` alongside the failure, so a swallowed ingest is still diagnosable. */
46
+ export type ErrorContext = {
47
+ operation: string;
48
+ runId?: string;
49
+ idempotencyKey?: string;
50
+ customerId?: string;
51
+ };
52
+ /**
53
+ * The second constructor argument: policy and transport, all of it optional and all of it with a
54
+ * working default. Nothing required lives here - the one thing the SDK cannot infer is the
55
+ * target, and that is the first argument.
56
+ */
57
+ export type StiggAgentSDKOptions = {
58
+ /**
59
+ * How checks are cached. See {@link CacheOptions} - it is the Node server SDK's own cache
60
+ * configuration, passed through, because that is where caching actually happens.
61
+ */
62
+ cache?: CacheOptions;
63
+ /**
64
+ * A Node server SDK client you already have, used instead of building one.
65
+ *
66
+ * Pass it when the application uses `@stigg/node-server-sdk` directly. It is the only way to
67
+ * settle ownership: `Stigg.initialize` keeps one global instance per process and hands it back
68
+ * when the API key matches, so without this the SDK may be handed your client, believe it built
69
+ * it, and close it out from under you on `stigg.close()` - taking its cache and socket along.
70
+ * A client passed here is borrowed and never closed.
71
+ *
72
+ * It replaces only the cached half. The governance REST client is still built from the API key,
73
+ * because the Node SDK has no governance surface. Mutually exclusive with
74
+ * {@link StiggAgentSDKOptions.cache}, which configures a client this SDK is no longer building.
75
+ */
76
+ client?: NodeSdkClient;
77
+ /**
78
+ * @deprecated Ignored. There is no local decision cache to put a TTL on any more - the Node
79
+ * server SDK's entitlement cache serves these checks and keeps itself fresh, so there is no
80
+ * window to size. Configure caching with {@link StiggAgentSDKOptions.cache}; for a hard limit
81
+ * with no cached state at all, there is nothing to turn off on the attributed governance path.
82
+ */
83
+ cacheTtlMs?: number;
84
+ /**
85
+ * What happens to a call when the check itself fails - Stigg unreachable, a timeout, a 5xx.
86
+ *
87
+ * Defaults to `'allow'`: a soft limit, so an outage does not take the application down with it.
88
+ * Pass `'deny'` to mirror governance's own fail-closed behaviour. Overridable per call through
89
+ * `stiggContext`, and by `wrap`'s own context for every call through one client.
90
+ */
91
+ checkErrorAction?: CheckErrorAction;
92
+ /**
93
+ * Which of Stigg's ingest APIs a run reports through. Defaults to `'async'`: the event API, which
94
+ * costs a fraction of the latency and takes a far higher rate. Overridable per run, and see
95
+ * {@link IngestMode} for what the two trade against each other.
96
+ */
97
+ ingest?: IngestMode;
98
+ timeoutMs?: number;
99
+ /**
100
+ * Where the SDK narrates what it did. Defaults to `'console'`, because the things it says are
101
+ * the ones you need before anything is configured - which transport resolved, that a run is
102
+ * gating nothing, that what was reported names no entitlement the budget converts.
103
+ *
104
+ * Pass `'none'` to silence it, or your own logger to route it.
105
+ */
106
+ logger?: Logger | 'console' | 'none';
107
+ /**
108
+ * Every instrumentation failure, as structure rather than as a sentence.
109
+ *
110
+ * The one observability hook that is environment-scoped, because it is the only one that fires
111
+ * with no run to belong to: engine resolution, a bare `register()`, `close()`. Watching a
112
+ * decision or an ingest is per-run and lives on `run.start(..., { hooks })`.
113
+ *
114
+ * It is also the only place metering failures surface at all - they are swallowed so they
115
+ * cannot break a run - so it defaults to a warning through `logger` rather than to silence.
116
+ * Replace it to route an `EngineError` and its {@link ErrorContext} into your own telemetry.
117
+ */
118
+ onError?: (error: EngineError, context: ErrorContext) => void;
119
+ };
120
+ export type ResolvedConfig = Omit<Required<z.infer<typeof dataSchema>>, 'baseUrl'> & {
121
+ baseUrl?: string;
122
+ cache?: CacheOptions;
123
+ client?: NodeSdkClient;
124
+ logger: Logger;
125
+ onError: (error: EngineError, context: ErrorContext) => void;
126
+ };
127
+ export declare function resolveConfig(target?: StiggTarget, baseUrl?: string, options?: StiggAgentSDKOptions): ResolvedConfig;
128
+ /** The slice of config the engine factory needs. */
129
+ export type EngineConfig = {
130
+ apiKey: string;
131
+ baseUrl?: string;
132
+ timeoutMs: number;
133
+ cache?: CacheOptions;
134
+ client?: NodeSdkClient;
135
+ logger: Logger;
136
+ };
package/dist/config.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveConfig = resolveConfig;
4
+ /**
5
+ * Client configuration - everything that is true of one Stigg *environment*, and nothing else.
6
+ *
7
+ * A Stigg API key is environment-bound, so the key identifies the environment: one instance per
8
+ * environment, like the Node server SDK. That draws the line for what belongs here. Anything that
9
+ * varies within an environment - which customer, which agent, what it bills against, who wants to
10
+ * watch it happen - is context for the call that needs it, not configuration for the process.
11
+ *
12
+ * The data half (ids, urls, numbers, enums) goes through zod so a typo fails at construction with
13
+ * a readable message instead of as a 404 three calls later. The collaborator half (logger,
14
+ * onError) is injected as-is - those are substitution seams, not data to validate.
15
+ */
16
+ const zod_1 = require("zod");
17
+ const context_1 = require("./core/context");
18
+ const logger_1 = require("./logger");
19
+ /** The key sent when the target is a URL that needs none. Never reaches a real environment. */
20
+ const KEYLESS = 'offline-mock';
21
+ const dataSchema = zod_1.z.object({
22
+ apiKey: zod_1.z.string().min(1),
23
+ baseUrl: zod_1.z.string().url().optional(),
24
+ // Both default through the context's own table rather than restating it: the two used to
25
+ // disagree, and the one written here was the one nothing read.
26
+ checkErrorAction: zod_1.z.enum(['deny', 'allow']).default(context_1.CONTEXT_DEFAULTS.checkErrorAction),
27
+ ingest: zod_1.z.enum(['sync', 'async']).default(context_1.CONTEXT_DEFAULTS.ingestMode),
28
+ timeoutMs: zod_1.z.number().int().positive().default(3_000),
29
+ });
30
+ const resolveLogger = (logger) => {
31
+ if (logger === 'console' || logger === undefined)
32
+ return logger_1.consoleLogger;
33
+ if (logger === 'none')
34
+ return logger_1.noopLogger;
35
+ return logger;
36
+ };
37
+ /**
38
+ * The target, split into the key and the URL the transport actually needs.
39
+ *
40
+ * An absent target falls back to the environment variable, the way every vendor SDK does, so the
41
+ * smallest working program is `new StiggAgentSDK()`. The failure for a missing key is raised here
42
+ * rather than left to zod, because "set STIGG_SERVER_API_KEY" is the actionable half and the
43
+ * schema only knows the field was too short.
44
+ */
45
+ function resolveTarget(target) {
46
+ if (typeof target === 'object')
47
+ return { apiKey: KEYLESS, baseUrl: target.baseUrl };
48
+ const apiKey = (target ?? process.env.STIGG_SERVER_API_KEY)?.trim();
49
+ if (!apiKey) {
50
+ throw new Error('No Stigg API key - pass a server key as the first argument, or set STIGG_SERVER_API_KEY. ' +
51
+ 'To run without one, pass a target with a `baseUrl` instead: a local stand-in, a ' +
52
+ 'self-hosted deployment, or the built-in mock.');
53
+ }
54
+ return { apiKey };
55
+ }
56
+ function resolveConfig(target, baseUrl, options = {}) {
57
+ const resolved = resolveTarget(target);
58
+ // A keyless target *is* a base URL, so naming a second one is two answers to one question.
59
+ // Refused rather than silently preferred: whichever the SDK picked, the other would be a URL
60
+ // the caller believes it is talking to and is not.
61
+ if (resolved.baseUrl && baseUrl) {
62
+ throw new Error(`This SDK was given a target with its own baseUrl (${resolved.baseUrl}) and a second one ` +
63
+ `(${baseUrl}). Pass the URL once: as the target when it needs no key, or as the second ` +
64
+ 'argument alongside a key.');
65
+ }
66
+ const parsed = dataSchema.parse({
67
+ apiKey: resolved.apiKey,
68
+ baseUrl: resolved.baseUrl ?? baseUrl,
69
+ checkErrorAction: options.checkErrorAction,
70
+ ingest: options.ingest,
71
+ timeoutMs: options.timeoutMs,
72
+ });
73
+ // Refused rather than silently preferred, like the two-URL case above: `cache` configures a
74
+ // client, and a client that was handed in is already configured. Whichever the SDK picked, the
75
+ // other would be settings the caller believes are in force and are not.
76
+ if (options.client && options.cache) {
77
+ throw new Error('This SDK was given both a `client` and `cache` options. The cache options configure a ' +
78
+ 'client at construction, and the one you passed is already built - so they would be ' +
79
+ 'ignored. Pass one: `client` for a client you own, or `cache` to have the SDK build one.');
80
+ }
81
+ const logger = resolveLogger(options.logger);
82
+ // Said rather than ignored in silence: someone who set this was sizing an overshoot window, and
83
+ // the answer to that question is now different rather than smaller.
84
+ if (options.cacheTtlMs !== undefined) {
85
+ logger.warn('[stigg] cacheTtlMs is ignored - this SDK no longer caches decisions. Checks are served by ' +
86
+ "the Node server SDK's entitlement cache, which invalidates on updates and writes reported " +
87
+ 'usage back, so there is no TTL to size. Configure it with `cache` instead.');
88
+ }
89
+ return {
90
+ ...parsed,
91
+ cache: options.cache,
92
+ client: options.client,
93
+ logger,
94
+ // Never silent, never fatal. Replace it to route into your own telemetry.
95
+ onError: options.onError ?? ((error, context) => logger.warn(`[stigg] ${context.operation} failed: ${error.message}`)),
96
+ };
97
+ }
@@ -0,0 +1,151 @@
1
+ import type { CheckErrorAction, Entitlement, IngestMode, Metrics, MetricsMapper } from '../types';
2
+ import type { EventIngest } from '../engine/engine';
3
+ /** The entity type an agent is provisioned and attributed as. Both halves, because they must agree. */
4
+ export type AgentType = {
5
+ /** The governance entity type, as it exists in the account. */
6
+ entityTypeId: string;
7
+ /** The attribution key that type declares, under which `agentId` is sent on every call. */
8
+ attributionKey: string;
9
+ };
10
+ /**
11
+ * What a call is governed by. Every field is optional here because each one may have been given
12
+ * at `wrap` instead - `customerId` is the only one that has to come from somewhere, and the
13
+ * wrapped client's types say which calls still owe it.
14
+ */
15
+ export type StiggContext = {
16
+ /** The tenant. Every governance call is scoped to one customer. */
17
+ customerId?: string;
18
+ /**
19
+ * The agent this call spends as. Travels as a dimension under {@link AgentType.attributionKey},
20
+ * which is the entire mechanism by which an agent is attributed - so naming one without an
21
+ * `agentType` is refused rather than guessed at.
22
+ *
23
+ * Omitted, the call resolves no entity and is checked against the customer's own budget.
24
+ */
25
+ agentId?: string;
26
+ /** The entity type `agentId` is identified as. Required whenever `agentId` is given. */
27
+ agentType?: AgentType;
28
+ /**
29
+ * Extra attribution, sent verbatim on the check and the report. Every key must be an attribution
30
+ * key on one of the entity types involved, or it matches nothing and the usage rolls up to the
31
+ * customer instead.
32
+ *
33
+ * Overriding this **merges** per key over the wrap's own, so a per-call `{ tenantId }` does not
34
+ * silently drop a client-wide `{ region }`.
35
+ */
36
+ dimensions?: Record<string, string>;
37
+ /**
38
+ * What this call **gates on** - a credit currency or a metered feature, never both. **Omit it
39
+ * and the call gates nothing**: no check leaves the process, nothing can be refused, and
40
+ * metering is unchanged. The presence of this field is the enforcement switch, because a call
41
+ * with nothing to check against cannot be enforced, and a separate flag would only be a second
42
+ * place for the two to disagree.
43
+ */
44
+ entitlement?: Entitlement;
45
+ /**
46
+ * What a check resolves to when the engine could not answer it. Defaults to `'allow'` - a soft
47
+ * limit, so an outage does not take the application down with it.
48
+ *
49
+ * Worth knowing what that trades: reporting fails in the same outage and is swallowed, so the
50
+ * spend allowed through is neither capped nor recorded afterwards. `'deny'` mirrors governance,
51
+ * which is fail-closed server-side.
52
+ */
53
+ checkErrorAction?: CheckErrorAction;
54
+ /**
55
+ * What to report for each completed call. Defaults to the vendor's own metric names and values
56
+ * exactly as they came back, so what lands in Stigg reconciles against that vendor's invoice.
57
+ *
58
+ * The name on the left is a Stigg feature or credit id, and metering the one the call checks -
59
+ * {@link StiggContext.entitlement} - is what makes the budget move.
60
+ */
61
+ usageMapper?: MetricsMapper;
62
+ /** Which of Stigg's ingest APIs this call reports through. Defaults to `'async'`. */
63
+ ingestMode?: IngestMode;
64
+ /**
65
+ * The event name to report under in `'async'` mode. Defaults to the vendor call that produced
66
+ * the usage - `anthropic.messages.create` - which is what a Stigg meter filters on.
67
+ */
68
+ eventName?: EventIngest;
69
+ /**
70
+ * The key that deduplicates this report. Defaults to the vendor's own request id when it has
71
+ * one - stable across the vendor's retries, which a local counter cannot be - and to a
72
+ * generated key otherwise.
73
+ */
74
+ idempotencyKey?: string;
75
+ /**
76
+ * Correlates work that continues in another process, as `x-stigg-run-id`. Defaults to the id
77
+ * the wrap generated, so a gateway sees one value per client unless a call says otherwise.
78
+ */
79
+ correlationId?: string;
80
+ };
81
+ /**
82
+ * A context with everything decided and everything derivable already derived.
83
+ *
84
+ * Nothing here is optional that the call path would otherwise have to default, and nothing is
85
+ * computed that a call could be asked to compute twice.
86
+ */
87
+ export type ResolvedContext = {
88
+ /**
89
+ * Empty only for a client wrapped without one, whose calls each name their own. Every path that
90
+ * reaches the engine resolves it first, so nothing downstream has to re-check it.
91
+ */
92
+ customerId: string;
93
+ entitlement?: Entitlement;
94
+ /** The agent folded in, so the check and the report send one object and neither re-derives it. */
95
+ dimensions: Record<string, string>;
96
+ checkErrorAction: CheckErrorAction;
97
+ ingestMode: IngestMode;
98
+ eventName?: EventIngest;
99
+ usageMapper: MetricsMapper;
100
+ idempotencyKey?: string;
101
+ correlationId: string;
102
+ /** The entitlement's own id, resolved once - undefined when this context gates on nothing. */
103
+ checkedId?: string;
104
+ };
105
+ /**
106
+ * What nobody told the SDK, and it will not guess.
107
+ *
108
+ * Exported because the constructor's own schema defaults to these: two copies of "what happens
109
+ * when nothing says" is how a configured `deny` came to be parsed, stored and then ignored in
110
+ * favour of an `allow` written somewhere else.
111
+ *
112
+ * `allow` is the soft limit - an unreachable Stigg does not take the application down with it.
113
+ */
114
+ export declare const CONTEXT_DEFAULTS: {
115
+ readonly checkErrorAction: "allow";
116
+ readonly ingestMode: "async";
117
+ };
118
+ /**
119
+ * Validates and derives, once. Everything expensive about a context happens here rather than on
120
+ * the call path - see the note at the top of this file for why that split exists.
121
+ */
122
+ /** The one field a call cannot proceed without, said the same way wherever it is missing. */
123
+ export declare const noCustomer: () => Error;
124
+ /**
125
+ * The client-wide policy a context falls back to before the SDK's own defaults.
126
+ *
127
+ * The chain is per-call, then `wrap`, then the constructor, then {@link CONTEXT_DEFAULTS}. The
128
+ * constructor link is this parameter: without it a configured policy is inert, because every
129
+ * context would land on the defaults instead.
130
+ */
131
+ export type ContextDefaults = {
132
+ checkErrorAction?: CheckErrorAction;
133
+ ingestMode?: IngestMode;
134
+ };
135
+ export declare function resolveContext(context: StiggContext, correlationId: string, { requireCustomer, defaults }?: {
136
+ requireCustomer?: boolean;
137
+ defaults?: ContextDefaults;
138
+ }): ResolvedContext;
139
+ /**
140
+ * One call's override, applied over what `wrap` already resolved.
141
+ *
142
+ * Only reached when a call actually carried a `stiggContext`; a call that did not reuses the
143
+ * resolved object as-is. The ordering below is the one the caller asked for: the base identity
144
+ * first, because a changed customer or agent changes which budget is consulted, and the metering
145
+ * fields last, because nothing before the response depends on them.
146
+ */
147
+ export declare function overrideContext(base: ResolvedContext, override: StiggContext): ResolvedContext;
148
+ /** Whether a context gates at all. The enforcement switch, read on every call. */
149
+ export declare const isEnforced: (context: ResolvedContext) => boolean;
150
+ /** Reported metrics, accumulated across a client's calls. */
151
+ export declare const addMetrics: (into: Metrics, add: Metrics) => void;
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addMetrics = exports.isEnforced = exports.noCustomer = exports.CONTEXT_DEFAULTS = void 0;
4
+ exports.resolveContext = resolveContext;
5
+ exports.overrideContext = overrideContext;
6
+ /**
7
+ * The governance context: who a call is for, what gates it, and what it reports.
8
+ *
9
+ * One shape, given in two places. `wrap(client, { stiggContext })` sets the defaults for every
10
+ * call through that client; `client.messages.create({ ..., stiggContext })` overrides them for one
11
+ * call. That is the whole of the configuration surface - there is no run to open and nothing to
12
+ * keep in sync between a setup step and a call site.
13
+ *
14
+ * ## Why resolution is split in two
15
+ *
16
+ * A gated call sits directly in front of a network request the application is paying for, so the
17
+ * SDK's own work has to disappear next to it. The split below is what makes that true:
18
+ *
19
+ * `resolveContext` - runs once, at `wrap`. Validates, folds the agent into the dimensions,
20
+ * and precomputes everything a call would otherwise derive: the cache key,
21
+ * the entitlement id, the mapper.
22
+ * `overrideContext` - runs only when a call actually carries a `stiggContext`, and only touches
23
+ * the keys that call named.
24
+ *
25
+ * So a call that overrides nothing does no merging, no validation and no allocation: it reads the
26
+ * object `wrap` already built. A client wrapped without a customer is the exception - its calls
27
+ * each name their own, so each one carries an override and pays for the merge.
28
+ */
29
+ const types_1 = require("../types");
30
+ /**
31
+ * What nobody told the SDK, and it will not guess.
32
+ *
33
+ * Exported because the constructor's own schema defaults to these: two copies of "what happens
34
+ * when nothing says" is how a configured `deny` came to be parsed, stored and then ignored in
35
+ * favour of an `allow` written somewhere else.
36
+ *
37
+ * `allow` is the soft limit - an unreachable Stigg does not take the application down with it.
38
+ */
39
+ exports.CONTEXT_DEFAULTS = { checkErrorAction: 'allow', ingestMode: 'async' };
40
+ /**
41
+ * The agent, as the dimension it travels as.
42
+ *
43
+ * Refused rather than defaulted when the type is missing: a guessed attribution key resolves no
44
+ * entity, so the spend lands on the customer and the output is indistinguishable from a call that
45
+ * was never governed. There is no error to read later, which is why there is one here.
46
+ */
47
+ function agentDimension(context) {
48
+ if (context.agentId === undefined)
49
+ return undefined;
50
+ if (!context.agentId.trim()) {
51
+ throw new Error('stiggContext.agentId is empty - an agent nobody can name is also one nothing can bill.');
52
+ }
53
+ if (!context.agentType) {
54
+ throw new Error(`stiggContext.agentId is '${context.agentId}' but no agentType says which key to send it under. ` +
55
+ 'Pass `agentType: { entityTypeId, attributionKey }` - the pair the entity type was created ' +
56
+ 'with. Without it the agent resolves no entity and the spend lands on the customer, which ' +
57
+ 'looks exactly like a call that was never governed.');
58
+ }
59
+ return { [context.agentType.attributionKey]: context.agentId };
60
+ }
61
+ /**
62
+ * Validates and derives, once. Everything expensive about a context happens here rather than on
63
+ * the call path - see the note at the top of this file for why that split exists.
64
+ */
65
+ /** The one field a call cannot proceed without, said the same way wherever it is missing. */
66
+ const noCustomer = () => new Error('No customerId - every governance call is scoped to a tenant. Give `wrap(client, ' +
67
+ '{ stiggContext: { customerId } })` a default for every call through this client, or name ' +
68
+ 'one per call in `stiggContext`.');
69
+ exports.noCustomer = noCustomer;
70
+ function resolveContext(context, correlationId,
71
+ // `wrap` passes false: a client whose calls each name their own customer is legal, and its
72
+ // types already say which calls still owe one. Every path that reaches the engine resolves it.
73
+ { requireCustomer = true, defaults } = {}) {
74
+ const customerId = context.customerId?.trim() ?? '';
75
+ if (!customerId && requireCustomer)
76
+ throw (0, exports.noCustomer)();
77
+ // Naming none is legal and means ungated. Naming an empty one is a typo that would meter into
78
+ // the dark, so it is refused here rather than three calls later.
79
+ const invalid = (0, types_1.entitlementError)(context.entitlement);
80
+ if (invalid)
81
+ throw invalid;
82
+ const agent = agentDimension(context);
83
+ const dimensions = agent ? { ...context.dimensions, ...agent } : (context.dimensions ?? {});
84
+ return {
85
+ customerId,
86
+ // Normalized, not passed through: `isEnforced` asks `!== undefined` while every other reader
87
+ // asks for truthiness, so a JS caller's `entitlement: null` would be enforced by one and
88
+ // absent to the rest - a call that runs the gate and then reports itself ungated.
89
+ entitlement: context.entitlement ?? undefined,
90
+ dimensions,
91
+ checkErrorAction: context.checkErrorAction ?? defaults?.checkErrorAction ?? exports.CONTEXT_DEFAULTS.checkErrorAction,
92
+ ingestMode: context.ingestMode ?? defaults?.ingestMode ?? exports.CONTEXT_DEFAULTS.ingestMode,
93
+ eventName: context.eventName,
94
+ usageMapper: context.usageMapper ?? types_1.vendorMetrics,
95
+ idempotencyKey: context.idempotencyKey,
96
+ correlationId: context.correlationId?.trim() || correlationId,
97
+ checkedId: context.entitlement ? (0, types_1.entitlementId)(context.entitlement) : undefined,
98
+ };
99
+ }
100
+ /**
101
+ * One call's override, applied over what `wrap` already resolved.
102
+ *
103
+ * Only reached when a call actually carried a `stiggContext`; a call that did not reuses the
104
+ * resolved object as-is. The ordering below is the one the caller asked for: the base identity
105
+ * first, because a changed customer or agent changes which budget is consulted, and the metering
106
+ * fields last, because nothing before the response depends on them.
107
+ */
108
+ function overrideContext(base, override) {
109
+ // The cheap test first: these are the only fields that can change which budget is consulted,
110
+ // so a call overriding only, say, `idempotencyKey` re-derives nothing.
111
+ const rebased = override.customerId !== undefined ||
112
+ override.agentId !== undefined ||
113
+ override.dimensions !== undefined ||
114
+ override.entitlement !== undefined;
115
+ let customerId = base.customerId;
116
+ let dimensions = base.dimensions;
117
+ let entitlement = base.entitlement;
118
+ let checkedId = base.checkedId;
119
+ if (rebased) {
120
+ customerId = override.customerId?.trim() || base.customerId;
121
+ if (override.entitlement !== undefined) {
122
+ const invalid = (0, types_1.entitlementError)(override.entitlement);
123
+ if (invalid)
124
+ throw invalid;
125
+ entitlement = override.entitlement;
126
+ checkedId = (0, types_1.entitlementId)(override.entitlement);
127
+ }
128
+ // Merged per key, not replaced: a per-call tenant must not drop the client's own attribution.
129
+ const agent = agentDimension({ ...override, agentType: override.agentType });
130
+ dimensions =
131
+ override.dimensions || agent ? { ...base.dimensions, ...override.dimensions, ...agent } : base.dimensions;
132
+ }
133
+ return {
134
+ customerId,
135
+ entitlement,
136
+ dimensions,
137
+ checkedId,
138
+ checkErrorAction: override.checkErrorAction ?? base.checkErrorAction,
139
+ ingestMode: override.ingestMode ?? base.ingestMode,
140
+ eventName: override.eventName ?? base.eventName,
141
+ usageMapper: override.usageMapper ?? base.usageMapper,
142
+ idempotencyKey: override.idempotencyKey ?? base.idempotencyKey,
143
+ correlationId: override.correlationId?.trim() || base.correlationId,
144
+ };
145
+ }
146
+ /** Whether a context gates at all. The enforcement switch, read on every call. */
147
+ const isEnforced = (context) => context.entitlement !== undefined;
148
+ exports.isEnforced = isEnforced;
149
+ /** Reported metrics, accumulated across a client's calls. */
150
+ const addMetrics = (into, add) => {
151
+ for (const [name, value] of Object.entries(add))
152
+ into[name] = (into[name] ?? 0) + value;
153
+ };
154
+ exports.addMetrics = addMetrics;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The vendor's usage object, reduced to something an event can carry.
3
+ *
4
+ * Stigg dimensions are a flat map of scalars, so a nested object has to be flattened or dropped -
5
+ * there is no third option, and stringifying a subtree would put something no meter can aggregate
6
+ * into a field a meter is meant to read.
7
+ *
8
+ * Everything here is a limit rather than a translation, because the input is not ours: a vendor
9
+ * adds fields whenever it likes, and this runs on every metered call. Depth, count and type are
10
+ * all capped so a response that grows a large nested block cannot grow the event with it.
11
+ */
12
+ /** Prefix for everything read off the vendor's object, so it can never shadow a metric. */
13
+ export declare const RAW_PREFIX = "raw.";
14
+ /**
15
+ * The vendor's own usage block as event dimensions, `raw.`-prefixed and flattened with dotted keys.
16
+ *
17
+ * Fractions are kept: an event states what the vendor said and a meter is free to sum decimals,
18
+ * which is the one place a gateway's `cost` can be reported honestly. `null`, `undefined`, arrays
19
+ * and anything past the depth cap are dropped rather than coerced - a zero that was never reported
20
+ * is worse than a counter that is absent.
21
+ */
22
+ export declare function flattenRaw(raw: Record<string, unknown> | undefined): Record<string, string | number | boolean>;