routstrd 0.3.2 → 0.3.3

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.
@@ -21081,12 +21081,12 @@ function toBytes7(data) {
21081
21081
  }
21082
21082
  function concatBytes8(...arrays) {
21083
21083
  const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0));
21084
- let pad2 = 0;
21084
+ let pad3 = 0;
21085
21085
  arrays.forEach((a) => {
21086
21086
  if (!u8a7(a))
21087
21087
  throw new Error("Uint8Array expected");
21088
- r.set(a, pad2);
21089
- pad2 += a.length;
21088
+ r.set(a, pad3);
21089
+ pad3 += a.length;
21090
21090
  });
21091
21091
  return r;
21092
21092
  }
@@ -21474,12 +21474,12 @@ function ensureBytes3(title, hex4, expectedLength) {
21474
21474
  }
21475
21475
  function concatBytes9(...arrays) {
21476
21476
  const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0));
21477
- let pad2 = 0;
21477
+ let pad3 = 0;
21478
21478
  arrays.forEach((a) => {
21479
21479
  if (!u8a8(a))
21480
21480
  throw new Error("Uint8Array expected");
21481
- r.set(a, pad2);
21482
- pad2 += a.length;
21481
+ r.set(a, pad3);
21482
+ pad3 += a.length;
21483
21483
  });
21484
21484
  return r;
21485
21485
  }
@@ -22715,16 +22715,16 @@ var init_hmac3 = __esm(() => {
22715
22715
  this.blockLen = this.iHash.blockLen;
22716
22716
  this.outputLen = this.iHash.outputLen;
22717
22717
  const blockLen = this.blockLen;
22718
- const pad2 = new Uint8Array(blockLen);
22719
- pad2.set(key.length > blockLen ? hash6.create().update(key).digest() : key);
22720
- for (let i4 = 0;i4 < pad2.length; i4++)
22721
- pad2[i4] ^= 54;
22722
- this.iHash.update(pad2);
22718
+ const pad3 = new Uint8Array(blockLen);
22719
+ pad3.set(key.length > blockLen ? hash6.create().update(key).digest() : key);
22720
+ for (let i4 = 0;i4 < pad3.length; i4++)
22721
+ pad3[i4] ^= 54;
22722
+ this.iHash.update(pad3);
22723
22723
  this.oHash = hash6.create();
22724
- for (let i4 = 0;i4 < pad2.length; i4++)
22725
- pad2[i4] ^= 54 ^ 92;
22726
- this.oHash.update(pad2);
22727
- pad2.fill(0);
22724
+ for (let i4 = 0;i4 < pad3.length; i4++)
22725
+ pad3[i4] ^= 54 ^ 92;
22726
+ this.oHash.update(pad3);
22727
+ pad3.fill(0);
22728
22728
  }
22729
22729
  update(buf) {
22730
22730
  exists6(this);
@@ -22995,12 +22995,12 @@ function toBytes8(data) {
22995
22995
  }
22996
22996
  function concatBytes10(...arrays) {
22997
22997
  const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0));
22998
- let pad2 = 0;
22998
+ let pad3 = 0;
22999
22999
  arrays.forEach((a) => {
23000
23000
  if (!u8a9(a))
23001
23001
  throw new Error("Uint8Array expected");
23002
- r.set(a, pad2);
23003
- pad2 += a.length;
23002
+ r.set(a, pad3);
23003
+ pad3 += a.length;
23004
23004
  });
23005
23005
  return r;
23006
23006
  }
@@ -24706,7 +24706,7 @@ class Poly13052 {
24706
24706
  h2[9] = d9;
24707
24707
  }
24708
24708
  finalize() {
24709
- const { h: h2, pad: pad2 } = this;
24709
+ const { h: h2, pad: pad3 } = this;
24710
24710
  const g2 = new Uint16Array(10);
24711
24711
  let c = h2[1] >>> 13;
24712
24712
  h2[1] &= 8191;
@@ -24745,10 +24745,10 @@ class Poly13052 {
24745
24745
  h2[5] = (h2[6] >>> 2 | h2[7] << 11) & 65535;
24746
24746
  h2[6] = (h2[7] >>> 5 | h2[8] << 8) & 65535;
24747
24747
  h2[7] = (h2[8] >>> 8 | h2[9] << 5) & 65535;
24748
- let f = h2[0] + pad2[0];
24748
+ let f = h2[0] + pad3[0];
24749
24749
  h2[0] = f & 65535;
24750
24750
  for (let i4 = 1;i4 < 8; i4++) {
24751
- f = (h2[i4] + pad2[i4] | 0) + (f >>> 16) | 0;
24751
+ f = (h2[i4] + pad3[i4] | 0) + (f >>> 16) | 0;
24752
24752
  h2[i4] = f & 65535;
24753
24753
  }
24754
24754
  }
@@ -25200,16 +25200,16 @@ var init_hmac4 = __esm(() => {
25200
25200
  this.blockLen = this.iHash.blockLen;
25201
25201
  this.outputLen = this.iHash.outputLen;
25202
25202
  const blockLen = this.blockLen;
25203
- const pad2 = new Uint8Array(blockLen);
25204
- pad2.set(key.length > blockLen ? hash7.create().update(key).digest() : key);
25205
- for (let i4 = 0;i4 < pad2.length; i4++)
25206
- pad2[i4] ^= 54;
25207
- this.iHash.update(pad2);
25203
+ const pad3 = new Uint8Array(blockLen);
25204
+ pad3.set(key.length > blockLen ? hash7.create().update(key).digest() : key);
25205
+ for (let i4 = 0;i4 < pad3.length; i4++)
25206
+ pad3[i4] ^= 54;
25207
+ this.iHash.update(pad3);
25208
25208
  this.oHash = hash7.create();
25209
- for (let i4 = 0;i4 < pad2.length; i4++)
25210
- pad2[i4] ^= 54 ^ 92;
25211
- this.oHash.update(pad2);
25212
- pad2.fill(0);
25209
+ for (let i4 = 0;i4 < pad3.length; i4++)
25210
+ pad3[i4] ^= 54 ^ 92;
25211
+ this.oHash.update(pad3);
25212
+ pad3.fill(0);
25213
25213
  }
25214
25214
  update(buf) {
25215
25215
  _assert_default3.exists(this);
@@ -25916,7 +25916,7 @@ function writeU16BE2(num) {
25916
25916
  new DataView(arr.buffer).setUint16(0, num, false);
25917
25917
  return arr;
25918
25918
  }
25919
- function pad2(plaintext) {
25919
+ function pad3(plaintext) {
25920
25920
  const unpadded = utf8Encoder5.encode(plaintext);
25921
25921
  const unpaddedLen = unpadded.length;
25922
25922
  const prefix = writeU16BE2(unpaddedLen);
@@ -25964,7 +25964,7 @@ function decodePayload2(payload) {
25964
25964
  }
25965
25965
  function encrypt23(plaintext, conversationKey, nonce = randomBytes7(32)) {
25966
25966
  const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys2(conversationKey, nonce);
25967
- const padded = pad2(plaintext);
25967
+ const padded = pad3(plaintext);
25968
25968
  const ciphertext = chacha202(chacha_key, chacha_nonce, padded);
25969
25969
  const mac = hmacAad2(hmac_key, ciphertext, nonce);
25970
25970
  return base644.encode(concatBytes10(new Uint8Array([2]), nonce, ciphertext, mac));
@@ -41756,6 +41756,11 @@ var NoProvidersAvailableError = class extends Error {
41756
41756
  this.name = "NoProvidersAvailableError";
41757
41757
  }
41758
41758
  };
41759
+ var DEFAULT_NOSTR_RELAYS = [
41760
+ "wss://relay.damus.io",
41761
+ "wss://nos.lol",
41762
+ "wss://relay.routstr.com"
41763
+ ];
41759
41764
  var ModelManager = class _ModelManager {
41760
41765
  constructor(adapter, config = {}) {
41761
41766
  this.adapter = adapter;
@@ -41899,15 +41904,11 @@ var ModelManager = class _ModelManager {
41899
41904
  }
41900
41905
  return this.bootstrapFromHttp(torMode, forceRefresh);
41901
41906
  }
41902
- getNostrRelays(defaults) {
41903
- return this.nostrRelays && this.nostrRelays.length > 0 ? this.nostrRelays : defaults;
41907
+ getNostrRelays() {
41908
+ return this.nostrRelays && this.nostrRelays.length > 0 ? this.nostrRelays : DEFAULT_NOSTR_RELAYS;
41904
41909
  }
41905
41910
  async bootstrapFromNostr(kind, torMode, forceRefresh = false) {
41906
- const relays2 = this.getNostrRelays([
41907
- "wss://relay.primal.net",
41908
- "wss://nos.lol",
41909
- "wss://relay.damus.io"
41910
- ]);
41911
+ const relays2 = this.getNostrRelays();
41911
41912
  const cached = await this.getCachedNostrEvents({ kinds: [kind] }, this.cacheTTL, forceRefresh);
41912
41913
  let sessionEvents = cached;
41913
41914
  if (cached.length === 0) {
@@ -42038,12 +42039,7 @@ var ModelManager = class _ModelManager {
42038
42039
  const cached = await this.getCachedNostrEvents({ kinds: [38425], "#t": ["lgtm"], authors: [this.routstrPubkey] }, this.cacheTTL, forceRefresh);
42039
42040
  let sessionEvents = cached;
42040
42041
  if (cached.length === 0) {
42041
- const lgtmRelays = this.getNostrRelays([
42042
- "wss://relay.primal.net",
42043
- "wss://nos.lol",
42044
- "wss://relay.damus.io",
42045
- "wss://relay.routstr.com"
42046
- ]);
42042
+ const lgtmRelays = this.getNostrRelays();
42047
42043
  const pool3 = new RelayPool;
42048
42044
  const timeoutMs = 5000;
42049
42045
  await new Promise((resolve) => {
@@ -42241,11 +42237,7 @@ var ModelManager = class _ModelManager {
42241
42237
  return cachedModels;
42242
42238
  }
42243
42239
  }
42244
- const relays2 = this.getNostrRelays([
42245
- "wss://relay.damus.io",
42246
- "wss://nos.lol",
42247
- "wss://relay.routstr.com"
42248
- ]);
42240
+ const relays2 = this.getNostrRelays();
42249
42241
  const cached = await this.getCachedNostrEvents({ kinds: [38423], "#d": ["routstr-21-models"], authors: [this.routstrPubkey] }, this.cacheTTL, forceRefresh);
42250
42242
  let sessionEvents = cached;
42251
42243
  if (cached.length === 0) {
@@ -43109,8 +43101,8 @@ var BalanceManager = class _BalanceManager {
43109
43101
  const totalMintBalance = Object.values(balanceState.mintBalances).reduce((sum, value) => sum + value, 0);
43110
43102
  const targetProviderBalance = balanceState.providerBalances[baseUrl] || 0;
43111
43103
  const refundableProviderBalance = Object.entries(balanceState.providerBalances).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
43112
- if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
43113
- await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
43104
+ if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 3) {
43105
+ await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount, adjustedAmount);
43114
43106
  return this.createProviderToken({
43115
43107
  ...options,
43116
43108
  retryCount: retryCount + 1
@@ -43229,27 +43221,42 @@ var BalanceManager = class _BalanceManager {
43229
43221
  }
43230
43222
  return candidates;
43231
43223
  }
43232
- async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
43224
+ async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount, requiredAmount) {
43233
43225
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
43234
43226
  const forceRefund = retryCount >= 2;
43235
- const apiKeysToRefund = apiKeyDistribution.filter((apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0);
43236
- const apiKeyRefundResults = await Promise.allSettled(apiKeysToRefund.map(async (apiKeyEntry) => {
43237
- const fullApiKeyEntry = this.storageAdapter.getApiKey(apiKeyEntry.baseUrl);
43238
- if (!fullApiKeyEntry) {
43239
- return { baseUrl: apiKeyEntry.baseUrl, success: false };
43227
+ const candidates = apiKeyDistribution.filter((apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0).map((apiKey) => {
43228
+ const full = this.storageAdapter.getApiKey(apiKey.baseUrl);
43229
+ return {
43230
+ baseUrl: apiKey.baseUrl,
43231
+ amount: apiKey.amount,
43232
+ lastUsed: full?.lastUsed ?? 0,
43233
+ key: full?.key
43234
+ };
43235
+ }).filter((c) => c.key != null).sort((a, b) => a.lastUsed - b.lastUsed);
43236
+ if (candidates.length === 0)
43237
+ return;
43238
+ if (forceRefund) {
43239
+ for (const candidate of candidates) {
43240
+ await this.refundApiKey({
43241
+ mintUrl,
43242
+ baseUrl: candidate.baseUrl,
43243
+ apiKey: candidate.key,
43244
+ forceRefund: true
43245
+ });
43246
+ const newState = await this.getBalanceState();
43247
+ const newAvailable = (newState.mintBalances[mintUrl] || 0) + (newState.providerBalances[baseUrl] || 0);
43248
+ if (newAvailable >= requiredAmount) {
43249
+ this.logger.log(`_refundOtherProvidersForTopUp: freed enough balance (${newAvailable} >= ${requiredAmount}), stopping early`);
43250
+ return;
43251
+ }
43240
43252
  }
43241
- const result = await this.refundApiKey({
43253
+ } else {
43254
+ await Promise.allSettled(candidates.map((candidate) => this.refundApiKey({
43242
43255
  mintUrl,
43243
- baseUrl: apiKeyEntry.baseUrl,
43244
- apiKey: fullApiKeyEntry.key,
43245
- forceRefund
43246
- });
43247
- return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
43248
- }));
43249
- for (const result of apiKeyRefundResults) {
43250
- if (result.status === "fulfilled" && result.value.success) {
43251
- this.storageAdapter.updateApiKeyBalance(result.value.baseUrl, 0);
43252
- }
43256
+ baseUrl: candidate.baseUrl,
43257
+ apiKey: candidate.key,
43258
+ forceRefund: false
43259
+ })));
43253
43260
  }
43254
43261
  }
43255
43262
  async _postTopUp(baseUrl, storedToken, cashuToken) {
@@ -43975,6 +43982,90 @@ var SDK_STORAGE_KEYS = {
43975
43982
  LAST_FAILED: "last_failed",
43976
43983
  PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
43977
43984
  };
43985
+ var pad2 = (n) => String(n).padStart(2, "0");
43986
+ var jsGroupKey = (entry, groupBy, tzOffsetMinutes) => {
43987
+ switch (groupBy) {
43988
+ case "modelId":
43989
+ return entry.modelId ?? null;
43990
+ case "baseUrl":
43991
+ return entry.baseUrl ?? null;
43992
+ case "client":
43993
+ return entry.client ?? null;
43994
+ case "sessionId":
43995
+ return entry.sessionId ?? null;
43996
+ case "provider":
43997
+ return entry.provider ?? null;
43998
+ case "day": {
43999
+ const d = new Date(entry.timestamp - tzOffsetMinutes * 60000);
44000
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
44001
+ }
44002
+ case "hour": {
44003
+ const d = new Date(entry.timestamp - tzOffsetMinutes * 60000);
44004
+ return pad2(d.getUTCHours());
44005
+ }
44006
+ }
44007
+ };
44008
+ var reduceAggregate = (entries, options = {}) => {
44009
+ const emptyRow = (group2) => ({
44010
+ group: group2,
44011
+ requests: 0,
44012
+ promptTokens: 0,
44013
+ completionTokens: 0,
44014
+ totalTokens: 0,
44015
+ cost: 0,
44016
+ satsCost: 0,
44017
+ baseMsats: 0,
44018
+ inputMsats: 0,
44019
+ outputMsats: 0,
44020
+ totalMsats: 0,
44021
+ totalUsd: 0,
44022
+ cacheReadInputTokens: 0,
44023
+ cacheCreationInputTokens: 0,
44024
+ cacheReadMsats: 0,
44025
+ cacheCreationMsats: 0
44026
+ });
44027
+ const accumulate = (row, entry) => {
44028
+ row.requests += 1;
44029
+ row.promptTokens += entry.promptTokens;
44030
+ row.completionTokens += entry.completionTokens;
44031
+ row.totalTokens += entry.totalTokens;
44032
+ row.cost += entry.cost;
44033
+ row.satsCost += entry.satsCost;
44034
+ row.baseMsats += entry.baseMsats ?? 0;
44035
+ row.inputMsats += entry.inputMsats ?? 0;
44036
+ row.outputMsats += entry.outputMsats ?? 0;
44037
+ row.totalMsats += entry.totalMsats ?? 0;
44038
+ row.totalUsd += entry.totalUsd ?? 0;
44039
+ row.cacheReadInputTokens += entry.cacheReadInputTokens ?? 0;
44040
+ row.cacheCreationInputTokens += entry.cacheCreationInputTokens ?? 0;
44041
+ row.cacheReadMsats += entry.cacheReadMsats ?? 0;
44042
+ row.cacheCreationMsats += entry.cacheCreationMsats ?? 0;
44043
+ };
44044
+ if (!options.groupBy) {
44045
+ const total = emptyRow(null);
44046
+ for (const entry of entries)
44047
+ accumulate(total, entry);
44048
+ return [total];
44049
+ }
44050
+ const tz = options.tzOffsetMinutes ?? 0;
44051
+ const groups = /* @__PURE__ */ new Map;
44052
+ for (const entry of entries) {
44053
+ const key = jsGroupKey(entry, options.groupBy, tz);
44054
+ let row = groups.get(key);
44055
+ if (!row) {
44056
+ row = emptyRow(key);
44057
+ groups.set(key, row);
44058
+ }
44059
+ accumulate(row, entry);
44060
+ }
44061
+ const rows = [...groups.values()];
44062
+ if (options.groupBy === "day" || options.groupBy === "hour") {
44063
+ rows.sort((a, b) => (a.group ?? "").localeCompare(b.group ?? ""));
44064
+ } else {
44065
+ rows.sort((a, b) => b.satsCost - a.satsCost);
44066
+ }
44067
+ return rows;
44068
+ };
43978
44069
  var DEFAULT_DB_NAME = "routstr-sdk";
43979
44070
  var DEFAULT_STORE_NAME = "usage_tracking";
43980
44071
  var MIGRATION_MARKER_KEY = "usage_tracking_migration_v1";
@@ -44034,6 +44125,9 @@ var matchesFilters = (entry, options = {}) => {
44034
44125
  if (options.client && entry.client !== options.client) {
44035
44126
  return false;
44036
44127
  }
44128
+ if (options.clients && options.clients.length > 0 && (entry.client == null || !options.clients.includes(entry.client))) {
44129
+ return false;
44130
+ }
44037
44131
  if (options.provider && entry.provider !== options.provider) {
44038
44132
  return false;
44039
44133
  }
@@ -44129,6 +44223,10 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
44129
44223
  const results = await this.list(options2);
44130
44224
  return results.length;
44131
44225
  },
44226
+ async aggregate(options2 = {}) {
44227
+ const entries = await this.list(options2);
44228
+ return reduceAggregate(entries, options2);
44229
+ },
44132
44230
  async deleteOlderThan(timestamp) {
44133
44231
  await ensureMigrated();
44134
44232
  const db = await getDb();
@@ -44184,6 +44282,9 @@ var matchesFilters2 = (entry, options = {}) => {
44184
44282
  if (options.client && entry.client !== options.client) {
44185
44283
  return false;
44186
44284
  }
44285
+ if (options.clients && options.clients.length > 0 && (entry.client == null || !options.clients.includes(entry.client))) {
44286
+ return false;
44287
+ }
44187
44288
  if (options.provider && entry.provider !== options.provider) {
44188
44289
  return false;
44189
44290
  }
@@ -44216,6 +44317,10 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
44216
44317
  async count(options = {}) {
44217
44318
  return (await this.list(options)).length;
44218
44319
  },
44320
+ async aggregate(options = {}) {
44321
+ const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options));
44322
+ return reduceAggregate(entries, options);
44323
+ },
44219
44324
  async deleteOlderThan(timestamp) {
44220
44325
  let deleted = 0;
44221
44326
  for (const [id, entry] of store.entries()) {
@@ -44947,13 +45052,14 @@ function hasUsageChanged(previous, next) {
44947
45052
  function isInspectionComplete(responseIdCaptured, usage) {
44948
45053
  return responseIdCaptured && !!usage && usage.totalTokens > 0 && typeof usage.totalMsats === "number" && !!usage.provider;
44949
45054
  }
44950
- async function inspectSSEWebStream(stream, onUsage, onResponseId) {
45055
+ async function inspectSSEWebStream(stream, onUsage, onResponseId, options) {
44951
45056
  const reader = stream.getReader();
44952
45057
  const decoder = new TextDecoder("utf-8");
44953
45058
  let buffer = "";
44954
45059
  let capturedUsage = null;
44955
45060
  let capturedResponseId;
44956
45061
  let responseIdCaptured = false;
45062
+ let rawChunkSequence = 0;
44957
45063
  const inspectDataPayload = (jsonText) => {
44958
45064
  const trimmed = jsonText.trim();
44959
45065
  if (!trimmed || trimmed === "[DONE]") {
@@ -45031,7 +45137,9 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
45031
45137
  if (done)
45032
45138
  break;
45033
45139
  if (value && value.byteLength > 0) {
45034
- buffer += decoder.decode(value, { stream: true });
45140
+ const text = decoder.decode(value, { stream: true });
45141
+ options?.onRawChunk?.(value, rawChunkSequence++, text);
45142
+ buffer += text;
45035
45143
  drainBufferedEvents();
45036
45144
  }
45037
45145
  }
@@ -45066,6 +45174,7 @@ var RoutstrClient = class {
45066
45174
  this.mode = mode;
45067
45175
  this.usageTrackingDriver = options.usageTrackingDriver;
45068
45176
  this.sdkStore = options.sdkStore;
45177
+ this.requestResponseLogSink = options.requestResponseLogSink;
45069
45178
  this.providerManager = options.providerManager ?? new ProviderManager(providerRegistry, this.sdkStore, this.logger);
45070
45179
  }
45071
45180
  walletAdapter;
@@ -45080,6 +45189,7 @@ var RoutstrClient = class {
45080
45189
  usageTrackingDriver;
45081
45190
  sdkStore;
45082
45191
  logger;
45192
+ requestResponseLogSink;
45083
45193
  getMode() {
45084
45194
  return this.mode;
45085
45195
  }
@@ -45232,6 +45342,7 @@ var RoutstrClient = class {
45232
45342
  let usagePromise = Promise.resolve({});
45233
45343
  if (contentType.includes("text/event-stream") && response.body) {
45234
45344
  const [clientStream, inspectStream] = response.body.tee();
45345
+ const requestResponseLogId = response.requestResponseLogId;
45235
45346
  processedResponse = new Response(clientStream, {
45236
45347
  status: response.status,
45237
45348
  statusText: response.statusText,
@@ -45239,12 +45350,23 @@ var RoutstrClient = class {
45239
45350
  });
45240
45351
  processedResponse.baseUrl = response.baseUrl;
45241
45352
  processedResponse.token = response.token;
45353
+ processedResponse.requestResponseLogId = requestResponseLogId;
45242
45354
  usagePromise = inspectSSEWebStream(inspectStream, (usage) => {
45243
45355
  capturedUsage = usage;
45244
45356
  processedResponse.usage = usage;
45245
45357
  }, (responseId) => {
45246
45358
  capturedResponseId = responseId;
45247
45359
  processedResponse.requestId = responseId;
45360
+ }, {
45361
+ onRawChunk: (_chunk, sequence, text) => {
45362
+ this.requestResponseLogSink?.logResponseChunk?.(requestResponseLogId, sequence, text);
45363
+ }
45364
+ }).then(async (result) => {
45365
+ await this.requestResponseLogSink?.logResponseEnd?.(requestResponseLogId);
45366
+ return result;
45367
+ }).catch(async (error) => {
45368
+ await this.requestResponseLogSink?.logResponseError?.(requestResponseLogId, error);
45369
+ throw error;
45248
45370
  });
45249
45371
  processedResponse.usagePromise = usagePromise;
45250
45372
  }
@@ -45273,18 +45395,32 @@ var RoutstrClient = class {
45273
45395
  const { path, method, body, baseUrl, token, headers } = params;
45274
45396
  try {
45275
45397
  const url2 = `${baseUrl.replace(/\/$/, "")}${path}`;
45398
+ const requestBodyText = body === undefined || method === "GET" ? undefined : JSON.stringify(body);
45399
+ const requestLogId = await this.requestResponseLogSink?.logRequest?.({
45400
+ method,
45401
+ url: url2,
45402
+ path,
45403
+ baseUrl,
45404
+ headers,
45405
+ body,
45406
+ rawBody: requestBodyText
45407
+ });
45276
45408
  if (this.mode === "xcashu")
45277
45409
  this._log("DEBUG", "HEADERS,", headers);
45278
45410
  const response = await fetch(url2, {
45279
45411
  method,
45280
45412
  headers,
45281
- body: body === undefined || method === "GET" ? undefined : JSON.stringify(body)
45413
+ body: requestBodyText
45282
45414
  });
45283
45415
  if (this.mode === "xcashu")
45284
45416
  this._log("DEBUG", "response,", response);
45285
45417
  response.baseUrl = baseUrl;
45286
45418
  response.token = token;
45419
+ response.requestResponseLogId = requestLogId;
45420
+ await this.requestResponseLogSink?.logResponseStart?.(requestLogId, response);
45421
+ const contentType = response.headers.get("content-type") || "";
45287
45422
  if (!response.ok) {
45423
+ this.requestResponseLogSink?.logResponseBody?.(requestLogId, response.clone());
45288
45424
  const requestId = response.headers.get("x-routstr-request-id") || undefined;
45289
45425
  let bodyText;
45290
45426
  try {
@@ -45294,6 +45430,9 @@ var RoutstrClient = class {
45294
45430
  }
45295
45431
  return await this._handleErrorResponse(params, token, response.status, requestId, this.mode === "xcashu" ? response.headers.get("x-cashu") ?? undefined : undefined, bodyText, params.retryCount ?? 0);
45296
45432
  }
45433
+ if (!contentType.includes("text/event-stream")) {
45434
+ this.requestResponseLogSink?.logResponseBody?.(requestLogId, response.clone());
45435
+ }
45297
45436
  return response;
45298
45437
  } catch (error) {
45299
45438
  if (isNetworkErrorMessage(error?.message || "")) {
@@ -45735,7 +45874,8 @@ async function resolveRouteRequestContext(options) {
45735
45874
  usageTrackingDriver,
45736
45875
  sdkStore,
45737
45876
  providerManager: providedProviderManager,
45738
- logger: logger3
45877
+ logger: logger3,
45878
+ requestResponseLogSink
45739
45879
  } = options;
45740
45880
  let modelManager;
45741
45881
  let providers;
@@ -45782,17 +45922,12 @@ async function resolveRouteRequestContext(options) {
45782
45922
  baseUrl = cheapest.baseUrl;
45783
45923
  selectedModel = cheapest.model;
45784
45924
  }
45785
- const balances = await walletAdapter.getBalances();
45786
- const totalBalance = Object.values(balances).reduce((sum, v3) => sum + v3, 0);
45787
- if (totalBalance <= 0) {
45788
- throw new Error("Wallet balance is empty. Add a mint and fund it before making requests.");
45789
- }
45790
45925
  const providerMints = providerRegistry.getProviderMints(baseUrl);
45791
- const mintUrl = walletAdapter.getActiveMintUrl() || providerMints[0] || Object.keys(balances)[0];
45926
+ const mintUrl = walletAdapter.getActiveMintUrl() || providerMints[0] || Object.keys(await walletAdapter.getBalances())[0];
45792
45927
  if (!mintUrl) {
45793
45928
  throw new Error("No mint configured in wallet");
45794
45929
  }
45795
- const client2 = options.client ?? new RoutstrClient(walletAdapter, storageAdapter, providerRegistry, "min", mode, { usageTrackingDriver, sdkStore, providerManager, logger: logger3 });
45930
+ const client2 = options.client ?? new RoutstrClient(walletAdapter, storageAdapter, providerRegistry, "min", mode, { usageTrackingDriver, sdkStore, providerManager, logger: logger3, requestResponseLogSink });
45796
45931
  const maxTokens = extractMaxTokens(requestBody);
45797
45932
  const stream = extractStream(requestBody);
45798
45933
  const proxiedBody = requestBody && typeof requestBody === "object" ? { ...requestBody } : {};
@@ -45881,6 +46016,11 @@ var NoProvidersAvailableError2 = class extends Error {
45881
46016
  this.name = "NoProvidersAvailableError";
45882
46017
  }
45883
46018
  };
46019
+ var DEFAULT_NOSTR_RELAYS2 = [
46020
+ "wss://relay.damus.io",
46021
+ "wss://nos.lol",
46022
+ "wss://relay.routstr.com"
46023
+ ];
45884
46024
  var ModelManager2 = class _ModelManager2 {
45885
46025
  constructor(adapter, config = {}) {
45886
46026
  this.adapter = adapter;
@@ -46024,15 +46164,11 @@ var ModelManager2 = class _ModelManager2 {
46024
46164
  }
46025
46165
  return this.bootstrapFromHttp(torMode, forceRefresh);
46026
46166
  }
46027
- getNostrRelays(defaults) {
46028
- return this.nostrRelays && this.nostrRelays.length > 0 ? this.nostrRelays : defaults;
46167
+ getNostrRelays() {
46168
+ return this.nostrRelays && this.nostrRelays.length > 0 ? this.nostrRelays : DEFAULT_NOSTR_RELAYS2;
46029
46169
  }
46030
46170
  async bootstrapFromNostr(kind, torMode, forceRefresh = false) {
46031
- const relays3 = this.getNostrRelays([
46032
- "wss://relay.primal.net",
46033
- "wss://nos.lol",
46034
- "wss://relay.damus.io"
46035
- ]);
46171
+ const relays3 = this.getNostrRelays();
46036
46172
  const cached = await this.getCachedNostrEvents({ kinds: [kind] }, this.cacheTTL, forceRefresh);
46037
46173
  let sessionEvents = cached;
46038
46174
  if (cached.length === 0) {
@@ -46163,12 +46299,7 @@ var ModelManager2 = class _ModelManager2 {
46163
46299
  const cached = await this.getCachedNostrEvents({ kinds: [38425], "#t": ["lgtm"], authors: [this.routstrPubkey] }, this.cacheTTL, forceRefresh);
46164
46300
  let sessionEvents = cached;
46165
46301
  if (cached.length === 0) {
46166
- const lgtmRelays = this.getNostrRelays([
46167
- "wss://relay.primal.net",
46168
- "wss://nos.lol",
46169
- "wss://relay.damus.io",
46170
- "wss://relay.routstr.com"
46171
- ]);
46302
+ const lgtmRelays = this.getNostrRelays();
46172
46303
  const pool3 = new RelayPool;
46173
46304
  const timeoutMs = 5000;
46174
46305
  await new Promise((resolve) => {
@@ -46366,11 +46497,7 @@ var ModelManager2 = class _ModelManager2 {
46366
46497
  return cachedModels;
46367
46498
  }
46368
46499
  }
46369
- const relays3 = this.getNostrRelays([
46370
- "wss://relay.damus.io",
46371
- "wss://nos.lol",
46372
- "wss://relay.routstr.com"
46373
- ]);
46500
+ const relays3 = this.getNostrRelays();
46374
46501
  const cached = await this.getCachedNostrEvents({ kinds: [38423], "#d": ["routstr-21-models"], authors: [this.routstrPubkey] }, this.cacheTTL, forceRefresh);
46375
46502
  let sessionEvents = cached;
46376
46503
  if (cached.length === 0) {
@@ -46478,6 +46605,7 @@ var PID_FILE = process.env.ROUTSTRD_PID || `${CONFIG_DIR}/routstrd.pid`;
46478
46605
  var DB_PATH = `${CONFIG_DIR}/routstr.db`;
46479
46606
  var CONFIG_FILE = `${CONFIG_DIR}/config.json`;
46480
46607
  var LOGS_DIR = `${CONFIG_DIR}/logs`;
46608
+ var REQUEST_RESPONSE_LOGS_DIR = `${CONFIG_DIR}/request-response-logs`;
46481
46609
  var DEFAULT_CONFIG = {
46482
46610
  port: 8008,
46483
46611
  provider: null,
@@ -46615,6 +46743,64 @@ var SDK_STORAGE_KEYS2 = {
46615
46743
  LAST_FAILED: "last_failed",
46616
46744
  PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
46617
46745
  };
46746
+ var aggregateColumns = "COUNT(*) AS requests, COALESCE(SUM(prompt_tokens), 0) AS promptTokens, COALESCE(SUM(completion_tokens), 0) AS completionTokens, COALESCE(SUM(total_tokens), 0) AS totalTokens, COALESCE(SUM(cost), 0) AS cost, COALESCE(SUM(sats_cost), 0) AS satsCost, COALESCE(SUM(base_msats), 0) AS baseMsats, COALESCE(SUM(input_msats), 0) AS inputMsats, COALESCE(SUM(output_msats), 0) AS outputMsats, COALESCE(SUM(total_msats), 0) AS totalMsats, COALESCE(SUM(total_usd), 0) AS totalUsd, COALESCE(SUM(cache_read_input_tokens), 0) AS cacheReadInputTokens, COALESCE(SUM(cache_creation_input_tokens), 0) AS cacheCreationInputTokens, COALESCE(SUM(cache_read_msats), 0) AS cacheReadMsats, COALESCE(SUM(cache_creation_msats), 0) AS cacheCreationMsats";
46747
+ var sqlGroupExpr = (groupBy) => {
46748
+ switch (groupBy) {
46749
+ case "modelId":
46750
+ return { expr: "model_id", usesTz: false };
46751
+ case "baseUrl":
46752
+ return { expr: "base_url", usesTz: false };
46753
+ case "client":
46754
+ return { expr: "client", usesTz: false };
46755
+ case "sessionId":
46756
+ return { expr: "session_id", usesTz: false };
46757
+ case "provider":
46758
+ return { expr: "provider", usesTz: false };
46759
+ case "day":
46760
+ return {
46761
+ expr: "strftime('%Y-%m-%d', (timestamp - ? * 60000) / 1000, 'unixepoch')",
46762
+ usesTz: true
46763
+ };
46764
+ case "hour":
46765
+ return {
46766
+ expr: "strftime('%H', (timestamp - ? * 60000) / 1000, 'unixepoch')",
46767
+ usesTz: true
46768
+ };
46769
+ }
46770
+ };
46771
+ var buildAggregateSql = (tableName, where, options = {}) => {
46772
+ if (!options.groupBy) {
46773
+ return {
46774
+ sql: `SELECT NULL AS grp, ${aggregateColumns} FROM ${tableName} ${where.sql}`,
46775
+ params: where.params
46776
+ };
46777
+ }
46778
+ const { expr, usesTz } = sqlGroupExpr(options.groupBy);
46779
+ const tzParams = usesTz ? [options.tzOffsetMinutes ?? 0] : [];
46780
+ const orderBy = options.groupBy === "day" || options.groupBy === "hour" ? "ORDER BY grp ASC" : "ORDER BY satsCost DESC";
46781
+ return {
46782
+ sql: `SELECT ${expr} AS grp, ${aggregateColumns} FROM ${tableName} ${where.sql} GROUP BY grp ${orderBy}`,
46783
+ params: [...tzParams, ...where.params]
46784
+ };
46785
+ };
46786
+ var mapAggregateRow = (row) => ({
46787
+ group: row.grp == null ? null : String(row.grp),
46788
+ requests: Number(row.requests ?? 0),
46789
+ promptTokens: Number(row.promptTokens ?? 0),
46790
+ completionTokens: Number(row.completionTokens ?? 0),
46791
+ totalTokens: Number(row.totalTokens ?? 0),
46792
+ cost: Number(row.cost ?? 0),
46793
+ satsCost: Number(row.satsCost ?? 0),
46794
+ baseMsats: Number(row.baseMsats ?? 0),
46795
+ inputMsats: Number(row.inputMsats ?? 0),
46796
+ outputMsats: Number(row.outputMsats ?? 0),
46797
+ totalMsats: Number(row.totalMsats ?? 0),
46798
+ totalUsd: Number(row.totalUsd ?? 0),
46799
+ cacheReadInputTokens: Number(row.cacheReadInputTokens ?? 0),
46800
+ cacheCreationInputTokens: Number(row.cacheCreationInputTokens ?? 0),
46801
+ cacheReadMsats: Number(row.cacheReadMsats ?? 0),
46802
+ cacheCreationMsats: Number(row.cacheCreationMsats ?? 0)
46803
+ });
46618
46804
  function makeConsoleLogger3(prefix) {
46619
46805
  const fmt = (args) => prefix ? [prefix, ...args] : args;
46620
46806
  return {
@@ -46944,6 +47130,11 @@ var buildWhereClause = (options = {}) => {
46944
47130
  clauses.push("client = ?");
46945
47131
  params.push(options.client);
46946
47132
  }
47133
+ if (options.clients && options.clients.length > 0) {
47134
+ const placeholders = options.clients.map(() => "?").join(", ");
47135
+ clauses.push(`client IN (${placeholders})`);
47136
+ params.push(...options.clients);
47137
+ }
46947
47138
  if (options.provider) {
46948
47139
  clauses.push("provider = ?");
46949
47140
  params.push(options.provider);
@@ -47084,6 +47275,13 @@ var createBunSqliteUsageTrackingDriver = (options = {}) => {
47084
47275
  const row = db.query(query).get(...params);
47085
47276
  return Number(row?.count ?? 0);
47086
47277
  },
47278
+ async aggregate(options2 = {}) {
47279
+ await ensureMigrated();
47280
+ const where = buildWhereClause(options2);
47281
+ const { sql, params } = buildAggregateSql(tableName, where, options2);
47282
+ const rows = db.query(sql).all(...params);
47283
+ return rows.map(mapAggregateRow);
47284
+ },
47087
47285
  async deleteOlderThan(timestamp) {
47088
47286
  await ensureMigrated();
47089
47287
  const before = timestamp;
@@ -50418,12 +50616,12 @@ function toBytes10(data) {
50418
50616
  }
50419
50617
  function concatBytes11(...arrays) {
50420
50618
  const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0));
50421
- let pad3 = 0;
50619
+ let pad4 = 0;
50422
50620
  arrays.forEach((a) => {
50423
50621
  if (!u8a10(a))
50424
50622
  throw new Error("Uint8Array expected");
50425
- r.set(a, pad3);
50426
- pad3 += a.length;
50623
+ r.set(a, pad4);
50624
+ pad4 += a.length;
50427
50625
  });
50428
50626
  return r;
50429
50627
  }
@@ -50803,12 +51001,12 @@ function ensureBytes4(title, hex5, expectedLength) {
50803
51001
  }
50804
51002
  function concatBytes12(...arrays) {
50805
51003
  const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0));
50806
- let pad3 = 0;
51004
+ let pad4 = 0;
50807
51005
  arrays.forEach((a) => {
50808
51006
  if (!u8a11(a))
50809
51007
  throw new Error("Uint8Array expected");
50810
- r.set(a, pad3);
50811
- pad3 += a.length;
51008
+ r.set(a, pad4);
51009
+ pad4 += a.length;
50812
51010
  });
50813
51011
  return r;
50814
51012
  }
@@ -52020,16 +52218,16 @@ class HMAC6 extends Hash8 {
52020
52218
  this.blockLen = this.iHash.blockLen;
52021
52219
  this.outputLen = this.iHash.outputLen;
52022
52220
  const blockLen = this.blockLen;
52023
- const pad3 = new Uint8Array(blockLen);
52024
- pad3.set(key.length > blockLen ? hash8.create().update(key).digest() : key);
52025
- for (let i6 = 0;i6 < pad3.length; i6++)
52026
- pad3[i6] ^= 54;
52027
- this.iHash.update(pad3);
52221
+ const pad4 = new Uint8Array(blockLen);
52222
+ pad4.set(key.length > blockLen ? hash8.create().update(key).digest() : key);
52223
+ for (let i6 = 0;i6 < pad4.length; i6++)
52224
+ pad4[i6] ^= 54;
52225
+ this.iHash.update(pad4);
52028
52226
  this.oHash = hash8.create();
52029
- for (let i6 = 0;i6 < pad3.length; i6++)
52030
- pad3[i6] ^= 54 ^ 92;
52031
- this.oHash.update(pad3);
52032
- pad3.fill(0);
52227
+ for (let i6 = 0;i6 < pad4.length; i6++)
52228
+ pad4[i6] ^= 54 ^ 92;
52229
+ this.oHash.update(pad4);
52230
+ pad4.fill(0);
52033
52231
  }
52034
52232
  update(buf) {
52035
52233
  exists9(this);
@@ -52300,12 +52498,12 @@ function toBytes11(data) {
52300
52498
  }
52301
52499
  function concatBytes13(...arrays) {
52302
52500
  const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0));
52303
- let pad3 = 0;
52501
+ let pad4 = 0;
52304
52502
  arrays.forEach((a) => {
52305
52503
  if (!u8a12(a))
52306
52504
  throw new Error("Uint8Array expected");
52307
- r.set(a, pad3);
52308
- pad3 += a.length;
52505
+ r.set(a, pad4);
52506
+ pad4 += a.length;
52309
52507
  });
52310
52508
  return r;
52311
52509
  }
@@ -53996,7 +54194,7 @@ class Poly13053 {
53996
54194
  h2[9] = d9;
53997
54195
  }
53998
54196
  finalize() {
53999
- const { h: h2, pad: pad3 } = this;
54197
+ const { h: h2, pad: pad4 } = this;
54000
54198
  const g2 = new Uint16Array(10);
54001
54199
  let c = h2[1] >>> 13;
54002
54200
  h2[1] &= 8191;
@@ -54035,10 +54233,10 @@ class Poly13053 {
54035
54233
  h2[5] = (h2[6] >>> 2 | h2[7] << 11) & 65535;
54036
54234
  h2[6] = (h2[7] >>> 5 | h2[8] << 8) & 65535;
54037
54235
  h2[7] = (h2[8] >>> 8 | h2[9] << 5) & 65535;
54038
- let f = h2[0] + pad3[0];
54236
+ let f = h2[0] + pad4[0];
54039
54237
  h2[0] = f & 65535;
54040
54238
  for (let i6 = 1;i6 < 8; i6++) {
54041
- f = (h2[i6] + pad3[i6] | 0) + (f >>> 16) | 0;
54239
+ f = (h2[i6] + pad4[i6] | 0) + (f >>> 16) | 0;
54042
54240
  h2[i6] = f & 65535;
54043
54241
  }
54044
54242
  }
@@ -54474,16 +54672,16 @@ class HMAC7 extends Hash9 {
54474
54672
  this.blockLen = this.iHash.blockLen;
54475
54673
  this.outputLen = this.iHash.outputLen;
54476
54674
  const blockLen = this.blockLen;
54477
- const pad3 = new Uint8Array(blockLen);
54478
- pad3.set(key.length > blockLen ? hash9.create().update(key).digest() : key);
54479
- for (let i6 = 0;i6 < pad3.length; i6++)
54480
- pad3[i6] ^= 54;
54481
- this.iHash.update(pad3);
54675
+ const pad4 = new Uint8Array(blockLen);
54676
+ pad4.set(key.length > blockLen ? hash9.create().update(key).digest() : key);
54677
+ for (let i6 = 0;i6 < pad4.length; i6++)
54678
+ pad4[i6] ^= 54;
54679
+ this.iHash.update(pad4);
54482
54680
  this.oHash = hash9.create();
54483
- for (let i6 = 0;i6 < pad3.length; i6++)
54484
- pad3[i6] ^= 54 ^ 92;
54485
- this.oHash.update(pad3);
54486
- pad3.fill(0);
54681
+ for (let i6 = 0;i6 < pad4.length; i6++)
54682
+ pad4[i6] ^= 54 ^ 92;
54683
+ this.oHash.update(pad4);
54684
+ pad4.fill(0);
54487
54685
  }
54488
54686
  update(buf) {
54489
54687
  _assert_default4.exists(this);
@@ -56035,7 +56233,7 @@ function writeU16BE3(num) {
56035
56233
  new DataView(arr.buffer).setUint16(0, num, false);
56036
56234
  return arr;
56037
56235
  }
56038
- function pad3(plaintext) {
56236
+ function pad5(plaintext) {
56039
56237
  const unpadded = utf8Encoder9.encode(plaintext);
56040
56238
  const unpaddedLen = unpadded.length;
56041
56239
  const prefix = writeU16BE3(unpaddedLen);
@@ -56083,7 +56281,7 @@ function decodePayload3(payload) {
56083
56281
  }
56084
56282
  function encrypt24(plaintext, conversationKey, nonce = randomBytes9(32)) {
56085
56283
  const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys3(conversationKey, nonce);
56086
- const padded = pad3(plaintext);
56284
+ const padded = pad5(plaintext);
56087
56285
  const ciphertext = chacha203(chacha_key, chacha_nonce, padded);
56088
56286
  const mac = hmacAad3(hmac_key, ciphertext, nonce);
56089
56287
  return base645.encode(concatBytes13(new Uint8Array([2]), nonce, ciphertext, mac));
@@ -67516,7 +67714,7 @@ function writeU32BE(num2) {
67516
67714
  new DataView(arr.buffer).setUint32(0, num2, false);
67517
67715
  return arr;
67518
67716
  }
67519
- function pad4(plaintext) {
67717
+ function pad6(plaintext) {
67520
67718
  const unpadded = utf8Encoder13.encode(plaintext);
67521
67719
  const unpaddedLen = unpadded.length;
67522
67720
  if (unpaddedLen < minPlaintextSize4 || unpaddedLen > maxPlaintextSize4)
@@ -67578,7 +67776,7 @@ function decodePayload4(payload) {
67578
67776
  }
67579
67777
  function encrypt25(plaintext, conversationKey, nonce = randomBytes10(32)) {
67580
67778
  const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys4(conversationKey, nonce);
67581
- const padded = pad4(plaintext);
67779
+ const padded = pad6(plaintext);
67582
67780
  const ciphertext = chacha204(chacha_key, chacha_nonce, padded);
67583
67781
  const mac = hmacAad4(hmac_key, ciphertext, nonce);
67584
67782
  return base646.encode(concatBytes14(new Uint8Array([2]), nonce, ciphertext, mac));
@@ -67596,7 +67794,7 @@ var v24 = {
67596
67794
  utils: {
67597
67795
  getConversationKey: getConversationKey4,
67598
67796
  calcPaddedLen: calcPaddedLen4,
67599
- pad: pad4,
67797
+ pad: pad6,
67600
67798
  unpad: unpad4
67601
67799
  },
67602
67800
  encrypt: encrypt25,
@@ -69523,6 +69721,221 @@ async function getClientsList() {
69523
69721
  }));
69524
69722
  }
69525
69723
 
69724
+ // src/daemon/http/usage-summary.ts
69725
+ function rowToStat(r) {
69726
+ return {
69727
+ requests: r.requests,
69728
+ promptTokens: r.promptTokens,
69729
+ completionTokens: r.completionTokens,
69730
+ totalTokens: r.totalTokens,
69731
+ cost: r.cost,
69732
+ satsCost: r.satsCost
69733
+ };
69734
+ }
69735
+ function rowToTopModel(r) {
69736
+ return {
69737
+ modelId: r.group ?? "unknown",
69738
+ requests: r.requests,
69739
+ satsCost: r.satsCost,
69740
+ totalTokens: r.totalTokens
69741
+ };
69742
+ }
69743
+ function emptyBucket() {
69744
+ return { count: 0, cost: 0 };
69745
+ }
69746
+ var SIZE_BUCKET_BOUNDS = {
69747
+ tiny: [0, 1000],
69748
+ small: [1000, 1e4],
69749
+ medium: [1e4, 50000],
69750
+ large: [50000, 1e5],
69751
+ huge: [1e5, Infinity]
69752
+ };
69753
+ function computeSizeBuckets(entries) {
69754
+ const buckets = {
69755
+ tiny: emptyBucket(),
69756
+ small: emptyBucket(),
69757
+ medium: emptyBucket(),
69758
+ large: emptyBucket(),
69759
+ huge: emptyBucket()
69760
+ };
69761
+ for (const entry of entries) {
69762
+ for (const [name, [min, max]] of Object.entries(SIZE_BUCKET_BOUNDS)) {
69763
+ if (entry.totalTokens >= min && entry.totalTokens < max) {
69764
+ const bucket = buckets[name];
69765
+ bucket.count++;
69766
+ bucket.cost += entry.satsCost;
69767
+ break;
69768
+ }
69769
+ }
69770
+ }
69771
+ return buckets;
69772
+ }
69773
+ function startOfLocalDayUtc(now5, tzOffsetMinutes) {
69774
+ return Math.floor((now5 - tzOffsetMinutes * 60000) / 86400000) * 86400000 + tzOffsetMinutes * 60000;
69775
+ }
69776
+ var _cache = null;
69777
+ var CACHE_TTL_MS = 60000;
69778
+ async function getUsageSummary(driver, clients, tzOffsetMinutes, clientFilter) {
69779
+ const count = await driver.count(clientFilter ? { clients: clientFilter } : {});
69780
+ const clientIdentity = clients.map((c) => `${c.clientId}:${c.ownerNpub ?? ""}`).join(",");
69781
+ const filterKey = clientFilter ? `:f:${clientFilter.sort().join(",")}` : "";
69782
+ const cacheKey = `${count}:${clientIdentity}:${tzOffsetMinutes}${filterKey}`;
69783
+ const now5 = Date.now();
69784
+ if (_cache !== null && _cache.key === cacheKey && now5 - _cache.summary.generatedAt <= CACHE_TTL_MS) {
69785
+ return _cache.summary;
69786
+ }
69787
+ const baseFilter = clientFilter ? { clients: clientFilter } : {};
69788
+ if (count === 0) {
69789
+ const zeroStat = {
69790
+ requests: 0,
69791
+ promptTokens: 0,
69792
+ completionTokens: 0,
69793
+ totalTokens: 0,
69794
+ cost: 0,
69795
+ satsCost: 0
69796
+ };
69797
+ const zeroSummary = {
69798
+ generatedAt: now5,
69799
+ totals: zeroStat,
69800
+ models: [],
69801
+ providers: [],
69802
+ clients: [],
69803
+ npubs: [],
69804
+ days: [],
69805
+ hoursToday: [],
69806
+ sizeBuckets: {
69807
+ tiny: emptyBucket(),
69808
+ small: emptyBucket(),
69809
+ medium: emptyBucket(),
69810
+ large: emptyBucket(),
69811
+ huge: emptyBucket()
69812
+ },
69813
+ recent: []
69814
+ };
69815
+ _cache = { key: cacheKey, summary: zeroSummary };
69816
+ return zeroSummary;
69817
+ }
69818
+ const [totalsRow] = await driver.aggregate({ ...baseFilter });
69819
+ const totals = totalsRow ? rowToStat(totalsRow) : {
69820
+ requests: 0,
69821
+ promptTokens: 0,
69822
+ completionTokens: 0,
69823
+ totalTokens: 0,
69824
+ cost: 0,
69825
+ satsCost: 0
69826
+ };
69827
+ const modelRows = await driver.aggregate({ ...baseFilter, groupBy: "modelId" });
69828
+ const models = modelRows.map((r) => ({
69829
+ modelId: r.group ?? "unknown",
69830
+ ...rowToStat(r)
69831
+ }));
69832
+ const providerRows = await driver.aggregate({ ...baseFilter, groupBy: "baseUrl" });
69833
+ const providers = providerRows.map((r) => ({
69834
+ baseUrl: r.group ?? "unknown",
69835
+ ...rowToStat(r)
69836
+ }));
69837
+ const clientRows = await driver.aggregate({ ...baseFilter, groupBy: "client" });
69838
+ const clientSummaries = clientRows.map((r) => ({
69839
+ client: r.group ?? "unknown",
69840
+ ...rowToStat(r),
69841
+ topModels: []
69842
+ }));
69843
+ const topClientRows = clientRows.filter((r) => r.group !== null).slice(0, 3);
69844
+ for (let i9 = 0;i9 < topClientRows.length; i9++) {
69845
+ const clientId = topClientRows[i9].group;
69846
+ const topModelRows = await driver.aggregate({
69847
+ ...baseFilter,
69848
+ groupBy: "modelId",
69849
+ client: clientId
69850
+ });
69851
+ const summary2 = clientSummaries.find((c) => c.client === clientId);
69852
+ if (summary2) {
69853
+ summary2.topModels = topModelRows.slice(0, 5).map(rowToTopModel);
69854
+ }
69855
+ }
69856
+ const clientToNpub = new Map;
69857
+ for (const c of clients) {
69858
+ if (c.ownerNpub) {
69859
+ clientToNpub.set(c.clientId, c.ownerNpub);
69860
+ }
69861
+ }
69862
+ const npubStats = new Map;
69863
+ const npubClientIds = new Map;
69864
+ for (const r of clientRows) {
69865
+ if (r.group === null)
69866
+ continue;
69867
+ const npub = clientToNpub.get(r.group);
69868
+ if (!npub)
69869
+ continue;
69870
+ const existing = npubStats.get(npub);
69871
+ if (existing) {
69872
+ existing.requests += r.requests;
69873
+ existing.promptTokens += r.promptTokens;
69874
+ existing.completionTokens += r.completionTokens;
69875
+ existing.totalTokens += r.totalTokens;
69876
+ existing.cost += r.cost;
69877
+ existing.satsCost += r.satsCost;
69878
+ } else {
69879
+ npubStats.set(npub, { ...rowToStat(r) });
69880
+ }
69881
+ const ids = npubClientIds.get(npub) ?? [];
69882
+ ids.push(r.group);
69883
+ npubClientIds.set(npub, ids);
69884
+ }
69885
+ const sortedNpubs = [...npubStats.entries()].sort((a, b2) => b2[1].satsCost - a[1].satsCost);
69886
+ const npubs = sortedNpubs.map(([npub, stat2]) => ({
69887
+ npub,
69888
+ ...stat2,
69889
+ topModels: []
69890
+ }));
69891
+ for (let i9 = 0;i9 < Math.min(5, npubs.length); i9++) {
69892
+ const npubSummary = npubs[i9];
69893
+ const ids = npubClientIds.get(npubSummary.npub) ?? [];
69894
+ if (ids.length > 0) {
69895
+ const topModelRows = await driver.aggregate({
69896
+ groupBy: "modelId",
69897
+ clients: ids
69898
+ });
69899
+ npubSummary.topModels = topModelRows.slice(0, 5).map(rowToTopModel);
69900
+ }
69901
+ }
69902
+ const dayRows = await driver.aggregate({
69903
+ ...baseFilter,
69904
+ groupBy: "day",
69905
+ tzOffsetMinutes,
69906
+ after: now5 - 30 * 86400000
69907
+ });
69908
+ const days = dayRows.map((r) => ({ date: r.group, ...rowToStat(r) })).reverse();
69909
+ const todayStartUtc = startOfLocalDayUtc(now5, tzOffsetMinutes);
69910
+ const hourRows = await driver.aggregate({
69911
+ ...baseFilter,
69912
+ groupBy: "hour",
69913
+ tzOffsetMinutes,
69914
+ after: todayStartUtc - 1
69915
+ });
69916
+ const hoursToday = hourRows.map((r) => ({
69917
+ hour: Number(r.group),
69918
+ ...rowToStat(r)
69919
+ }));
69920
+ const allEntries = await driver.list(baseFilter);
69921
+ const sizeBuckets = computeSizeBuckets(allEntries);
69922
+ const recent = await driver.list({ ...baseFilter, limit: 50 });
69923
+ const summary = {
69924
+ generatedAt: now5,
69925
+ totals,
69926
+ models,
69927
+ providers,
69928
+ clients: clientSummaries,
69929
+ npubs,
69930
+ days,
69931
+ hoursToday,
69932
+ sizeBuckets,
69933
+ recent
69934
+ };
69935
+ _cache = { key: cacheKey, summary };
69936
+ return summary;
69937
+ }
69938
+
69526
69939
  // src/daemon/http/index.ts
69527
69940
  function generateApiKey() {
69528
69941
  const bytes12 = randomBytes12(24);
@@ -70310,8 +70723,12 @@ function createDaemonRequestHandler(deps) {
70310
70723
  }
70311
70724
  if (req.method === "GET" && url4.pathname === "/usage") {
70312
70725
  try {
70726
+ const npubFilter = url4.searchParams.get("npub")?.trim();
70727
+ const clients = npubFilter ? getClientsFromStore(deps.store) : undefined;
70728
+ const clientFilter = npubFilter ? clients.filter((c) => c.ownerNpub === npubFilter).map((c) => c.clientId) : undefined;
70313
70729
  const output12 = await deps.usageTrackingDriver.list({
70314
- limit: parseLimit(url4.searchParams.get("limit"))
70730
+ limit: parseLimit(url4.searchParams.get("limit")),
70731
+ ...clientFilter ? { clients: clientFilter } : {}
70315
70732
  });
70316
70733
  res.writeHead(200, { "Content-Type": "application/json" });
70317
70734
  res.end(JSON.stringify({ output: output12 }));
@@ -70320,6 +70737,19 @@ function createDaemonRequestHandler(deps) {
70320
70737
  }
70321
70738
  return;
70322
70739
  }
70740
+ if (req.method === "GET" && url4.pathname === "/usage/summary") {
70741
+ try {
70742
+ const tz = Number.parseInt(url4.searchParams.get("tz") || "0", 10) || 0;
70743
+ const npubFilter = url4.searchParams.get("npub")?.trim();
70744
+ const clients = getClientsFromStore(deps.store);
70745
+ const clientFilter = npubFilter ? clients.filter((c) => c.ownerNpub === npubFilter).map((c) => c.clientId) : undefined;
70746
+ const summary = await getUsageSummary(deps.usageTrackingDriver, clients, tz, clientFilter);
70747
+ sendJson(res, 200, { output: summary });
70748
+ } catch (error) {
70749
+ sendJson(res, 500, { error: toErrorMessage(error) });
70750
+ }
70751
+ return;
70752
+ }
70323
70753
  if (req.method === "GET" && url4.pathname === "/usagePi") {
70324
70754
  try {
70325
70755
  const timestamp = (url4.searchParams.get("timestamp") || "").trim();
@@ -70406,6 +70836,7 @@ function createDaemonRequestHandler(deps) {
70406
70836
  sdkStore: deps.store,
70407
70837
  providerManager: deps.providerManager,
70408
70838
  logger: reqLogger,
70839
+ ...deps.requestResponseLogSink ? { requestResponseLogSink: deps.requestResponseLogSink } : {},
70409
70840
  ...deps.routstrPubkey ? { routstrPubkey: deps.routstrPubkey } : {}
70410
70841
  });
70411
70842
  res.statusCode = response3.status;
@@ -70475,6 +70906,218 @@ function createDaemonRequestHandler(deps) {
70475
70906
  };
70476
70907
  }
70477
70908
 
70909
+ // src/daemon/request-response-log-sink.ts
70910
+ import { createWriteStream, mkdirSync as mkdirSync6 } from "fs";
70911
+ import { writeFile as writeFile7 } from "fs/promises";
70912
+ import { join as join9 } from "path";
70913
+ var SENSITIVE_HEADER_NAMES = new Set([
70914
+ "authorization",
70915
+ "x-cashu",
70916
+ "cookie",
70917
+ "set-cookie",
70918
+ "proxy-authorization"
70919
+ ]);
70920
+ var SENSITIVE_BODY_FIELD_NAMES = new Set([
70921
+ "authorization",
70922
+ "api_key",
70923
+ "apikey",
70924
+ "apiKey",
70925
+ "access_token",
70926
+ "accessToken",
70927
+ "bearer",
70928
+ "cashu",
70929
+ "cookie",
70930
+ "key",
70931
+ "password",
70932
+ "secret",
70933
+ "token",
70934
+ "x-cashu"
70935
+ ]);
70936
+ var REDACTED = "[REDACTED]";
70937
+ var sanitizeForFilename = (value) => value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
70938
+ var makeId = () => {
70939
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
70940
+ const random = crypto.randomUUID().slice(0, 8);
70941
+ return `${timestamp}-${random}`;
70942
+ };
70943
+ var headersToObject = (headers) => {
70944
+ const out = {};
70945
+ headers.forEach((value, key) => {
70946
+ out[key] = value;
70947
+ });
70948
+ return out;
70949
+ };
70950
+ var redactHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([key, value]) => [
70951
+ key,
70952
+ SENSITIVE_HEADER_NAMES.has(key.toLowerCase()) ? REDACTED : value
70953
+ ]));
70954
+ var redactBody = (value) => {
70955
+ if (!value || typeof value !== "object")
70956
+ return value;
70957
+ if (Array.isArray(value))
70958
+ return value.map(redactBody);
70959
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
70960
+ key,
70961
+ SENSITIVE_BODY_FIELD_NAMES.has(key) || SENSITIVE_BODY_FIELD_NAMES.has(key.toLowerCase()) ? REDACTED : redactBody(entry)
70962
+ ]));
70963
+ };
70964
+ var redactRawBody = (rawBody) => {
70965
+ if (!rawBody)
70966
+ return;
70967
+ try {
70968
+ return JSON.stringify(redactBody(JSON.parse(rawBody)));
70969
+ } catch {
70970
+ return rawBody;
70971
+ }
70972
+ };
70973
+
70974
+ class FileRequestResponseLogSink {
70975
+ options;
70976
+ requestsDir;
70977
+ responsesDir;
70978
+ activeResponses = new Map;
70979
+ constructor(options) {
70980
+ this.options = options;
70981
+ this.requestsDir = join9(options.dir, "requests");
70982
+ this.responsesDir = join9(options.dir, "responses");
70983
+ mkdirSync6(this.requestsDir, { recursive: true });
70984
+ mkdirSync6(this.responsesDir, { recursive: true });
70985
+ }
70986
+ async logRequest(input) {
70987
+ try {
70988
+ const id = makeId();
70989
+ const filePath = join9(this.requestsDir, `${sanitizeForFilename(id)}.json`);
70990
+ await writeFile7(filePath, JSON.stringify({
70991
+ id,
70992
+ timestamp: new Date().toISOString(),
70993
+ method: input.method,
70994
+ url: input.url,
70995
+ path: input.path,
70996
+ baseUrl: input.baseUrl,
70997
+ headers: redactHeaders(input.headers),
70998
+ body: redactBody(input.body),
70999
+ rawBody: redactRawBody(input.rawBody)
71000
+ }, null, 2));
71001
+ return id;
71002
+ } catch (error) {
71003
+ this.options.logger?.error?.("[request-response-log] failed to log request:", error);
71004
+ return;
71005
+ }
71006
+ }
71007
+ async logResponseStart(id, response3) {
71008
+ if (!id)
71009
+ return;
71010
+ await this.append(id, {
71011
+ type: "response_start",
71012
+ status: response3.status,
71013
+ statusText: response3.statusText,
71014
+ headers: redactHeaders(headersToObject(response3.headers))
71015
+ });
71016
+ }
71017
+ logResponseChunk(id, sequence, text) {
71018
+ if (!id)
71019
+ return;
71020
+ this.append(id, {
71021
+ type: "chunk",
71022
+ sequence,
71023
+ text
71024
+ });
71025
+ }
71026
+ async logResponseEnd(id) {
71027
+ if (!id)
71028
+ return;
71029
+ await this.append(id, { type: "end" });
71030
+ await this.close(id);
71031
+ }
71032
+ async logResponseError(id, error) {
71033
+ if (!id)
71034
+ return;
71035
+ await this.append(id, {
71036
+ type: "error",
71037
+ error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error)
71038
+ });
71039
+ await this.close(id);
71040
+ }
71041
+ async logResponseBody(id, response3) {
71042
+ if (!id)
71043
+ return;
71044
+ try {
71045
+ if (!response3.body) {
71046
+ await this.logResponseEnd(id);
71047
+ return;
71048
+ }
71049
+ const reader = response3.body.getReader();
71050
+ const decoder = new TextDecoder("utf-8");
71051
+ let sequence = 0;
71052
+ while (true) {
71053
+ const { done, value } = await reader.read();
71054
+ if (done)
71055
+ break;
71056
+ if (value && value.byteLength > 0) {
71057
+ await this.append(id, {
71058
+ type: "chunk",
71059
+ sequence: sequence++,
71060
+ text: decoder.decode(value, { stream: true })
71061
+ });
71062
+ }
71063
+ }
71064
+ const tail = decoder.decode();
71065
+ if (tail) {
71066
+ await this.append(id, {
71067
+ type: "chunk",
71068
+ sequence: sequence++,
71069
+ text: tail
71070
+ });
71071
+ }
71072
+ await this.logResponseEnd(id);
71073
+ } catch (error) {
71074
+ await this.logResponseError(id, error);
71075
+ }
71076
+ }
71077
+ getOrCreate(id) {
71078
+ const existing = this.activeResponses.get(id);
71079
+ if (existing)
71080
+ return existing;
71081
+ const filePath = join9(this.responsesDir, `${sanitizeForFilename(id)}.jsonl`);
71082
+ const stream = createWriteStream(filePath, { flags: "a" });
71083
+ const active = {
71084
+ stream,
71085
+ pending: Promise.resolve()
71086
+ };
71087
+ stream.on("error", (error) => {
71088
+ this.options.logger?.error?.("[request-response-log] response log stream error:", error);
71089
+ });
71090
+ this.activeResponses.set(id, active);
71091
+ return active;
71092
+ }
71093
+ async append(id, event11) {
71094
+ try {
71095
+ const active = this.getOrCreate(id);
71096
+ const line = JSON.stringify({ requestLogId: id, timestamp: new Date().toISOString(), ...event11 }) + `
71097
+ `;
71098
+ active.pending = active.pending.then(() => new Promise((resolve, reject) => {
71099
+ active.stream.write(line, (error) => {
71100
+ if (error)
71101
+ reject(error);
71102
+ else
71103
+ resolve();
71104
+ });
71105
+ }));
71106
+ await active.pending;
71107
+ } catch (error) {
71108
+ this.options.logger?.error?.("[request-response-log] failed to append response event:", error);
71109
+ }
71110
+ }
71111
+ async close(id) {
71112
+ const active = this.activeResponses.get(id);
71113
+ if (!active)
71114
+ return;
71115
+ this.activeResponses.delete(id);
71116
+ await active.pending;
71117
+ await new Promise((resolve) => active.stream.end(resolve));
71118
+ }
71119
+ }
71120
+
70478
71121
  // src/integrations/index.ts
70479
71122
  async function refreshModelsAndIntegrations(getRoutstr21Models, config, label = "Scheduled") {
70480
71123
  await getRoutstr21Models(true);
@@ -70505,6 +71148,11 @@ async function main() {
70505
71148
  const config = await loadDaemonConfig();
70506
71149
  const port = args.port;
70507
71150
  const provider = args.provider || config.provider;
71151
+ const requestResponseLogDir = process.env.ROUTSTRD_REQUEST_RESPONSE_LOG_DIR || (config.requestResponseLogging?.enabled ? config.requestResponseLogging.dir || REQUEST_RESPONSE_LOGS_DIR : undefined);
71152
+ const requestResponseLogSink = requestResponseLogDir ? new FileRequestResponseLogSink({
71153
+ dir: requestResponseLogDir,
71154
+ logger: daemonSdkLogger.child("request-response-log")
71155
+ }) : undefined;
70508
71156
  await ensureDirs();
70509
71157
  const updatedConfig = { ...config, port, provider };
70510
71158
  saveDaemonConfig(updatedConfig);
@@ -70564,7 +71212,8 @@ async function main() {
70564
71212
  routstrPubkey: config.routstrPubkey,
70565
71213
  usageTrackingDriver,
70566
71214
  providerManager,
70567
- refundClient
71215
+ refundClient,
71216
+ requestResponseLogSink
70568
71217
  }));
70569
71218
  Bun.write(PID_FILE, String(process.pid));
70570
71219
  try {
@@ -70639,6 +71288,9 @@ async function main() {
70639
71288
  });
70640
71289
  server.listen(port, async () => {
70641
71290
  logger5.log(`Routstr daemon listening on http://localhost:${port}/v1`);
71291
+ if (requestResponseLogDir) {
71292
+ logger5.log(`Raw request/response logs: ${requestResponseLogDir}`);
71293
+ }
70642
71294
  ensureProvidersBootstrapped().then(async () => {
70643
71295
  startModelRefreshJob();
70644
71296
  startRefundJob();