@usagetap/sdk 1.3.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +372 -39
  2. package/dist/adapters/anthropic.cjs +995 -69
  3. package/dist/adapters/anthropic.cjs.map +1 -1
  4. package/dist/adapters/anthropic.d.cts +45 -3
  5. package/dist/adapters/anthropic.d.ts +45 -3
  6. package/dist/adapters/anthropic.mjs +995 -70
  7. package/dist/adapters/anthropic.mjs.map +1 -1
  8. package/dist/adapters/openai.cjs +1208 -106
  9. package/dist/adapters/openai.cjs.map +1 -1
  10. package/dist/adapters/openai.d.cts +46 -3
  11. package/dist/adapters/openai.d.ts +46 -3
  12. package/dist/adapters/openai.mjs +1208 -107
  13. package/dist/adapters/openai.mjs.map +1 -1
  14. package/dist/adapters/openrouter.cjs +3912 -53
  15. package/dist/adapters/openrouter.cjs.map +1 -1
  16. package/dist/adapters/openrouter.d.cts +6 -3
  17. package/dist/adapters/openrouter.d.ts +6 -3
  18. package/dist/adapters/openrouter.mjs +3910 -54
  19. package/dist/adapters/openrouter.mjs.map +1 -1
  20. package/dist/anthropic/index.cjs +995 -69
  21. package/dist/anthropic/index.cjs.map +1 -1
  22. package/dist/anthropic/index.d.cts +2 -2
  23. package/dist/anthropic/index.d.ts +2 -2
  24. package/dist/anthropic/index.mjs +995 -70
  25. package/dist/anthropic/index.mjs.map +1 -1
  26. package/dist/client-C0UiaqVB.d.cts +1305 -0
  27. package/dist/client-C0UiaqVB.d.ts +1305 -0
  28. package/dist/express/index.cjs +399 -64
  29. package/dist/express/index.cjs.map +1 -1
  30. package/dist/express/index.d.cts +2 -2
  31. package/dist/express/index.d.ts +2 -2
  32. package/dist/express/index.mjs +399 -64
  33. package/dist/express/index.mjs.map +1 -1
  34. package/dist/index.cjs +1044 -163
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +16 -5
  37. package/dist/index.d.ts +16 -5
  38. package/dist/index.mjs +1044 -163
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/openai/index.cjs +1209 -107
  41. package/dist/openai/index.cjs.map +1 -1
  42. package/dist/openai/index.d.cts +2 -2
  43. package/dist/openai/index.d.ts +2 -2
  44. package/dist/openai/index.mjs +1209 -108
  45. package/dist/openai/index.mjs.map +1 -1
  46. package/dist/openrouter/index.cjs +1226 -109
  47. package/dist/openrouter/index.cjs.map +1 -1
  48. package/dist/openrouter/index.d.cts +3 -3
  49. package/dist/openrouter/index.d.ts +3 -3
  50. package/dist/openrouter/index.mjs +1224 -108
  51. package/dist/openrouter/index.mjs.map +1 -1
  52. package/dist/react/index.cjs +19 -1
  53. package/dist/react/index.cjs.map +1 -1
  54. package/dist/react/index.d.cts +17 -4
  55. package/dist/react/index.d.ts +17 -4
  56. package/dist/react/index.mjs +19 -1
  57. package/dist/react/index.mjs.map +1 -1
  58. package/package.json +2 -2
  59. package/dist/client-BD8O2J8Z.d.cts +0 -668
  60. package/dist/client-BD8O2J8Z.d.ts +0 -668
package/README.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  Server-only JavaScript/TypeScript client for UsageTap. The SDK helps you instrument `call_begin → vendor call → call_end` flows with built-in retries, idempotency helpers, and vendor adapters.
4
4
 
5
+ For API requests and MCP tool calls, use the dedicated call meter without model or token fields:
6
+
7
+ ```ts
8
+ await client.incrementCustomMeter(
9
+ { customerId, meterSlot: "AGENTIC_API", amount: 1, feature: "mcp.tools.call" },
10
+ { idempotencyKey: `mcp:${requestId}` },
11
+ );
12
+ ```
13
+
14
+ Configure its included allocation and single-rate or graduated overage pricing in **More > Agentic & API** on the usage plan. It uses the same reporting, credit funding, and asynchronous PAYG settlement as custom meters. This does not reserve credit or impose a rolling rate limit. See [Agentic/API integration and limits](../../docs/agentic-api-meter.md).
15
+
5
16
  ## Module formats
6
17
 
7
18
  `@usagetap/sdk` ships real dual ESM (`.mjs`) and CommonJS (`.cjs`) entrypoints. In ESM projects use `import { UsageTapClient } from "@usagetap/sdk";`. For CommonJS runtimes (including VS Code extensions) rely on `const { UsageTapClient } = require("@usagetap/sdk");`.
@@ -9,7 +20,7 @@ Server-only JavaScript/TypeScript client for UsageTap. The SDK helps you instrum
9
20
  Optional adapters live behind subpath exports so their peer dependencies stay out of the core bundle:
10
21
 
11
22
  - `@usagetap/sdk/openai` – OpenAI/OpenRouter helpers (`wrapOpenAI`, `streamOpenAIRoute`, etc.)
12
- - `@usagetap/sdk/anthropic` – Anthropic helper (`wrapAnthropic`)
23
+ - `@usagetap/sdk/anthropic` – Anthropic helpers (`withMetering`, `wrapAnthropic`)
13
24
  - `@usagetap/sdk/openrouter` – discoverable OpenRouter aliases for the OpenAI-compatible wrappers
14
25
  - `@usagetap/sdk/express` – Express middleware
15
26
  - `@usagetap/sdk/react` – React chat hook
@@ -33,7 +44,7 @@ import { withMetering } from "@usagetap/sdk/openai";
33
44
 
34
45
  const openai = withMetering(new OpenAI(), "cust_123");
35
46
  const completion = await openai.responses.create({
36
- model: "gpt-5.5-mini",
47
+ model: "gpt-5.6-luna",
37
48
  input: "Draft a welcome email for our Pro plan",
38
49
  });
39
50
 
@@ -52,12 +63,9 @@ calls:
52
63
  import OpenAI from "openai";
53
64
  import { withCompression } from "@usagetap/sdk/openai";
54
65
 
55
- const openai = withCompression(new OpenAI(), {
56
- // Defaults to 1,000. Use 0 to always attempt compression.
57
- minContextTokens: 2_000,
58
- });
66
+ const openai = withCompression(new OpenAI());
59
67
  const completion = await openai.responses.create({
60
- model: "gpt-5.5-mini",
68
+ model: "gpt-5.6-luna",
61
69
  input: longPrompt,
62
70
  });
63
71
  ```
@@ -66,12 +74,32 @@ The same `withMetering` and `withCompression` APIs are exported from
66
74
  `@usagetap/sdk/anthropic` and `@usagetap/sdk/openrouter`. Remove the wrapper or
67
75
  call `.unwrap()` to recover the original provider client.
68
76
 
69
- `withCompression` uses a fast token estimate over the combined request context
70
- and skips the compression step below 1,000 estimated tokens by default. This
71
- avoids an extra network round trip when likely savings are small. Override the
72
- cutoff with `minContextTokens`; set it to `0` to always attempt compression.
77
+ `withCompression` compresses user messages only, uses a fast token estimate
78
+ over the combined request context, and skips the compression step below 1,000
79
+ estimated tokens by default. System instructions, tool content, and assistant
80
+ messages remain unchanged. Override roles or the `minContextTokens` cutoff only
81
+ when you need more control; set the cutoff to `0` to always attempt compression.
73
82
  The separate `minTokens` option remains a per-text-segment cutoff.
74
83
 
84
+ When workload evidence calls for tuning, the wrapper also accepts the hosted
85
+ Messages API controls directly:
86
+
87
+ ```ts
88
+ const openai = withCompression(new OpenAI(), {
89
+ mode: "model_auto", // or "model_force" / "deterministic"
90
+ roles: {
91
+ user: { aggressiveness: 0.2 },
92
+ system: { aggressiveness: 0.1 },
93
+ },
94
+ latencyBudgetMs: 1_000,
95
+ compactEmptyUserMessages: false,
96
+ compactDuplicateUserTextParts: false,
97
+ failOpen: true,
98
+ });
99
+ ```
100
+
101
+ Omitting the options object remains the recommended starting point.
102
+
75
103
  Wrappers compose. Put metering outside compression so the metered operation
76
104
  includes compression and the provider call:
77
105
 
@@ -85,6 +113,161 @@ const openai = withMetering(
85
113
  Each `.unwrap()` removes one layer. Do not also set `promptCompression: true` on
86
114
  `withMetering` when using a separate `withCompression` layer.
87
115
 
116
+ ### UsageTap Gateway
117
+
118
+ The core client can call the OpenAI-compatible UsageTap Gateway without a
119
+ second SDK. Use a `utk-` key with `gateway:invoke`. Add
120
+ `compression:invoke` if the same workflow also requests hosted Compression.
121
+ Existing `gk-` and compatible `cmp-` keys continue to work:
122
+
123
+ ```ts
124
+ import { UsageTap } from "@usagetap/sdk";
125
+
126
+ const usageTap = new UsageTap();
127
+ const completion = await usageTap.gateway.chat.completions.create({
128
+ model: "usagetap/standard",
129
+ customerId: "cust_123",
130
+ feature: "chat.reply",
131
+ messages: [{ role: "user", content: "Summarize this account." }],
132
+ });
133
+
134
+ console.log(completion.choices[0].message?.content);
135
+ ```
136
+
137
+ The typed Responses resource supports portable built-in web search, structured
138
+ output, verbosity, prompt caching, and Gateway compression. It is buffered in
139
+ this release, so omit `stream` or set it to `false`:
140
+
141
+ ```ts
142
+ const response = await usageTap.gateway.responses.create({
143
+ model: "usagetap/standard",
144
+ customerId: "cust_123",
145
+ feature: "answer.with.sources",
146
+ instructions: stablePolicy,
147
+ input: "Find the current answer and return it as JSON.",
148
+ tools: [{ type: "web_search" }],
149
+ text: {
150
+ verbosity: "low",
151
+ format: {
152
+ type: "json_schema",
153
+ name: "sourced_answer",
154
+ strict: true,
155
+ schema: {
156
+ type: "object",
157
+ properties: { answer: { type: "string" } },
158
+ required: ["answer"],
159
+ additionalProperties: false,
160
+ },
161
+ },
162
+ },
163
+ prompt_cache_key: "answer-policy-v1",
164
+ prompt_cache_options: { mode: "implicit" },
165
+ compress: true,
166
+ });
167
+
168
+ console.log(response.output_text);
169
+ console.log(response.usage?.input_tokens_details?.cached_tokens);
170
+ ```
171
+
172
+ The same resource exposes `models.list()` and the complete native batch
173
+ lifecycle. Batch creation generates the required idempotency key unless one is
174
+ provided:
175
+
176
+ ```ts
177
+ const submitted = await usageTap.gateway.batches.create({
178
+ requests: reports.map((report) => ({
179
+ custom_id: report.id,
180
+ body: {
181
+ model: "usagetap/standard",
182
+ customerId: report.customerId,
183
+ messages: [{ role: "user", content: report.prompt }],
184
+ },
185
+ })),
186
+ });
187
+
188
+ const batch = await usageTap.gateway.batches.wait(submitted);
189
+ if (batch.status !== "completed") {
190
+ throw new Error(`Batch ended with status: ${batch.status}`);
191
+ }
192
+
193
+ // Parses the Gateway's NDJSON result stream into typed objects.
194
+ const results = await usageTap.gateway.batches.results(batch.id);
195
+ ```
196
+
197
+ Use `gateway.batches.retrieve()`, `cancel()`, and `results()` when you want to
198
+ manage polling yourself. Set `gatewayBaseUrl` or `USAGETAP_GATEWAY_URL` for a
199
+ non-default deployment.
200
+
201
+ ### Context summarization
202
+
203
+ Published context-summarization profiles are available through
204
+ `usageTap.summarization`. A managed single summary can wait for completion in
205
+ the initial request:
206
+
207
+ ```ts
208
+ const summary = await usageTap.summarization.summaries.create({
209
+ profile: "weekly-account-summary-abcd5678",
210
+ wait: true,
211
+ context: {
212
+ id: "account-123",
213
+ type: "account_history",
214
+ content: accountHistory,
215
+ },
216
+ });
217
+
218
+ console.log(summary.result);
219
+ ```
220
+
221
+ Batch submissions and polling use the same resource pattern as Gateway
222
+ batches:
223
+
224
+ ```ts
225
+ const submitted = await usageTap.summarization.batches.create({
226
+ profile: "weekly-account-summary-abcd5678",
227
+ items: accounts.map((account) => ({
228
+ id: account.id,
229
+ type: "account_history",
230
+ content: account.history,
231
+ })),
232
+ });
233
+
234
+ const batch = await usageTap.summarization.batches.wait(submitted);
235
+ ```
236
+
237
+ Self-managed workflows can use `summarization.profiles.retrieve()` to load the
238
+ published prompt and model settings, then
239
+ `summarization.measurements.create()` to report source and summary token
240
+ counts.
241
+
242
+ ### Runaway circuit breaker
243
+
244
+ Set a local per-run call cap and pass the same `runId` on every model call in a
245
+ workflow. Once the cap is reached, the SDK throws `USAGETAP_CIRCUIT_OPEN` before
246
+ `call_begin` or the paid provider request can start:
247
+
248
+ ```ts
249
+ import { UsageTapClient } from "@usagetap/sdk";
250
+
251
+ const usageTap = new UsageTapClient({
252
+ circuitBreaker: { maxCallsPerRun: 20 },
253
+ });
254
+ const run = { customerId: "cust_123", runId: crypto.randomUUID() };
255
+
256
+ try {
257
+ for (;;) {
258
+ const result = await usageTap.meter(run, async () => callModel());
259
+ if (result.done) break;
260
+ }
261
+ } finally {
262
+ usageTap.resetRun(run);
263
+ }
264
+ ```
265
+
266
+ `canRunContinue(run)` returns the current decision for graceful partial-result
267
+ handling. Idempotent retries do not consume another slot. The guard is
268
+ process-local by design, so use a stable `runId` in each SDK process and keep
269
+ account-level UsageTap limits enabled for distributed enforcement.
270
+
88
271
  For advanced entitlement control, `wrapOpenAI` exposes the full UsageTap context
89
272
  and applies entitlement-aware defaults when you omit `model`.
90
273
 
@@ -105,6 +288,25 @@ const ai = wrapOpenAI(openai, usageTap, {
105
288
  });
106
289
  ```
107
290
 
291
+ ### Optional end-user attribution
292
+
293
+ `customerId` identifies the customer account. When that account has multiple
294
+ users, add `customerUserId` to attribute the call to the responsible end user
295
+ in Live activity. Prefer a stable, non-PII application user ID.
296
+ `customerUserName` and `customerUserEmail` are optional display metadata. The
297
+ SDK does not infer them, and existing integrations remain valid when these
298
+ fields are omitted.
299
+
300
+ ```ts
301
+ const context = {
302
+ customerId: currentCustomer.id,
303
+ customerUserId: currentUser.id, // Optional, recommended when available
304
+ customerUserName: currentUser.name, // Optional display metadata
305
+ customerUserEmail: currentUser.email, // Optional display metadata
306
+ feature: "chat.send",
307
+ };
308
+ ```
309
+
108
310
  ### Prompt compression
109
311
 
110
312
  Prompt compression is an explicit step after `call_begin`. `beginCall` only starts the call and returns the `callId`; `promptCompress` compresses locally, records savings metadata against that call, and returns the compressed prompt for your vendor request. Raw prompt content is not sent to UsageTap.
@@ -114,6 +316,9 @@ import { protectPromptText } from "@usagetap/sdk";
114
316
 
115
317
  const begin = await usageTap.beginCall({
116
318
  customerId: "cust_123",
319
+ customerUserId: currentUser.id,
320
+ customerUserName: currentUser.name,
321
+ customerUserEmail: currentUser.email,
117
322
  feature: "chat.send",
118
323
  });
119
324
 
@@ -181,7 +386,7 @@ When using The Token Company, configure `tokenCompanyApiKey` on `UsageTapClient`
181
386
 
182
387
  For advanced custom flows, `compressPromptInput(input, options?)` returns compression results without recording telemetry, and `recordPromptCompression({ callId, promptCompression })` records precomputed savings metadata against a call.
183
388
 
184
- > **Heads up:** `UsageTapClient` always negotiates the canonical UsageTap media type by sending `Accept: application/vnd.usagetap.v1+json`. Every response now uses the `{ result, data, correlationId }` envelope exclusively and the begin payload includes `data.idempotency.key` (always matching `callId`), per-meter snapshots, and subscription metadata. Set `autoIdempotency: false` (or pass your own `idempotency`) to skip the SDK's auto-generated key and rely on the server's deterministic fallback when retriable semantics are acceptable.
389
+ > **Heads up:** `UsageTapClient` always negotiates the canonical UsageTap media type by sending `Accept: application/vnd.usagetap.v1+json`. Every response uses the `{ result, data, correlationId }` envelope and the begin payload includes `data.idempotency.key` (matching `callId`), per-meter snapshots, and subscription metadata. Keep `autoIdempotency` enabled unless you provide a unique key yourself. The server fallback is deterministic, so identical inputs can replay an earlier call.
185
390
 
186
391
  ### Streaming helpers
187
392
 
@@ -207,7 +412,32 @@ export async function POST() {
207
412
  }
208
413
  ```
209
414
 
210
- `wrapOpenAI` inspects `begin.data.vendorHints.preferredModel`: premium entitlements resolve to `gpt5`, otherwise the wrapper falls back to `gpt5-mini`. Use the manual pattern shown earlier when you need to toggle reasoning effort or attach search tools based on the returned allowances.
415
+ `wrapOpenAI` preserves the model already supplied by the application. It does not invent a model mapping or fallback. Use the manual `withUsage` pattern when BLOCK or DOWNGRADE must control provider invocation, model selection, reasoning effort, or search tools.
416
+
417
+ Starting in `@usagetap/sdk` 1.6.0, the Anthropic adapter preserves both
418
+ streaming forms. Use
419
+ `messages.create({ stream: true })` for the lower-level async iterable, or use
420
+ the synchronous `messages.stream()` helper when the application relies on
421
+ events and final-message accumulation:
422
+
423
+ ```ts
424
+ const stream = anthropic.messages
425
+ .stream({
426
+ model: "claude-sonnet-4-5",
427
+ max_tokens: 512,
428
+ messages: [{ role: "user", content: "Stream it" }],
429
+ })
430
+ .on("text", (text) => process.stdout.write(text));
431
+
432
+ const message = await stream.finalMessage();
433
+ ```
434
+
435
+ `messages.stream()` still returns Anthropic's `MessageStream` immediately and
436
+ retains `.on()`, async iteration, `.abort()`, `.done()`, `.finalText()`, and
437
+ `.finalMessage()`. UsageTap begins metering before the provider request and
438
+ finalizes once when the underlying stream completes, fails, or is aborted.
439
+ Wrapped `messages.create()` results also retain Anthropic's `.withResponse()`
440
+ and `.asResponse()` helpers.
211
441
 
212
442
  ### Overriding usage context per request
213
443
 
@@ -218,7 +448,8 @@ await ai.chat.completions.create(
218
448
  { messages },
219
449
  {
220
450
  usageTap: {
221
- customerId: currentUser.id,
451
+ customerId: currentCustomer.id,
452
+ customerUserId: currentUser.id, // Optional
222
453
  feature: "chat.assist",
223
454
  tags: ["beta"],
224
455
  requested: { standard: true, premium: true, search: true, reasoningLevel: "HIGH" },
@@ -227,13 +458,18 @@ await ai.chat.completions.create(
227
458
  );
228
459
  ```
229
460
 
230
- The begin response for that call will promote premium plans to `gpt5`, fall back to `gpt5-mini` otherwise, and cap reasoning to the granted tier.
461
+ The begin response returns the granted entitlements. The application must use those fields to select only a model and capabilities it has explicitly configured.
231
462
 
232
463
  For streaming calls created with `{ stream: true }`, UsageTap automatically calculates usage from the final OpenAI response (or falls back to estimates when available). The wrapped stream retains OpenAI-specific helpers like `finalChatCompletion()`.
233
464
 
234
465
  ### responses.create support
235
466
 
236
- The wrapper also instruments `openai.responses.create`, applying vendor hints (preferred models, token limits) and collecting usage data the same way as chat completions.
467
+ The wrapper also instruments `openai.responses.create`, preserves text controls,
468
+ tools, prompt-cache controls and breakpoints through compression, and records
469
+ cache reads, cache writes, reasoning tokens, and completed web-search calls.
470
+ Install OpenAI SDK 7.5 or newer when you want its native TypeScript definitions
471
+ for the latest Responses fields; the UsageTap Gateway resource has its own types
472
+ and does not require the OpenAI package.
237
473
 
238
474
  ### OpenRouter support
239
475
 
@@ -264,6 +500,7 @@ const ai = wrapOpenAI(openrouter, usageTap, {
264
500
 
265
501
  const completion = await ai.chat.completions.create(
266
502
  {
503
+ model: "your-existing-openrouter-model",
267
504
  messages: [{ role: "user", content: "Hello from OpenRouter!" }],
268
505
  },
269
506
  {
@@ -274,7 +511,7 @@ const completion = await ai.chat.completions.create(
274
511
  );
275
512
  ```
276
513
 
277
- `begin.data.models` will surface the OpenRouter-specific identifiers the customer can use (for example, `standard` ⇒ `gpt5-mini`, `premium` ⇒ `gpt5`). Since `wrapOpenAI` honors those hints, you can omit `model` and let UsageTap keep the request aligned with the active entitlement.
514
+ `begin.data.models` may surface organization-configured model shortlists. Treat them as guidance; keep the application's existing model or select an explicitly approved fallback after checking `allowed`.
278
515
 
279
516
  ### Express middleware
280
517
 
@@ -319,7 +556,7 @@ app.post("/api/chat", async (req, res) => {
319
556
  });
320
557
  ```
321
558
 
322
- With that context in place, premium calls receive `gpt5` and everyone else falls back to `gpt5-mini`. To respect `allowed.reasoningLevel` or `allowed.search`, read the begin payload inside route handlers (see the manual `withUsage` example above) and shape the OpenAI request accordingly.
559
+ The middleware meters the supplied provider request. To enforce model tier, `allowed.reasoningLevel`, or `allowed.search`, read the begin payload inside route handlers (see the manual `withUsage` example above) and shape the provider request accordingly.
323
560
 
324
561
  ### React hook for chat UIs
325
562
 
@@ -328,10 +565,11 @@ Build chat interfaces with automatic UsageTap tracking:
328
565
  ```tsx
329
566
  import { useChatWithUsage } from "@usagetap/sdk/react";
330
567
 
331
- function ChatComponent({ userId }) {
568
+ function ChatComponent({ customerId, currentUser }) {
332
569
  const { messages, input, setInput, handleSubmit, isLoading } = useChatWithUsage({
333
570
  api: "/api/chat",
334
- customerId: userId,
571
+ customerId,
572
+ customerUserId: currentUser.id, // Optional hint; validate on the server
335
573
  feature: "chat.assistant",
336
574
  });
337
575
 
@@ -378,6 +616,11 @@ const wrappedFetch = wrapFetch(usageTap, {
378
616
  feature: "chat",
379
617
  requested: { standard: true, premium: true, search: true, reasoningLevel: "MEDIUM" },
380
618
  },
619
+ onMeteringError: ({ stage, callId, error }) => {
620
+ console.error("UsageTap metering failed", { stage, callId, error });
621
+ },
622
+ // Set true when an unmetered vendor response must fail the application call.
623
+ strictMetering: false,
381
624
  });
382
625
 
383
626
  const openai = new OpenAI({
@@ -395,7 +638,7 @@ const completion = await openai.chat.completions.create({
395
638
  });
396
639
  ```
397
640
 
398
- `wrapFetch` detects OpenAI API endpoints, handles streaming and non-streaming responses, and automatically extracts usage data. Persist the `begin.data.allowed` blob wherever you store session context so every downstream `openai` call can resolve to `gpt5` (premium) or `gpt5-mini` (standard). You can override context per-request using special headers:
641
+ `wrapFetch` detects OpenAI API endpoints, handles streaming and non-streaming responses, requests final usage for chat streams, and automatically extracts usage data. It preserves the application's model. If limits must control provider selection, use an explicit begin decision before this layer. Use `onMeteringError` to export request parsing, begin, or end failures after SDK retries are exhausted. Set `strictMetering` when those failures should reject the application call. You can override metering context per request using special headers; the wrapper consumes these headers and removes them before calling the provider:
399
642
 
400
643
  ```ts
401
644
  await openai.chat.completions.create(
@@ -463,6 +706,48 @@ const { begin, end, vendor, endUsage } = envelope.data;
463
706
  - Non-2xx vendor responses still trigger `call_end`; the envelope returns `CALL_VENDOR_WARNING` alongside vendor error metadata.
464
707
  - The canonical media type `application/vnd.usagetap.v1+json` is required; the SDK already sends this header automatically when you rely on `UsageTapClient`.
465
708
 
709
+ ### Retrieve finalized call pricing (API-only)
710
+
711
+ An external server can retrieve the stored result later with `GET
712
+ /calls/{callId}`. Use a server API key containing `usage:read`; do not expose
713
+ the key in browser code.
714
+
715
+ ```ts
716
+ const response = await fetch(
717
+ `${baseUrl}/calls/${encodeURIComponent(callId)}`,
718
+ {
719
+ method: "GET",
720
+ headers: {
721
+ Authorization: `Bearer ${process.env.USAGETAP_API_KEY}`,
722
+ Accept: "application/vnd.usagetap.v1+json",
723
+ },
724
+ },
725
+ );
726
+
727
+ const envelope = await response.json();
728
+ if (!response.ok || envelope.result.status !== "ACCEPTED") {
729
+ throw new Error(`UsageTap call lookup failed: ${envelope.result.code}`);
730
+ }
731
+ if (envelope.data.pricingStatus !== "FINAL") {
732
+ throw new Error(`Call pricing is ${envelope.data.pricingStatus}`);
733
+ }
734
+
735
+ const providerCostUsd = envelope.data.costUSD;
736
+ const customerChargeUsd = envelope.data.payg.chargedUsd;
737
+ const customerId = envelope.data.customerId;
738
+ const customerUserId = envelope.data.customerUserId; // string or null
739
+ ```
740
+
741
+ `pricingStatus` is `PENDING` while the call is open and `UNAVAILABLE` when a
742
+ completed call could not resolve model pricing. Only treat `costUSD` as final
743
+ when the status is `FINAL`.
744
+
745
+ The response includes only the stable customer identifiers recorded on the
746
+ call: `customerId` and nullable `customerUserId`. Use opaque, non-PII values
747
+ for both. Customer and end-user names or emails, billing-provider identifiers,
748
+ `organizationId`, and the internal `orgIdCustomerId` composite key are not
749
+ returned.
750
+
466
751
  ## Exports
467
752
 
468
753
  Key exports from `@usagetap/sdk`:
@@ -524,9 +809,9 @@ console.log("New subscription:", result.data.subscription);
524
809
  ```
525
810
 
526
811
  **Strategy options:**
527
- - `IMMEDIATE_RESET`: Switch plan immediately and reset all usage counters to zero
528
- - `IMMEDIATE_PRORATED`: Switch plan immediately and prorate existing usage against new limits
529
- - `AT_NEXT_REPLENISH`: Schedule the plan change for the next replenishment cycle (default)
812
+ - `IMMEDIATE_RESET`: Switch immediately, grant the target plan's full allowances, and start a new replenishment cycle (default when the SDK option is omitted)
813
+ - `IMMEDIATE_PRORATED`: Switch immediately, preserve usage already consumed, adjust remaining allowances to the target limits, and keep the current replenishment date
814
+ - `AT_NEXT_REPLENISH`: Keep the current plan until its existing scheduled replenishment, then switch
530
815
 
531
816
  The response includes the updated subscription details, including the new plan version, limits, and next replenishment timestamp. If `strategy: "AT_NEXT_REPLENISH"` is used, the `subscription.pending` field will indicate the scheduled plan change.
532
817
 
@@ -552,7 +837,8 @@ Custom meters allow you to track usage beyond standard LLM metrics—ideal for a
552
837
  ```ts
553
838
  const result = await usageTap.incrementCustomMeter({
554
839
  customerId: "cust_123",
555
- meterSlot: "CUSTOM1", // or "CUSTOM2"
840
+ customerUserId: currentUser.id,
841
+ meterSlot: "CUSTOM1", // or "CUSTOM2" or "AGENTIC_API"
556
842
  amount: 5,
557
843
  feature: "agent_actions",
558
844
  tags: ["workflow_automation"],
@@ -570,13 +856,15 @@ console.log("Blocked:", result.data.blocked);
570
856
  **Parameters:**
571
857
 
572
858
  - `customerId` (string, required): Customer identifier
573
- - `meterSlot` ("CUSTOM1" | "CUSTOM2", required): Which custom meter to increment
859
+ - `customerUserId` (string, optional): Stable identifier for the end user responsible for the event
860
+ - `customerUserName` and `customerUserEmail` (string, optional): Display fields for live activity
861
+ - `meterSlot` ("CUSTOM1" | "CUSTOM2" | "AGENTIC_API", required): Which meter to increment
574
862
  - `amount` (number, required): Positive number to decrement from quota
575
863
  - `feature` (string, optional): Feature identifier for tracking
576
864
  - `tags` (string[], optional): Tags for categorization
577
865
  - `metadata` (object, optional): Additional metadata
578
866
 
579
- The method returns the updated meter snapshot showing remaining quota, limits, and usage. If the customer's plan has `limitType: "BLOCK"` and quota is exceeded, a `UsageTapError` is thrown with code `USAGETAP_AUTH_ERROR`.
867
+ The method returns the updated meter snapshot showing remaining quota, limits, and usage. If usage exceeds the allocation and eligible overage funding, it returns `data.blocked: true`. The SDK does not throw for this accepted usage report; your application must enforce the flag.
580
868
 
581
869
  **Use cases:**
582
870
 
@@ -584,10 +872,11 @@ The method returns the updated meter snapshot showing remaining quota, limits, a
584
872
  // Track agent tool invocations
585
873
  await usageTap.incrementCustomMeter({
586
874
  customerId: "cust_123",
587
- meterSlot: "CUSTOM1",
875
+ meterSlot: "AGENTIC_API",
588
876
  amount: 1,
589
877
  feature: "agent.tool_call",
590
878
  tags: ["web_search"],
879
+ metadata: { kind: "mcp", serverName: "search", toolName: "web_search" },
591
880
  });
592
881
 
593
882
  // Track document processing (10 pages)
@@ -602,20 +891,26 @@ await usageTap.incrementCustomMeter({
602
891
  // Track external API calls
603
892
  await usageTap.incrementCustomMeter({
604
893
  customerId: "cust_789",
605
- meterSlot: "CUSTOM1",
894
+ meterSlot: "AGENTIC_API",
606
895
  amount: 1,
607
896
  feature: "external_api.maps",
608
897
  tags: ["geocoding"],
898
+ metadata: { kind: "api", method: "GET", endpoint: "/v1/geocode" },
609
899
  });
610
900
  ```
611
901
 
902
+ For Agentic & API Calls, use `metadata.toolName` (and optionally
903
+ `metadata.serverName`) for MCP operations, or `metadata.endpoint` and
904
+ `metadata.method` for HTTP APIs. Live View displays these values and removes URL
905
+ query strings from endpoint labels.
906
+
612
907
  **Important notes:**
613
908
 
614
909
  1. Custom meters must be enabled in the customer's usage plan
615
- 2. The `amount` decrements the remaining quota (like token usage)
616
- 3. With `BLOCK` policy, exceeding quota throws an error
617
- 4. With `DOWNGRADE` policy, usage continues but quota can go negative
618
- 5. Unlimited meters don't track usage but still record events for analytics
910
+ 2. Report a positive safe integer quantity. Usage consumes allocation, then eligible purchased meter credits and/or PAYG funding. The last included unit is allowed.
911
+ 3. Unfunded usage is recorded with `blocked: true`; the application must enforce that flag. LLM `BLOCK`/`DOWNGRADE` policies do not apply to meter reports.
912
+ 4. PAYG overages require enabled PAYG and a configured single or graduated price. Settlement is asynchronous and does not reserve dollar credit or impose a rolling rate limit.
913
+ 5. Unlimited meters still record events for analytics but do not consume allocation or charge overages.
619
914
 
620
915
  ## Response envelope (canonical only)
621
916
 
@@ -710,7 +1005,7 @@ UsageTap responds exclusively with the canonical `{ result, data, correlationId
710
1005
  - `meters` is a per-counter snapshot including remaining quotas, total limits, usage to date, and convenience ratios. `remainingRatios` mirrors the same information in a compact map for quick lookups.
711
1006
  - `subscription` contains the active plan identity, versioning, and upcoming replenishment timestamps so you can render customer-facing UI without querying Dynamo yourself.
712
1007
  - `models` surfaces per-organization vendor hints (e.g. standard vs. premium model shortlists).
713
- - `idempotency` reveals the actual key that was persisted (`callId` mirrors this value). When you omit `idempotency` in the request, the backend derives a deterministic hash from organization, customer, feature, and requested entitlements.
1008
+ - `idempotency` reveals the actual key that was persisted (`callId` mirrors this value). The SDK generates a unique key by default. With SDK auto-generation disabled, the backend derives a deterministic hash from organization, customer, feature, requested entitlements, call type, and pricing mode; identical inputs can replay an earlier call.
714
1009
  - `plan` and `balances` remain available alongside the core begin payload for backwards compatibility with earlier SDK versions.
715
1010
 
716
1011
  ### Example `call_end` success
@@ -733,7 +1028,7 @@ UsageTap responds exclusively with the canonical `{ result, data, correlationId
733
1028
  "reasoningTokens": 0
734
1029
  },
735
1030
  "metered": {
736
- "tokens": 768,
1031
+ "tokens": 288,
737
1032
  "calls": 1,
738
1033
  "searches": 1
739
1034
  },
@@ -767,9 +1062,22 @@ UsageTap responds exclusively with the canonical `{ result, data, correlationId
767
1062
  }
768
1063
  ```
769
1064
 
770
- Send `cachedInputTokens` when available so UsageTap can apply provider cache-read pricing correctly.
1065
+ Send `cachedInputTokens` and `cacheWriteInputTokens` when available so UsageTap
1066
+ can apply provider prompt-cache pricing correctly. `inputTokens` is always the
1067
+ total input count and includes both subsets. OpenAI's prompt-token total already
1068
+ includes cache reads. Anthropic reports ordinary input, cache reads, and cache
1069
+ writes separately, so the Anthropic wrapper adds the three counters for
1070
+ `inputTokens` while retaining both cache subsets.
771
1071
 
772
- `metered` is derived from the raw Dynamo deltas. Additional meters (audio seconds, reasoning tokens, balances) will populate in later phases without breaking the contract.
1072
+ `metered.tokens` is the provider-reported output-token count. Input tokens,
1073
+ including cache reads and cache writes, remain recorded and part of provider
1074
+ cost but do not consume the plan's Standard Output Tokens or Premium Output
1075
+ Tokens allowance and are not sent to the corresponding Stripe token meters.
1076
+ Reasoning tokens are a breakdown of provider output and are not added again.
1077
+
1078
+ The persisted meter keys remain `standardTokens` and `premiumTokens` for API
1079
+ compatibility; they represent standard and premium output tokens in both plan
1080
+ limits and Stripe usage metering.
773
1081
 
774
1082
  `spendVelocity` is aggregate-backed current UTC hour/day telemetry. UsageTap does not enforce limits from this section; `currentCallCostUsd` is included separately because aggregate updates are asynchronous.
775
1083
 
@@ -798,7 +1106,10 @@ This is useful when:
798
1106
 
799
1107
  ### Batch pricing
800
1108
 
801
- Batch mode applies a **50% discount** to standard pricing rates. Set `batch: true` or `pricingMode: "batch"` on either `call_begin` or `call_end`.
1109
+ Batch mode applies a **50% discount** to standard pricing rates. UsageTap accepts
1110
+ the execution mode reported by your application; it does not attempt to infer
1111
+ or verify the vendor workflow. Prefer `pricingMode: "batch"`; `batch: true` is
1112
+ the compatibility form.
802
1113
 
803
1114
  When set on `call_begin`, the pricing mode carries through to `call_end` automatically. Setting it on `call_end` overrides the `call_begin` value.
804
1115
 
@@ -813,7 +1124,7 @@ const begin = await usageTap.beginCall({
813
1124
  // Option 2: Set on call_end (overrides call_begin)
814
1125
  await usageTap.endCall({
815
1126
  callId: begin.data.callId,
816
- modelUsed: "gpt-4o",
1127
+ modelUsed: "gpt-5.6-sol",
817
1128
  inputTokens: 100,
818
1129
  responseTokens: 200,
819
1130
  batch: true,
@@ -822,6 +1133,28 @@ await usageTap.endCall({
822
1133
  ```
823
1134
 
824
1135
  Both `batch` and `pricingMode` are echoed in the responses from `call_begin` and `call_end`.
1136
+ When both request fields are supplied, `pricingMode` is authoritative.
1137
+
1138
+ OpenAI and Anthropic completion/message usage records provide token counts, but
1139
+ they do not provide a dependable per-response signal that proves the request
1140
+ received vendor batch pricing. The wrappers therefore never infer batch mode
1141
+ from `usage`. Set it explicitly in wrapper context when your surrounding
1142
+ workflow knows the request is a vendor batch:
1143
+
1144
+ ```ts
1145
+ const metered = withMetering(openai, {
1146
+ customerId: "cust_123",
1147
+ pricingMode: "batch",
1148
+ usageTapClient: usageTap,
1149
+ });
1150
+ ```
1151
+
1152
+ The ordinary `wrapOpenAI` and `wrapAnthropic` create-method wrappers do not
1153
+ submit native vendor batch jobs. For OpenAI Batch, Anthropic Message Batches, or
1154
+ another asynchronous provider, open one UsageTap call per batch item, retain
1155
+ its `callId`, then call `endCall` with the usage returned for that item. The
1156
+ LLMAsAService `POST /v1/batches` integration performs this lifecycle
1157
+ automatically.
825
1158
 
826
1159
  ### Raw fetch integrations
827
1160