@stigg/agent-sdk-js 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -43
- package/dist/bindings/client/gateways/bedrock/bedrock-recipe.d.ts +17 -0
- package/dist/bindings/client/gateways/bedrock/bedrock-recipe.js +134 -0
- package/dist/bindings/client/gateways/bedrock/types.d.ts +13 -0
- package/dist/bindings/client/gateways/bedrock/types.js +2 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +5 -0
- package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +137 -34
- package/dist/bindings/client/gateways/openrouter/sdk-usage.d.ts +37 -0
- package/dist/bindings/client/gateways/openrouter/sdk-usage.js +63 -0
- package/dist/bindings/client/gateways/openrouter/types.d.ts +49 -0
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +5 -1
- package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +103 -13
- package/dist/bindings/shared/token-usage.d.ts +30 -1
- package/dist/bindings/shared/token-usage.js +58 -3
- package/dist/bindings/shared/types.d.ts +16 -0
- package/dist/bindings/shared/weighted-metrics.js +5 -2
- package/dist/config.d.ts +26 -17
- package/dist/config.js +29 -27
- package/dist/core/context.d.ts +21 -12
- package/dist/core/context.js +76 -9
- package/dist/core/session.d.ts +38 -9
- package/dist/core/session.js +57 -12
- package/dist/decision-text.d.ts +3 -3
- package/dist/engine/decision.d.ts +11 -6
- package/dist/engine/decision.js +9 -2
- package/dist/engine/report.js +4 -1
- package/dist/errors.d.ts +14 -6
- package/dist/errors.js +13 -3
- package/dist/headers.d.ts +13 -0
- package/dist/headers.js +20 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +8 -2
- package/dist/stigg-agent-sdk.d.ts +29 -13
- package/dist/stigg-agent-sdk.js +103 -56
- package/dist/types.d.ts +124 -35
- package/dist/types.js +66 -16
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -33,6 +33,11 @@ is a `stiggContext`, given once at `wrap` as the default for that client and aga
|
|
|
33
33
|
single call's own parameters to override it. One wrapped client therefore serves every customer
|
|
34
34
|
and every agent, which is what a multi-tenant server actually needs.
|
|
35
35
|
|
|
36
|
+
**No subscription required.** A customer's credit balance can be checked and drawn with no
|
|
37
|
+
subscription in place at all, so the shortest path from nothing to a working integration is a
|
|
38
|
+
credit currency and a `customerId` — no plan, no entitlement wiring, no catalog modelling first.
|
|
39
|
+
Model the pricing once the metering is real.
|
|
40
|
+
|
|
36
41
|
`check` happens when the context names an `entitlement`. Omit it and the call meters only: the
|
|
37
42
|
request still goes out, and it is still billed.
|
|
38
43
|
|
|
@@ -59,11 +64,11 @@ request (a budget, a parent) is the only thing that fails without it.
|
|
|
59
64
|
```ts
|
|
60
65
|
import { StiggAgentSDK } from '@stigg/agent-sdk-js';
|
|
61
66
|
|
|
62
|
-
// One per environment
|
|
63
|
-
// replaced by a `
|
|
64
|
-
//
|
|
65
|
-
// new StiggAgentSDK(apiKey, 'https://stigg.internal')
|
|
66
|
-
//
|
|
67
|
+
// One per environment, and everything it takes is one object with a working default. The key can
|
|
68
|
+
// be left out entirely to read STIGG_SERVER_API_KEY, or replaced by a `baseUrl` that needs none -
|
|
69
|
+
// a local stand-in, or a self-hosted Stigg:
|
|
70
|
+
// new StiggAgentSDK({ apiKey, baseUrl: 'https://stigg.internal', ingest: 'sync' })
|
|
71
|
+
// A bare key is the shorthand for the common case.
|
|
67
72
|
const stigg = new StiggAgentSDK(apiKey);
|
|
68
73
|
|
|
69
74
|
// Provisioning the agent: a write, so it is its own call. A *bare* registration is remembered per
|
|
@@ -72,11 +77,18 @@ const stigg = new StiggAgentSDK(apiKey);
|
|
|
72
77
|
await stigg.register({
|
|
73
78
|
customerId: 'customer-acme',
|
|
74
79
|
agentId: 'agent-support-bot',
|
|
80
|
+
// The entity: what the agent IS. `register` is the one call that creates it, so it is the one
|
|
81
|
+
// call that needs the type.
|
|
75
82
|
entityTypeId: 'agent',
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
// The assignment: what it may SPEND, and where it sits. One object, because it is one write -
|
|
84
|
+
// over (entity x entitlement), which is why the entitlement is required inside it.
|
|
85
|
+
budget: {
|
|
86
|
+
entitlement: { currencyId: 'credits' },
|
|
87
|
+
parentId: 'team-eng',
|
|
88
|
+
limit: 5,
|
|
89
|
+
apply: 'on-create',
|
|
90
|
+
cadence: 'P1M',
|
|
91
|
+
},
|
|
80
92
|
});
|
|
81
93
|
|
|
82
94
|
// Governs the client and hands back a COPY whose calls are checked and metered. The client you
|
|
@@ -90,11 +102,11 @@ const govClient = stigg.wrap(new Anthropic(), {
|
|
|
90
102
|
// Naming an entitlement is the enforcement switch. Omit it and the calls meter without
|
|
91
103
|
// gating - every call still goes out, and nothing is ever refused.
|
|
92
104
|
entitlement: { currencyId: 'credits' },
|
|
93
|
-
// What the calls are attributed to. The
|
|
105
|
+
// What the calls are attributed to. The attribution key is required with it - a guessed one
|
|
94
106
|
// resolves no entity, and the spend would land on the customer looking exactly like a call
|
|
95
107
|
// that was never governed. The parent needs no key: governance walks the chain up from here.
|
|
96
108
|
agentId: 'agent-support-bot',
|
|
97
|
-
|
|
109
|
+
attributionKey: 'agentId',
|
|
98
110
|
// Optional extra attribution, sent verbatim on every check and ingest.
|
|
99
111
|
// dimensions: { tenantId: 'acme-eu' },
|
|
100
112
|
},
|
|
@@ -238,7 +250,9 @@ that vendor's API would have guessed — so these are what your meters filter on
|
|
|
238
250
|
| OpenAI (chat) | `openai.chat.completions.create` | `prompt_tokens`, `completion_tokens`, `cached_tokens`, `total_tokens` |
|
|
239
251
|
| OpenAI (responses) | `openai.responses.create` | `input_tokens`, `output_tokens`, `cached_tokens`, `total_tokens` |
|
|
240
252
|
| OpenRouter | `openrouter.chat.completions.create`, `openrouter.responses.create` | the OpenAI set, plus `cost` |
|
|
253
|
+
| OpenRouter — its own SDK | `openrouter.chat.send`, `openrouter.responses.send` | the same set — `@openrouter/sdk` camelCases its counters and the recipe reads them back under the wire's names, so one meter covers either client |
|
|
241
254
|
| LiteLLM | `litellm.chat.completions.create`, `litellm.responses.create` | the OpenAI set, plus `cost` |
|
|
255
|
+
| Bedrock | `bedrock.send` | `inputTokens`, `outputTokens`, `cacheReadInputTokens`, `cacheWriteInputTokens`, `totalTokens` — and no `cost`, because Bedrock states none |
|
|
242
256
|
| TypeSafe | `typesafe.systemOne` | `input_tokens`, `output_tokens`, `total_tokens` |
|
|
243
257
|
| Gemini | `gemini.models.generateContent`, `gemini.models.generateContentStream` | `promptTokenCount`, `candidatesTokenCount`, `cachedContentTokenCount`, `thoughtsTokenCount`, `toolUsePromptTokenCount`, `totalTokenCount` |
|
|
244
258
|
| Gemini — a chat | `gemini.chats.sendMessage`, `gemini.chats.sendMessageStream` | the same five |
|
|
@@ -266,6 +280,11 @@ A meter to put behind one of them, in Stigg's own terms:
|
|
|
266
280
|
> event API answers with an acknowledgement, not a balance, so nothing downstream can notice on its
|
|
267
281
|
> own — configure the meter first, or run with `ingest: 'sync'`, which does answer.
|
|
268
282
|
|
|
283
|
+
> **None of this applies to a metric named after a credit currency.** That draws the wallet
|
|
284
|
+
> directly and needs no event name and no meter — see [the three ingest paths](#the-engine-sdk-first-rest-to-complete)
|
|
285
|
+
> below. If you are using `unitsPerTokens(currencyId, …)`, that is the path you are on, and the
|
|
286
|
+
> table above is about the other one.
|
|
287
|
+
|
|
269
288
|
#### Pricing them yourself
|
|
270
289
|
|
|
271
290
|
`weightedMetrics` turns a price-per-metric table into the mapper:
|
|
@@ -292,10 +311,25 @@ optional — an unpriced one is free, and a table that prices nothing meters not
|
|
|
292
311
|
A weight is the price of **one unit** of that metric, so the example above is half a credit per
|
|
293
312
|
output token. Pass `{ per: 1000 }` when the numbers read better per thousand.
|
|
294
313
|
|
|
295
|
-
It also handles the
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
314
|
+
It also handles the two things a hand-written mapper has to get right: a turn that measured
|
|
315
|
+
nothing reports nothing rather than a zero, and a counter the response omitted is absent rather
|
|
316
|
+
than zero. For one number without weights, `unitsPerTokens(id, n)` is the short form.
|
|
317
|
+
|
|
318
|
+
Neither rounds. A credit currency takes a fractional amount, so a turn worth a fifth of a unit
|
|
319
|
+
costs a fifth of one — rounding it up would be this SDK's arithmetic on your price. **A feature is
|
|
320
|
+
the exception**: `POST /api/v1/usage` answers `400 Expected integer, received float`, so when the
|
|
321
|
+
target is a feature rather than a currency, pick a unit size that divides or round in your own
|
|
322
|
+
mapper.
|
|
323
|
+
|
|
324
|
+
> **An attributed run is the other exception, and the SDK rounds it up for you.** Per-entity usage
|
|
325
|
+
> is counted in whole units — governance validates its ingest as an integer and increments a Redis
|
|
326
|
+
> counter with `HINCRBY` — so a fractional amount is rejected there *while the wallet still moves*.
|
|
327
|
+
> The agent's budget would stop advancing with nothing in the response to say so.
|
|
328
|
+
>
|
|
329
|
+
> So a run naming an `agentId` rounds its wallet draw up, and logs that it did. A run with no
|
|
330
|
+
> entity attribution reports the fraction as measured. Denominate the currency finely enough that
|
|
331
|
+
> one unit is small, and the overcharge is noise; this goes away when governance counts fractions.
|
|
332
|
+
|
|
299
333
|
|
|
300
334
|
#### Writing the mapper yourself
|
|
301
335
|
|
|
@@ -502,23 +536,75 @@ Two clients, composed per operation, and the SDK tells you which one it picked:
|
|
|
502
536
|
| `ingest` | `@stigg/node-server-sdk` | it has the in-memory cache, and usage reporting is the hot path |
|
|
503
537
|
| provision / archive | `@stigg/typescript` (REST, `v1-beta`) | entities and assignments exist only on the governance surface |
|
|
504
538
|
|
|
505
|
-
**Ingest has
|
|
539
|
+
**Ingest has three paths, not two.** `ingest` picks between the first two — but **the name of the
|
|
540
|
+
metric decides first**, and that is the rule to read before the table:
|
|
541
|
+
|
|
542
|
+
> **If a metric is named after the credit currency the context checks, it draws the wallet
|
|
543
|
+
> directly, in both modes.** No event, no usage report, and no meter involved — `ingest` then only
|
|
544
|
+
> decides whether the call waits for the balance to come back.
|
|
545
|
+
|
|
546
|
+
That is the path `unitsPerTokens(currencyId, …)` puts you on, and it is why switching `async` and
|
|
547
|
+
`sync` can look like it changes nothing: both deduct credits, neither needs a metered feature, and
|
|
548
|
+
the only visible difference is whether `IngestResult.creditsConsumed` is filled in. Metrics named
|
|
549
|
+
after a **feature** take one of the other two columns.
|
|
506
550
|
|
|
507
|
-
| | `ingest: 'async'` — the default | `ingest: 'sync'` |
|
|
508
|
-
| ---------------------- | ----------------------------------- | ---------------------------------------------------- |
|
|
509
|
-
| Call | `events.report` / `reportEvent` | `usage.report` / `reportUsageBulk` |
|
|
510
|
-
| Latency and rate limit | low latency, much higher limit | higher latency, lower limit |
|
|
511
|
-
| The next check | lags while Stigg aggregates | already includes the spend |
|
|
512
|
-
| What you send | the call, as one event | a `featureId` and a whole-number `value`, per metric |
|
|
513
|
-
| Meter type | **Raw events**, configured in Stigg | calculated / incremental |
|
|
514
|
-
| What comes back | an acknowledgement
|
|
551
|
+
| | A metric named after the currency | `ingest: 'async'` — the default | `ingest: 'sync'` |
|
|
552
|
+
| ---------------------- | --------------------------------------- | ----------------------------------- | ---------------------------------------------------- |
|
|
553
|
+
| Call | `consumeCredits` / `consumeCreditsAsync` | `events.report` / `reportEvent` | `usage.report` / `reportUsageBulk` |
|
|
554
|
+
| Latency and rate limit | follows the mode | low latency, much higher limit | higher latency, lower limit |
|
|
555
|
+
| The next check | includes it under `'sync'`; lags under `'async'` | lags while Stigg aggregates | already includes the spend |
|
|
556
|
+
| What you send | a currency id and an amount | the call, as one event | a `featureId` and a whole-number `value`, per metric |
|
|
557
|
+
| Meter type | **none — the wallet is drawn directly** | **Raw events**, configured in Stigg | calculated / incremental |
|
|
558
|
+
| What comes back | a balance under `'sync'`, nothing under `'async'` | an acknowledgement | a credit balance |
|
|
515
559
|
|
|
516
560
|
Async sends **one event for the whole turn**, every metric on its own dimension, because one event
|
|
517
561
|
feeds any number of meters — fanning out per metric buys nothing and costs a request each. Sync
|
|
518
562
|
sends one report per metric, batched into a single bulk call.
|
|
519
563
|
|
|
520
|
-
|
|
521
|
-
|
|
564
|
+
There is no event path from a raw event to a currency, which is why the first column exists at all:
|
|
565
|
+
a usage report takes only a `featureId`, so a currency has to be drawn rather than metered.
|
|
566
|
+
|
|
567
|
+
> **`'async'` does not settle the wallet before the next check.** `consumeCreditsAsync` is
|
|
568
|
+
> reconciled into the balance on Stigg's side, and nothing invalidates the entitlement cache in the
|
|
569
|
+
> meantime — so a check made immediately after a draw can still see the balance from before it, and
|
|
570
|
+
> approve a call the wallet could not afford. `'sync'` is what closes that window.
|
|
571
|
+
|
|
572
|
+
**The match is exact.** A metric draws the wallet only when its name *equals* the currency the
|
|
573
|
+
context checks. `unitsPerTokens('data_credit', …)` against `entitlement: { currencyId: 'credits' }`
|
|
574
|
+
is a feature report named `data_credit`, not a draw on `credits` — so it needs a meter like any
|
|
575
|
+
other feature, and silently meters nothing until one exists.
|
|
576
|
+
|
|
577
|
+
**A check asks for one unit by default, so the last call can overshoot.** The budget is allowed
|
|
578
|
+
below zero, and `'async'` adds a reconciliation window on top — calls made while Stigg is still
|
|
579
|
+
turning events into usage all see the same balance. Worst case is therefore one call's cost, plus
|
|
580
|
+
whatever lands in that window.
|
|
581
|
+
|
|
582
|
+
Narrow it per call with the recipe's own estimator, which asks for what the call could cost
|
|
583
|
+
instead of for one unit:
|
|
584
|
+
|
|
585
|
+
```ts
|
|
586
|
+
import { anthropicRecipe, estimateFromMaxTokens } from '@stigg/agent-sdk-js';
|
|
587
|
+
|
|
588
|
+
stigg.wrap(new Anthropic(), {
|
|
589
|
+
stiggContext: { customerId, entitlement: { currencyId: 'credits' } },
|
|
590
|
+
// `estimate` reads the call's own parameters - here `max_tokens` - and asks for that many
|
|
591
|
+
// units instead of one. The ingest that follows still corrects the balance.
|
|
592
|
+
recipe: {
|
|
593
|
+
...anthropicRecipe,
|
|
594
|
+
methods: anthropicRecipe.methods.map((method) => ({ ...method, estimate: estimateFromMaxTokens(1_000) })),
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
It is opt-in on purpose: an estimate that overshoots denies calls the budget could have afforded,
|
|
600
|
+
and the ingest that follows corrects the balance anyway. For a hard cap, pair it with
|
|
601
|
+
`ingest: 'sync'` — `'async'` trades exactness for throughput, so it is not what a hard cap is for.
|
|
602
|
+
|
|
603
|
+
**Which to reach for.** Credits when the unit is money-like, shared across features, pre-purchased
|
|
604
|
+
or topped up — one balance, many consumers. A metered feature when the limit is per-capability,
|
|
605
|
+
belongs to a plan entitlement, or resets on a billing cadence. Both when a plan grants a feature
|
|
606
|
+
allowance and a credit pool covers the overage; name the feature in `usageMapper` and let Stigg's
|
|
607
|
+
consumption mapping convert it, rather than pricing it in code.
|
|
522
608
|
|
|
523
609
|
**Governance is optional.** Not every account has it. When it is missing the factory falls back
|
|
524
610
|
to customer-level enforcement on the Node SDK alone — every example still runs, denials just name
|
|
@@ -764,34 +850,37 @@ team-research (team) usage=0/30
|
|
|
764
850
|
the parent team, a funded pool). That is the more realistic split: an admin sets up the account, an
|
|
765
851
|
application provisions its own agents at runtime. `register()` carries it:
|
|
766
852
|
|
|
767
|
-
One caveat about the cap, because the two halves can overlap. `
|
|
853
|
+
One caveat about the cap, because the two halves can overlap. `apply: 'on-create'` is exactly that
|
|
768
854
|
— on create — so if the seed has already written an assignment for that agent, the application's
|
|
769
855
|
cap is **not** applied and the seeded one stands. That is the point of the option (an admin who
|
|
770
856
|
raises a budget out of band should not have it stamped back on every process start), but it does
|
|
771
|
-
mean `--agent-limit` on the seed wins over the number below.
|
|
772
|
-
application is meant to own it.
|
|
857
|
+
mean `--agent-limit` on the seed wins over the number below. Leave `apply` at its default
|
|
858
|
+
(`'always'`) if the application is meant to own it.
|
|
773
859
|
|
|
774
860
|
```ts
|
|
775
861
|
await stigg.register({
|
|
776
862
|
customerId: 'customer-acme',
|
|
777
863
|
agentId: 'agent-support-bot-anthropic',
|
|
778
864
|
entityTypeId: 'agent',
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
865
|
+
budget: {
|
|
866
|
+
// A budget is written over (entity x entitlement), so the entitlement is required here: a
|
|
867
|
+
// cap has nowhere to land without one.
|
|
868
|
+
entitlement: { currencyId: 'credits' },
|
|
869
|
+
parentId: 'team-research',
|
|
870
|
+
limit: 5,
|
|
871
|
+
cadence: 'P1M',
|
|
872
|
+
},
|
|
785
873
|
});
|
|
786
874
|
```
|
|
787
875
|
|
|
788
876
|
A cap is **not** a field on the entity. An entity is just a node — id, type, display name,
|
|
789
877
|
metadata — and a limit is an _assignment_ over `(entity × capability)`. So that one call is two
|
|
790
878
|
idempotent upserts underneath: `PUT …/entities` then `PUT …/assignments`. It is also why
|
|
791
|
-
`parentId` rides the assignment rather than the entity
|
|
792
|
-
agent can sit under different parents for
|
|
879
|
+
`parentId` rides the assignment rather than the entity — and sits inside `budget` for the same
|
|
880
|
+
reason: the hierarchy is per-capability, so one agent can sit under different parents for
|
|
881
|
+
different currencies.
|
|
793
882
|
|
|
794
|
-
`
|
|
883
|
+
`budget.limit` is tri-state — a number caps it, `null` tracks without blocking (the observe-only
|
|
795
884
|
rollout), and omitting it leaves whatever is already assigned untouched. Governance requires both
|
|
796
885
|
it and `cadence` when it creates a budget, which is why both are passed above.
|
|
797
886
|
|
|
@@ -834,6 +923,8 @@ there (or exporting it in your shell) overrides the fixture without editing `dem
|
|
|
834
923
|
| `AGENT_SDK_GEMINI_MODEL` | `gemini-3.8-flash` | Model for the Gemini client example |
|
|
835
924
|
| `AGENT_SDK_OPENROUTER_MODEL` | `anthropic/claude-opus-5` | Primary model requested from the gateway |
|
|
836
925
|
| `AGENT_SDK_OPENROUTER_FALLBACK_MODELS` | `anthropic/claude-sonnet-5,openai/gpt-5` | Comma-separated fallbacks tried in order |
|
|
926
|
+
| `AGENT_SDK_BEDROCK_REGION` | `us-east-1` (falls back to `AWS_REGION`) | Region the Bedrock model is called in. There is no Bedrock key variable: AWS authenticates through its own credential chain, and the example asks that chain rather than looking for one |
|
|
927
|
+
| `AGENT_SDK_BEDROCK_MODEL` | `anthropic.claude-sonnet-4-5-20250929-v1:0` | Bedrock model id. Names one model exactly — Bedrock does not route, so there is no fallback list |
|
|
837
928
|
| `AGENT_SDK_LITELLM_API_KEY` | — (falls back to `LITELLM_API_KEY`) | Layer 2. Blank ⇒ the offline mock |
|
|
838
929
|
| `AGENT_SDK_LITELLM_BASE_URL` | `http://localhost:4000` | Your own proxy — self-hosted, so there is no public default |
|
|
839
930
|
| `AGENT_SDK_LITELLM_MODEL` | `gpt-5` | A model **group** from the proxy's config, not a provider slug |
|
|
@@ -847,7 +938,7 @@ change it to play with an example, it is a fixture.** A real application decides
|
|
|
847
938
|
in code — what it bills against is part of its pricing, not of its deployment.
|
|
848
939
|
|
|
849
940
|
The SDK's own settings are arguments rather than variables, which is why none of them appear here:
|
|
850
|
-
`new StiggAgentSDK(apiKey, baseUrl,
|
|
941
|
+
`new StiggAgentSDK({ apiKey, baseUrl, cache, client, timeoutMs, logger, onError })` for the
|
|
851
942
|
environment, and `wrap(client, { stiggContext })` — overridable inside any call's own parameters —
|
|
852
943
|
for what a call is governed by.
|
|
853
944
|
|
|
@@ -878,8 +969,8 @@ The defaults are mock-only on purpose: against an environment you can write to,
|
|
|
878
969
|
| `AGENT_SDK_DEMO_CUSTOMER_ID` | `customer-acme` | **Error.** Every governance call is scoped to a tenant, and the examples will not invent one |
|
|
879
970
|
| `AGENT_SDK_DEMO_TEAM_ID` | `team-research` | The agents are **roots** — no ancestor budget binds them. `two-agents` needs one and says so |
|
|
880
971
|
| `AGENT_SDK_DEMO_AGENT_ID` | `agent-support-bot` | **No agent at all**: checks and usage land on the customer's own pool. Blank it to see that path against the mock. Each client example appends its vendor (`…-anthropic`) |
|
|
881
|
-
| `AGENT_SDK_DEMO_ENTITY_TYPE_ID` | `agent` *(mock only)* | `
|
|
882
|
-
| `AGENT_SDK_DEMO_ATTRIBUTION_KEY` | `agentId` *(mock only)* | `stiggContext.
|
|
972
|
+
| `AGENT_SDK_DEMO_ENTITY_TYPE_ID` | `agent` *(mock only)* | `register`'s own `entityTypeId` — the entity type agents are provisioned as |
|
|
973
|
+
| `AGENT_SDK_DEMO_ATTRIBUTION_KEY` | `agentId` *(mock only)* | `stiggContext.attributionKey` — the key that type identifies its agents under. **Both or neither**: the SDK defaults neither half, because a guessed key resolves no entity and the spend lands on the customer looking exactly like an ungoverned call |
|
|
883
974
|
| `AGENT_SDK_OFFLINE` | empty | Set to `1` to force the mocks even when keys are present |
|
|
884
975
|
| `AGENT_SDK_DEMO_MOCK_GOVERNANCE_FAIL` | empty | Mock only: make the runtime check return `503` |
|
|
885
976
|
|
|
@@ -907,6 +998,7 @@ Stigg account.
|
|
|
907
998
|
| `example-two-agents` | [`client/two-agents.ts`](./examples/client/two-agents.ts) | Two agents, one team: a sibling denied by its own cap, then the survivor denied by the team above it |
|
|
908
999
|
| `example-gateway-openrouter`| [`gateway/openrouter.ts`](./examples/gateway/openrouter.ts) | The two gateway topologies: calling one gates, operating one only ingests. `example-gateway` runs this one |
|
|
909
1000
|
| `example-gateway-litellm` | [`gateway/litellm.ts`](./examples/gateway/litellm.ts) | The same two topologies when the cost is in a header: real spend, then tokens, from one request |
|
|
1001
|
+
| `example-gateway-bedrock` | [`gateway/bedrock.ts`](./examples/gateway/bedrock.ts) | A gateway that states no price: tokens are all there is to meter, and nothing proxies it |
|
|
910
1002
|
| `example-setup` | [`shared/setup-governance.ts`](./examples/shared/setup-governance.ts) | Seeds a customer, a pool and per-entity budgets. Idempotent |
|
|
911
1003
|
|
|
912
1004
|
`example-client` is an alias for the Anthropic one. Prefix any of them with `AGENT_SDK_OFFLINE=1`
|
|
@@ -1059,7 +1151,7 @@ AGENT_SDK_OFFLINE=1 AGENT_SDK_DEMO_MOCK_GOVERNANCE_FAIL=1 \
|
|
|
1059
1151
|
|
|
1060
1152
|
# The explicit opt-in - refuse the call when the check itself could not be made. An SDK option,
|
|
1061
1153
|
# not an env var, and overridable per call through `stiggContext`:
|
|
1062
|
-
# new StiggAgentSDK(apiKey, baseUrl,
|
|
1154
|
+
# new StiggAgentSDK({ apiKey, baseUrl, checkErrorAction: 'deny' })
|
|
1063
1155
|
```
|
|
1064
1156
|
|
|
1065
1157
|
`allow` is the default: a soft limit, so an unreachable Stigg does not take the application down
|
|
@@ -1092,7 +1184,7 @@ them first.
|
|
|
1092
1184
|
| `403 GovernanceNotEnabled` | The account has no governance. Checks and usage still work; `register()` throws, because there is nowhere to create the agent entity. Contact Stigg to enable it. |
|
|
1093
1185
|
| `No STIGG_API_KEY` | Set it to a server key for the environment in `STIGG_BASE_URL`, or leave it blank to run on the built-in mock. |
|
|
1094
1186
|
| `503` and every turn denied | The governance cache is unavailable and `checkErrorAction` is `deny`. The default is `allow`, so this is a policy that was asked for; drop it to keep running through the outage, or relax a single call with `stiggContext: { checkErrorAction: 'allow' }`. |
|
|
1095
|
-
| `usage value must be a whole number`
|
|
1187
|
+
| `usage value must be a whole number to report against a feature` | `POST /api/v1/usage` takes integers. Report against a **credit currency** instead, which takes fractions, or raise the unit size in your `usageMapper` so the division comes out whole. |
|
|
1096
1188
|
| `... is backed by a raw-events meter` | A `sync` run reported to a feature only a meter can fill. Report that run with `ingest: 'async'` (the default), or point it at a feature whose value your application computes. |
|
|
1097
1189
|
| Ingest reports `ok=true` but usage stays 0 | Under `async` an acknowledgement is all the event API returns. Check a raw-events meter filters on the event name (see the event table above); against a **local** Stigg, also that the `ingest-usage-stream-processor` and `calculate-usage-stream-processor` lambdas are running. `ingest: 'sync'` answers with a balance and will say so instead. |
|
|
1098
1190
|
| Still denied right after a top-up | Budgets reset per cadence window, so a spent agent stays spent. Re-run `example-setup --agent-limit N` with a higher limit, or use a new `AGENT_SDK_DEMO_AGENT_ID`. |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BEDROCK_METRICS } from '../../../shared/token-usage';
|
|
2
|
+
import type { BedrockRecipeOptions } from './types';
|
|
3
|
+
import type { ClientRecipe, MetricsFor, MetricsOf } from '../../core/types';
|
|
4
|
+
type BedrockMetric = (typeof BEDROCK_METRICS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* One recipe for every model Bedrock serves.
|
|
7
|
+
*
|
|
8
|
+
* Converse normalizes Anthropic, Nova, Llama and the rest to one request and one response, so the
|
|
9
|
+
* usage block never changes shape - which is the whole reason this can be a single recipe rather
|
|
10
|
+
* than one per upstream vendor.
|
|
11
|
+
*/
|
|
12
|
+
export declare function bedrockRecipe(options?: BedrockRecipeOptions): ClientRecipe<BedrockMetric>;
|
|
13
|
+
/** One of the metric names Bedrock reports. */
|
|
14
|
+
export type BedrockMetricName = MetricsOf<ReturnType<typeof bedrockRecipe>>;
|
|
15
|
+
/** What a mapper reads off a Bedrock call: those names, each possibly absent. */
|
|
16
|
+
export type BedrockMetrics = MetricsFor<ReturnType<typeof bedrockRecipe>>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bedrockRecipe = bedrockRecipe;
|
|
4
|
+
/**
|
|
5
|
+
* The Bedrock recipe - a gateway whose client is a command bus, and which never states a price.
|
|
6
|
+
*
|
|
7
|
+
* Bedrock is the third shape of "gateway" these bindings cover, and it differs from the other two
|
|
8
|
+
* in both halves:
|
|
9
|
+
*
|
|
10
|
+
* - **The client is not a method tree.** `openai-node` and `@openrouter/sdk` hang a method off a
|
|
11
|
+
* namespace - `chat.completions.create`, `chat.send` - and a recipe names that path. The AWS
|
|
12
|
+
* SDK has exactly one method, `send`, and the operation is the *argument*: `send(new
|
|
13
|
+
* ConverseCommand(...))`. So the gated path is `['send']` and the recipe has to look at what
|
|
14
|
+
* it was handed, because the same method also lists async invocations and fetches guardrails,
|
|
15
|
+
* and gating those would charge a budget for calls that spend nothing.
|
|
16
|
+
*
|
|
17
|
+
* - **There is no cost on the response.** OpenRouter writes the credits it charged into the
|
|
18
|
+
* body; LiteLLM prices every call and puts dollars in a header. Bedrock reports tokens and
|
|
19
|
+
* stops - the price lives in the AWS bill, not in the response - so a run that wants to bill
|
|
20
|
+
* real spend cannot do it here, and one that wants to meter tokens is served exactly as well
|
|
21
|
+
* as anywhere else. `metrics` says so by having no `cost` in it, rather than by reporting a
|
|
22
|
+
* zero that a mapper would happily multiply.
|
|
23
|
+
*
|
|
24
|
+
* There is no served-model dimension either, and that absence is also a fact about Bedrock rather
|
|
25
|
+
* than an omission: it does not route. You name a model id and that model answers, so there is no
|
|
26
|
+
* drift between what was asked for and what served, and nothing for a dimension to tell you.
|
|
27
|
+
*/
|
|
28
|
+
const meter_stream_1 = require("../../../shared/meter-stream");
|
|
29
|
+
const token_usage_1 = require("../../../shared/token-usage");
|
|
30
|
+
/** The service a command belongs to, as the AWS SDK's own schema spells it. */
|
|
31
|
+
const BEDROCK_SERVICE = 'com.amazonaws.bedrockruntime';
|
|
32
|
+
/**
|
|
33
|
+
* The two operations that run inference through the Converse API, and so the two worth gating.
|
|
34
|
+
*
|
|
35
|
+
* `InvokeModel` is inference too, and is deliberately not here: its request and response bodies
|
|
36
|
+
* are the *upstream vendor's* - Anthropic's shape for a Claude model, Amazon's for a Nova one - so
|
|
37
|
+
* one reader cannot meter them, and a recipe that gated the call while measuring nothing would
|
|
38
|
+
* report every one of them as a metering failure. An application on `InvokeModel` wants a recipe
|
|
39
|
+
* per upstream; an application on Converse gets one that works for every model Bedrock serves.
|
|
40
|
+
*/
|
|
41
|
+
const METERED_OPERATIONS = new Set(['Converse', 'ConverseStream']);
|
|
42
|
+
/**
|
|
43
|
+
* Which AWS operation a command object is for.
|
|
44
|
+
*
|
|
45
|
+
* Read off the SDK's own serialization schema - `[version, service, operation, http]` - rather
|
|
46
|
+
* than from the class name, because a class name is the first thing a bundler rewrites and this
|
|
47
|
+
* decides whether a call is gated at all. The constructor name is the fallback for an SDK version
|
|
48
|
+
* that shapes the schema differently, where being approximately right beats refusing to gate.
|
|
49
|
+
*/
|
|
50
|
+
function operationOf(command) {
|
|
51
|
+
const schema = command?.schema;
|
|
52
|
+
if (Array.isArray(schema) && typeof schema[1] === 'string' && typeof schema[2] === 'string') {
|
|
53
|
+
return { service: schema[1], operation: schema[2] };
|
|
54
|
+
}
|
|
55
|
+
const name = command?.constructor?.name;
|
|
56
|
+
return typeof name === 'string' && name.endsWith('Command') ? { operation: name.slice(0, -'Command'.length) } : {};
|
|
57
|
+
}
|
|
58
|
+
/** True for a call this recipe must leave alone - anything that is not Converse inference. */
|
|
59
|
+
function isNotMetered(command) {
|
|
60
|
+
const { service, operation } = operationOf(command);
|
|
61
|
+
if (service !== undefined && service !== BEDROCK_SERVICE)
|
|
62
|
+
return true;
|
|
63
|
+
return !operation || !METERED_OPERATIONS.has(operation);
|
|
64
|
+
}
|
|
65
|
+
/** Tokens from one Converse response, under Bedrock's own names. */
|
|
66
|
+
function measure(usage, requestId) {
|
|
67
|
+
if (!usage)
|
|
68
|
+
return undefined;
|
|
69
|
+
return {
|
|
70
|
+
metrics: (0, token_usage_1.pickMetrics)(usage, token_usage_1.BEDROCK_PATHS),
|
|
71
|
+
usage: (0, token_usage_1.fromBedrockUsage)(usage),
|
|
72
|
+
raw: usage,
|
|
73
|
+
// AWS's own request id, which is stable across the SDK's retries - and the SDK retries by
|
|
74
|
+
// default, so a local counter would bill the same answer more than once.
|
|
75
|
+
idempotencyKey: requestId,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One recipe for every model Bedrock serves.
|
|
80
|
+
*
|
|
81
|
+
* Converse normalizes Anthropic, Nova, Llama and the rest to one request and one response, so the
|
|
82
|
+
* usage block never changes shape - which is the whole reason this can be a single recipe rather
|
|
83
|
+
* than one per upstream vendor.
|
|
84
|
+
*/
|
|
85
|
+
function bedrockRecipe(options = {}) {
|
|
86
|
+
const estimate = options.estimate ?? (() => 1);
|
|
87
|
+
return {
|
|
88
|
+
vendor: 'bedrock',
|
|
89
|
+
metrics: token_usage_1.BEDROCK_METRICS,
|
|
90
|
+
totalTokensMetric: 'totalTokens',
|
|
91
|
+
// The client's own configuration names the service, which is what separates this from every
|
|
92
|
+
// other AWS v3 client - they all have `send`, and none of the rest are inference. Data rather
|
|
93
|
+
// than a class name, for the same reason `operationOf` prefers the schema.
|
|
94
|
+
matches: (client) => {
|
|
95
|
+
const config = client?.config;
|
|
96
|
+
if (!config || typeof client?.send !== 'function')
|
|
97
|
+
return false;
|
|
98
|
+
return config.defaultSigningName === 'bedrock' && typeof config.serviceId === 'string';
|
|
99
|
+
},
|
|
100
|
+
methods: [
|
|
101
|
+
{
|
|
102
|
+
path: ['send'],
|
|
103
|
+
estimate,
|
|
104
|
+
// The operation rides in the argument, so this is where a non-inference call is let
|
|
105
|
+
// through ungated rather than charged for.
|
|
106
|
+
passthrough: (command) => isNotMetered(command),
|
|
107
|
+
// A streamed Converse resolves to `{ stream, $metadata }` - the usage is not on the
|
|
108
|
+
// response at all, so this returns nothing and `attachStream` takes it from there.
|
|
109
|
+
extractUsage: (result) => result?.stream ? undefined : measure(result?.usage, result?.$metadata?.requestId),
|
|
110
|
+
/**
|
|
111
|
+
* ConverseStream puts its usage in a `metadata` event at the end, so the stream has to be
|
|
112
|
+
* run to the end to know what the call cost - the same bargain every streaming binding
|
|
113
|
+
* makes.
|
|
114
|
+
*
|
|
115
|
+
* The iterable is `result.stream`, not `result`: AWS hands back an envelope with the
|
|
116
|
+
* stream hanging off it, where the other SDKs hand back the stream itself. Metering the
|
|
117
|
+
* envelope would patch an object nobody iterates and report nothing, with no error.
|
|
118
|
+
*/
|
|
119
|
+
attachStream: (result, ingest) => {
|
|
120
|
+
const requestId = result?.$metadata?.requestId;
|
|
121
|
+
let usage;
|
|
122
|
+
(0, meter_stream_1.meterStream)(result?.stream, (event) => {
|
|
123
|
+
if (event?.metadata?.usage)
|
|
124
|
+
usage = event.metadata.usage;
|
|
125
|
+
}, () => {
|
|
126
|
+
const measurement = measure(usage, requestId);
|
|
127
|
+
if (measurement)
|
|
128
|
+
ingest(measurement);
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type BedrockRecipeOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* How much to ask permission for before the call.
|
|
4
|
+
*
|
|
5
|
+
* The default asks for 1 - "is there any budget left at all?" - and lets the ingest afterwards
|
|
6
|
+
* settle the balance, which leaves a gap of about one call's worth of tokens. A run that wants
|
|
7
|
+
* that gap closed knows something this recipe cannot: `inferenceConfig.maxTokens` is the only
|
|
8
|
+
* ceiling Bedrock is given, and whether it is worth asking for depends on how a budget is
|
|
9
|
+
* denominated. Reading it is one line - `(command) => command?.input?.inferenceConfig?.maxTokens
|
|
10
|
+
* ?? 1` - and it is the caller's line to write.
|
|
11
|
+
*/
|
|
12
|
+
estimate?: (command: any) => number;
|
|
13
|
+
};
|
|
@@ -6,6 +6,11 @@ type GatewayMetric = (typeof GATEWAY_METRICS)[number];
|
|
|
6
6
|
/**
|
|
7
7
|
* One recipe covers every upstream. OpenRouter normalizes Anthropic, OpenAI and the rest to the
|
|
8
8
|
* same wire shape, so the response never changes form - `model` is just a field saying who served.
|
|
9
|
+
*
|
|
10
|
+
* It also covers both ways of reaching OpenRouter from Node: the OpenAI SDK pointed at the
|
|
11
|
+
* gateway's base URL, and OpenRouter's own `@openrouter/sdk`. Those are different objects with
|
|
12
|
+
* different method names reporting the same call, and a recipe that governed only the first would
|
|
13
|
+
* meter nothing at all for an application that took OpenRouter's own documented advice.
|
|
9
14
|
*/
|
|
10
15
|
export declare function openrouterRecipe(options?: OpenRouterRecipeOptions): ClientRecipe<GatewayMetric>;
|
|
11
16
|
/** One of the metric names OpenRouter reports. */
|