@routstr/sdk 0.3.12 → 0.3.13

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 (48) hide show
  1. package/README.md +6 -19
  2. package/dist/browser.d.mts +2 -1
  3. package/dist/browser.d.ts +2 -1
  4. package/dist/browser.js +360 -28
  5. package/dist/browser.js.map +1 -1
  6. package/dist/browser.mjs +355 -29
  7. package/dist/browser.mjs.map +1 -1
  8. package/dist/bun.d.mts +2 -1
  9. package/dist/bun.d.ts +2 -1
  10. package/dist/bun.js +363 -31
  11. package/dist/bun.js.map +1 -1
  12. package/dist/bun.mjs +358 -32
  13. package/dist/bun.mjs.map +1 -1
  14. package/dist/client/index.d.mts +85 -1
  15. package/dist/client/index.d.ts +85 -1
  16. package/dist/client/index.js +358 -27
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/index.mjs +353 -28
  19. package/dist/client/index.mjs.map +1 -1
  20. package/dist/index.d.mts +2 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +360 -28
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.mjs +355 -29
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/node.d.mts +2 -1
  27. package/dist/node.d.ts +2 -1
  28. package/dist/node.js +363 -31
  29. package/dist/node.js.map +1 -1
  30. package/dist/node.mjs +358 -32
  31. package/dist/node.mjs.map +1 -1
  32. package/dist/storage/bun.js +2 -1
  33. package/dist/storage/bun.js.map +1 -1
  34. package/dist/storage/bun.mjs +2 -1
  35. package/dist/storage/bun.mjs.map +1 -1
  36. package/dist/storage/index.js +2 -1
  37. package/dist/storage/index.js.map +1 -1
  38. package/dist/storage/index.mjs +2 -1
  39. package/dist/storage/index.mjs.map +1 -1
  40. package/dist/storage/node.js +2 -1
  41. package/dist/storage/node.js.map +1 -1
  42. package/dist/storage/node.mjs +2 -1
  43. package/dist/storage/node.mjs.map +1 -1
  44. package/dist/wallet/index.js +36 -8
  45. package/dist/wallet/index.js.map +1 -1
  46. package/dist/wallet/index.mjs +36 -8
  47. package/dist/wallet/index.mjs.map +1 -1
  48. package/package.json +3 -1
package/dist/browser.mjs CHANGED
@@ -1858,15 +1858,27 @@ var BalanceManager = class _BalanceManager {
1858
1858
  clearTimeout(timeoutId);
1859
1859
  const requestId = response.headers.get("x-routstr-request-id") || void 0;
1860
1860
  if (!response.ok) {
1861
- const errorData = await response.json().catch(() => ({}));
1862
- this.logger.warn(
1863
- `fetchRefundToken: non-ok response for ${url} status=${response.status} statusText=${response.statusText}`,
1864
- errorData
1865
- );
1861
+ const responseBody = await response.text().catch(() => void 0);
1862
+ let errorData = {};
1863
+ if (responseBody) {
1864
+ try {
1865
+ errorData = JSON.parse(responseBody);
1866
+ } catch {
1867
+ errorData = {};
1868
+ }
1869
+ }
1870
+ this.logger.error("Upstream wallet refund error response", {
1871
+ baseUrl,
1872
+ url,
1873
+ status: response.status,
1874
+ statusText: response.statusText,
1875
+ requestId,
1876
+ body: responseBody ?? "<unable to read response body>"
1877
+ });
1866
1878
  return {
1867
1879
  success: false,
1868
1880
  requestId,
1869
- error: `API key refund failed: ${errorData?.detail || response.statusText}`
1881
+ error: `API key refund failed: ${errorData?.detail || responseBody || response.statusText}`
1870
1882
  };
1871
1883
  }
1872
1884
  const data = await response.json();
@@ -2202,11 +2214,27 @@ var BalanceManager = class _BalanceManager {
2202
2214
  clearTimeout(timeoutId);
2203
2215
  const requestId = response.headers.get("x-routstr-request-id") || void 0;
2204
2216
  if (!response.ok) {
2205
- const errorData = await response.json().catch(() => ({}));
2217
+ const responseBody = await response.text().catch(() => void 0);
2218
+ let errorData = {};
2219
+ if (responseBody) {
2220
+ try {
2221
+ errorData = JSON.parse(responseBody);
2222
+ } catch {
2223
+ errorData = {};
2224
+ }
2225
+ }
2226
+ this.logger.error("Upstream wallet topup error response", {
2227
+ baseUrl,
2228
+ url,
2229
+ status: response.status,
2230
+ statusText: response.statusText,
2231
+ requestId,
2232
+ body: responseBody ?? "<unable to read response body>"
2233
+ });
2206
2234
  return {
2207
2235
  success: false,
2208
2236
  requestId,
2209
- error: errorData?.detail || `Top up failed with status ${response.status}`
2237
+ error: errorData?.detail || responseBody || `Top up failed with status ${response.status}`
2210
2238
  };
2211
2239
  }
2212
2240
  return { success: true, requestId };
@@ -3039,7 +3067,7 @@ var openDatabase = (dbName, storeName) => {
3039
3067
  return Promise.reject(new Error("IndexedDB is not available"));
3040
3068
  }
3041
3069
  return new Promise((resolve, reject) => {
3042
- const request = indexedDB.open(dbName, 2);
3070
+ const request = indexedDB.open(dbName, 3);
3043
3071
  request.onupgradeneeded = () => {
3044
3072
  const db = request.result;
3045
3073
  if (!db.objectStoreNames.contains(storeName)) {
@@ -3052,6 +3080,7 @@ var openDatabase = (dbName, storeName) => {
3052
3080
  utStore.createIndex("baseUrl", "baseUrl", { unique: false });
3053
3081
  utStore.createIndex("sessionId", "sessionId", { unique: false });
3054
3082
  utStore.createIndex("client", "client", { unique: false });
3083
+ utStore.createIndex("provider", "provider", { unique: false });
3055
3084
  }
3056
3085
  if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
3057
3086
  db.createObjectStore("sdk_storage");
@@ -4614,6 +4643,29 @@ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
4614
4643
  }
4615
4644
  return result;
4616
4645
  }
4646
+ function extractUsageFromResponseHeaders(headers) {
4647
+ const get = (name) => {
4648
+ if (headers instanceof Headers) return headers.get(name);
4649
+ const lower = name.toLowerCase();
4650
+ for (const [k, v] of Object.entries(headers)) {
4651
+ if (k.toLowerCase() === lower) return v;
4652
+ }
4653
+ return null;
4654
+ };
4655
+ const totalMsats = Number(get("X-Routstr-Cost-Msats"));
4656
+ if (!totalMsats || !Number.isFinite(totalMsats)) return null;
4657
+ return {
4658
+ promptTokens: 0,
4659
+ completionTokens: 0,
4660
+ totalTokens: 0,
4661
+ cost: Number(get("X-Routstr-Cost-Usd")) || 0,
4662
+ satsCost: totalMsats / 1e3,
4663
+ totalMsats,
4664
+ inputMsats: Number(get("X-Routstr-Input-Cost-Msats")) || 0,
4665
+ outputMsats: Number(get("X-Routstr-Output-Cost-Msats")) || 0,
4666
+ totalUsd: Number(get("X-Routstr-Cost-Usd")) || void 0
4667
+ };
4668
+ }
4617
4669
  function toUsageStats(usage) {
4618
4670
  if (!usage) return void 0;
4619
4671
  return {
@@ -4700,14 +4752,11 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId, options) {
4700
4752
  }
4701
4753
  const usage = extractUsageFromSSEJson(data);
4702
4754
  if (usage) {
4703
- console.log("[routstr:sse] \u2192 usage detected:", usage);
4704
4755
  const merged = mergeUsage(capturedUsage, usage);
4705
4756
  if (hasUsageChanged(capturedUsage, merged)) {
4706
4757
  capturedUsage = merged;
4707
- console.log("[routstr:sse] \u2192 merged (changed):", merged);
4708
4758
  onUsage(merged);
4709
4759
  } else {
4710
- console.log("[routstr:sse] \u2192 merged (no change)");
4711
4760
  }
4712
4761
  }
4713
4762
  } catch {
@@ -4864,6 +4913,177 @@ function createSSEParserTransform(onUsage, onResponseId) {
4864
4913
  });
4865
4914
  }
4866
4915
 
4916
+ // client/TinfoilSecure.ts
4917
+ var TINFOIL_MODEL_PREFIX = "tinfoil-";
4918
+ var clientCache = /* @__PURE__ */ new Map();
4919
+ function isTinfoilModel(modelId) {
4920
+ return modelId.startsWith(TINFOIL_MODEL_PREFIX);
4921
+ }
4922
+ function getTinfoilUpstreamModelId(modelId) {
4923
+ return modelId.slice(TINFOIL_MODEL_PREFIX.length);
4924
+ }
4925
+ function normalizeBaseUrl5(baseUrl) {
4926
+ return baseUrl.replace(/\/+$/, "");
4927
+ }
4928
+ function cacheKey(options) {
4929
+ return JSON.stringify({
4930
+ baseUrl: options.baseUrl,
4931
+ attestationBundleURL: options.attestationBundleURL,
4932
+ enclaveURL: options.enclaveURL,
4933
+ configRepo: options.configRepo
4934
+ });
4935
+ }
4936
+ function envOrUndefined(name) {
4937
+ const maybeProcess = globalThis;
4938
+ const value = maybeProcess.process?.env?.[name];
4939
+ return value && value.trim() ? value.trim() : void 0;
4940
+ }
4941
+ function resolveOptions(options) {
4942
+ return {
4943
+ ...options,
4944
+ baseUrl: normalizeBaseUrl5(options.baseUrl),
4945
+ attestationBundleURL: options.attestationBundleURL ?? envOrUndefined("ROUTSTR_TINFOIL_ATTESTATION_BUNDLE_URL"),
4946
+ enclaveURL: options.enclaveURL ?? envOrUndefined("ROUTSTR_TINFOIL_ENCLAVE_URL"),
4947
+ configRepo: options.configRepo ?? envOrUndefined("ROUTSTR_TINFOIL_CONFIG_REPO")
4948
+ };
4949
+ }
4950
+ async function prepareTinfoilClient(options) {
4951
+ const resolved = resolveOptions(options);
4952
+ const key = cacheKey(resolved);
4953
+ let pending = clientCache.get(key);
4954
+ if (!pending) {
4955
+ pending = (async () => {
4956
+ const { SecureClient } = await import('tinfoil');
4957
+ const client = new SecureClient({
4958
+ // baseURL is the proxy/provider URL that receives the EHBP request.
4959
+ baseURL: resolved.baseUrl,
4960
+ // Leave undefined by default so tinfoil uses its public ATC. If set,
4961
+ // SecureClient will fetch `${attestationBundleURL}/attestation`.
4962
+ attestationBundleURL: resolved.attestationBundleURL,
4963
+ enclaveURL: resolved.enclaveURL,
4964
+ configRepo: resolved.configRepo,
4965
+ transport: "ehbp"
4966
+ });
4967
+ await client.ready();
4968
+ const verification = client.getVerificationDocument();
4969
+ return { client, verification };
4970
+ })();
4971
+ clientCache.set(key, pending);
4972
+ }
4973
+ try {
4974
+ return await pending;
4975
+ } catch (error) {
4976
+ clientCache.delete(key);
4977
+ throw error;
4978
+ }
4979
+ }
4980
+ function normalizeFetchArgs(input, init) {
4981
+ if (typeof input === "string") {
4982
+ return { url: input, init };
4983
+ }
4984
+ if (input instanceof URL) {
4985
+ return { url: input.toString(), init };
4986
+ }
4987
+ const cloned = input.clone();
4988
+ return {
4989
+ url: cloned.url,
4990
+ init: {
4991
+ method: cloned.method,
4992
+ headers: new Headers(cloned.headers),
4993
+ body: cloned.body ?? void 0,
4994
+ signal: cloned.signal,
4995
+ ...init
4996
+ }
4997
+ };
4998
+ }
4999
+ function isProblemJsonContentType(contentType) {
5000
+ const mediaType = contentType?.split(";", 1)[0]?.trim().toLowerCase();
5001
+ return mediaType === "application/problem+json";
5002
+ }
5003
+ async function isEhbpKeyConfigMismatchResponse(response, protocol) {
5004
+ if (response.status !== 422) {
5005
+ return false;
5006
+ }
5007
+ if (!isProblemJsonContentType(response.headers.get("content-type"))) {
5008
+ return false;
5009
+ }
5010
+ try {
5011
+ const problem = await response.clone().json();
5012
+ return problem?.type === protocol.KEY_CONFIG_PROBLEM_TYPE;
5013
+ } catch {
5014
+ return false;
5015
+ }
5016
+ }
5017
+ async function fetchTinfoilEhbpOnce(context, options, normalized, ehbp) {
5018
+ const { Identity, PROTOCOL, decryptResponseWithToken, extractSessionRecoveryToken } = ehbp;
5019
+ const resolved = resolveOptions(options);
5020
+ const baseURL = context.client.getBaseURL() ?? resolved.baseUrl;
5021
+ const enclaveURL = context.client.getEnclaveURL();
5022
+ const baseOrigin = new URL(baseURL).origin;
5023
+ const allowedOrigins = /* @__PURE__ */ new Set([baseOrigin]);
5024
+ if (enclaveURL) {
5025
+ allowedOrigins.add(new URL(enclaveURL).origin);
5026
+ }
5027
+ const targetUrl = new URL(normalized.url, baseURL);
5028
+ if (!allowedOrigins.has(targetUrl.origin)) {
5029
+ throw new Error(
5030
+ `refusing to send Tinfoil request to ${targetUrl.origin}: client is bound to the verified enclave/proxy`
5031
+ );
5032
+ }
5033
+ const headers = new Headers(normalized.init?.headers);
5034
+ if (enclaveURL && new URL(enclaveURL).origin !== baseOrigin) {
5035
+ headers.set("X-Tinfoil-Enclave-Url", enclaveURL);
5036
+ }
5037
+ const method = normalized.init?.method ?? "GET";
5038
+ const body = normalized.init?.body ?? null;
5039
+ const serverIdentity = await Identity.fromPublicKeyHex(
5040
+ context.verification.hpkePublicKey
5041
+ );
5042
+ const request = new Request(targetUrl.toString(), {
5043
+ method,
5044
+ headers,
5045
+ body,
5046
+ duplex: "half"
5047
+ });
5048
+ const { request: encryptedRequest, context: requestContext } = await serverIdentity.encryptRequestWithContext(request);
5049
+ const response = await fetch(encryptedRequest);
5050
+ if (!requestContext) {
5051
+ return response;
5052
+ }
5053
+ if (await isEhbpKeyConfigMismatchResponse(response, PROTOCOL)) {
5054
+ throw new ehbp.KeyConfigMismatchError("EHBP key configuration mismatch");
5055
+ }
5056
+ if (!response.headers.get(PROTOCOL.RESPONSE_NONCE_HEADER)) {
5057
+ return response;
5058
+ }
5059
+ const token = await extractSessionRecoveryToken(requestContext);
5060
+ return await decryptResponseWithToken(response, token);
5061
+ }
5062
+ async function fetchTinfoilPreservingPlaintextErrors(options, input, init) {
5063
+ const context = await prepareTinfoilClient(options);
5064
+ const ehbp = await import('ehbp');
5065
+ const normalized = normalizeFetchArgs(input, init);
5066
+ try {
5067
+ return await fetchTinfoilEhbpOnce(context, options, normalized, ehbp);
5068
+ } catch (error) {
5069
+ if (error instanceof ehbp.KeyConfigMismatchError) {
5070
+ context.client.reset();
5071
+ try {
5072
+ await context.client.ready();
5073
+ context.verification = context.client.getVerificationDocument();
5074
+ } catch (reattestError) {
5075
+ clientCache.delete(cacheKey(resolveOptions(options)));
5076
+ throw reattestError;
5077
+ }
5078
+ return await fetchTinfoilEhbpOnce(context, options, normalized, ehbp);
5079
+ }
5080
+ throw error;
5081
+ }
5082
+ }
5083
+ function clearTinfoilClientCache() {
5084
+ clientCache.clear();
5085
+ }
5086
+
4867
5087
  // client/RoutstrClient.ts
4868
5088
  var TOPUP_MARGIN = 1.2;
4869
5089
  var RoutstrClient = class {
@@ -5049,11 +5269,6 @@ var RoutstrClient = class {
5049
5269
  );
5050
5270
  }
5051
5271
  }
5052
- const { token, tokenBalance, tokenBalanceUnit, tokenBalanceUnknown } = await this._spendToken({
5053
- mintUrl,
5054
- amount: requiredSats,
5055
- baseUrl
5056
- });
5057
5272
  let requestBody = body;
5058
5273
  if (body && typeof body === "object") {
5059
5274
  const bodyObj = body;
@@ -5062,7 +5277,36 @@ var RoutstrClient = class {
5062
5277
  }
5063
5278
  }
5064
5279
  const baseHeaders = this._buildBaseHeaders();
5065
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
5280
+ const tinfoilEnabled = Boolean(modelId && isTinfoilModel(modelId));
5281
+ if (tinfoilEnabled) {
5282
+ this._log(
5283
+ "DEBUG",
5284
+ `[RoutstrClient] Attesting Tinfoil model ${modelId} before spend`
5285
+ );
5286
+ const { verification } = await prepareTinfoilClient({ baseUrl });
5287
+ this._log(
5288
+ "DEBUG",
5289
+ `[RoutstrClient] Tinfoil attestation passed, enclave=${verification.enclaveHost}, codeFingerprint=${verification.codeFingerprint.slice(0, 16)}...`
5290
+ );
5291
+ if (requestBody && typeof requestBody === "object" && modelId) {
5292
+ requestBody = {
5293
+ ...requestBody,
5294
+ model: getTinfoilUpstreamModelId(modelId)
5295
+ };
5296
+ }
5297
+ }
5298
+ const spendResult = await this._spendToken({
5299
+ mintUrl,
5300
+ amount: requiredSats,
5301
+ baseUrl
5302
+ });
5303
+ const { token, tokenBalance, tokenBalanceUnit, tokenBalanceUnknown } = spendResult;
5304
+ const finalHeaders = this._withAuthAndTinfoilHeaders(
5305
+ baseHeaders,
5306
+ token,
5307
+ tinfoilEnabled,
5308
+ modelId
5309
+ );
5066
5310
  const response = await this._makeRequest({
5067
5311
  path: requestPath,
5068
5312
  method,
@@ -5071,9 +5315,10 @@ var RoutstrClient = class {
5071
5315
  mintUrl,
5072
5316
  token,
5073
5317
  requiredSats,
5074
- headers: requestHeaders,
5318
+ headers: finalHeaders,
5075
5319
  baseHeaders,
5076
- selectedModel
5320
+ selectedModel,
5321
+ tinfoilEnabled
5077
5322
  });
5078
5323
  let tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
5079
5324
  let initialTokenBalanceUnknown = tokenBalanceUnknown;
@@ -5161,7 +5406,7 @@ var RoutstrClient = class {
5161
5406
  * Make the API request with failover support
5162
5407
  */
5163
5408
  async _makeRequest(params) {
5164
- const { path, method, body, baseUrl, token, headers } = params;
5409
+ const { path, method, body, baseUrl, token, headers, tinfoilEnabled } = params;
5165
5410
  try {
5166
5411
  const url = `${baseUrl.replace(/\/$/, "")}${path}`;
5167
5412
  const requestBodyText = body === void 0 || method === "GET" ? void 0 : JSON.stringify(body);
@@ -5175,7 +5420,15 @@ var RoutstrClient = class {
5175
5420
  rawBody: requestBodyText
5176
5421
  });
5177
5422
  if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
5178
- const response = await fetch(url, {
5423
+ const response = tinfoilEnabled ? await fetchTinfoilPreservingPlaintextErrors(
5424
+ { baseUrl },
5425
+ url,
5426
+ {
5427
+ method,
5428
+ headers,
5429
+ body: requestBodyText
5430
+ }
5431
+ ) : await fetch(url, {
5179
5432
  method,
5180
5433
  headers,
5181
5434
  body: requestBodyText
@@ -5195,6 +5448,15 @@ var RoutstrClient = class {
5195
5448
  } catch (e) {
5196
5449
  bodyText = void 0;
5197
5450
  }
5451
+ this._log("ERROR", "[RoutstrClient] Upstream error response", {
5452
+ baseUrl,
5453
+ url,
5454
+ path,
5455
+ status: response.status,
5456
+ statusText: response.statusText,
5457
+ requestId,
5458
+ body: bodyText ?? "<unable to read response body>"
5459
+ });
5198
5460
  return await this._handleErrorResponse(
5199
5461
  params,
5200
5462
  token,
@@ -5225,6 +5487,9 @@ var RoutstrClient = class {
5225
5487
  throw error;
5226
5488
  }
5227
5489
  }
5490
+ /**
5491
+ * Store request details to a file in the reqs/ folder before fetch.
5492
+ */
5228
5493
  /**
5229
5494
  * Handle error responses with failover
5230
5495
  */
@@ -5376,7 +5641,12 @@ var RoutstrClient = class {
5376
5641
  return this._makeRequest({
5377
5642
  ...params,
5378
5643
  token: params.token,
5379
- headers: this._withAuthHeader(params.baseHeaders, params.token),
5644
+ headers: this._withAuthAndTinfoilHeaders(
5645
+ params.baseHeaders,
5646
+ params.token,
5647
+ params.tinfoilEnabled,
5648
+ params.selectedModel?.id
5649
+ ),
5380
5650
  retryCount: retryCount + 1
5381
5651
  });
5382
5652
  } else {
@@ -5437,7 +5707,12 @@ var RoutstrClient = class {
5437
5707
  return this._makeRequest({
5438
5708
  ...params,
5439
5709
  token: retryToken,
5440
- headers: this._withAuthHeader(params.baseHeaders, retryToken),
5710
+ headers: this._withAuthAndTinfoilHeaders(
5711
+ params.baseHeaders,
5712
+ retryToken,
5713
+ params.tinfoilEnabled,
5714
+ params.selectedModel?.id
5715
+ ),
5441
5716
  retryCount: retryCount + 1
5442
5717
  });
5443
5718
  } else {
@@ -5532,6 +5807,13 @@ var RoutstrClient = class {
5532
5807
  messagesForPricing,
5533
5808
  params.maxTokens
5534
5809
  );
5810
+ if (params.tinfoilEnabled) {
5811
+ this._log(
5812
+ "DEBUG",
5813
+ `[RoutstrClient] _handleErrorResponse: Attesting Tinfoil failover provider ${nextProvider} before spend`
5814
+ );
5815
+ await prepareTinfoilClient({ baseUrl: nextProvider });
5816
+ }
5535
5817
  this._log(
5536
5818
  "DEBUG",
5537
5819
  `[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
@@ -5550,7 +5832,12 @@ var RoutstrClient = class {
5550
5832
  selectedModel: newModel,
5551
5833
  token: spendResult.token,
5552
5834
  requiredSats: newRequiredSats,
5553
- headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
5835
+ headers: this._withAuthAndTinfoilHeaders(
5836
+ params.baseHeaders,
5837
+ spendResult.token,
5838
+ params.tinfoilEnabled,
5839
+ newModel.id
5840
+ ),
5554
5841
  retryCount: 0
5555
5842
  });
5556
5843
  retryResponse.initialTokenBalanceInSats = spendResult.tokenBalanceUnit === "msat" ? spendResult.tokenBalance / 1e3 : spendResult.tokenBalance;
@@ -5617,10 +5904,10 @@ var RoutstrClient = class {
5617
5904
  if (latestTokenBalance !== void 0) {
5618
5905
  this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
5619
5906
  }
5620
- satsSpent = latestTokenBalance !== void 0 && !initialTokenBalanceUnknown ? Math.max(0, initialTokenBalance - latestTokenBalance) : fallbackSatsSpent ?? usage?.satsCost ?? 0;
5907
+ satsSpent = latestTokenBalance !== void 0 && !initialTokenBalanceUnknown ? Math.max(0, initialTokenBalance - latestTokenBalance) : fallbackSatsSpent ?? usage?.satsCost ?? this._headerSatsCost(response) ?? 0;
5621
5908
  } catch (e) {
5622
5909
  this._log("WARN", "Could not get updated API key balance:", e);
5623
- satsSpent = fallbackSatsSpent ?? usage?.satsCost ?? 0;
5910
+ satsSpent = fallbackSatsSpent ?? usage?.satsCost ?? this._headerSatsCost(response) ?? 0;
5624
5911
  }
5625
5912
  }
5626
5913
  await this._trackResponseUsage({
@@ -5637,6 +5924,15 @@ var RoutstrClient = class {
5637
5924
  })();
5638
5925
  return satsSpent;
5639
5926
  }
5927
+ /**
5928
+ * Extract sats cost from EHBP/Tinfoil response headers as a last-resort
5929
+ * fallback when neither balance delta nor SSE/body usage provides a cost.
5930
+ */
5931
+ _headerSatsCost(response) {
5932
+ if (!response) return void 0;
5933
+ const headerUsage = extractUsageFromResponseHeaders(response.headers);
5934
+ return headerUsage?.satsCost;
5935
+ }
5640
5936
  async _trackResponseUsage(params) {
5641
5937
  const {
5642
5938
  token,
@@ -5670,7 +5966,24 @@ var RoutstrClient = class {
5670
5966
  }
5671
5967
  }
5672
5968
  if (!usage) {
5673
- return;
5969
+ const headerUsage = extractUsageFromResponseHeaders(response.headers);
5970
+ if (headerUsage) {
5971
+ usage = headerUsage;
5972
+ } else {
5973
+ return;
5974
+ }
5975
+ } else {
5976
+ const headerUsage = extractUsageFromResponseHeaders(response.headers);
5977
+ if (headerUsage) {
5978
+ if (headerUsage.totalMsats) {
5979
+ usage.totalMsats = headerUsage.totalMsats;
5980
+ usage.satsCost = headerUsage.satsCost;
5981
+ }
5982
+ if (headerUsage.cost) usage.cost = headerUsage.cost;
5983
+ if (headerUsage.inputMsats) usage.inputMsats = headerUsage.inputMsats;
5984
+ if (headerUsage.outputMsats) usage.outputMsats = headerUsage.outputMsats;
5985
+ if (headerUsage.totalUsd) usage.totalUsd = headerUsage.totalUsd;
5986
+ }
5674
5987
  }
5675
5988
  const finalRequestId = requestId || "unknown";
5676
5989
  const store = this.sdkStore ?? await getDefaultSdkStore();
@@ -5867,6 +6180,19 @@ var RoutstrClient = class {
5867
6180
  }
5868
6181
  return nextHeaders;
5869
6182
  }
6183
+ /**
6184
+ * Attach auth headers and preserve the plaintext model hint required by the
6185
+ * Routstr proxy for Tinfoil/EHBP requests. EHBP encrypts the JSON body, so
6186
+ * retries/failover must not rebuild headers from baseHeaders alone or the
6187
+ * proxy cannot route/price the encrypted request.
6188
+ */
6189
+ _withAuthAndTinfoilHeaders(headers, token, tinfoilEnabled, modelId) {
6190
+ const nextHeaders = this._withAuthHeader(headers, token);
6191
+ if (tinfoilEnabled && modelId) {
6192
+ nextHeaders["X-Routstr-Model"] = modelId;
6193
+ }
6194
+ return nextHeaders;
6195
+ }
5870
6196
  };
5871
6197
 
5872
6198
  // client/StreamProcessor.ts
@@ -6356,6 +6682,6 @@ function extractStream(requestBody) {
6356
6682
  return typeof stream === "boolean" ? stream : void 0;
6357
6683
  }
6358
6684
 
6359
- export { BalanceManager, CashuSpender, FailoverError, InsufficientBalanceError, MintDiscovery, MintDiscoveryError, MintUnreachableError, ModelManager, ModelNotFoundError, NoProvidersAvailableError, ProviderBootstrapError, ProviderError, ProviderManager, RoutstrClient, SDK_STORAGE_KEYS, StreamProcessor, StreamingError, TokenOperationError, consoleLogger, createDiscoveryAdapterFromStore, createIndexedDBDriver, createIndexedDBUsageTrackingDriver, createMemoryDriver, createMemoryUsageTrackingDriver, createProviderRegistryFromDiscoveryAdapter, createProviderRegistryFromStore, createSSEParserTransform, createSdkStore, createShardedDiscoveryAdapter, createStorageAdapterFromStore, extractResponseId, extractUsageFromResponseBody, extractUsageFromSSEJson, fetchAIResponse, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, inspectSSEWebStream, isOnionUrl, isTorContext, localStorageDriver, noopLogger, normalizeProviderUrl, routeRequests, setDefaultUsageTrackingDriver, toUsageStats };
6685
+ export { BalanceManager, CashuSpender, FailoverError, InsufficientBalanceError, MintDiscovery, MintDiscoveryError, MintUnreachableError, ModelManager, ModelNotFoundError, NoProvidersAvailableError, ProviderBootstrapError, ProviderError, ProviderManager, RoutstrClient, SDK_STORAGE_KEYS, StreamProcessor, StreamingError, TokenOperationError, clearTinfoilClientCache, consoleLogger, createDiscoveryAdapterFromStore, createIndexedDBDriver, createIndexedDBUsageTrackingDriver, createMemoryDriver, createMemoryUsageTrackingDriver, createProviderRegistryFromDiscoveryAdapter, createProviderRegistryFromStore, createSSEParserTransform, createSdkStore, createShardedDiscoveryAdapter, createStorageAdapterFromStore, extractResponseId, extractUsageFromResponseBody, extractUsageFromResponseHeaders, extractUsageFromSSEJson, fetchAIResponse, fetchTinfoilPreservingPlaintextErrors, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, getTinfoilUpstreamModelId, inspectSSEWebStream, isOnionUrl, isTinfoilModel, isTorContext, localStorageDriver, noopLogger, normalizeProviderUrl, prepareTinfoilClient, routeRequests, setDefaultUsageTrackingDriver, toUsageStats };
6360
6686
  //# sourceMappingURL=browser.mjs.map
6361
6687
  //# sourceMappingURL=browser.mjs.map