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