@usagetap/sdk 1.4.0 → 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 (52) hide show
  1. package/README.md +156 -18
  2. package/dist/adapters/anthropic.cjs +195 -235
  3. package/dist/adapters/anthropic.cjs.map +1 -1
  4. package/dist/adapters/anthropic.d.cts +1 -1
  5. package/dist/adapters/anthropic.d.ts +1 -1
  6. package/dist/adapters/anthropic.mjs +195 -235
  7. package/dist/adapters/anthropic.mjs.map +1 -1
  8. package/dist/adapters/openai.cjs +256 -274
  9. package/dist/adapters/openai.cjs.map +1 -1
  10. package/dist/adapters/openai.d.cts +1 -1
  11. package/dist/adapters/openai.d.ts +1 -1
  12. package/dist/adapters/openai.mjs +256 -274
  13. package/dist/adapters/openai.mjs.map +1 -1
  14. package/dist/adapters/openrouter.cjs +256 -274
  15. package/dist/adapters/openrouter.cjs.map +1 -1
  16. package/dist/adapters/openrouter.d.cts +1 -1
  17. package/dist/adapters/openrouter.d.ts +1 -1
  18. package/dist/adapters/openrouter.mjs +256 -274
  19. package/dist/adapters/openrouter.mjs.map +1 -1
  20. package/dist/anthropic/index.cjs +195 -235
  21. package/dist/anthropic/index.cjs.map +1 -1
  22. package/dist/anthropic/index.d.cts +1 -1
  23. package/dist/anthropic/index.d.ts +1 -1
  24. package/dist/anthropic/index.mjs +195 -235
  25. package/dist/anthropic/index.mjs.map +1 -1
  26. package/dist/{client-CExQ8e1T.d.cts → client-C0UiaqVB.d.cts} +141 -61
  27. package/dist/{client-CExQ8e1T.d.ts → client-C0UiaqVB.d.ts} +141 -61
  28. package/dist/express/index.cjs +189 -246
  29. package/dist/express/index.cjs.map +1 -1
  30. package/dist/express/index.d.cts +1 -1
  31. package/dist/express/index.d.ts +1 -1
  32. package/dist/express/index.mjs +189 -246
  33. package/dist/express/index.mjs.map +1 -1
  34. package/dist/index.cjs +391 -175
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +13 -4
  37. package/dist/index.d.ts +13 -4
  38. package/dist/index.mjs +391 -175
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/openai/index.cjs +256 -274
  41. package/dist/openai/index.cjs.map +1 -1
  42. package/dist/openai/index.d.cts +1 -1
  43. package/dist/openai/index.d.ts +1 -1
  44. package/dist/openai/index.mjs +256 -274
  45. package/dist/openai/index.mjs.map +1 -1
  46. package/dist/openrouter/index.cjs +256 -274
  47. package/dist/openrouter/index.cjs.map +1 -1
  48. package/dist/openrouter/index.d.cts +1 -1
  49. package/dist/openrouter/index.d.ts +1 -1
  50. package/dist/openrouter/index.mjs +256 -274
  51. package/dist/openrouter/index.mjs.map +1 -1
  52. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -1027,6 +1027,8 @@ var SummarizationResource = class {
1027
1027
  };
1028
1028
  var GatewayResource = class {
1029
1029
  chat;
1030
+ /** Buffered OpenAI Responses-compatible requests. */
1031
+ responses;
1030
1032
  models;
1031
1033
  batches;
1032
1034
  transport;
@@ -1048,6 +1050,15 @@ var GatewayResource = class {
1048
1050
  })
1049
1051
  }
1050
1052
  };
1053
+ this.responses = {
1054
+ create: (params, options) => this.transport.request({
1055
+ method: "POST",
1056
+ path: "/v1/responses",
1057
+ body: params,
1058
+ options,
1059
+ response: "json"
1060
+ })
1061
+ };
1051
1062
  this.models = {
1052
1063
  list: (options) => this.transport.request({
1053
1064
  method: "GET",
@@ -1133,7 +1144,7 @@ var USER_AGENT = "UsageTapClient";
1133
1144
  var CANONICAL_MEDIA_TYPE2 = "application/vnd.usagetap.v1+json";
1134
1145
  var DEFAULT_BASE_URL = "https://api.usagetap.com";
1135
1146
  var DEFAULT_RUN_INACTIVITY_MS = 60 * 60 * 1e3;
1136
- var SDK_VERSION = "1.4.0" ;
1147
+ var SDK_VERSION = "1.7.0" ;
1137
1148
  var HAS_WINDOW = typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
1138
1149
  var UsageTapClient = class {
1139
1150
  /** OpenAI-compatible chat, model, and native batch operations. */
@@ -1535,9 +1546,18 @@ var UsageTapClient = class {
1535
1546
  responseTokens: request.responseTokens,
1536
1547
  cachedInputTokens: request.cachedInputTokens,
1537
1548
  cacheWriteInputTokens: request.cacheWriteInputTokens,
1549
+ cacheWrite5mInputTokens: request.cacheWrite5mInputTokens,
1550
+ cacheWrite1hInputTokens: request.cacheWrite1hInputTokens,
1538
1551
  reasoningTokens: request.reasoningTokens,
1539
1552
  searches: request.searches,
1540
1553
  audioSeconds: request.audioSeconds,
1554
+ imageInputCount: request.imageInputCount,
1555
+ imageInputTokens: request.imageInputTokens,
1556
+ imageOutputCount: request.imageOutputCount,
1557
+ imageOutputTokens: request.imageOutputTokens,
1558
+ audioInputTokens: request.audioInputTokens,
1559
+ cachedAudioInputTokens: request.cachedAudioInputTokens,
1560
+ audioOutputTokens: request.audioOutputTokens,
1541
1561
  costUsd: response.data.costUSD
1542
1562
  },
1543
1563
  correlationId: response.correlationId
@@ -1624,10 +1644,10 @@ var UsageTapClient = class {
1624
1644
  "incrementCustomMeter requires meterSlot"
1625
1645
  );
1626
1646
  }
1627
- if (!["CUSTOM1", "CUSTOM2"].includes(request.meterSlot)) {
1647
+ if (!["CUSTOM1", "CUSTOM2", "AGENTIC_API"].includes(request.meterSlot)) {
1628
1648
  throw new UsageTapError(
1629
1649
  "USAGETAP_BAD_REQUEST",
1630
- "meterSlot must be CUSTOM1 or CUSTOM2"
1650
+ "meterSlot must be CUSTOM1, CUSTOM2 or AGENTIC_API"
1631
1651
  );
1632
1652
  }
1633
1653
  if (typeof request.amount !== "number" || !Number.isFinite(request.amount) || request.amount <= 0) {
@@ -1700,6 +1720,28 @@ var UsageTapClient = class {
1700
1720
  let handlerResult;
1701
1721
  let handlerError;
1702
1722
  let endCallError;
1723
+ let finalizationDeferred = false;
1724
+ let finalizationPromise;
1725
+ const finalize = () => {
1726
+ if (!finalizationPromise) {
1727
+ finalizationPromise = this.endCall(
1728
+ {
1729
+ callId: beginResponse.data.callId,
1730
+ // Pass context for metric tracking
1731
+ customerId: beginRequest.customerId,
1732
+ feature: beginRequest.feature ?? this.defaultFeature,
1733
+ tags: beginRequest.tags ?? this.defaultTags,
1734
+ ...usage,
1735
+ error: errorPayload
1736
+ },
1737
+ {
1738
+ ...options,
1739
+ correlationId: beginResponse.correlationId
1740
+ }
1741
+ ).then(() => void 0);
1742
+ }
1743
+ return finalizationPromise;
1744
+ };
1703
1745
  const context = {
1704
1746
  begin: beginResponse,
1705
1747
  setUsage: (u) => {
@@ -1707,6 +1749,10 @@ var UsageTapClient = class {
1707
1749
  },
1708
1750
  setError: (err) => {
1709
1751
  errorPayload = err;
1752
+ },
1753
+ deferFinalization: () => {
1754
+ finalizationDeferred = true;
1755
+ return finalize;
1710
1756
  }
1711
1757
  };
1712
1758
  try {
@@ -1720,24 +1766,12 @@ var UsageTapClient = class {
1720
1766
  };
1721
1767
  }
1722
1768
  } finally {
1723
- try {
1724
- await this.endCall(
1725
- {
1726
- callId: beginResponse.data.callId,
1727
- // Pass context for metric tracking
1728
- customerId: beginRequest.customerId,
1729
- feature: beginRequest.feature ?? this.defaultFeature,
1730
- tags: beginRequest.tags ?? this.defaultTags,
1731
- ...usage,
1732
- error: errorPayload
1733
- },
1734
- {
1735
- ...options,
1736
- correlationId: beginResponse.correlationId
1737
- }
1738
- );
1739
- } catch (error) {
1740
- endCallError = error;
1769
+ if (handlerError || !finalizationDeferred) {
1770
+ try {
1771
+ await finalize();
1772
+ } catch (error) {
1773
+ endCallError = error;
1774
+ }
1741
1775
  }
1742
1776
  }
1743
1777
  if (handlerError) {
@@ -2079,7 +2113,8 @@ var UsageTapClient = class {
2079
2113
  }
2080
2114
  toHttpError(status, payload, correlationId) {
2081
2115
  const code = mapStatusToErrorCode(status);
2082
- const retryable = isRetryableStatus(status);
2116
+ const apiCode = payload?.error?.code ?? payload?.result?.code ?? "UNKNOWN";
2117
+ const retryable = isRetryableStatus(status) || isRetryableApiCode(apiCode);
2083
2118
  const message = payload?.error?.message ?? payload?.result?.message ?? `UsageTap responded with HTTP ${status}`;
2084
2119
  return new UsageTapError(code, message, {
2085
2120
  status,
@@ -2112,7 +2147,7 @@ function isRetryableStatus(status) {
2112
2147
  }
2113
2148
  function isRetryableApiCode(code) {
2114
2149
  const normalized = code.toUpperCase();
2115
- return normalized.includes("TRANSIENT") || normalized.includes("RETRY") || normalized.includes("TIMEOUT") || normalized.includes("THROTTLE") || normalized.includes("RATE_LIMIT");
2150
+ return normalized === "PAYG_CONFLICT" || normalized.includes("TRANSIENT") || normalized.includes("RETRY") || normalized.includes("TIMEOUT") || normalized.includes("THROTTLE") || normalized.includes("RATE_LIMIT");
2116
2151
  }
2117
2152
  function mapApiCodeToError(code) {
2118
2153
  const normalized = code.toUpperCase();
@@ -2181,6 +2216,12 @@ function wrapEndCallError(error, correlationId) {
2181
2216
  }
2182
2217
 
2183
2218
  // src/adapters/fetch-wrapper.ts
2219
+ var USAGETAP_CONTEXT_HEADERS = [
2220
+ "x-usagetap-customer-id",
2221
+ "x-usagetap-feature",
2222
+ "x-usagetap-tags",
2223
+ "x-usagetap-run-id"
2224
+ ];
2184
2225
  function isJsonRecord(value) {
2185
2226
  return typeof value === "object" && value !== null && !Array.isArray(value);
2186
2227
  }
@@ -2209,59 +2250,103 @@ function parseStringArray(value) {
2209
2250
  }
2210
2251
  return void 0;
2211
2252
  }
2253
+ function contextOverrideFromHeaders(headers) {
2254
+ const context = {};
2255
+ const customerId = headers.get("x-usagetap-customer-id");
2256
+ const feature = headers.get("x-usagetap-feature");
2257
+ const tags = headers.get("x-usagetap-tags");
2258
+ const runId = headers.get("x-usagetap-run-id");
2259
+ if (customerId) context.customerId = customerId;
2260
+ if (feature) context.feature = feature;
2261
+ if (tags) {
2262
+ const parsed = parseStringArray(tags);
2263
+ if (parsed) context.tags = parsed;
2264
+ }
2265
+ if (runId) context.runId = runId;
2266
+ return context;
2267
+ }
2268
+ function ensureStreamUsageBody(body) {
2269
+ const streamOptions = isJsonRecord(body.stream_options) ? body.stream_options : {};
2270
+ return {
2271
+ ...body,
2272
+ stream_options: {
2273
+ ...streamOptions,
2274
+ include_usage: true
2275
+ }
2276
+ };
2277
+ }
2278
+ async function bodyTextFromInit(body) {
2279
+ if (typeof body === "string") return body;
2280
+ if (body instanceof URLSearchParams) return body.toString();
2281
+ if (body instanceof Blob) return body.text();
2282
+ if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
2283
+ if (ArrayBuffer.isView(body)) {
2284
+ return new TextDecoder().decode(
2285
+ new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
2286
+ );
2287
+ }
2288
+ return void 0;
2289
+ }
2212
2290
  function wrapFetch(usageTap, options) {
2213
2291
  const {
2214
2292
  defaultContext,
2215
2293
  baseFetch = globalThis.fetch,
2216
2294
  autoIdempotency = true,
2217
2295
  isOpenAIEndpoint = defaultIsOpenAIEndpoint,
2218
- provider = "openai"
2296
+ provider = "openai",
2297
+ onMeteringError,
2298
+ strictMetering = false
2219
2299
  } = options;
2300
+ const meteringFailureOptions = { onMeteringError, strictMetering };
2220
2301
  return async function wrappedFetch(input, init) {
2221
2302
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2222
2303
  if (!isOpenAIEndpoint(url)) {
2223
2304
  return baseFetch(input, init);
2224
2305
  }
2306
+ const inputRequest = input instanceof Request ? input : void 0;
2307
+ const effectiveHeaders = new Headers(inputRequest?.headers);
2308
+ new Headers(init?.headers).forEach((value, key) => effectiveHeaders.set(key, value));
2309
+ const vendorHeaders = new Headers(effectiveHeaders);
2310
+ const contextOverride = contextOverrideFromHeaders(effectiveHeaders);
2311
+ for (const header of USAGETAP_CONTEXT_HEADERS) {
2312
+ vendorHeaders.delete(header);
2313
+ }
2225
2314
  let body;
2226
2315
  let isStreaming = false;
2227
- const contextOverride = {};
2228
2316
  try {
2229
- if (init?.body && typeof init.body === "string") {
2230
- const parsedBody = parseJsonRecord(init.body);
2231
- if (!parsedBody) {
2232
- return baseFetch(input, init);
2233
- }
2234
- body = parsedBody;
2235
- isStreaming = parsedBody.stream === true;
2236
- const headers = new Headers(init.headers);
2237
- const customerIdHeader = headers.get("x-usagetap-customer-id");
2238
- const featureHeader = headers.get("x-usagetap-feature");
2239
- const tagsHeader = headers.get("x-usagetap-tags");
2240
- const runIdHeader = headers.get("x-usagetap-run-id");
2241
- if (customerIdHeader) {
2242
- contextOverride.customerId = customerIdHeader;
2243
- }
2244
- if (featureHeader) {
2245
- contextOverride.feature = featureHeader;
2246
- }
2247
- if (tagsHeader) {
2248
- const tags = parseStringArray(tagsHeader);
2249
- if (tags) {
2250
- contextOverride.tags = tags;
2251
- }
2252
- }
2253
- if (runIdHeader) {
2254
- contextOverride.runId = runIdHeader;
2255
- }
2317
+ if (init?.body) {
2318
+ const bodyText = await bodyTextFromInit(init.body);
2319
+ body = bodyText ? parseJsonRecord(bodyText) : void 0;
2320
+ } else if (inputRequest?.body) {
2321
+ body = parseJsonRecord(await inputRequest.clone().text());
2256
2322
  }
2257
2323
  } catch {
2258
- return baseFetch(input, init);
2324
+ body = void 0;
2325
+ }
2326
+ if (!body) {
2327
+ const error = new Error(
2328
+ "Unable to parse the intercepted provider request body as JSON"
2329
+ );
2330
+ await reportMeteringError(meteringFailureOptions, {
2331
+ stage: "request",
2332
+ error
2333
+ });
2334
+ if (strictMetering) throw error;
2335
+ return baseFetch(input, { ...init, headers: vendorHeaders });
2259
2336
  }
2337
+ isStreaming = body.stream === true;
2338
+ const vendorBody = isStreaming && url.includes("/v1/chat/completions") ? JSON.stringify(ensureStreamUsageBody(body)) : void 0;
2260
2339
  const context = {
2261
2340
  ...defaultContext,
2262
2341
  ...contextOverride,
2263
2342
  idempotency: autoIdempotency ? crypto.randomUUID() : void 0
2264
2343
  };
2344
+ if (requestUsesWebSearch(body)) {
2345
+ context.requested = {
2346
+ ...context.requested ?? {},
2347
+ search: true
2348
+ };
2349
+ }
2265
2350
  let callState;
2266
2351
  try {
2267
2352
  const beginResponse = await usageTap.beginCall(context);
@@ -2280,36 +2365,64 @@ function wrapFetch(usageTap, options) {
2280
2365
  batch: context.batch,
2281
2366
  pricingMode: context.batch ? "batch" : "standard"
2282
2367
  } : {}
2283
- },
2284
- finalized: false
2368
+ }
2285
2369
  };
2286
2370
  } catch (error) {
2287
2371
  if (isUsageTapError(error) && error.code === "USAGETAP_CIRCUIT_OPEN") {
2288
2372
  throw error;
2289
2373
  }
2290
- console.error("[wrapFetch] Failed to begin call:", error);
2291
- return baseFetch(input, init);
2374
+ await reportMeteringError(meteringFailureOptions, {
2375
+ stage: "begin",
2376
+ error
2377
+ });
2378
+ if (strictMetering) throw error;
2379
+ return baseFetch(input, {
2380
+ ...init,
2381
+ headers: vendorHeaders,
2382
+ ...vendorBody ? { body: vendorBody } : {}
2383
+ });
2384
+ }
2385
+ if (callState.correlationId) {
2386
+ vendorHeaders.set("x-usage-correlation-id", callState.correlationId);
2292
2387
  }
2293
2388
  const modifiedInit = {
2294
2389
  ...init,
2295
- headers: {
2296
- ...init?.headers,
2297
- "x-usage-correlation-id": callState.correlationId || ""
2298
- }
2390
+ headers: vendorHeaders,
2391
+ ...vendorBody ? { body: vendorBody } : {}
2299
2392
  };
2300
2393
  try {
2301
2394
  const response = await baseFetch(input, modifiedInit);
2395
+ if (!response.ok) {
2396
+ await finalizeHttpError(
2397
+ response,
2398
+ callState,
2399
+ usageTap,
2400
+ meteringFailureOptions
2401
+ );
2402
+ return response;
2403
+ }
2302
2404
  if (isStreaming) {
2303
- return wrapStreamingResponse(response, callState, usageTap);
2405
+ return await wrapStreamingResponse(
2406
+ response,
2407
+ callState,
2408
+ usageTap,
2409
+ meteringFailureOptions
2410
+ );
2304
2411
  } else {
2305
- return await wrapNonStreamingResponse(response, callState, usageTap, body);
2412
+ return await wrapNonStreamingResponse(
2413
+ response,
2414
+ callState,
2415
+ usageTap,
2416
+ body,
2417
+ meteringFailureOptions
2418
+ );
2306
2419
  }
2307
2420
  } catch (error) {
2308
2421
  const message = error instanceof Error ? error.message : String(error);
2309
- await finalizeCall(callState, usageTap, {
2422
+ await finalizeCall(callState, usageTap, meteringFailureOptions, {
2310
2423
  code: "VENDOR_ERROR",
2311
2424
  message
2312
- });
2425
+ }, { responseErrorMessage: message });
2313
2426
  throw error;
2314
2427
  }
2315
2428
  };
@@ -2317,137 +2430,217 @@ function wrapFetch(usageTap, options) {
2317
2430
  function defaultIsOpenAIEndpoint(url) {
2318
2431
  return url.includes("/v1/chat/completions") || url.includes("/v1/responses") || url.includes("/v1/embeddings");
2319
2432
  }
2320
- async function wrapNonStreamingResponse(response, callState, usageTap, requestBody) {
2433
+ function requestUsesWebSearch(body) {
2434
+ return Array.isArray(body?.tools) && body.tools.some((tool) => {
2435
+ if (!isJsonRecord(tool)) return false;
2436
+ return tool.type === "web_search" || tool.type === "web_search_preview";
2437
+ });
2438
+ }
2439
+ function usageFromOpenAIResponse(parsed, provider) {
2440
+ const usage = {};
2441
+ const usageBlock = parsed.usage;
2442
+ if (isJsonRecord(usageBlock)) {
2443
+ const usageRecord = usageBlock;
2444
+ const promptDetails = isJsonRecord(
2445
+ usageRecord.prompt_tokens_details ?? usageRecord.input_tokens_details
2446
+ ) ? usageRecord.prompt_tokens_details ?? usageRecord.input_tokens_details : void 0;
2447
+ const completionDetails = isJsonRecord(
2448
+ usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details
2449
+ ) ? usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details : void 0;
2450
+ const cachedTokenDetails = isJsonRecord(promptDetails?.cached_tokens_details) ? promptDetails.cached_tokens_details : void 0;
2451
+ const cacheCreation = isJsonRecord(usageRecord.cache_creation) ? usageRecord.cache_creation : void 0;
2452
+ const promptTokens = readNumber(
2453
+ usageRecord.prompt_tokens ?? usageRecord.input_tokens
2454
+ );
2455
+ if (promptTokens !== void 0) usage.inputTokens = promptTokens;
2456
+ const completionTokens = readNumber(
2457
+ usageRecord.completion_tokens ?? usageRecord.output_tokens
2458
+ );
2459
+ if (completionTokens !== void 0) usage.responseTokens = completionTokens;
2460
+ const cachedTokens = readNumber(
2461
+ usageRecord.prompt_cache_hit_tokens ?? usageRecord.cache_read_input_tokens ?? usageRecord.cached_tokens ?? promptDetails?.cached_tokens
2462
+ );
2463
+ if (cachedTokens !== void 0) usage.cachedInputTokens = cachedTokens;
2464
+ const cacheWriteTokens = readNumber(
2465
+ usageRecord.cache_creation_input_tokens ?? usageRecord.cache_write_input_tokens ?? usageRecord.cache_write_tokens ?? promptDetails?.cache_write_tokens ?? promptDetails?.cache_creation_tokens
2466
+ );
2467
+ if (cacheWriteTokens !== void 0) {
2468
+ usage.cacheWriteInputTokens = cacheWriteTokens;
2469
+ }
2470
+ const cacheWrite5m = readNumber(
2471
+ usageRecord.cache_write_5m_input_tokens ?? cacheCreation?.ephemeral_5m_input_tokens
2472
+ );
2473
+ if (cacheWrite5m !== void 0) usage.cacheWrite5mInputTokens = cacheWrite5m;
2474
+ const cacheWrite1h = readNumber(
2475
+ usageRecord.cache_write_1h_input_tokens ?? cacheCreation?.ephemeral_1h_input_tokens
2476
+ );
2477
+ if (cacheWrite1h !== void 0) usage.cacheWrite1hInputTokens = cacheWrite1h;
2478
+ if (provider?.toLowerCase() === "anthropic" && promptTokens !== void 0) {
2479
+ usage.inputTokens = promptTokens + (cachedTokens ?? 0) + (cacheWriteTokens ?? 0);
2480
+ }
2481
+ const audioInputTokens = readNumber(promptDetails?.audio_tokens);
2482
+ if (audioInputTokens !== void 0) usage.audioInputTokens = audioInputTokens;
2483
+ const cachedAudioInputTokens = readNumber(
2484
+ promptDetails?.cached_audio_tokens ?? cachedTokenDetails?.audio_tokens
2485
+ );
2486
+ if (cachedAudioInputTokens !== void 0) {
2487
+ usage.cachedAudioInputTokens = cachedAudioInputTokens;
2488
+ }
2489
+ const imageInputTokens = readNumber(promptDetails?.image_tokens);
2490
+ if (imageInputTokens !== void 0) usage.imageInputTokens = imageInputTokens;
2491
+ const imageOutputTokens = readNumber(completionDetails?.image_tokens);
2492
+ if (imageOutputTokens !== void 0) usage.imageOutputTokens = imageOutputTokens;
2493
+ const audioOutputTokens = readNumber(completionDetails?.audio_tokens);
2494
+ if (audioOutputTokens !== void 0) usage.audioOutputTokens = audioOutputTokens;
2495
+ const reasoningTokens = readNumber(
2496
+ usageRecord.reasoning_tokens ?? completionDetails?.reasoning_tokens
2497
+ );
2498
+ if (reasoningTokens !== void 0) usage.reasoningTokens = reasoningTokens;
2499
+ const serverToolUse = isJsonRecord(usageRecord.server_tool_use) ? usageRecord.server_tool_use : void 0;
2500
+ const outputSearches = Array.isArray(parsed.output) ? parsed.output.filter(
2501
+ (item) => isJsonRecord(item) && item.type === "web_search_call"
2502
+ ).length : 0;
2503
+ const searches = readNumber(
2504
+ usageRecord.searches ?? usageRecord.web_search_queries ?? serverToolUse?.web_search_requests ?? (outputSearches > 0 ? outputSearches : void 0)
2505
+ );
2506
+ if (searches !== void 0 && searches > 0) usage.searches = searches;
2507
+ }
2508
+ const modelFromResponse = readString(parsed.model);
2509
+ if (modelFromResponse) usage.modelUsed = modelFromResponse;
2510
+ const reasoning = isJsonRecord(parsed.reasoning) ? parsed.reasoning : void 0;
2511
+ const responseEffort = normalizeReasoningEffort(reasoning?.effort);
2512
+ if (responseEffort) {
2513
+ usage.reasoningEffort = responseEffort;
2514
+ usage.reasoningEffortSource = "provider_response";
2515
+ }
2516
+ const responseMode = readString(reasoning?.type ?? reasoning?.mode);
2517
+ if (responseMode) usage.reasoningMode = responseMode;
2518
+ return usage;
2519
+ }
2520
+ async function wrapNonStreamingResponse(response, callState, usageTap, requestBody, meteringFailureOptions) {
2321
2521
  const clonedResponse = response.clone();
2322
2522
  try {
2323
2523
  const parsed = await clonedResponse.json();
2324
- const usage = {};
2325
- if (isJsonRecord(parsed)) {
2326
- const usageBlock = parsed.usage;
2327
- if (isJsonRecord(usageBlock)) {
2328
- const usageRecord = usageBlock;
2329
- const promptDetails = isJsonRecord(usageRecord.prompt_tokens_details) ? usageRecord.prompt_tokens_details : void 0;
2330
- const completionDetails = isJsonRecord(
2331
- usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details
2332
- ) ? usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details : void 0;
2333
- const promptTokens = readNumber(usageRecord.prompt_tokens ?? usageRecord.input_tokens);
2334
- if (promptTokens !== void 0) {
2335
- usage.inputTokens = promptTokens;
2336
- }
2337
- const completionTokens = readNumber(usageRecord.completion_tokens ?? usageRecord.output_tokens);
2338
- if (completionTokens !== void 0) {
2339
- usage.responseTokens = completionTokens;
2340
- }
2341
- const cachedTokens = readNumber(
2342
- usageRecord.prompt_cache_hit_tokens ?? usageRecord.cache_read_input_tokens ?? usageRecord.cached_tokens ?? promptDetails?.cached_tokens
2343
- );
2344
- if (cachedTokens !== void 0) {
2345
- usage.cachedInputTokens = cachedTokens;
2346
- }
2347
- const reasoningTokens = readNumber(
2348
- usageRecord.reasoning_tokens ?? completionDetails?.reasoning_tokens
2349
- );
2350
- if (reasoningTokens !== void 0) {
2351
- usage.reasoningTokens = reasoningTokens;
2352
- }
2353
- }
2354
- const modelFromResponse = readString(parsed.model);
2355
- if (modelFromResponse) {
2356
- usage.modelUsed = modelFromResponse;
2357
- }
2358
- const reasoning = isJsonRecord(parsed.reasoning) ? parsed.reasoning : void 0;
2359
- const responseEffort = normalizeReasoningEffort(reasoning?.effort);
2360
- if (responseEffort) {
2361
- usage.reasoningEffort = responseEffort;
2362
- usage.reasoningEffortSource = "provider_response";
2363
- }
2364
- const responseMode = readString(reasoning?.type ?? reasoning?.mode);
2365
- if (responseMode) usage.reasoningMode = responseMode;
2366
- }
2524
+ const usage = isJsonRecord(parsed) ? usageFromOpenAIResponse(parsed, readString(callState.usage.providerUsed)) : {};
2367
2525
  if (!usage.modelUsed && requestBody) {
2368
2526
  const requestModel = readString(requestBody.model);
2369
2527
  if (requestModel) {
2370
2528
  usage.modelUsed = requestModel;
2371
2529
  }
2372
2530
  }
2373
- await finalizeCall(callState, usageTap, void 0, usage);
2531
+ await finalizeCall(callState, usageTap, meteringFailureOptions, void 0, {
2532
+ ...usage,
2533
+ responseStatusCode: response.status
2534
+ });
2374
2535
  return response;
2375
2536
  } catch (error) {
2376
2537
  const message = error instanceof Error ? error.message : String(error);
2377
- await finalizeCall(callState, usageTap, {
2538
+ await finalizeCall(callState, usageTap, meteringFailureOptions, {
2378
2539
  code: "RESPONSE_PARSE_ERROR",
2379
2540
  message
2541
+ }, {
2542
+ responseStatusCode: response.status,
2543
+ responseErrorMessage: message
2380
2544
  });
2381
2545
  return response;
2382
2546
  }
2383
2547
  }
2384
- function wrapStreamingResponse(response, callState, usageTap) {
2548
+ async function finalizeHttpError(response, callState, usageTap, meteringFailureOptions) {
2549
+ let message = `${response.status} ${response.statusText || "Provider request failed"}`.trim();
2550
+ try {
2551
+ const text = await response.clone().text();
2552
+ const parsed = parseJsonRecord(text);
2553
+ const errorRecord = parsed && isJsonRecord(parsed.error) ? parsed.error : void 0;
2554
+ message = readString(errorRecord?.message) ?? readString(parsed?.message) ?? (text.trim() || message);
2555
+ } catch {
2556
+ }
2557
+ await finalizeCall(callState, usageTap, meteringFailureOptions, {
2558
+ code: "VENDOR_HTTP_ERROR",
2559
+ message
2560
+ }, {
2561
+ responseStatusCode: response.status,
2562
+ responseErrorMessage: message
2563
+ });
2564
+ }
2565
+ async function wrapStreamingResponse(response, callState, usageTap, meteringFailureOptions) {
2385
2566
  if (!response.body) {
2386
- void finalizeCall(callState, usageTap, {
2567
+ await finalizeCall(callState, usageTap, meteringFailureOptions, {
2387
2568
  code: "NO_RESPONSE_BODY",
2388
2569
  message: "Streaming response has no body"
2570
+ }, {
2571
+ responseStatusCode: response.status,
2572
+ responseErrorMessage: "Streaming response has no body"
2389
2573
  });
2390
2574
  return response;
2391
2575
  }
2392
- const originalBody = response.body;
2576
+ const reader = response.body.getReader();
2393
2577
  const accumulatedUsage = {};
2394
2578
  const textDecoder = new TextDecoder();
2395
- const transformStream = new TransformStream({
2396
- transform(chunk, controller) {
2397
- controller.enqueue(chunk);
2579
+ let pendingSseText = "";
2580
+ const applyUsageEvent = (eventText) => {
2581
+ const data = eventText.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
2582
+ if (!data || data === "[DONE]") return;
2583
+ const parsedRecord = parseJsonRecord(data);
2584
+ if (!parsedRecord) return;
2585
+ const responseRecord = isJsonRecord(parsedRecord.response) ? parsedRecord.response : parsedRecord;
2586
+ Object.assign(
2587
+ accumulatedUsage,
2588
+ usageFromOpenAIResponse(
2589
+ responseRecord,
2590
+ readString(callState.usage.providerUsed)
2591
+ )
2592
+ );
2593
+ };
2594
+ const applyCompleteSseEvents = (final = false) => {
2595
+ const events = pendingSseText.split(/\r?\n\r?\n/);
2596
+ const tail = events.pop() ?? "";
2597
+ pendingSseText = final ? "" : tail;
2598
+ for (const eventText of events) applyUsageEvent(eventText);
2599
+ if (final && tail) applyUsageEvent(tail);
2600
+ };
2601
+ const finalizeStream = async (error) => {
2602
+ pendingSseText += textDecoder.decode();
2603
+ applyCompleteSseEvents(true);
2604
+ await finalizeCall(
2605
+ callState,
2606
+ usageTap,
2607
+ meteringFailureOptions,
2608
+ error,
2609
+ {
2610
+ ...accumulatedUsage,
2611
+ responseStatusCode: response.status,
2612
+ ...error ? { responseErrorMessage: error.message } : {}
2613
+ }
2614
+ );
2615
+ };
2616
+ const wrappedBody = new ReadableStream({
2617
+ async pull(controller) {
2398
2618
  try {
2399
- const text = textDecoder.decode(chunk, { stream: true });
2400
- const lines = text.split("\n");
2401
- for (const line of lines) {
2402
- if (line.startsWith("data: ")) {
2403
- const data = line.slice(6);
2404
- if (data === "[DONE]") continue;
2405
- const parsedRecord = parseJsonRecord(data);
2406
- if (!parsedRecord) {
2407
- continue;
2408
- }
2409
- const usageBlock = parsedRecord.usage;
2410
- if (isJsonRecord(usageBlock)) {
2411
- const usageRecord = usageBlock;
2412
- const promptDetails = isJsonRecord(usageRecord.prompt_tokens_details) ? usageRecord.prompt_tokens_details : void 0;
2413
- const completionDetails = isJsonRecord(
2414
- usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details
2415
- ) ? usageRecord.completion_tokens_details ?? usageRecord.output_tokens_details : void 0;
2416
- const promptTokens = readNumber(usageRecord.prompt_tokens ?? usageRecord.input_tokens);
2417
- if (promptTokens !== void 0) {
2418
- accumulatedUsage.inputTokens = promptTokens;
2419
- }
2420
- const completionTokens = readNumber(usageRecord.completion_tokens ?? usageRecord.output_tokens);
2421
- if (completionTokens !== void 0) {
2422
- accumulatedUsage.responseTokens = completionTokens;
2423
- }
2424
- const cachedTokens = readNumber(
2425
- usageRecord.prompt_cache_hit_tokens ?? usageRecord.cache_read_input_tokens ?? usageRecord.cached_tokens ?? promptDetails?.cached_tokens
2426
- );
2427
- if (cachedTokens !== void 0) {
2428
- accumulatedUsage.cachedInputTokens = cachedTokens;
2429
- }
2430
- const reasoningTokens = readNumber(
2431
- usageRecord.reasoning_tokens ?? completionDetails?.reasoning_tokens
2432
- );
2433
- if (reasoningTokens !== void 0) {
2434
- accumulatedUsage.reasoningTokens = reasoningTokens;
2435
- }
2436
- }
2437
- const model = readString(parsedRecord.model);
2438
- if (model) {
2439
- accumulatedUsage.modelUsed = model;
2440
- }
2441
- }
2619
+ const result = await reader.read();
2620
+ if (result.done) {
2621
+ await finalizeStream();
2622
+ controller.close();
2623
+ return;
2442
2624
  }
2443
- } catch {
2625
+ const chunk = result.value;
2626
+ pendingSseText += textDecoder.decode(chunk, { stream: true });
2627
+ applyCompleteSseEvents();
2628
+ controller.enqueue(chunk);
2629
+ } catch (error) {
2630
+ const message = error instanceof Error ? error.message : String(error);
2631
+ await finalizeStream({ code: "VENDOR_STREAM_ERROR", message }).catch(() => void 0);
2632
+ controller.error(error);
2444
2633
  }
2445
2634
  },
2446
- async flush() {
2447
- await finalizeCall(callState, usageTap, void 0, accumulatedUsage);
2635
+ async cancel(reason) {
2636
+ const message = reason instanceof Error ? reason.message : typeof reason === "string" && reason ? reason : "Provider stream consumption was cancelled before completion";
2637
+ try {
2638
+ await reader.cancel(reason);
2639
+ } finally {
2640
+ await finalizeStream({ code: "STREAM_ABORTED", message });
2641
+ }
2448
2642
  }
2449
2643
  });
2450
- const wrappedBody = originalBody.pipeThrough(transformStream);
2451
2644
  return new Response(wrappedBody, {
2452
2645
  status: response.status,
2453
2646
  statusText: response.statusText,
@@ -2468,23 +2661,46 @@ function executionMetadataFromOpenAIRequest(body, provider) {
2468
2661
  );
2469
2662
  return {
2470
2663
  providerUsed: provider,
2664
+ ...typeof body?.model === "string" ? { modelUsed: body.model } : {},
2471
2665
  ...effort ? { reasoningEffort: effort, reasoningEffortSource: "provider_request" } : {},
2472
2666
  ...mode ? { reasoningMode: mode } : {},
2473
2667
  ...budget !== void 0 ? { reasoningBudgetTokens: budget } : {}
2474
2668
  };
2475
2669
  }
2476
- async function finalizeCall(callState, usageTap, error, usage) {
2477
- if (callState.finalized) return;
2478
- callState.finalized = true;
2670
+ async function finalizeCall(callState, usageTap, meteringFailureOptions, error, usage) {
2671
+ if (!callState.finalizationPromise) {
2672
+ callState.finalizationPromise = (async () => {
2673
+ try {
2674
+ await usageTap.endCall({
2675
+ callId: callState.callId,
2676
+ ...callState.usage,
2677
+ ...usage,
2678
+ error
2679
+ });
2680
+ } catch (endError) {
2681
+ await reportMeteringError(meteringFailureOptions, {
2682
+ stage: "end",
2683
+ callId: callState.callId,
2684
+ error: endError
2685
+ });
2686
+ if (meteringFailureOptions.strictMetering) {
2687
+ throw endError;
2688
+ }
2689
+ }
2690
+ })();
2691
+ }
2692
+ await callState.finalizationPromise;
2693
+ }
2694
+ async function reportMeteringError(options, event) {
2695
+ console.error("[wrapFetch] Metering failure", {
2696
+ stage: event.stage,
2697
+ callId: event.callId,
2698
+ message: event.error instanceof Error ? event.error.message : String(event.error)
2699
+ });
2479
2700
  try {
2480
- await usageTap.endCall({
2481
- callId: callState.callId,
2482
- ...callState.usage,
2483
- ...usage,
2484
- error
2485
- });
2486
- } catch (err) {
2487
- console.error("[wrapFetch] Failed to finalize call:", err);
2701
+ await options.onMeteringError?.(event);
2702
+ } catch (callbackError) {
2703
+ console.error("[wrapFetch] onMeteringError callback failed", callbackError);
2488
2704
  }
2489
2705
  }
2490
2706