@usagetap/sdk 1.3.2 → 1.4.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 +219 -24
- package/dist/adapters/anthropic.cjs +990 -24
- 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 +990 -25
- package/dist/adapters/anthropic.mjs.map +1 -1
- package/dist/adapters/openai.cjs +1164 -44
- 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 +1164 -45
- package/dist/adapters/openai.mjs.map +1 -1
- package/dist/adapters/openrouter.cjs +3899 -22
- 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 +3897 -23
- package/dist/adapters/openrouter.mjs.map +1 -1
- package/dist/anthropic/index.cjs +990 -24
- 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 +990 -25
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/client-CExQ8e1T.d.cts +1225 -0
- package/dist/client-CExQ8e1T.d.ts +1225 -0
- package/dist/express/index.cjs +421 -29
- 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 +421 -29
- package/dist/express/index.mjs.map +1 -1
- package/dist/index.cjs +680 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.mjs +680 -15
- package/dist/index.mjs.map +1 -1
- package/dist/openai/index.cjs +1165 -45
- 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 +1165 -46
- package/dist/openai/index.mjs.map +1 -1
- package/dist/openrouter/index.cjs +1182 -47
- 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 +1180 -46
- 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 +1 -1
- package/dist/client-BD8O2J8Z.d.cts +0 -668
- package/dist/client-BD8O2J8Z.d.ts +0 -668
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ import { withMetering } from "@usagetap/sdk/openai";
|
|
|
33
33
|
|
|
34
34
|
const openai = withMetering(new OpenAI(), "cust_123");
|
|
35
35
|
const completion = await openai.responses.create({
|
|
36
|
-
model: "gpt-5.
|
|
36
|
+
model: "gpt-5.6-luna",
|
|
37
37
|
input: "Draft a welcome email for our Pro plan",
|
|
38
38
|
});
|
|
39
39
|
|
|
@@ -52,12 +52,9 @@ calls:
|
|
|
52
52
|
import OpenAI from "openai";
|
|
53
53
|
import { withCompression } from "@usagetap/sdk/openai";
|
|
54
54
|
|
|
55
|
-
const openai = withCompression(new OpenAI()
|
|
56
|
-
// Defaults to 1,000. Use 0 to always attempt compression.
|
|
57
|
-
minContextTokens: 2_000,
|
|
58
|
-
});
|
|
55
|
+
const openai = withCompression(new OpenAI());
|
|
59
56
|
const completion = await openai.responses.create({
|
|
60
|
-
model: "gpt-5.
|
|
57
|
+
model: "gpt-5.6-luna",
|
|
61
58
|
input: longPrompt,
|
|
62
59
|
});
|
|
63
60
|
```
|
|
@@ -66,12 +63,32 @@ The same `withMetering` and `withCompression` APIs are exported from
|
|
|
66
63
|
`@usagetap/sdk/anthropic` and `@usagetap/sdk/openrouter`. Remove the wrapper or
|
|
67
64
|
call `.unwrap()` to recover the original provider client.
|
|
68
65
|
|
|
69
|
-
`withCompression` uses a fast token estimate
|
|
70
|
-
and skips the compression step below 1,000
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
`withCompression` compresses user messages only, uses a fast token estimate
|
|
67
|
+
over the combined request context, and skips the compression step below 1,000
|
|
68
|
+
estimated tokens by default. System instructions, tool content, and assistant
|
|
69
|
+
messages remain unchanged. Override roles or the `minContextTokens` cutoff only
|
|
70
|
+
when you need more control; set the cutoff to `0` to always attempt compression.
|
|
73
71
|
The separate `minTokens` option remains a per-text-segment cutoff.
|
|
74
72
|
|
|
73
|
+
When workload evidence calls for tuning, the wrapper also accepts the hosted
|
|
74
|
+
Messages API controls directly:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const openai = withCompression(new OpenAI(), {
|
|
78
|
+
mode: "model_auto", // or "model_force" / "deterministic"
|
|
79
|
+
roles: {
|
|
80
|
+
user: { aggressiveness: 0.2 },
|
|
81
|
+
system: { aggressiveness: 0.1 },
|
|
82
|
+
},
|
|
83
|
+
latencyBudgetMs: 1_000,
|
|
84
|
+
compactEmptyUserMessages: false,
|
|
85
|
+
compactDuplicateUserTextParts: false,
|
|
86
|
+
failOpen: true,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Omitting the options object remains the recommended starting point.
|
|
91
|
+
|
|
75
92
|
Wrappers compose. Put metering outside compression so the metered operation
|
|
76
93
|
includes compression and the provider call:
|
|
77
94
|
|
|
@@ -85,6 +102,126 @@ const openai = withMetering(
|
|
|
85
102
|
Each `.unwrap()` removes one layer. Do not also set `promptCompression: true` on
|
|
86
103
|
`withMetering` when using a separate `withCompression` layer.
|
|
87
104
|
|
|
105
|
+
### UsageTap Gateway
|
|
106
|
+
|
|
107
|
+
The core client can call the OpenAI-compatible UsageTap Gateway without a
|
|
108
|
+
second SDK. Use a `utk-` key with `gateway:invoke`. Add
|
|
109
|
+
`compression:invoke` if the same workflow also requests hosted Compression.
|
|
110
|
+
Existing `gk-` and compatible `cmp-` keys continue to work:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { UsageTap } from "@usagetap/sdk";
|
|
114
|
+
|
|
115
|
+
const usageTap = new UsageTap();
|
|
116
|
+
const completion = await usageTap.gateway.chat.completions.create({
|
|
117
|
+
model: "usagetap/standard",
|
|
118
|
+
customerId: "cust_123",
|
|
119
|
+
feature: "chat.reply",
|
|
120
|
+
messages: [{ role: "user", content: "Summarize this account." }],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
console.log(completion.choices[0].message?.content);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The same resource exposes `models.list()` and the complete native batch
|
|
127
|
+
lifecycle. Batch creation generates the required idempotency key unless one is
|
|
128
|
+
provided:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const submitted = await usageTap.gateway.batches.create({
|
|
132
|
+
requests: reports.map((report) => ({
|
|
133
|
+
custom_id: report.id,
|
|
134
|
+
body: {
|
|
135
|
+
model: "usagetap/standard",
|
|
136
|
+
customerId: report.customerId,
|
|
137
|
+
messages: [{ role: "user", content: report.prompt }],
|
|
138
|
+
},
|
|
139
|
+
})),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const batch = await usageTap.gateway.batches.wait(submitted);
|
|
143
|
+
if (batch.status !== "completed") {
|
|
144
|
+
throw new Error(`Batch ended with status: ${batch.status}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Parses the Gateway's NDJSON result stream into typed objects.
|
|
148
|
+
const results = await usageTap.gateway.batches.results(batch.id);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use `gateway.batches.retrieve()`, `cancel()`, and `results()` when you want to
|
|
152
|
+
manage polling yourself. Set `gatewayBaseUrl` or `USAGETAP_GATEWAY_URL` for a
|
|
153
|
+
non-default deployment.
|
|
154
|
+
|
|
155
|
+
### Context summarization
|
|
156
|
+
|
|
157
|
+
Published context-summarization profiles are available through
|
|
158
|
+
`usageTap.summarization`. A managed single summary can wait for completion in
|
|
159
|
+
the initial request:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const summary = await usageTap.summarization.summaries.create({
|
|
163
|
+
profile: "weekly-account-summary-abcd5678",
|
|
164
|
+
wait: true,
|
|
165
|
+
context: {
|
|
166
|
+
id: "account-123",
|
|
167
|
+
type: "account_history",
|
|
168
|
+
content: accountHistory,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
console.log(summary.result);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Batch submissions and polling use the same resource pattern as Gateway
|
|
176
|
+
batches:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const submitted = await usageTap.summarization.batches.create({
|
|
180
|
+
profile: "weekly-account-summary-abcd5678",
|
|
181
|
+
items: accounts.map((account) => ({
|
|
182
|
+
id: account.id,
|
|
183
|
+
type: "account_history",
|
|
184
|
+
content: account.history,
|
|
185
|
+
})),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const batch = await usageTap.summarization.batches.wait(submitted);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Self-managed workflows can use `summarization.profiles.retrieve()` to load the
|
|
192
|
+
published prompt and model settings, then
|
|
193
|
+
`summarization.measurements.create()` to report source and summary token
|
|
194
|
+
counts.
|
|
195
|
+
|
|
196
|
+
### Runaway circuit breaker
|
|
197
|
+
|
|
198
|
+
Set a local per-run call cap and pass the same `runId` on every model call in a
|
|
199
|
+
workflow. Once the cap is reached, the SDK throws `USAGETAP_CIRCUIT_OPEN` before
|
|
200
|
+
`call_begin` or the paid provider request can start:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { UsageTapClient } from "@usagetap/sdk";
|
|
204
|
+
|
|
205
|
+
const usageTap = new UsageTapClient({
|
|
206
|
+
circuitBreaker: { maxCallsPerRun: 20 },
|
|
207
|
+
});
|
|
208
|
+
const run = { customerId: "cust_123", runId: crypto.randomUUID() };
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
for (;;) {
|
|
212
|
+
const result = await usageTap.meter(run, async () => callModel());
|
|
213
|
+
if (result.done) break;
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
usageTap.resetRun(run);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`canRunContinue(run)` returns the current decision for graceful partial-result
|
|
221
|
+
handling. Idempotent retries do not consume another slot. The guard is
|
|
222
|
+
process-local by design, so use a stable `runId` in each SDK process and keep
|
|
223
|
+
account-level UsageTap limits enabled for distributed enforcement.
|
|
224
|
+
|
|
88
225
|
For advanced entitlement control, `wrapOpenAI` exposes the full UsageTap context
|
|
89
226
|
and applies entitlement-aware defaults when you omit `model`.
|
|
90
227
|
|
|
@@ -105,6 +242,25 @@ const ai = wrapOpenAI(openai, usageTap, {
|
|
|
105
242
|
});
|
|
106
243
|
```
|
|
107
244
|
|
|
245
|
+
### Optional end-user attribution
|
|
246
|
+
|
|
247
|
+
`customerId` identifies the customer account. When that account has multiple
|
|
248
|
+
users, add `customerUserId` to attribute the call to the responsible end user
|
|
249
|
+
in Live activity. Prefer a stable, non-PII application user ID.
|
|
250
|
+
`customerUserName` and `customerUserEmail` are optional display metadata. The
|
|
251
|
+
SDK does not infer them, and existing integrations remain valid when these
|
|
252
|
+
fields are omitted.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
const context = {
|
|
256
|
+
customerId: currentCustomer.id,
|
|
257
|
+
customerUserId: currentUser.id, // Optional, recommended when available
|
|
258
|
+
customerUserName: currentUser.name, // Optional display metadata
|
|
259
|
+
customerUserEmail: currentUser.email, // Optional display metadata
|
|
260
|
+
feature: "chat.send",
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
108
264
|
### Prompt compression
|
|
109
265
|
|
|
110
266
|
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 +270,9 @@ import { protectPromptText } from "@usagetap/sdk";
|
|
|
114
270
|
|
|
115
271
|
const begin = await usageTap.beginCall({
|
|
116
272
|
customerId: "cust_123",
|
|
273
|
+
customerUserId: currentUser.id,
|
|
274
|
+
customerUserName: currentUser.name,
|
|
275
|
+
customerUserEmail: currentUser.email,
|
|
117
276
|
feature: "chat.send",
|
|
118
277
|
});
|
|
119
278
|
|
|
@@ -181,7 +340,7 @@ When using The Token Company, configure `tokenCompanyApiKey` on `UsageTapClient`
|
|
|
181
340
|
|
|
182
341
|
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
342
|
|
|
184
|
-
> **Heads up:** `UsageTapClient` always negotiates the canonical UsageTap media type by sending `Accept: application/vnd.usagetap.v1+json`. Every response
|
|
343
|
+
> **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
344
|
|
|
186
345
|
### Streaming helpers
|
|
187
346
|
|
|
@@ -207,7 +366,7 @@ export async function POST() {
|
|
|
207
366
|
}
|
|
208
367
|
```
|
|
209
368
|
|
|
210
|
-
`wrapOpenAI`
|
|
369
|
+
`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.
|
|
211
370
|
|
|
212
371
|
### Overriding usage context per request
|
|
213
372
|
|
|
@@ -218,7 +377,8 @@ await ai.chat.completions.create(
|
|
|
218
377
|
{ messages },
|
|
219
378
|
{
|
|
220
379
|
usageTap: {
|
|
221
|
-
customerId:
|
|
380
|
+
customerId: currentCustomer.id,
|
|
381
|
+
customerUserId: currentUser.id, // Optional
|
|
222
382
|
feature: "chat.assist",
|
|
223
383
|
tags: ["beta"],
|
|
224
384
|
requested: { standard: true, premium: true, search: true, reasoningLevel: "HIGH" },
|
|
@@ -227,13 +387,13 @@ await ai.chat.completions.create(
|
|
|
227
387
|
);
|
|
228
388
|
```
|
|
229
389
|
|
|
230
|
-
The begin response
|
|
390
|
+
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
391
|
|
|
232
392
|
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
393
|
|
|
234
394
|
### responses.create support
|
|
235
395
|
|
|
236
|
-
The wrapper also instruments `openai.responses.create`,
|
|
396
|
+
The wrapper also instruments `openai.responses.create`, preserves the supplied model and request, and collects usage data the same way as chat completions.
|
|
237
397
|
|
|
238
398
|
### OpenRouter support
|
|
239
399
|
|
|
@@ -264,6 +424,7 @@ const ai = wrapOpenAI(openrouter, usageTap, {
|
|
|
264
424
|
|
|
265
425
|
const completion = await ai.chat.completions.create(
|
|
266
426
|
{
|
|
427
|
+
model: "your-existing-openrouter-model",
|
|
267
428
|
messages: [{ role: "user", content: "Hello from OpenRouter!" }],
|
|
268
429
|
},
|
|
269
430
|
{
|
|
@@ -274,7 +435,7 @@ const completion = await ai.chat.completions.create(
|
|
|
274
435
|
);
|
|
275
436
|
```
|
|
276
437
|
|
|
277
|
-
`begin.data.models`
|
|
438
|
+
`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
439
|
|
|
279
440
|
### Express middleware
|
|
280
441
|
|
|
@@ -319,7 +480,7 @@ app.post("/api/chat", async (req, res) => {
|
|
|
319
480
|
});
|
|
320
481
|
```
|
|
321
482
|
|
|
322
|
-
|
|
483
|
+
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
484
|
|
|
324
485
|
### React hook for chat UIs
|
|
325
486
|
|
|
@@ -328,10 +489,11 @@ Build chat interfaces with automatic UsageTap tracking:
|
|
|
328
489
|
```tsx
|
|
329
490
|
import { useChatWithUsage } from "@usagetap/sdk/react";
|
|
330
491
|
|
|
331
|
-
function ChatComponent({
|
|
492
|
+
function ChatComponent({ customerId, currentUser }) {
|
|
332
493
|
const { messages, input, setInput, handleSubmit, isLoading } = useChatWithUsage({
|
|
333
494
|
api: "/api/chat",
|
|
334
|
-
customerId
|
|
495
|
+
customerId,
|
|
496
|
+
customerUserId: currentUser.id, // Optional hint; validate on the server
|
|
335
497
|
feature: "chat.assistant",
|
|
336
498
|
});
|
|
337
499
|
|
|
@@ -395,7 +557,7 @@ const completion = await openai.chat.completions.create({
|
|
|
395
557
|
});
|
|
396
558
|
```
|
|
397
559
|
|
|
398
|
-
`wrapFetch` detects OpenAI API endpoints, handles streaming and non-streaming responses, and automatically extracts usage data.
|
|
560
|
+
`wrapFetch` detects OpenAI API endpoints, handles streaming and non-streaming responses, 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. You can override metering context per request using special headers:
|
|
399
561
|
|
|
400
562
|
```ts
|
|
401
563
|
await openai.chat.completions.create(
|
|
@@ -552,6 +714,7 @@ Custom meters allow you to track usage beyond standard LLM metrics—ideal for a
|
|
|
552
714
|
```ts
|
|
553
715
|
const result = await usageTap.incrementCustomMeter({
|
|
554
716
|
customerId: "cust_123",
|
|
717
|
+
customerUserId: currentUser.id,
|
|
555
718
|
meterSlot: "CUSTOM1", // or "CUSTOM2"
|
|
556
719
|
amount: 5,
|
|
557
720
|
feature: "agent_actions",
|
|
@@ -570,6 +733,8 @@ console.log("Blocked:", result.data.blocked);
|
|
|
570
733
|
**Parameters:**
|
|
571
734
|
|
|
572
735
|
- `customerId` (string, required): Customer identifier
|
|
736
|
+
- `customerUserId` (string, optional): Stable identifier for the end user responsible for the event
|
|
737
|
+
- `customerUserName` and `customerUserEmail` (string, optional): Display fields for live activity
|
|
573
738
|
- `meterSlot` ("CUSTOM1" | "CUSTOM2", required): Which custom meter to increment
|
|
574
739
|
- `amount` (number, required): Positive number to decrement from quota
|
|
575
740
|
- `feature` (string, optional): Feature identifier for tracking
|
|
@@ -710,7 +875,7 @@ UsageTap responds exclusively with the canonical `{ result, data, correlationId
|
|
|
710
875
|
- `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
876
|
- `subscription` contains the active plan identity, versioning, and upcoming replenishment timestamps so you can render customer-facing UI without querying Dynamo yourself.
|
|
712
877
|
- `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).
|
|
878
|
+
- `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, PAYG hold, and pricing mode; identical inputs can replay an earlier call.
|
|
714
879
|
- `plan` and `balances` remain available alongside the core begin payload for backwards compatibility with earlier SDK versions.
|
|
715
880
|
|
|
716
881
|
### Example `call_end` success
|
|
@@ -767,7 +932,12 @@ UsageTap responds exclusively with the canonical `{ result, data, correlationId
|
|
|
767
932
|
}
|
|
768
933
|
```
|
|
769
934
|
|
|
770
|
-
Send `cachedInputTokens` when available so UsageTap
|
|
935
|
+
Send `cachedInputTokens` and `cacheWriteInputTokens` when available so UsageTap
|
|
936
|
+
can apply provider prompt-cache pricing correctly. `inputTokens` is always the
|
|
937
|
+
total input count and includes both subsets. OpenAI's prompt-token total already
|
|
938
|
+
includes cache reads. Anthropic reports ordinary input, cache reads, and cache
|
|
939
|
+
writes separately, so the Anthropic wrapper adds the three counters for
|
|
940
|
+
`inputTokens` while retaining both cache subsets.
|
|
771
941
|
|
|
772
942
|
`metered` is derived from the raw Dynamo deltas. Additional meters (audio seconds, reasoning tokens, balances) will populate in later phases without breaking the contract.
|
|
773
943
|
|
|
@@ -798,7 +968,10 @@ This is useful when:
|
|
|
798
968
|
|
|
799
969
|
### Batch pricing
|
|
800
970
|
|
|
801
|
-
Batch mode applies a **50% discount** to standard pricing rates.
|
|
971
|
+
Batch mode applies a **50% discount** to standard pricing rates. UsageTap accepts
|
|
972
|
+
the execution mode reported by your application; it does not attempt to infer
|
|
973
|
+
or verify the vendor workflow. Prefer `pricingMode: "batch"`; `batch: true` is
|
|
974
|
+
the compatibility form.
|
|
802
975
|
|
|
803
976
|
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
977
|
|
|
@@ -813,7 +986,7 @@ const begin = await usageTap.beginCall({
|
|
|
813
986
|
// Option 2: Set on call_end (overrides call_begin)
|
|
814
987
|
await usageTap.endCall({
|
|
815
988
|
callId: begin.data.callId,
|
|
816
|
-
modelUsed: "gpt-
|
|
989
|
+
modelUsed: "gpt-5.6-sol",
|
|
817
990
|
inputTokens: 100,
|
|
818
991
|
responseTokens: 200,
|
|
819
992
|
batch: true,
|
|
@@ -822,6 +995,28 @@ await usageTap.endCall({
|
|
|
822
995
|
```
|
|
823
996
|
|
|
824
997
|
Both `batch` and `pricingMode` are echoed in the responses from `call_begin` and `call_end`.
|
|
998
|
+
When both request fields are supplied, `pricingMode` is authoritative.
|
|
999
|
+
|
|
1000
|
+
OpenAI and Anthropic completion/message usage records provide token counts, but
|
|
1001
|
+
they do not provide a dependable per-response signal that proves the request
|
|
1002
|
+
received vendor batch pricing. The wrappers therefore never infer batch mode
|
|
1003
|
+
from `usage`. Set it explicitly in wrapper context when your surrounding
|
|
1004
|
+
workflow knows the request is a vendor batch:
|
|
1005
|
+
|
|
1006
|
+
```ts
|
|
1007
|
+
const metered = withMetering(openai, {
|
|
1008
|
+
customerId: "cust_123",
|
|
1009
|
+
pricingMode: "batch",
|
|
1010
|
+
usageTapClient: usageTap,
|
|
1011
|
+
});
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
The ordinary `wrapOpenAI` and `wrapAnthropic` create-method wrappers do not
|
|
1015
|
+
submit native vendor batch jobs. For OpenAI Batch, Anthropic Message Batches, or
|
|
1016
|
+
another asynchronous provider, open one UsageTap call per batch item, retain
|
|
1017
|
+
its `callId`, then call `endCall` with the usage returned for that item. The
|
|
1018
|
+
LLMAsAService `POST /v1/batches` integration performs this lifecycle
|
|
1019
|
+
automatically.
|
|
825
1020
|
|
|
826
1021
|
### Raw fetch integrations
|
|
827
1022
|
|