@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.
- package/README.md +372 -39
- package/dist/adapters/anthropic.cjs +995 -69
- package/dist/adapters/anthropic.cjs.map +1 -1
- package/dist/adapters/anthropic.d.cts +45 -3
- package/dist/adapters/anthropic.d.ts +45 -3
- package/dist/adapters/anthropic.mjs +995 -70
- package/dist/adapters/anthropic.mjs.map +1 -1
- package/dist/adapters/openai.cjs +1208 -106
- package/dist/adapters/openai.cjs.map +1 -1
- package/dist/adapters/openai.d.cts +46 -3
- package/dist/adapters/openai.d.ts +46 -3
- package/dist/adapters/openai.mjs +1208 -107
- package/dist/adapters/openai.mjs.map +1 -1
- package/dist/adapters/openrouter.cjs +3912 -53
- package/dist/adapters/openrouter.cjs.map +1 -1
- package/dist/adapters/openrouter.d.cts +6 -3
- package/dist/adapters/openrouter.d.ts +6 -3
- package/dist/adapters/openrouter.mjs +3910 -54
- package/dist/adapters/openrouter.mjs.map +1 -1
- package/dist/anthropic/index.cjs +995 -69
- package/dist/anthropic/index.cjs.map +1 -1
- package/dist/anthropic/index.d.cts +2 -2
- package/dist/anthropic/index.d.ts +2 -2
- package/dist/anthropic/index.mjs +995 -70
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/client-C0UiaqVB.d.cts +1305 -0
- package/dist/client-C0UiaqVB.d.ts +1305 -0
- package/dist/express/index.cjs +399 -64
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.d.cts +2 -2
- package/dist/express/index.d.ts +2 -2
- package/dist/express/index.mjs +399 -64
- package/dist/express/index.mjs.map +1 -1
- package/dist/index.cjs +1044 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -5
- package/dist/index.d.ts +16 -5
- package/dist/index.mjs +1044 -163
- package/dist/index.mjs.map +1 -1
- package/dist/openai/index.cjs +1209 -107
- package/dist/openai/index.cjs.map +1 -1
- package/dist/openai/index.d.cts +2 -2
- package/dist/openai/index.d.ts +2 -2
- package/dist/openai/index.mjs +1209 -108
- package/dist/openai/index.mjs.map +1 -1
- package/dist/openrouter/index.cjs +1226 -109
- package/dist/openrouter/index.cjs.map +1 -1
- package/dist/openrouter/index.d.cts +3 -3
- package/dist/openrouter/index.d.ts +3 -3
- package/dist/openrouter/index.mjs +1224 -108
- package/dist/openrouter/index.mjs.map +1 -1
- package/dist/react/index.cjs +19 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +17 -4
- package/dist/react/index.d.ts +17 -4
- package/dist/react/index.mjs +19 -1
- package/dist/react/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/client-BD8O2J8Z.d.cts +0 -668
- 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
|
|
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.
|
|
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.
|
|
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
|
|
70
|
-
and skips the compression step below 1,000
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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`
|
|
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:
|
|
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
|
|
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`,
|
|
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`
|
|
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
|
-
|
|
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({
|
|
568
|
+
function ChatComponent({ customerId, currentUser }) {
|
|
332
569
|
const { messages, input, setInput, handleSubmit, isLoading } = useChatWithUsage({
|
|
333
570
|
api: "/api/chat",
|
|
334
|
-
customerId
|
|
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.
|
|
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
|
|
528
|
-
- `IMMEDIATE_PRORATED`: Switch
|
|
529
|
-
- `AT_NEXT_REPLENISH`:
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
|
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: "
|
|
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: "
|
|
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.
|
|
616
|
-
3.
|
|
617
|
-
4.
|
|
618
|
-
5. Unlimited meters
|
|
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).
|
|
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":
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
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
|
|