@usagetap/sdk 0.1.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 +561 -0
- package/dist/adapters/openai.cjs +769 -0
- package/dist/adapters/openai.cjs.map +1 -0
- package/dist/adapters/openai.d.cts +2 -0
- package/dist/adapters/openai.d.ts +2 -0
- package/dist/adapters/openai.js +763 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/adapters/openrouter.cjs +192 -0
- package/dist/adapters/openrouter.cjs.map +1 -0
- package/dist/adapters/openrouter.d.cts +10 -0
- package/dist/adapters/openrouter.d.ts +10 -0
- package/dist/adapters/openrouter.js +190 -0
- package/dist/adapters/openrouter.js.map +1 -0
- package/dist/index.cjs +1573 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +159 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +1559 -0
- package/dist/index.js.map +1 -0
- package/dist/openai-CKyw08rB.d.cts +398 -0
- package/dist/openai-CKyw08rB.d.ts +398 -0
- package/dist/react/index.cjs +239 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +147 -0
- package/dist/react/index.d.ts +147 -0
- package/dist/react/index.js +237 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
# @usagetap/sdk
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
Install the peer dependency for your vendor (e.g. `openai`) and the UsageTap SDK in your server runtime.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @usagetap/sdk openai
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Create a UsageTap client, request entitlements, and choose the right model every time:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import OpenAI from "openai";
|
|
17
|
+
import { UsageTapClient } from "@usagetap/sdk";
|
|
18
|
+
|
|
19
|
+
const usageTap = new UsageTapClient({
|
|
20
|
+
apiKey: process.env.USAGETAP_API_KEY!,
|
|
21
|
+
baseUrl: process.env.USAGETAP_BASE_URL!,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
|
|
25
|
+
|
|
26
|
+
function selectCapabilities(allowed: {
|
|
27
|
+
standard?: boolean;
|
|
28
|
+
premium?: boolean;
|
|
29
|
+
reasoningLevel?: "LOW" | "MEDIUM" | "HIGH" | null;
|
|
30
|
+
search?: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const tier = allowed.premium ? "premium" : "standard";
|
|
33
|
+
const model = tier === "premium" ? "gpt5" : "gpt5-mini";
|
|
34
|
+
const reasoningEffort = allowed.reasoningLevel === "HIGH"
|
|
35
|
+
? "high"
|
|
36
|
+
: allowed.reasoningLevel === "MEDIUM"
|
|
37
|
+
? "medium"
|
|
38
|
+
: allowed.reasoningLevel === "LOW"
|
|
39
|
+
? "low"
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
model,
|
|
44
|
+
reasoning: reasoningEffort ? { effort: reasoningEffort } : undefined,
|
|
45
|
+
tools: allowed.search ? [{ type: "web_search" as const }] : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const completion = await usageTap.withUsage(
|
|
50
|
+
{
|
|
51
|
+
customerId: "cust_123",
|
|
52
|
+
feature: "chat.send",
|
|
53
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "HIGH" },
|
|
54
|
+
},
|
|
55
|
+
async ({ begin, setUsage }) => {
|
|
56
|
+
const { model, reasoning, tools } = selectCapabilities(begin.data.allowed);
|
|
57
|
+
|
|
58
|
+
const response = await openai.responses.create({
|
|
59
|
+
model,
|
|
60
|
+
input: "Draft a welcome email for our Pro plan",
|
|
61
|
+
reasoning,
|
|
62
|
+
tools,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
setUsage({
|
|
66
|
+
modelUsed: model,
|
|
67
|
+
inputTokens: response.usage?.input_tokens ?? response.usage?.prompt_tokens ?? 0,
|
|
68
|
+
responseTokens: response.usage?.output_tokens ?? response.usage?.completion_tokens ?? 0,
|
|
69
|
+
reasoningTokens: reasoning ? response.usage?.reasoning_tokens ?? 0 : 0,
|
|
70
|
+
searches: tools?.length ? response.usage?.web_search_queries ?? 0 : 0,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return response;
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
console.log(completion.output_text);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If you only need to toggle web search, keep the selected model and conditionally add the tool when UsageTap says it’s allowed:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const response = await openai.responses.create({
|
|
84
|
+
model: "gpt5",
|
|
85
|
+
tools: begin.data.allowed.search ? [{ type: "web_search" }] : undefined,
|
|
86
|
+
input: "What was a positive news story from today?",
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Prefer a zero-boilerplate integration? Keep scrolling—`wrapOpenAI` applies the same entitlement-aware defaults if you omit `model` from your request.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { wrapOpenAI } from "@usagetap/sdk";
|
|
94
|
+
|
|
95
|
+
const ai = wrapOpenAI(openai, usageTap, {
|
|
96
|
+
defaultContext: {
|
|
97
|
+
customerId: "cust_123",
|
|
98
|
+
feature: "chat.send",
|
|
99
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "HIGH" },
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **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.
|
|
105
|
+
|
|
106
|
+
### Streaming helpers
|
|
107
|
+
|
|
108
|
+
`wrapOpenAI` automatically instruments streaming responses. You can feed the wrapped stream directly into Next.js or an Express response using the exported helpers:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { toNextResponse } from "@usagetap/sdk";
|
|
112
|
+
|
|
113
|
+
export async function POST() {
|
|
114
|
+
const stream = await ai.chat.completions.create(
|
|
115
|
+
{
|
|
116
|
+
messages: [{ role: "user", content: "Stream it" }],
|
|
117
|
+
stream: true,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
usageTap: {
|
|
121
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "MEDIUM" },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return toNextResponse(stream, { mode: "text" });
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`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.
|
|
131
|
+
|
|
132
|
+
### Overriding usage context per request
|
|
133
|
+
|
|
134
|
+
You can override the UsageTap begin payload on a per-call basis via the `usageTap` option:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
await ai.chat.completions.create(
|
|
138
|
+
{ messages },
|
|
139
|
+
{
|
|
140
|
+
usageTap: {
|
|
141
|
+
customerId: currentUser.id,
|
|
142
|
+
feature: "chat.assist",
|
|
143
|
+
tags: ["beta"],
|
|
144
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "HIGH" },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
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.
|
|
151
|
+
|
|
152
|
+
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()`.
|
|
153
|
+
|
|
154
|
+
### responses.create support
|
|
155
|
+
|
|
156
|
+
The wrapper also instruments `openai.responses.create`, applying vendor hints (preferred models, token limits) and collecting usage data the same way as chat completions.
|
|
157
|
+
|
|
158
|
+
### OpenRouter support
|
|
159
|
+
|
|
160
|
+
`wrapOpenAI` works seamlessly with OpenRouter since it uses an OpenAI-compatible API. Just point the base URL to OpenRouter:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import OpenAI from "openai";
|
|
164
|
+
import { UsageTapClient, wrapOpenAI } from "@usagetap/sdk";
|
|
165
|
+
|
|
166
|
+
const usageTap = new UsageTapClient({
|
|
167
|
+
apiKey: process.env.USAGETAP_API_KEY!,
|
|
168
|
+
baseUrl: process.env.USAGETAP_BASE_URL!,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const openrouter = new OpenAI({
|
|
172
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
173
|
+
apiKey: process.env.OPENROUTER_API_KEY!,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const ai = wrapOpenAI(openrouter, usageTap, {
|
|
177
|
+
defaultContext: {
|
|
178
|
+
customerId: "cust_123",
|
|
179
|
+
feature: "chat.send",
|
|
180
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "HIGH" },
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const completion = await ai.chat.completions.create(
|
|
185
|
+
{
|
|
186
|
+
messages: [{ role: "user", content: "Hello from OpenRouter!" }],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
usageTap: {
|
|
190
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "MEDIUM" },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`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.
|
|
197
|
+
|
|
198
|
+
### Express middleware
|
|
199
|
+
|
|
200
|
+
For Express applications, use the `withUsage` middleware to attach UsageTap context to requests:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import express from "express";
|
|
204
|
+
import OpenAI from "openai";
|
|
205
|
+
import { UsageTapClient, withUsage } from "@usagetap/sdk";
|
|
206
|
+
|
|
207
|
+
const app = express();
|
|
208
|
+
const usageTap = new UsageTapClient({
|
|
209
|
+
apiKey: process.env.USAGETAP_API_KEY!,
|
|
210
|
+
baseUrl: process.env.USAGETAP_BASE_URL!,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Extract customer ID from your auth system
|
|
214
|
+
app.use(withUsage(usageTap, (req) => req.user.id));
|
|
215
|
+
|
|
216
|
+
app.post("/api/chat", async (req, res) => {
|
|
217
|
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
|
|
218
|
+
const ai = req.usageTap!.openai(openai, {
|
|
219
|
+
feature: "chat.assistant",
|
|
220
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "HIGH" },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const stream = await ai.chat.completions.create(
|
|
224
|
+
{
|
|
225
|
+
messages: req.body.messages,
|
|
226
|
+
stream: true,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
usageTap: {
|
|
230
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "HIGH" },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Pipes stream to response and finalizes usage
|
|
236
|
+
req.usageTap!.pipeToResponse(stream, res);
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
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.
|
|
241
|
+
|
|
242
|
+
### React hook for chat UIs
|
|
243
|
+
|
|
244
|
+
Build chat interfaces with automatic UsageTap tracking:
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
import { useChatWithUsage } from "@usagetap/sdk/react";
|
|
248
|
+
|
|
249
|
+
function ChatComponent({ userId }) {
|
|
250
|
+
const { messages, input, setInput, handleSubmit, isLoading } = useChatWithUsage({
|
|
251
|
+
api: "/api/chat",
|
|
252
|
+
customerId: userId,
|
|
253
|
+
feature: "chat.assistant",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div>
|
|
258
|
+
{messages.map((m) => (
|
|
259
|
+
<div key={m.id}>
|
|
260
|
+
<strong>{m.role}:</strong> {m.content}
|
|
261
|
+
</div>
|
|
262
|
+
))}
|
|
263
|
+
<form onSubmit={handleSubmit}>
|
|
264
|
+
<input
|
|
265
|
+
value={input}
|
|
266
|
+
onChange={(e) => setInput(e.target.value)}
|
|
267
|
+
disabled={isLoading}
|
|
268
|
+
/>
|
|
269
|
+
<button type="submit" disabled={isLoading}>
|
|
270
|
+
Send
|
|
271
|
+
</button>
|
|
272
|
+
</form>
|
|
273
|
+
</div>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The hook works with server routes that use UsageTap SDK (see `streamOpenAIRoute` above).
|
|
279
|
+
|
|
280
|
+
### wrapFetch: minimal integration
|
|
281
|
+
|
|
282
|
+
For the smallest possible integration, use `wrapFetch` to wrap the `fetch` function passed to the OpenAI SDK. This requires zero changes to your OpenAI code:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import OpenAI from "openai";
|
|
286
|
+
import { UsageTapClient, wrapFetch } from "@usagetap/sdk";
|
|
287
|
+
|
|
288
|
+
const usageTap = new UsageTapClient({
|
|
289
|
+
apiKey: process.env.USAGETAP_API_KEY!,
|
|
290
|
+
baseUrl: process.env.USAGETAP_BASE_URL!,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const wrappedFetch = wrapFetch(usageTap, {
|
|
294
|
+
defaultContext: {
|
|
295
|
+
customerId: "cust_123",
|
|
296
|
+
feature: "chat",
|
|
297
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "MEDIUM" },
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const openai = new OpenAI({
|
|
302
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
303
|
+
fetch: wrappedFetch,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Reuse the selectCapabilities helper shown above to map entitlements to models
|
|
307
|
+
// Pull the entitlements you cached after call_begin and pick the right tier
|
|
308
|
+
const { model } = selectCapabilities(session.entitlements.allowed);
|
|
309
|
+
|
|
310
|
+
const completion = await openai.chat.completions.create({
|
|
311
|
+
model,
|
|
312
|
+
messages: [{ role: "user", content: "Hello!" }],
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`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:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
await openai.chat.completions.create(
|
|
320
|
+
{ messages: [{ role: "user", content: "Hello!" }] },
|
|
321
|
+
{
|
|
322
|
+
headers: {
|
|
323
|
+
"x-usagetap-customer-id": currentUser.id,
|
|
324
|
+
"x-usagetap-feature": "chat.premium",
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Unified `/call` endpoint (API-only)
|
|
331
|
+
|
|
332
|
+
Need a single round-trip without the SDK? The public REST API exposes `POST /call`, which wraps `call_begin`, an optional vendor invocation, and `call_end` into one atomic request. Supply your usual begin payload plus an optional `vendor` block containing the URL, headers, and body to execute. UsageTap merges usage metrics from the vendor response with any explicit overrides before finalizing the call.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
async function getEntitlementsFor(customerId: string) {
|
|
336
|
+
// Call begin upfront or reuse a cached begin payload for this customer + feature
|
|
337
|
+
return sessionStore.read(customerId); // pseudo-code: use your own persistence layer
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const entitlements = await getEntitlementsFor("cust_123"); // stash begin.data.allowed somewhere durable
|
|
341
|
+
const { model } = selectCapabilities(entitlements.allowed);
|
|
342
|
+
|
|
343
|
+
const response = await fetch(`${baseUrl}/call`, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: {
|
|
346
|
+
Authorization: `Bearer ${process.env.USAGETAP_API_KEY}`,
|
|
347
|
+
Accept: "application/vnd.usagetap.v1+json",
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify({
|
|
351
|
+
customerId: "cust_123",
|
|
352
|
+
requested: { standard: true, premium: true, search: true, reasoningLevel: "MEDIUM" },
|
|
353
|
+
feature: "chat.completions",
|
|
354
|
+
idempotency: crypto.randomUUID(),
|
|
355
|
+
vendor: {
|
|
356
|
+
url: "https://api.openai.com/v1/chat/completions",
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: {
|
|
359
|
+
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
|
360
|
+
"Content-Type": "application/json",
|
|
361
|
+
},
|
|
362
|
+
body: {
|
|
363
|
+
model,
|
|
364
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
365
|
+
},
|
|
366
|
+
responseType: "json",
|
|
367
|
+
},
|
|
368
|
+
usage: { modelUsed: model },
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const envelope = await response.json();
|
|
373
|
+
if (!response.ok || envelope.result.status !== "ACCEPTED") {
|
|
374
|
+
throw new Error(`UsageTap /call failed: ${envelope.result.code}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const { begin, end, vendor, endUsage } = envelope.data;
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
- When the `vendor` block is omitted, `/call` simply runs begin → end using the provided `usage` overrides.
|
|
381
|
+
- Non-2xx vendor responses still trigger `call_end`; the envelope returns `CALL_VENDOR_WARNING` alongside vendor error metadata.
|
|
382
|
+
- The canonical media type `application/vnd.usagetap.v1+json` is required; the SDK already sends this header automatically when you rely on `UsageTapClient`.
|
|
383
|
+
|
|
384
|
+
## Exports
|
|
385
|
+
|
|
386
|
+
Key exports from `@usagetap/sdk`:
|
|
387
|
+
|
|
388
|
+
- `UsageTapClient` – minimal HTTP client for `call_begin` and `call_end`.
|
|
389
|
+
- `wrapOpenAI` – wraps an OpenAI client instance with automatic begin/end handling.
|
|
390
|
+
- `wrapFetch` – wraps a fetch function to automatically instrument OpenAI API calls (minimal integration).
|
|
391
|
+
- `withUsage` / `withUsageMiddleware` – Express middleware for server-side usage tracking (requires `express` peer dependency).
|
|
392
|
+
- `useChatWithUsage` – React hook for chat UIs with automatic usage tracking (import from `@usagetap/sdk/react`, requires `react` peer dependency).
|
|
393
|
+
- `toNextResponse` / `pipeToResponse` – helpers for turning wrapped streams into HTTP responses.
|
|
394
|
+
- `streamOpenAIRoute` – factory for Next.js App Router handlers that stream chat completions with UsageTap instrumentation.
|
|
395
|
+
- `createOpenAIAdapter` – lower-level adapter if you need direct control instead of the proxy client.
|
|
396
|
+
|
|
397
|
+
All helpers are designed for server runtimes. Use `UsageTapClient` with `allowBrowser: true` only for sandbox/test scenarios.
|
|
398
|
+
|
|
399
|
+
## Response envelope (canonical only)
|
|
400
|
+
|
|
401
|
+
UsageTap responds exclusively with the canonical `{ result, data, correlationId }` envelope for every endpoint. The SDK automatically sends `Accept: application/vnd.usagetap.v1+json`, parses the envelope, and returns strongly typed data structures. Transitional `raw` payloads and the `normalize*` helpers have been removed—`response.data` already contains the canonical shape you should persist or render.
|
|
402
|
+
|
|
403
|
+
### Example `call_begin` success
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"result": {
|
|
408
|
+
"status": "ACCEPTED",
|
|
409
|
+
"code": "CALL_BEGIN_SUCCESS",
|
|
410
|
+
"timestamp": "2025-10-04T18:21:37.482Z"
|
|
411
|
+
},
|
|
412
|
+
"data": {
|
|
413
|
+
"callId": "call_123",
|
|
414
|
+
"startTime": "2025-10-04T18:21:37.482Z",
|
|
415
|
+
"policy": "DOWNGRADE",
|
|
416
|
+
"newCustomer": false,
|
|
417
|
+
"canceled": false,
|
|
418
|
+
"allowed": {
|
|
419
|
+
"standard": true,
|
|
420
|
+
"premium": true,
|
|
421
|
+
"audio": false,
|
|
422
|
+
"image": false,
|
|
423
|
+
"search": true,
|
|
424
|
+
"reasoningLevel": "MEDIUM"
|
|
425
|
+
},
|
|
426
|
+
"entitlementHints": {
|
|
427
|
+
"suggestedModelTier": "standard",
|
|
428
|
+
"reasoningLevel": "MEDIUM",
|
|
429
|
+
"policy": "DOWNGRADE",
|
|
430
|
+
"downgrade": {
|
|
431
|
+
"reason": "PREMIUM_QUOTA_EXHAUSTED",
|
|
432
|
+
"fallbackTier": "standard"
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
"meters": {
|
|
436
|
+
"standardCalls": {
|
|
437
|
+
"remaining": 12,
|
|
438
|
+
"limit": 20,
|
|
439
|
+
"used": 8,
|
|
440
|
+
"unlimited": false,
|
|
441
|
+
"ratio": 0.6
|
|
442
|
+
},
|
|
443
|
+
"premiumCalls": {
|
|
444
|
+
"remaining": null,
|
|
445
|
+
"limit": null,
|
|
446
|
+
"used": null,
|
|
447
|
+
"unlimited": true,
|
|
448
|
+
"ratio": null
|
|
449
|
+
},
|
|
450
|
+
"standardTokens": {
|
|
451
|
+
"remaining": 800,
|
|
452
|
+
"limit": 1000,
|
|
453
|
+
"used": 200,
|
|
454
|
+
"unlimited": false,
|
|
455
|
+
"ratio": 0.8
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
"remainingRatios": {
|
|
459
|
+
"standardCalls": 0.6,
|
|
460
|
+
"standardTokens": 0.8
|
|
461
|
+
},
|
|
462
|
+
"subscription": {
|
|
463
|
+
"id": "sub_123",
|
|
464
|
+
"usagePlanVersionId": "plan_2025_01",
|
|
465
|
+
"planName": "Pro",
|
|
466
|
+
"planVersion": "2025-01",
|
|
467
|
+
"limitType": "DOWNGRADE",
|
|
468
|
+
"reasoningLevel": "MEDIUM",
|
|
469
|
+
"lastReplenishedAt": "2025-10-04T00:00:00.000Z",
|
|
470
|
+
"nextReplenishAt": "2025-11-04T00:00:00.000Z",
|
|
471
|
+
"subscriptionVersion": 14
|
|
472
|
+
},
|
|
473
|
+
"models": {
|
|
474
|
+
"standard": ["gpt5-mini"],
|
|
475
|
+
"premium": ["gpt5"]
|
|
476
|
+
},
|
|
477
|
+
"idempotency": {
|
|
478
|
+
"key": "call_123",
|
|
479
|
+
"source": "derived"
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
"correlationId": "corr_abc123"
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
`UsageTapClient` exposes the normalized structure via `UsageTapSuccessResponse<BeginCallResponseBody>`. In addition to the flattened `allowed` map, the begin response now ships richer metadata:
|
|
487
|
+
|
|
488
|
+
- `entitlementHints` summarises the recommended model tier and downgrade rationale based on the active policy.
|
|
489
|
+
- `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.
|
|
490
|
+
- `subscription` contains the active plan identity, versioning, and upcoming replenishment timestamps so you can render customer-facing UI without querying Dynamo yourself.
|
|
491
|
+
- `models` surfaces per-organization vendor hints (e.g. standard vs. premium model shortlists).
|
|
492
|
+
- `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.
|
|
493
|
+
- `plan` and `balances` remain available alongside the core begin payload for backwards compatibility with earlier SDK versions.
|
|
494
|
+
|
|
495
|
+
### Example `call_end` success
|
|
496
|
+
|
|
497
|
+
```json
|
|
498
|
+
{
|
|
499
|
+
"result": {
|
|
500
|
+
"status": "ACCEPTED",
|
|
501
|
+
"code": "CALL_END_SUCCESS",
|
|
502
|
+
"timestamp": "2025-10-04T18:21:52.103Z"
|
|
503
|
+
},
|
|
504
|
+
"data": {
|
|
505
|
+
"callId": "call_123",
|
|
506
|
+
"costUSD": 0,
|
|
507
|
+
"metered": {
|
|
508
|
+
"tokens": 768,
|
|
509
|
+
"calls": 1,
|
|
510
|
+
"searches": 1
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
"correlationId": "corr_abc123"
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
`metered` is derived from the raw Dynamo deltas. Additional meters (audio seconds, reasoning tokens, balances) will populate in later phases without breaking the contract.
|
|
518
|
+
|
|
519
|
+
### Raw fetch integrations
|
|
520
|
+
|
|
521
|
+
Prefer `UsageTapClient` whenever possible—it handles retries, headers, and idempotency for you. If you still need to work with `fetch` directly, remember to request the canonical media type and consume the envelope shape directly:
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
import type { BeginCallResponseBody, EndCallResponseBody } from "@usagetap/sdk";
|
|
525
|
+
|
|
526
|
+
const beginResponse = await fetch(`${baseUrl}/call_begin`, {
|
|
527
|
+
method: "POST",
|
|
528
|
+
headers: {
|
|
529
|
+
Authorization: `Bearer ${apiKey}`,
|
|
530
|
+
Accept: "application/vnd.usagetap.v1+json",
|
|
531
|
+
"Content-Type": "application/json",
|
|
532
|
+
},
|
|
533
|
+
body: JSON.stringify(payload),
|
|
534
|
+
}).then((r) => r.json());
|
|
535
|
+
|
|
536
|
+
if (beginResponse.result.status !== "ACCEPTED") {
|
|
537
|
+
throw new Error(`call_begin failed: ${beginResponse.result.code}`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const begin = beginResponse.data as BeginCallResponseBody;
|
|
541
|
+
|
|
542
|
+
// ...later, when closing the call
|
|
543
|
+
|
|
544
|
+
const endResponse = await fetch(`${baseUrl}/call_end`, {
|
|
545
|
+
method: "POST",
|
|
546
|
+
headers: {
|
|
547
|
+
Authorization: `Bearer ${apiKey}`,
|
|
548
|
+
Accept: "application/vnd.usagetap.v1+json",
|
|
549
|
+
"Content-Type": "application/json",
|
|
550
|
+
},
|
|
551
|
+
body: JSON.stringify({ callId: begin.callId }),
|
|
552
|
+
}).then((r) => r.json());
|
|
553
|
+
|
|
554
|
+
if (endResponse.result.status !== "ACCEPTED") {
|
|
555
|
+
throw new Error(`call_end failed: ${endResponse.result.code}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const end = endResponse.data as EndCallResponseBody;
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
The canonical payloads (`BeginCallResponseBody`, `EndCallResponseBody`, etc.) now match the envelope exactly, keeping SDK and raw integrations aligned without extra helper utilities.
|