@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,531 @@
1
+ /**
2
+ * The vocabulary shared by the core, the engines and every binding, and the argument list of
3
+ * every call on the public surface.
4
+ *
5
+ * Terms follow Stigg governance rather than the draft design notes: provision/archive (not
6
+ * register/de-register), ingest (not report), assignment (not budget), dimensions (not tags).
7
+ *
8
+ * The imports below are all `import type`, so they are erased at compile time: this file pulls in
9
+ * no module at runtime and stays the leaf everything else can depend on.
10
+ */
11
+ import { EngineError } from './errors';
12
+ import type { StiggContext } from './core/context';
13
+ import type { GovernanceSummary, SessionHooks } from './core/session';
14
+ import type { GovernanceHeaders } from './headers';
15
+ import type { ClientRecipe } from './bindings/client/core/types';
16
+ import type { ObservedVendor } from './bindings/shared/token-usage';
17
+ import type { EventIngest } from './engine/engine';
18
+ /**
19
+ * What a budget is measured against. Exactly one of the two - the same mutual exclusion the
20
+ * Stigg check endpoint enforces, expressed so the compiler catches it instead of the API.
21
+ */
22
+ export type Entitlement = {
23
+ currencyId: string;
24
+ featureId?: never;
25
+ } | {
26
+ featureId: string;
27
+ currencyId?: never;
28
+ };
29
+ /**
30
+ * Which of Stigg's two ingest APIs a run reports through.
31
+ *
32
+ * `'sync'` is `usage.report`/`reportUsageBulk`: a fixed DTO of `featureId` and a whole-number
33
+ * `value` per metric, aggregated the moment it lands, so the next check sees it. It costs more
34
+ * latency and has the lower rate limit.
35
+ *
36
+ * `'async'` is `events.report`/`reportEvent`: the call is reported as one event and Stigg's own
37
+ * meters turn it into usage, seconds later. Low latency, a far higher rate limit, and nothing
38
+ * meters at all until a **raw-events meter** is configured for the event name on Stigg's side.
39
+ */
40
+ export type IngestMode = 'sync' | 'async';
41
+ /** The id to send as `featureId`/`currencyId`, whichever kind the entitlement is. */
42
+ export declare const entitlementId: (entitlement: Entitlement) => string;
43
+ /** 'credit' targets read the customer's credit pool; 'feature' targets need an entitling subscription. */
44
+ export declare const entitlementKind: (entitlement: Entitlement) => "credit" | "feature";
45
+ /**
46
+ * The entitlement as every Stigg endpoint takes it: one of two mutually exclusive id fields,
47
+ * ready to spread.
48
+ *
49
+ * ```ts
50
+ * await client.v1Beta.customers.entitlements.check(customerId, { ...entitlementRef(entitlement), requestedUsage });
51
+ * ```
52
+ *
53
+ * Narrowing on the property rather than through {@link entitlementKind} is the point: a function
54
+ * call cannot narrow a union, so every hand-written version of this needed a `!` on each branch -
55
+ * an assertion that the union already guarantees and that would survive the union changing.
56
+ */
57
+ export declare const entitlementRef: (entitlement: Entitlement) => {
58
+ currencyId: string;
59
+ } | {
60
+ featureId: string;
61
+ };
62
+ /**
63
+ * Why an entitlement cannot be billed against, if one was named at all.
64
+ *
65
+ * Naming none is legal: it is how a caller asks for metering without enforcement, so undefined is
66
+ * not an error here. The one place an entitlement is genuinely required is an assignment, which is
67
+ * written over `(entity x entitlement)` - and `register` refuses that case itself, because only it
68
+ * knows the agent to name in the message.
69
+ *
70
+ * What stays refused is an entitlement that is named and names nothing. The union permits
71
+ * `{ currencyId: '' }`, and an empty currency is falsy - so it classifies as a feature and every
72
+ * branch downstream then asks for a `featureId` that is not there. An empty id is a typo rather
73
+ * than an intention, and letting it through meters a run against nothing while it still looks
74
+ * enforced.
75
+ */
76
+ export declare function entitlementError(entitlement: Entitlement | undefined): Error | undefined;
77
+ /**
78
+ * A provider-neutral token measurement. Every client/orchestrator binding normalizes into this
79
+ * before anything touches Stigg, so the mapper below is written once for all of them.
80
+ */
81
+ export type TokenUsage = {
82
+ input: number;
83
+ output: number;
84
+ cacheRead: number;
85
+ cacheCreate: number;
86
+ /**
87
+ * What the vendor says the call cost in total, when it says anything.
88
+ *
89
+ * Apart from the four above, and deliberately so: those are disjoint and add up to the call,
90
+ * while this one *overlaps all of them*. It is here because a mapper that wants one number
91
+ * should be able to use the vendor's own rather than re-adding the parts and hoping the
92
+ * arithmetic matches - the two can differ, and when they do the vendor's is the one that gets
93
+ * invoiced.
94
+ *
95
+ * Read from the response where the vendor publishes a total, and summed from the four where it
96
+ * publishes none and they are disjoint. Undefined where neither is true, which is the honest
97
+ * answer rather than a guess: see `COMPUTED_TOTALS`.
98
+ */
99
+ total?: number;
100
+ };
101
+ export declare const emptyTokenUsage: () => TokenUsage;
102
+ /**
103
+ * What one completed call reports: feature id -> whole-number value. One report each.
104
+ *
105
+ * A record rather than a list of `{featureId, value}` pairs, so the same feature cannot appear
106
+ * twice - two reports of one feature in one call would collide on idempotency keys and silently
107
+ * lose one of them.
108
+ */
109
+ export type Metrics = Record<string, number>;
110
+ /**
111
+ * Turns what a binding observed into the metrics to report.
112
+ *
113
+ * Defaults to the vendor's own: whatever names and values came back on the response. Supply your
114
+ * own to report something else - see `creditsPerTokens` for the one-number case, and each client
115
+ * recipe for what that vendor meters by default.
116
+ */
117
+ export type MetricsMapper = (measurement: Measurement) => Metrics;
118
+ /**
119
+ * What one completed call is worth, as the binding that observed it saw it.
120
+ *
121
+ * Two views of one call, and both are useful. `metrics` is what the vendor said, in the vendor's
122
+ * own words - reported as-is, so what lands in Stigg reconciles against what the vendor bills.
123
+ * `usage` is the same call flattened into four comparable counters, which is what a mapper writes
124
+ * against when it wants one number or one vocabulary across vendors.
125
+ */
126
+ export type Measurement = {
127
+ /** The vendor's own field names and values, verbatim. What gets reported by default. */
128
+ metrics?: Metrics;
129
+ /** The same call, normalized and non-overlapping. Context for writing your own mapper. */
130
+ usage?: TokenUsage;
131
+ /**
132
+ * The vendor's usage object as it arrived, before any counter was picked out of it.
133
+ *
134
+ * Carried only so an event can state what the vendor said in full - it rides the event's
135
+ * dimensions under a `raw.` prefix, flattened to scalars, and a meter can then aggregate a
136
+ * counter this SDK does not know about yet. Never reported as a metric: a feature id has to be
137
+ * something the customer's catalog knows, and a vendor's changelog must not be able to invent
138
+ * one. The usage block only - never a response body, so no prompt or completion text leaves
139
+ * the process.
140
+ */
141
+ raw?: Record<string, unknown>;
142
+ /** Per-call attribution, merged over the run's own dimensions. e.g. the model that served. */
143
+ dimensions?: Record<string, string>;
144
+ /** The vendor's own request id, when it has one that is stable across its retries. */
145
+ idempotencyKey?: string;
146
+ /**
147
+ * Why this measurement does not cover everything the vendor billed, when it does not.
148
+ *
149
+ * For the case where usage exists that the response never carried - not a disagreement between
150
+ * two numbers the vendor gave, which is data the caller can judge for themselves, but work
151
+ * that was charged for and never reported. What is here is still ingested: a short measurement
152
+ * beats none. The reason is then raised through the same channel a failed read uses, so
153
+ * `onError` fires and `summary().ingestFailures` counts it, rather than the shortfall being
154
+ * something a reader of the docs was supposed to have known about.
155
+ */
156
+ incomplete?: string;
157
+ };
158
+ /** What a run reports when nobody says otherwise: exactly what the vendor stated. */
159
+ export declare const vendorMetrics: MetricsMapper;
160
+ /** A binding may report either shape; `TokenUsage` is the common case and stays terse. */
161
+ export declare const toMeasurement: (value: TokenUsage | Measurement) => Measurement;
162
+ /**
163
+ * Why a measured value cannot be reported, if it cannot.
164
+ *
165
+ * Zero is not an error - a cached refusal or an empty stream is a normal turn - but a mapper
166
+ * dividing by an unset config returns NaN or Infinity, and reading those as "nothing was used"
167
+ * meters the run at zero while every call still reports success. Those are refused in both modes;
168
+ * a fraction is refused only under `'sync'`, whose DTO takes whole units.
169
+ *
170
+ * Shared, because every path that reports usage has to answer this the same way.
171
+ */
172
+ export declare function usageValueError(value: number, mode?: IngestMode): EngineError | undefined;
173
+ /**
174
+ * One metric, summed from every token class and divided by the unit size.
175
+ *
176
+ * The mapper for an application that prices tokens itself rather than letting Stigg convert a
177
+ * credit-backed feature. Weighting cache reads cheaper is a pricing decision and belongs in
178
+ * Stigg's consumption mapping, not here; Stigg rejects fractions, so the result rounds up.
179
+ */
180
+ export declare const creditsPerTokens: (featureId: string, tokensPerUnit?: number) => MetricsMapper;
181
+ /** What the caller should do about a denial. A refusal is a commercial event, not an error. */
182
+ export type GovernanceRemedy = 'top-up' | 'raise-budget' | 'subscribe' | 'retry';
183
+ /**
184
+ * Which client answered, or - for 'cache', 'policy' and 'unenforced' - that nothing was asked.
185
+ *
186
+ * `'unenforced'` is the one that could not have been asked: the run named no entitlement, so there
187
+ * is no budget to consult and every decision it hands back is an allow.
188
+ */
189
+ export type DecisionSource = 'rest' | 'node-sdk' | 'cache' | 'policy' | 'unenforced';
190
+ /**
191
+ * The structured refusal. Returned, never thrown, everywhere the caller has a return path:
192
+ * the denial path is a revenue surface (top-up, upgrade), so it must be inspectable.
193
+ */
194
+ export type GovernanceDecision = {
195
+ hasAccess: boolean;
196
+ /** Which entity blocked - a governance node, or the customer when governance is not in play. */
197
+ deniedBy?: string;
198
+ /**
199
+ * Why it was refused. Stigg's `accessDeniedReason` verbatim, except where `locallyDerived` says
200
+ * the SDK reached the verdict itself. `InsufficientCredits` and `BudgetExceeded` differ.
201
+ */
202
+ reason?: string;
203
+ remedy?: GovernanceRemedy;
204
+ /** Tightest remaining budget across the pool and the chain. Undefined when nothing is bounded. */
205
+ remaining?: number;
206
+ usageLimit?: number | null;
207
+ currentUsage?: number;
208
+ source: DecisionSource;
209
+ checkedAt: number;
210
+ /** True when the engine served this from its own fallback rather than live state. */
211
+ isFallback?: boolean;
212
+ /**
213
+ * True when the SDK decided this rather than Stigg. A credit check reads the balance and
214
+ * compares it against the requested amount here, because the credit read takes no requested
215
+ * usage to adjudicate - so the balance is Stigg's, and the refusal is the SDK's.
216
+ *
217
+ * Distinct from `isFallback`: that one says the state was stale, this one says the state was
218
+ * live and the arithmetic was ours. Both can be true, and `strictCheck: false` on the engine's
219
+ * capabilities is the same admission at the level above.
220
+ */
221
+ locallyDerived?: boolean;
222
+ /** One line for a log: every field that matters, already formatted. */
223
+ summary: string;
224
+ /** What to tell the user. A refusal is a commercial event, so it carries its own remedy. */
225
+ message: string;
226
+ };
227
+ /**
228
+ * The outcome of one ingest - which is now one report per metric, so the result is per metric too.
229
+ * Never a rejection: metering must not break the caller's run.
230
+ */
231
+ export type IngestResult = {
232
+ /** True only when every metric landed. One failure among four is not a success. */
233
+ ok: boolean;
234
+ /** What was reported, by feature id. Empty when the call measured nothing at all. */
235
+ reported: Metrics;
236
+ /**
237
+ * The key the report deduped on. Under `'sync'` each metric is keyed
238
+ * `${idempotencyKey}:${featureId}`, so they dedupe independently; under `'async'` the whole
239
+ * turn is one event under this key alone.
240
+ */
241
+ idempotencyKey: string;
242
+ /** Which API this went through, because the two answer with different amounts of truth. */
243
+ mode: IngestMode;
244
+ /**
245
+ * Stigg reported nothing consumed for every metric: these keys were already ingested.
246
+ * **Undefined under `'async'`** - the event API answers with an empty body, so a replay is
247
+ * something Stigg does and the SDK cannot observe. Saying `false` would be a claim, not a fact.
248
+ */
249
+ replayed?: boolean;
250
+ /**
251
+ * Credits this report actually drew, when Stigg said so. Absent when the features reported are
252
+ * not credit-backed - which is also when the run's budget will never drain - and always absent
253
+ * under `'async'`, which returns no balance at all.
254
+ */
255
+ creditsConsumed?: number;
256
+ error?: unknown;
257
+ };
258
+ /**
259
+ * What a check resolves to when the engine could not answer it. A policy, not a notification:
260
+ * hooks are told what happened and cannot change it, while this decides the outcome. Ingest has
261
+ * no equivalent - a failed ingest never affects a run, it only reaches `hooks.onError`.
262
+ */
263
+ export type CheckErrorAction = 'deny' | 'allow';
264
+ /** What binding a vendor client needs: how to recognise it, what to watch, and what governs it. */
265
+ export type ClientOptions = {
266
+ /**
267
+ * Use this adapter instead of matching the client by its shape and base URL. The built-in ones
268
+ * cover Anthropic, OpenAI, OpenRouter and LiteLLM; reach for this when a vendor none of them
269
+ * identifies needs one of its own.
270
+ */
271
+ recipe?: ClientRecipe;
272
+ /** Everything this client reports back while it works. See {@link SessionHooks}. */
273
+ hooks?: SessionHooks;
274
+ /** The default context for every call through this client. Any call may override it. */
275
+ stiggContext?: StiggContext;
276
+ };
277
+ /**
278
+ * `register` - giving an agent an identity, a budget, and a place in the tree.
279
+ *
280
+ * A write, which is why it is its own call rather than something a gated call carries: a limit is
281
+ * not a field on the entity, so this is two idempotent upserts underneath - the entity, then an
282
+ * assignment over `(entity x entitlement)`. That is also why `parentId` belongs here and not on
283
+ * the entity: the hierarchy is per-entitlement.
284
+ *
285
+ * Optional throughout. Skip it and the agent has no entity - calls still work, and checks and
286
+ * usage apply to the customer instead.
287
+ */
288
+ export type AgentOptions = {
289
+ /** The durable agent. This is the entity budgets attach to. */
290
+ agentId: string;
291
+ /**
292
+ * The governance entity type to create the agent as. Required, and not defaulted: a guessed
293
+ * type pairs the agent with an attribution key nothing resolves, and the spend then lands on
294
+ * the customer looking exactly like a call that was never governed.
295
+ */
296
+ entityTypeId: string;
297
+ displayName?: string;
298
+ metadata?: Record<string, string>;
299
+ /** Places the agent under a parent entity. Tri-state: omit to leave it, null to make it a root. */
300
+ parentId?: string | null;
301
+ /** Budget for this agent, written on every call. Omit to leave whatever is already assigned. */
302
+ usageLimit?: number | null;
303
+ /**
304
+ * Budget applied only when this agent has no assignment yet. Use this rather than `usageLimit`
305
+ * when an admin may change the budget out of band: `usageLimit` would overwrite their change on
306
+ * every process start. Mutually exclusive with `usageLimit`.
307
+ */
308
+ usageLimitOnCreate?: number;
309
+ /** ISO-8601 single-unit reset window, e.g. 'P1M'. Governance requires it when creating a budget. */
310
+ cadence?: string;
311
+ };
312
+ export type AgentScope = {
313
+ /** The tenant. Every governance call is scoped to one customer. */
314
+ customerId: string;
315
+ /**
316
+ * Which entitlement the budget is written against. Optional only because a bare registration is
317
+ * an identity and nothing else, and a metering-only run has none to hand over. A call carrying
318
+ * `usageLimit`, `usageLimitOnCreate` or `parentId` still needs one and `register` refuses it
319
+ * without: an assignment is written over `(entity x entitlement)`, so a placement with nothing
320
+ * to write it against has nowhere to land.
321
+ */
322
+ entitlement?: Entitlement;
323
+ };
324
+ /** `register` - the standalone call, which has no run to take the scope from. */
325
+ export type RegisterInput = AgentOptions & AgentScope;
326
+ /** `deregister` - archiving the entity. No entitlement: an entity is not archived per-entitlement. */
327
+ export type DeregisterInput = Pick<AgentScope, 'customerId'> & Pick<AgentOptions, 'agentId'>;
328
+ /**
329
+ * Every property path the shipped recipes gate, written as the overlay walks it.
330
+ *
331
+ * Paths, not leaf names. The extra field has to appear in the vendor's own parameter type or an
332
+ * object literal carrying it is an excess-property error - but matching on the last segment alone
333
+ * also typed `stiggContext` onto every *other* method that happened to share the name. `create` is
334
+ * the common one: `openai.files.create` and `openai.batches.create` are not gated by any recipe,
335
+ * so a caller who passed a context there type-checked, was never checked, and handed the vendor an
336
+ * unknown field it would forward rather than strip - the exact harm the narrow list existed to
337
+ * prevent.
338
+ *
339
+ * `messages.stream` is here without being in any recipe's `methods`: it reaches the wire through
340
+ * the gated `messages.create`, so a context given to it does arrive.
341
+ */
342
+ export type GatedPath = ['messages', 'create'] | ['messages', 'stream'] | ['chat', 'completions', 'create'] | ['responses', 'create'] | ['systemOne'] | ['models', 'generateContent'] | ['models', 'generateContentStream'];
343
+ /**
344
+ * Gemini's conversational surface, which is wrapped as an object in its own right: a `Chat` holds
345
+ * the models module privately, so wrapping the client never reaches these. The paths are one
346
+ * segment long because the `Chat` *is* the client there.
347
+ *
348
+ * Kept OUT of {@link GatedPath} and reached only through {@link ChatOf}, because one segment is
349
+ * the whole path here and `sendMessage` is far too common a verb to claim globally. In the
350
+ * fallback list it typed `stiggContext` onto any client that happened to have one - a Slack
351
+ * client, a queue client - and on a client that some *other* recipe matched, that was the silent
352
+ * case the narrow list exists to prevent: `wrap` succeeded, the method was never gated, and the
353
+ * context object was forwarded to the vendor as an unknown field.
354
+ */
355
+ export type ChatGatedPath = ['sendMessage'] | ['sendMessageStream'];
356
+ /**
357
+ * The paths to overlay when no recipe was named, given what the client actually looks like.
358
+ *
359
+ * `getHistory` is the discriminator, and it is exactly what `geminiChatRecipe.matches` requires -
360
+ * so the types promise a context on precisely the methods the runtime will gate, rather than on
361
+ * every object with a method of that name.
362
+ */
363
+ /**
364
+ * The shapes the registry matches *before* it ever reaches the chat recipe.
365
+ *
366
+ * `geminiChatRecipe` is last in the shipped list, so any of these wins - which means a client
367
+ * carrying one of them has its chat methods left unpatched however chat-shaped it also looks.
368
+ */
369
+ type GatedVendorShape = {
370
+ messages: {
371
+ create: unknown;
372
+ };
373
+ } | {
374
+ chat: {
375
+ completions: {
376
+ create: unknown;
377
+ };
378
+ };
379
+ } | {
380
+ responses: {
381
+ create: unknown;
382
+ };
383
+ } | {
384
+ models: {
385
+ generateContent: unknown;
386
+ };
387
+ } | {
388
+ models: {
389
+ generateContentStream: unknown;
390
+ };
391
+ } | {
392
+ systemOne: unknown;
393
+ };
394
+ /**
395
+ * Which paths to overlay when no recipe was named, given what the client looks like.
396
+ *
397
+ * `getHistory` is the discriminator, and it is what `geminiChatRecipe.matches` requires - so the
398
+ * types promise a context on the methods the runtime will gate rather than on every object with
399
+ * a method of that name.
400
+ *
401
+ * It is not enough on its own, though. A client carrying *both* a gated vendor's shape and a
402
+ * chat's is matched as that vendor, leaving `sendMessage` unpatched - and an overlay keyed on
403
+ * `getHistory` alone still invited a context onto it, which is never checked and is handed to
404
+ * the vendor as an unknown field. So anything the registry would claim first is excluded here,
405
+ * mirroring that precedence.
406
+ */
407
+ export type ChatOf<T> = T extends GatedVendorShape ? GatedPath : T extends {
408
+ getHistory: (...args: any[]) => any;
409
+ } ? GatedPath | ChatGatedPath : GatedPath;
410
+ /**
411
+ * The paths still live after stepping through `K`, or `never` when that key leads nowhere gated.
412
+ *
413
+ * This is what bounds the walk: recursion follows the paths themselves, so there is no depth
414
+ * budget to keep in step with the deepest vendor surface, and no mapping of branches - a client's
415
+ * whole `files` or `batches` subtree - that could never hold a gated method.
416
+ */
417
+ type Below<Paths extends readonly string[], K> = Paths extends readonly [infer Head, ...infer Rest] ? Head extends K ? Rest extends readonly string[] ? Rest : never : never : never;
418
+ /** One signature, with the context threaded into whatever parameter object it already takes. */
419
+ type GatedSignature<P, R extends unknown[], Ret, Ctx> = P extends object ? (params: P & {
420
+ stiggContext?: Ctx;
421
+ }, ...rest: R) => Ret : (params: P, ...rest: R) => Ret;
422
+ /**
423
+ * A gated method, overloads and all.
424
+ *
425
+ * The arms are tried widest first because a single `infer` arm would silently collapse an
426
+ * overloaded method to its **last** signature - and `chat.completions.create` is overloaded
427
+ * precisely on `stream`, so collapsing it would hand a streaming caller the non-streaming return
428
+ * type. Three arms covers every vendor method the recipes gate; a fourth overload would fall
429
+ * through to the last arm and keep working, minus the earlier signatures.
430
+ */
431
+ type GatedMethod<F, Ctx> = F extends {
432
+ (params: infer P1, ...rest: infer R1): infer T1;
433
+ (params: infer P2, ...rest: infer R2): infer T2;
434
+ (params: infer P3, ...rest: infer R3): infer T3;
435
+ } ? GatedSignature<P1, R1, T1, Ctx> & GatedSignature<P2, R2, T2, Ctx> & GatedSignature<P3, R3, T3, Ctx> : F extends {
436
+ (params: infer P1, ...rest: infer R1): infer T1;
437
+ (params: infer P2, ...rest: infer R2): infer T2;
438
+ } ? GatedSignature<P1, R1, T1, Ctx> & GatedSignature<P2, R2, T2, Ctx> : F extends (params: infer P, ...rest: infer R) => infer Ret ? GatedSignature<P, R, Ret, Ctx> : F;
439
+ /**
440
+ * The client's own surface, with every gated method taught to accept a `stiggContext`.
441
+ *
442
+ * Rebuilt structurally, which on its own would lose the vendor's class identity - so
443
+ * {@link GovernedClient} intersects it back with `T` rather than replacing it. The overlay comes
444
+ * first in that intersection on purpose: an intersection of two call signatures is an overload
445
+ * set resolved in order, and the vendor's own signature would reject `stiggContext` as an excess
446
+ * property if it were tried first.
447
+ */
448
+ export type GatedSurface<T, Ctx, Paths extends readonly string[] = GatedPath> = {
449
+ [K in keyof T]: [Below<Paths, K>] extends [never] ? T[K] : [
450
+ ] extends Below<Paths, K> ? GatedMethod<T[K], Ctx> : GatedSurface<T[K], Ctx, Below<Paths, K>>;
451
+ };
452
+ /**
453
+ * Which paths a wrapped client accepts a context on: the recipe's own, or the shipped ones.
454
+ *
455
+ * `wrap` infers the options object it was handed, so a recipe passed inline keeps whatever types
456
+ * it was written with - and a recipe that declares its paths as literals (`path: [...] as const`)
457
+ * gets the overlay on exactly the methods it gates, rather than on the shipped vendors' paths.
458
+ *
459
+ * Anything else falls back to {@link GatedPath}: no recipe at all, which is the detection case, and
460
+ * a recipe whose paths widened to `string[]`, where there is nothing to narrow to and guessing
461
+ * would be worse than the default. So this only ever adds precision; it cannot take it away.
462
+ */
463
+ export type PathsOf<R, T = unknown> = R extends {
464
+ methods: readonly {
465
+ path: infer P;
466
+ }[];
467
+ } ? P extends readonly [string, ...string[]] ? P : ChatOf<T> : ChatOf<T>;
468
+ /**
469
+ * What a call still owes, given what `wrap` was already told.
470
+ *
471
+ * `customerId` is the one field that has to come from somewhere, so a client wrapped without one
472
+ * types it as required on every call and a client wrapped with one types it as optional. The
473
+ * runtime checks it either way, for callers the types never reached.
474
+ */
475
+ export type CallContext<Wrapped> = Wrapped extends {
476
+ customerId: string;
477
+ } ? StiggContext : StiggContext & {
478
+ customerId: string;
479
+ };
480
+ /**
481
+ * A vendor client bound to this environment. Every call its recipe covers is checked before it
482
+ * leaves and metered after, against the context `wrap` was given and whatever the call overrides.
483
+ */
484
+ export type GovernedClient<T, Wrapped = StiggContext, Paths extends readonly string[] = GatedPath> = GatedSurface<T, CallContext<Wrapped>, Paths> & T & {
485
+ /** What this client did, and what is still in flight. */
486
+ readonly stigg: ClientGovernance;
487
+ };
488
+ /** The governance handle hung off a wrapped client. */
489
+ export type ClientGovernance = {
490
+ /** Waits for in-flight ingests, then reports what this client did. */
491
+ flush(): Promise<GovernanceSummary>;
492
+ /** Drains, then releases what this client holds. */
493
+ close(): Promise<void>;
494
+ /** What this client has done so far, without waiting. */
495
+ summary(): GovernanceSummary;
496
+ /** The last decision this client made, or undefined if it has made none. */
497
+ readonly lastDecision?: GovernanceDecision;
498
+ /** The `x-stigg-*` attribution for work continuing in another process. */
499
+ headers(override?: StiggContext): GovernanceHeaders;
500
+ /** The gate, for a call this client does not wrap. */
501
+ check(override?: StiggContext, requestedUsage?: number): Promise<GovernanceDecision>;
502
+ /** The meter, for usage this client did not observe. */
503
+ report(metrics: Metrics, override?: StiggContext): Promise<IngestResult>;
504
+ };
505
+ /** What an out-of-process ingest needs, since it has no run to carry it. */
506
+ export type IngestContext = {
507
+ /** What the run this usage belongs to checks against. Defaults to the `credits` currency. */
508
+ entitlement?: Entitlement;
509
+ /**
510
+ * What to report. Defaults to the vendor's own names, read off the usage object the proxy
511
+ * observed - the same default a wrapped client gets.
512
+ */
513
+ metrics?: MetricsMapper;
514
+ /**
515
+ * Which shape the observed usage is. Named rather than sniffed: Anthropic and the Responses API
516
+ * both count `input_tokens`, and a wrong guess would report to another customer's meter.
517
+ * Omitted, only the normalized counters are available - so supply a `metrics` mapper too.
518
+ */
519
+ vendor?: ObservedVendor;
520
+ /** Which of Stigg's ingest APIs to report through. Defaults to the client's setting. */
521
+ ingest?: IngestMode;
522
+ /**
523
+ * What the event is called in `'async'` mode. There is no wrapped call out here to name it
524
+ * after, so a proxy either says, or is reported under the vendor it declared.
525
+ *
526
+ * Singular, and named to match `IngestOptions.event`: one report is one event, whatever the
527
+ * turn measured.
528
+ */
529
+ event?: EventIngest;
530
+ };
531
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.creditsPerTokens = exports.toMeasurement = exports.vendorMetrics = exports.emptyTokenUsage = exports.entitlementRef = exports.entitlementKind = exports.entitlementId = void 0;
4
+ exports.entitlementError = entitlementError;
5
+ exports.usageValueError = usageValueError;
6
+ /**
7
+ * The vocabulary shared by the core, the engines and every binding, and the argument list of
8
+ * every call on the public surface.
9
+ *
10
+ * Terms follow Stigg governance rather than the draft design notes: provision/archive (not
11
+ * register/de-register), ingest (not report), assignment (not budget), dimensions (not tags).
12
+ *
13
+ * The imports below are all `import type`, so they are erased at compile time: this file pulls in
14
+ * no module at runtime and stays the leaf everything else can depend on.
15
+ */
16
+ const errors_1 = require("./errors");
17
+ /** The id to send as `featureId`/`currencyId`, whichever kind the entitlement is. */
18
+ const entitlementId = (entitlement) => entitlement.currencyId ?? entitlement.featureId;
19
+ exports.entitlementId = entitlementId;
20
+ /** 'credit' targets read the customer's credit pool; 'feature' targets need an entitling subscription. */
21
+ const entitlementKind = (entitlement) => entitlement.currencyId ? 'credit' : 'feature';
22
+ exports.entitlementKind = entitlementKind;
23
+ /**
24
+ * The entitlement as every Stigg endpoint takes it: one of two mutually exclusive id fields,
25
+ * ready to spread.
26
+ *
27
+ * ```ts
28
+ * await client.v1Beta.customers.entitlements.check(customerId, { ...entitlementRef(entitlement), requestedUsage });
29
+ * ```
30
+ *
31
+ * Narrowing on the property rather than through {@link entitlementKind} is the point: a function
32
+ * call cannot narrow a union, so every hand-written version of this needed a `!` on each branch -
33
+ * an assertion that the union already guarantees and that would survive the union changing.
34
+ */
35
+ const entitlementRef = (entitlement) => entitlement.currencyId !== undefined ? { currencyId: entitlement.currencyId } : { featureId: entitlement.featureId };
36
+ exports.entitlementRef = entitlementRef;
37
+ /**
38
+ * Why an entitlement cannot be billed against, if one was named at all.
39
+ *
40
+ * Naming none is legal: it is how a caller asks for metering without enforcement, so undefined is
41
+ * not an error here. The one place an entitlement is genuinely required is an assignment, which is
42
+ * written over `(entity x entitlement)` - and `register` refuses that case itself, because only it
43
+ * knows the agent to name in the message.
44
+ *
45
+ * What stays refused is an entitlement that is named and names nothing. The union permits
46
+ * `{ currencyId: '' }`, and an empty currency is falsy - so it classifies as a feature and every
47
+ * branch downstream then asks for a `featureId` that is not there. An empty id is a typo rather
48
+ * than an intention, and letting it through meters a run against nothing while it still looks
49
+ * enforced.
50
+ */
51
+ function entitlementError(entitlement) {
52
+ if (!entitlement)
53
+ return undefined;
54
+ if ((0, exports.entitlementId)(entitlement)?.trim())
55
+ return undefined;
56
+ return new Error('a run needs a non-empty currencyId or featureId - an empty one bills against nothing.');
57
+ }
58
+ const emptyTokenUsage = () => ({ input: 0, output: 0, cacheRead: 0, cacheCreate: 0 });
59
+ exports.emptyTokenUsage = emptyTokenUsage;
60
+ /** What a run reports when nobody says otherwise: exactly what the vendor stated. */
61
+ const vendorMetrics = (measurement) => measurement.metrics ?? {};
62
+ exports.vendorMetrics = vendorMetrics;
63
+ /** A binding may report either shape; `TokenUsage` is the common case and stays terse. */
64
+ const toMeasurement = (value) => 'input' in value ? { usage: value } : value;
65
+ exports.toMeasurement = toMeasurement;
66
+ /**
67
+ * Why a measured value cannot be reported, if it cannot.
68
+ *
69
+ * Zero is not an error - a cached refusal or an empty stream is a normal turn - but a mapper
70
+ * dividing by an unset config returns NaN or Infinity, and reading those as "nothing was used"
71
+ * meters the run at zero while every call still reports success. Those are refused in both modes;
72
+ * a fraction is refused only under `'sync'`, whose DTO takes whole units.
73
+ *
74
+ * Shared, because every path that reports usage has to answer this the same way.
75
+ */
76
+ function usageValueError(value, mode = 'sync') {
77
+ if (!Number.isFinite(value) || value < 0) {
78
+ return new errors_1.EngineError('ingest', `usage value must be a finite non-negative number, got ${value}`);
79
+ }
80
+ // An event carries the number as a dimension for a meter to aggregate, and a meter is free to
81
+ // sum fractions - which is the only way a gateway's `cost` reports honestly. The usage report
82
+ // takes a whole number or nothing.
83
+ if (mode === 'async' || Number.isInteger(value))
84
+ return undefined;
85
+ return new errors_1.EngineError('ingest', `usage value must be a positive whole number, got ${value}`);
86
+ }
87
+ /**
88
+ * One metric, summed from every token class and divided by the unit size.
89
+ *
90
+ * The mapper for an application that prices tokens itself rather than letting Stigg convert a
91
+ * credit-backed feature. Weighting cache reads cheaper is a pricing decision and belongs in
92
+ * Stigg's consumption mapping, not here; Stigg rejects fractions, so the result rounds up.
93
+ */
94
+ const creditsPerTokens = (featureId, tokensPerUnit = 1) => ({ usage }) => usage
95
+ ? { [featureId]: Math.ceil((usage.input + usage.output + usage.cacheRead + usage.cacheCreate) / tokensPerUnit) }
96
+ : {};
97
+ exports.creditsPerTokens = creditsPerTokens;