@routstr/sdk 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/client/index.d.mts +21 -8
- package/dist/client/index.d.ts +21 -8
- package/dist/client/index.js +1406 -69
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +1406 -70
- package/dist/client/index.mjs.map +1 -1
- package/dist/discovery/index.d.mts +2 -2
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +1 -4
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/index.mjs +1 -4
- package/dist/discovery/index.mjs.map +1 -1
- package/dist/index.d.mts +15 -19
- package/dist/index.d.ts +15 -19
- package/dist/index.js +2385 -1574
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2380 -1575
- package/dist/index.mjs.map +1 -1
- package/dist/{interfaces-DGdP8fQp.d.mts → interfaces-BWJJTCXO.d.mts} +1 -1
- package/dist/{interfaces-CC0LT9p9.d.ts → interfaces-BxDEka72.d.ts} +1 -1
- package/dist/{interfaces-B85Wx7ni.d.mts → interfaces-C6Dr6hKy.d.mts} +1 -1
- package/dist/{interfaces-BVNyAmKu.d.ts → interfaces-CluftN4z.d.ts} +1 -1
- package/dist/storage/index.d.mts +56 -34
- package/dist/storage/index.d.ts +56 -34
- package/dist/storage/index.js +500 -51
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/index.mjs +497 -52
- package/dist/storage/index.mjs.map +1 -1
- package/dist/{types-BlHjmWRK.d.mts → types-BYj_8c5c.d.mts} +3 -0
- package/dist/{types-BlHjmWRK.d.ts → types-BYj_8c5c.d.ts} +3 -0
- package/dist/wallet/index.d.mts +9 -5
- package/dist/wallet/index.d.ts +9 -5
- package/dist/wallet/index.js +54 -22
- package/dist/wallet/index.js.map +1 -1
- package/dist/wallet/index.mjs +54 -22
- package/dist/wallet/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { EventStore } from 'applesauce-core';
|
|
|
3
3
|
import { tap } from 'rxjs';
|
|
4
4
|
import { createStore } from 'zustand/vanilla';
|
|
5
5
|
import { getDecodedToken } from '@cashu/cashu-ts';
|
|
6
|
+
import { Transform, Readable } from 'stream';
|
|
6
7
|
|
|
7
8
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
9
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -383,10 +384,7 @@ var ModelManager = class _ModelManager {
|
|
|
383
384
|
throw new Error(`Failed to fetch models: ${res.status}`);
|
|
384
385
|
}
|
|
385
386
|
const json = await res.json();
|
|
386
|
-
const list = Array.isArray(json?.data) ? json.data
|
|
387
|
-
...m,
|
|
388
|
-
id: m.id.split("/").pop() || m.id
|
|
389
|
-
})) : [];
|
|
387
|
+
const list = Array.isArray(json?.data) ? json.data : [];
|
|
390
388
|
return list;
|
|
391
389
|
}
|
|
392
390
|
isProviderDownError(error) {
|
|
@@ -1113,7 +1111,7 @@ var CashuSpender = class {
|
|
|
1113
1111
|
/**
|
|
1114
1112
|
* Refund specific providers without retrying spend
|
|
1115
1113
|
*/
|
|
1116
|
-
async refundProviders(baseUrls, mintUrl, refundApiKeys = false) {
|
|
1114
|
+
async refundProviders(baseUrls, mintUrl, refundApiKeys = false, forceRefund) {
|
|
1117
1115
|
const results = [];
|
|
1118
1116
|
const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
|
|
1119
1117
|
const toRefund = pendingDistribution.filter(
|
|
@@ -1163,7 +1161,8 @@ var CashuSpender = class {
|
|
|
1163
1161
|
const refundResult = await this.balanceManager.refundApiKey({
|
|
1164
1162
|
mintUrl,
|
|
1165
1163
|
baseUrl: apiKeyEntry.baseUrl,
|
|
1166
|
-
apiKey: apiKeyEntryFull.key
|
|
1164
|
+
apiKey: apiKeyEntryFull.key,
|
|
1165
|
+
forceRefund
|
|
1167
1166
|
});
|
|
1168
1167
|
if (refundResult.success) {
|
|
1169
1168
|
this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
|
|
@@ -1336,12 +1335,29 @@ var BalanceManager = class {
|
|
|
1336
1335
|
}
|
|
1337
1336
|
/**
|
|
1338
1337
|
* Refund API key balance - convert remaining API key balance to cashu token
|
|
1338
|
+
* @param options - Refund options including forceRefund flag
|
|
1339
|
+
* @returns Refund result
|
|
1339
1340
|
*/
|
|
1340
1341
|
async refundApiKey(options) {
|
|
1341
|
-
const { mintUrl, baseUrl, apiKey } = options;
|
|
1342
|
+
const { mintUrl, baseUrl, apiKey, forceRefund } = options;
|
|
1342
1343
|
if (!apiKey) {
|
|
1343
1344
|
return { success: false, message: "No API key to refund" };
|
|
1344
1345
|
}
|
|
1346
|
+
if (!forceRefund) {
|
|
1347
|
+
const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
1348
|
+
if (apiKeyEntry?.lastUsed) {
|
|
1349
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
|
|
1350
|
+
if (apiKeyEntry.lastUsed > fiveMinutesAgo) {
|
|
1351
|
+
console.log(
|
|
1352
|
+
`[BalanceManager] Skipping refund for ${baseUrl} - used ${Math.round((Date.now() - apiKeyEntry.lastUsed) / 1e3)}s ago`
|
|
1353
|
+
);
|
|
1354
|
+
return {
|
|
1355
|
+
success: false,
|
|
1356
|
+
message: "API key was used recently, skipping refund"
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1345
1361
|
let fetchResult;
|
|
1346
1362
|
try {
|
|
1347
1363
|
fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
|
|
@@ -1513,9 +1529,13 @@ var BalanceManager = class {
|
|
|
1513
1529
|
p2pkPubkey
|
|
1514
1530
|
} = options;
|
|
1515
1531
|
const adjustedAmount = Math.ceil(amount);
|
|
1516
|
-
console.log(
|
|
1532
|
+
console.log(
|
|
1533
|
+
`[BalanceManager.createProviderToken] Starting: baseUrl=${baseUrl}, mintUrl=${mintUrl}, amount=${amount}, adjustedAmount=${adjustedAmount}, retryCount=${retryCount}`
|
|
1534
|
+
);
|
|
1517
1535
|
if (!adjustedAmount || isNaN(adjustedAmount)) {
|
|
1518
|
-
console.error(
|
|
1536
|
+
console.error(
|
|
1537
|
+
`[BalanceManager.createProviderToken] FAILURE: Invalid amount - amount=${amount}, adjustedAmount=${adjustedAmount}`
|
|
1538
|
+
);
|
|
1519
1539
|
return { success: false, error: "Invalid top up amount" };
|
|
1520
1540
|
}
|
|
1521
1541
|
const balanceState = await this.getBalanceState();
|
|
@@ -1529,8 +1549,8 @@ var BalanceManager = class {
|
|
|
1529
1549
|
const refundableProviderBalance = Object.entries(
|
|
1530
1550
|
balanceState.providerBalances
|
|
1531
1551
|
).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
|
|
1532
|
-
if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount <
|
|
1533
|
-
await this._refundOtherProvidersForTopUp(baseUrl, mintUrl);
|
|
1552
|
+
if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
|
|
1553
|
+
await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
|
|
1534
1554
|
return this.createProviderToken({
|
|
1535
1555
|
...options,
|
|
1536
1556
|
retryCount: retryCount + 1
|
|
@@ -1546,15 +1566,11 @@ var BalanceManager = class {
|
|
|
1546
1566
|
{ url: "", balance: 0 }
|
|
1547
1567
|
).url
|
|
1548
1568
|
);
|
|
1549
|
-
console.error(
|
|
1569
|
+
console.error(
|
|
1570
|
+
`[BalanceManager.createProviderToken] FAILURE: Insufficient balance - required=${adjustedAmount}, available=${totalMintBalance + targetProviderBalance}, totalMintBalance=${totalMintBalance}, targetProviderBalance=${targetProviderBalance}, refundableProviderBalance=${refundableProviderBalance}`
|
|
1571
|
+
);
|
|
1550
1572
|
return { success: false, error: error.message };
|
|
1551
1573
|
}
|
|
1552
|
-
if (targetProviderBalance >= adjustedAmount) {
|
|
1553
|
-
return {
|
|
1554
|
-
success: true,
|
|
1555
|
-
amountSpent: 0
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1558
1574
|
const providerMints = baseUrl && this.providerRegistry ? this.providerRegistry.getProviderMints(baseUrl) : [];
|
|
1559
1575
|
let requiredAmount = adjustedAmount;
|
|
1560
1576
|
const supportedMintsOnly = providerMints.length > 0;
|
|
@@ -1588,7 +1604,9 @@ var BalanceManager = class {
|
|
|
1588
1604
|
maxMintUrl = mintUrl2;
|
|
1589
1605
|
}
|
|
1590
1606
|
}
|
|
1591
|
-
console.error(
|
|
1607
|
+
console.error(
|
|
1608
|
+
`[BalanceManager.createProviderToken] FAILURE: No candidate mints found - requiredAmount=${requiredAmount}, totalMintBalance=${totalMintBalance}, maxBalance=${maxBalance}, maxMintUrl=${maxMintUrl}, providerMints=${JSON.stringify(providerMints)}`
|
|
1609
|
+
);
|
|
1592
1610
|
const error = new InsufficientBalanceError(
|
|
1593
1611
|
adjustedAmount,
|
|
1594
1612
|
totalMintBalance,
|
|
@@ -1600,13 +1618,17 @@ var BalanceManager = class {
|
|
|
1600
1618
|
let lastError;
|
|
1601
1619
|
for (const candidateMint of candidates) {
|
|
1602
1620
|
try {
|
|
1603
|
-
console.log(
|
|
1621
|
+
console.log(
|
|
1622
|
+
`[BalanceManager.createProviderToken] Attempting mint: ${candidateMint}, amount: ${requiredAmount}`
|
|
1623
|
+
);
|
|
1604
1624
|
const token = await this.walletAdapter.sendToken(
|
|
1605
1625
|
candidateMint,
|
|
1606
1626
|
requiredAmount,
|
|
1607
1627
|
p2pkPubkey
|
|
1608
1628
|
);
|
|
1609
|
-
console.log(
|
|
1629
|
+
console.log(
|
|
1630
|
+
`[BalanceManager.createProviderToken] SUCCESS: Token created from mint ${candidateMint}`
|
|
1631
|
+
);
|
|
1610
1632
|
return {
|
|
1611
1633
|
success: true,
|
|
1612
1634
|
token,
|
|
@@ -1615,11 +1637,15 @@ var BalanceManager = class {
|
|
|
1615
1637
|
};
|
|
1616
1638
|
} catch (error) {
|
|
1617
1639
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1618
|
-
console.error(
|
|
1640
|
+
console.error(
|
|
1641
|
+
`[BalanceManager.createProviderToken] FAILURE: Mint ${candidateMint} failed with error: ${errorMsg}`
|
|
1642
|
+
);
|
|
1619
1643
|
if (error instanceof Error) {
|
|
1620
1644
|
lastError = errorMsg;
|
|
1621
1645
|
if (isNetworkErrorMessage(error.message)) {
|
|
1622
|
-
console.warn(
|
|
1646
|
+
console.warn(
|
|
1647
|
+
`[BalanceManager.createProviderToken] Network error from ${candidateMint}, trying next mint...`
|
|
1648
|
+
);
|
|
1623
1649
|
continue;
|
|
1624
1650
|
}
|
|
1625
1651
|
}
|
|
@@ -1629,7 +1655,9 @@ var BalanceManager = class {
|
|
|
1629
1655
|
};
|
|
1630
1656
|
}
|
|
1631
1657
|
}
|
|
1632
|
-
console.error(
|
|
1658
|
+
console.error(
|
|
1659
|
+
`[BalanceManager.createProviderToken] FAILURE: All candidate mints exhausted - lastError=${lastError}, candidates=${JSON.stringify(candidates)}`
|
|
1660
|
+
);
|
|
1633
1661
|
return {
|
|
1634
1662
|
success: false,
|
|
1635
1663
|
error: lastError || "All candidate mints failed while creating top up token"
|
|
@@ -1675,9 +1703,10 @@ var BalanceManager = class {
|
|
|
1675
1703
|
}
|
|
1676
1704
|
return candidates;
|
|
1677
1705
|
}
|
|
1678
|
-
async _refundOtherProvidersForTopUp(baseUrl, mintUrl) {
|
|
1706
|
+
async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
|
|
1679
1707
|
const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
|
|
1680
1708
|
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
1709
|
+
const forceRefund = retryCount >= 2;
|
|
1681
1710
|
const toRefund = pendingDistribution.filter(
|
|
1682
1711
|
(pending) => pending.baseUrl !== baseUrl
|
|
1683
1712
|
);
|
|
@@ -1718,7 +1747,8 @@ var BalanceManager = class {
|
|
|
1718
1747
|
const result = await this.refundApiKey({
|
|
1719
1748
|
mintUrl,
|
|
1720
1749
|
baseUrl: apiKeyEntry.baseUrl,
|
|
1721
|
-
apiKey: fullApiKeyEntry.key
|
|
1750
|
+
apiKey: fullApiKeyEntry.key,
|
|
1751
|
+
forceRefund
|
|
1722
1752
|
});
|
|
1723
1753
|
return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
|
|
1724
1754
|
})
|
|
@@ -1972,6 +2002,77 @@ var BalanceManager = class {
|
|
|
1972
2002
|
}
|
|
1973
2003
|
};
|
|
1974
2004
|
|
|
2005
|
+
// client/usage.ts
|
|
2006
|
+
function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
|
|
2007
|
+
if (!body || typeof body !== "object") return null;
|
|
2008
|
+
const usage = body.usage;
|
|
2009
|
+
if (!usage || typeof usage !== "object") return null;
|
|
2010
|
+
const promptTokens = Number(usage.prompt_tokens ?? 0);
|
|
2011
|
+
const completionTokens = Number(usage.completion_tokens ?? 0);
|
|
2012
|
+
const totalTokens = Number(usage.total_tokens ?? 0);
|
|
2013
|
+
const costValue = usage.cost;
|
|
2014
|
+
let cost = 0;
|
|
2015
|
+
let satsCost = fallbackSatsCost;
|
|
2016
|
+
if (typeof costValue === "number") {
|
|
2017
|
+
cost = costValue;
|
|
2018
|
+
} else if (costValue && typeof costValue === "object") {
|
|
2019
|
+
const costObj = costValue;
|
|
2020
|
+
const totalUsd = costObj.total_usd;
|
|
2021
|
+
const totalMsats = costObj.total_msats;
|
|
2022
|
+
cost = typeof totalUsd === "number" ? totalUsd : 0;
|
|
2023
|
+
if (typeof totalMsats === "number") {
|
|
2024
|
+
satsCost = totalMsats / 1e3;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
|
|
2028
|
+
return null;
|
|
2029
|
+
}
|
|
2030
|
+
return {
|
|
2031
|
+
promptTokens,
|
|
2032
|
+
completionTokens,
|
|
2033
|
+
totalTokens,
|
|
2034
|
+
cost,
|
|
2035
|
+
satsCost
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
function extractResponseId(body) {
|
|
2039
|
+
if (!body || typeof body !== "object") return void 0;
|
|
2040
|
+
const id = body.id;
|
|
2041
|
+
if (typeof id !== "string") return void 0;
|
|
2042
|
+
const trimmed = id.trim();
|
|
2043
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
2044
|
+
}
|
|
2045
|
+
function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
|
|
2046
|
+
if (!parsed || typeof parsed !== "object" || !parsed.usage) {
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
const usage = parsed.usage;
|
|
2050
|
+
const usageCost = usage.cost;
|
|
2051
|
+
const cost = typeof usageCost === "number" ? usageCost : usageCost?.total_usd ?? parsed.metadata?.routstr?.cost?.total_usd ?? 0;
|
|
2052
|
+
const msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
|
|
2053
|
+
const result = {
|
|
2054
|
+
promptTokens: Number(usage.prompt_tokens ?? 0),
|
|
2055
|
+
completionTokens: Number(usage.completion_tokens ?? 0),
|
|
2056
|
+
totalTokens: Number(usage.total_tokens ?? 0),
|
|
2057
|
+
cost: Number(cost ?? 0),
|
|
2058
|
+
satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
|
|
2059
|
+
};
|
|
2060
|
+
if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
return result;
|
|
2064
|
+
}
|
|
2065
|
+
function toUsageStats(usage) {
|
|
2066
|
+
if (!usage) return void 0;
|
|
2067
|
+
return {
|
|
2068
|
+
total_tokens: usage.totalTokens,
|
|
2069
|
+
prompt_tokens: usage.promptTokens,
|
|
2070
|
+
completion_tokens: usage.completionTokens,
|
|
2071
|
+
cost: usage.cost,
|
|
2072
|
+
sats_cost: usage.satsCost
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1975
2076
|
// client/StreamProcessor.ts
|
|
1976
2077
|
var StreamProcessor = class {
|
|
1977
2078
|
accumulatedContent = "";
|
|
@@ -1999,6 +2100,7 @@ var StreamProcessor = class {
|
|
|
1999
2100
|
let finish_reason;
|
|
2000
2101
|
let citations;
|
|
2001
2102
|
let annotations;
|
|
2103
|
+
let responseId;
|
|
2002
2104
|
try {
|
|
2003
2105
|
while (true) {
|
|
2004
2106
|
const { done, value } = await reader.read();
|
|
@@ -2027,6 +2129,9 @@ var StreamProcessor = class {
|
|
|
2027
2129
|
if (parsed.finish_reason) {
|
|
2028
2130
|
finish_reason = parsed.finish_reason;
|
|
2029
2131
|
}
|
|
2132
|
+
if (parsed.responseId) {
|
|
2133
|
+
responseId = parsed.responseId;
|
|
2134
|
+
}
|
|
2030
2135
|
if (parsed.citations) {
|
|
2031
2136
|
citations = parsed.citations;
|
|
2032
2137
|
}
|
|
@@ -2047,6 +2152,7 @@ var StreamProcessor = class {
|
|
|
2047
2152
|
images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
|
|
2048
2153
|
usage,
|
|
2049
2154
|
model,
|
|
2155
|
+
responseId,
|
|
2050
2156
|
finish_reason,
|
|
2051
2157
|
citations,
|
|
2052
2158
|
annotations
|
|
@@ -2074,12 +2180,15 @@ var StreamProcessor = class {
|
|
|
2074
2180
|
result.reasoning = parsed.choices[0].delta.reasoning;
|
|
2075
2181
|
}
|
|
2076
2182
|
if (parsed.usage) {
|
|
2077
|
-
result.usage = {
|
|
2183
|
+
result.usage = toUsageStats(extractUsageFromSSEJson(parsed)) ?? {
|
|
2078
2184
|
total_tokens: parsed.usage.total_tokens,
|
|
2079
2185
|
prompt_tokens: parsed.usage.prompt_tokens,
|
|
2080
2186
|
completion_tokens: parsed.usage.completion_tokens
|
|
2081
2187
|
};
|
|
2082
2188
|
}
|
|
2189
|
+
if (parsed.id) {
|
|
2190
|
+
result.responseId = parsed.id;
|
|
2191
|
+
}
|
|
2083
2192
|
if (parsed.model) {
|
|
2084
2193
|
result.model = parsed.model;
|
|
2085
2194
|
}
|
|
@@ -2527,7 +2636,6 @@ var ProviderManager = class _ProviderManager {
|
|
|
2527
2636
|
* Get providers for a model sorted by prompt+completion pricing
|
|
2528
2637
|
*/
|
|
2529
2638
|
getProviderPriceRankingForModel(modelId, options = {}) {
|
|
2530
|
-
const normalizedId = this.normalizeModelId(modelId);
|
|
2531
2639
|
const includeDisabled = options.includeDisabled ?? false;
|
|
2532
2640
|
const torMode = options.torMode ?? false;
|
|
2533
2641
|
const disabledProviders = new Set(
|
|
@@ -2541,9 +2649,7 @@ var ProviderManager = class _ProviderManager {
|
|
|
2541
2649
|
if (torMode && !baseUrl.includes(".onion")) continue;
|
|
2542
2650
|
if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
|
|
2543
2651
|
continue;
|
|
2544
|
-
const match = models.find(
|
|
2545
|
-
(model) => this.normalizeModelId(model.id) === normalizedId
|
|
2546
|
-
);
|
|
2652
|
+
const match = models.find((model) => model.id === modelId);
|
|
2547
2653
|
if (!match?.sats_pricing) continue;
|
|
2548
2654
|
const prompt = match.sats_pricing.prompt;
|
|
2549
2655
|
const completion = match.sats_pricing.completion;
|
|
@@ -2656,1323 +2762,740 @@ var ProviderManager = class _ProviderManager {
|
|
|
2656
2762
|
}
|
|
2657
2763
|
};
|
|
2658
2764
|
|
|
2659
|
-
//
|
|
2660
|
-
var
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
);
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
return this.mode;
|
|
2694
|
-
}
|
|
2695
|
-
getDebugLevel() {
|
|
2696
|
-
return this.debugLevel;
|
|
2697
|
-
}
|
|
2698
|
-
setDebugLevel(level) {
|
|
2699
|
-
this.debugLevel = level;
|
|
2700
|
-
}
|
|
2701
|
-
_log(level, ...args) {
|
|
2702
|
-
const levelPriority = {
|
|
2703
|
-
DEBUG: 0,
|
|
2704
|
-
WARN: 1,
|
|
2705
|
-
ERROR: 2
|
|
2706
|
-
};
|
|
2707
|
-
if (levelPriority[level] >= levelPriority[this.debugLevel]) {
|
|
2708
|
-
switch (level) {
|
|
2709
|
-
case "DEBUG":
|
|
2710
|
-
console.log(...args);
|
|
2711
|
-
break;
|
|
2712
|
-
case "WARN":
|
|
2713
|
-
console.warn(...args);
|
|
2714
|
-
break;
|
|
2715
|
-
case "ERROR":
|
|
2716
|
-
console.error(...args);
|
|
2717
|
-
break;
|
|
2765
|
+
// storage/drivers/localStorage.ts
|
|
2766
|
+
var canUseLocalStorage = () => {
|
|
2767
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
2768
|
+
};
|
|
2769
|
+
var isQuotaExceeded = (error) => {
|
|
2770
|
+
const e = error;
|
|
2771
|
+
return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
|
|
2772
|
+
};
|
|
2773
|
+
var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
|
|
2774
|
+
var localStorageDriver = {
|
|
2775
|
+
async getItem(key, defaultValue) {
|
|
2776
|
+
if (!canUseLocalStorage()) return defaultValue;
|
|
2777
|
+
try {
|
|
2778
|
+
const item = window.localStorage.getItem(key);
|
|
2779
|
+
if (item === null) return defaultValue;
|
|
2780
|
+
try {
|
|
2781
|
+
return JSON.parse(item);
|
|
2782
|
+
} catch (parseError) {
|
|
2783
|
+
if (typeof defaultValue === "string") {
|
|
2784
|
+
return item;
|
|
2785
|
+
}
|
|
2786
|
+
throw parseError;
|
|
2787
|
+
}
|
|
2788
|
+
} catch (error) {
|
|
2789
|
+
console.error(`Error retrieving item with key "${key}":`, error);
|
|
2790
|
+
if (canUseLocalStorage()) {
|
|
2791
|
+
try {
|
|
2792
|
+
window.localStorage.removeItem(key);
|
|
2793
|
+
} catch (removeError) {
|
|
2794
|
+
console.error(
|
|
2795
|
+
`Error removing corrupted item with key "${key}":`,
|
|
2796
|
+
removeError
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2718
2799
|
}
|
|
2800
|
+
return defaultValue;
|
|
2801
|
+
}
|
|
2802
|
+
},
|
|
2803
|
+
async setItem(key, value) {
|
|
2804
|
+
if (!canUseLocalStorage()) return;
|
|
2805
|
+
try {
|
|
2806
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
2807
|
+
} catch (error) {
|
|
2808
|
+
if (isQuotaExceeded(error)) {
|
|
2809
|
+
if (NON_CRITICAL_KEYS.has(key)) {
|
|
2810
|
+
console.warn(
|
|
2811
|
+
`Storage quota exceeded; skipping non-critical key "${key}".`
|
|
2812
|
+
);
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
try {
|
|
2816
|
+
window.localStorage.removeItem("modelsFromAllProviders");
|
|
2817
|
+
} catch {
|
|
2818
|
+
}
|
|
2819
|
+
try {
|
|
2820
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
2821
|
+
return;
|
|
2822
|
+
} catch (retryError) {
|
|
2823
|
+
console.warn(
|
|
2824
|
+
`Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
|
|
2825
|
+
retryError
|
|
2826
|
+
);
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
console.error(`Error storing item with key "${key}":`, error);
|
|
2831
|
+
}
|
|
2832
|
+
},
|
|
2833
|
+
async removeItem(key) {
|
|
2834
|
+
if (!canUseLocalStorage()) return;
|
|
2835
|
+
try {
|
|
2836
|
+
window.localStorage.removeItem(key);
|
|
2837
|
+
} catch (error) {
|
|
2838
|
+
console.error(`Error removing item with key "${key}":`, error);
|
|
2719
2839
|
}
|
|
2720
2840
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
getBalanceManager() {
|
|
2731
|
-
return this.balanceManager;
|
|
2732
|
-
}
|
|
2733
|
-
/**
|
|
2734
|
-
* Get the ProviderManager instance
|
|
2735
|
-
*/
|
|
2736
|
-
getProviderManager() {
|
|
2737
|
-
return this.providerManager;
|
|
2738
|
-
}
|
|
2739
|
-
/**
|
|
2740
|
-
* Check if the client is currently busy (in critical section)
|
|
2741
|
-
*/
|
|
2742
|
-
get isBusy() {
|
|
2743
|
-
return this.cashuSpender.isBusy;
|
|
2841
|
+
};
|
|
2842
|
+
|
|
2843
|
+
// storage/drivers/memory.ts
|
|
2844
|
+
var createMemoryDriver = (seed) => {
|
|
2845
|
+
const store = /* @__PURE__ */ new Map();
|
|
2846
|
+
if (seed) {
|
|
2847
|
+
for (const [key, value] of Object.entries(seed)) {
|
|
2848
|
+
store.set(key, value);
|
|
2849
|
+
}
|
|
2744
2850
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
method,
|
|
2757
|
-
body,
|
|
2758
|
-
headers = {},
|
|
2759
|
-
baseUrl,
|
|
2760
|
-
mintUrl,
|
|
2761
|
-
modelId
|
|
2762
|
-
} = params;
|
|
2763
|
-
await this._checkBalance();
|
|
2764
|
-
let requiredSats = 1;
|
|
2765
|
-
let selectedModel;
|
|
2766
|
-
if (modelId) {
|
|
2767
|
-
const providerModel = await this.providerManager.getModelForProvider(
|
|
2768
|
-
baseUrl,
|
|
2769
|
-
modelId
|
|
2770
|
-
);
|
|
2771
|
-
selectedModel = providerModel ?? void 0;
|
|
2772
|
-
if (selectedModel) {
|
|
2773
|
-
requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
2774
|
-
selectedModel,
|
|
2775
|
-
[]
|
|
2776
|
-
);
|
|
2851
|
+
return {
|
|
2852
|
+
async getItem(key, defaultValue) {
|
|
2853
|
+
const item = store.get(key);
|
|
2854
|
+
if (item === void 0) return defaultValue;
|
|
2855
|
+
try {
|
|
2856
|
+
return JSON.parse(item);
|
|
2857
|
+
} catch (parseError) {
|
|
2858
|
+
if (typeof defaultValue === "string") {
|
|
2859
|
+
return item;
|
|
2860
|
+
}
|
|
2861
|
+
throw parseError;
|
|
2777
2862
|
}
|
|
2863
|
+
},
|
|
2864
|
+
async setItem(key, value) {
|
|
2865
|
+
store.set(key, JSON.stringify(value));
|
|
2866
|
+
},
|
|
2867
|
+
async removeItem(key) {
|
|
2868
|
+
store.delete(key);
|
|
2778
2869
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
}
|
|
2792
|
-
const baseHeaders = this._buildBaseHeaders(headers);
|
|
2793
|
-
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
2794
|
-
const response = await this._makeRequest({
|
|
2795
|
-
path,
|
|
2796
|
-
method,
|
|
2797
|
-
body: method === "GET" ? void 0 : requestBody,
|
|
2798
|
-
baseUrl,
|
|
2799
|
-
mintUrl,
|
|
2800
|
-
token,
|
|
2801
|
-
requiredSats,
|
|
2802
|
-
headers: requestHeaders,
|
|
2803
|
-
baseHeaders,
|
|
2804
|
-
selectedModel
|
|
2805
|
-
});
|
|
2806
|
-
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
2807
|
-
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
2808
|
-
const tokenUsed = response.token || token;
|
|
2809
|
-
await this._handlePostResponseBalanceUpdate({
|
|
2810
|
-
token: tokenUsed,
|
|
2811
|
-
baseUrl: baseUrlUsed,
|
|
2812
|
-
initialTokenBalance: tokenBalanceInSats,
|
|
2813
|
-
response
|
|
2814
|
-
});
|
|
2815
|
-
return response;
|
|
2870
|
+
};
|
|
2871
|
+
};
|
|
2872
|
+
|
|
2873
|
+
// storage/drivers/sqlite.ts
|
|
2874
|
+
var isBun = () => {
|
|
2875
|
+
return typeof process.versions.bun !== "undefined";
|
|
2876
|
+
};
|
|
2877
|
+
var createDatabase = (dbPath) => {
|
|
2878
|
+
if (isBun()) {
|
|
2879
|
+
throw new Error(
|
|
2880
|
+
"SQLite driver not supported in Bun. Use createMemoryDriver() instead."
|
|
2881
|
+
);
|
|
2816
2882
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
selectedModel,
|
|
2824
|
-
baseUrl,
|
|
2825
|
-
mintUrl,
|
|
2826
|
-
balance,
|
|
2827
|
-
transactionHistory,
|
|
2828
|
-
maxTokens,
|
|
2829
|
-
headers
|
|
2830
|
-
} = options;
|
|
2831
|
-
const apiMessages = await this._convertMessages(messageHistory);
|
|
2832
|
-
const requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
2833
|
-
selectedModel,
|
|
2834
|
-
apiMessages,
|
|
2835
|
-
maxTokens
|
|
2883
|
+
let Database = null;
|
|
2884
|
+
try {
|
|
2885
|
+
Database = __require("better-sqlite3");
|
|
2886
|
+
} catch (error) {
|
|
2887
|
+
throw new Error(
|
|
2888
|
+
`better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
|
|
2836
2889
|
);
|
|
2837
|
-
try {
|
|
2838
|
-
await this._checkBalance();
|
|
2839
|
-
callbacks.onPaymentProcessing?.(true);
|
|
2840
|
-
const spendResult = await this._spendToken({
|
|
2841
|
-
mintUrl,
|
|
2842
|
-
amount: requiredSats,
|
|
2843
|
-
baseUrl
|
|
2844
|
-
});
|
|
2845
|
-
let token = spendResult.token;
|
|
2846
|
-
let tokenBalance = spendResult.tokenBalance;
|
|
2847
|
-
let tokenBalanceUnit = spendResult.tokenBalanceUnit;
|
|
2848
|
-
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
2849
|
-
callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
|
|
2850
|
-
const baseHeaders = this._buildBaseHeaders(headers);
|
|
2851
|
-
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
2852
|
-
this.providerManager.resetFailedProviders();
|
|
2853
|
-
const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
|
|
2854
|
-
const providerVersion = providerInfo?.version ?? "";
|
|
2855
|
-
let modelIdForRequest = selectedModel.id;
|
|
2856
|
-
if (/^0\.1\./.test(providerVersion)) {
|
|
2857
|
-
const newModel = await this.providerManager.getModelForProvider(
|
|
2858
|
-
baseUrl,
|
|
2859
|
-
selectedModel.id
|
|
2860
|
-
);
|
|
2861
|
-
modelIdForRequest = newModel?.id ?? selectedModel.id;
|
|
2862
|
-
}
|
|
2863
|
-
const body = {
|
|
2864
|
-
model: modelIdForRequest,
|
|
2865
|
-
messages: apiMessages,
|
|
2866
|
-
stream: true
|
|
2867
|
-
};
|
|
2868
|
-
if (maxTokens !== void 0) {
|
|
2869
|
-
body.max_tokens = maxTokens;
|
|
2870
|
-
}
|
|
2871
|
-
if (selectedModel?.name?.startsWith("OpenAI:")) {
|
|
2872
|
-
body.tools = [{ type: "web_search" }];
|
|
2873
|
-
}
|
|
2874
|
-
const response = await this._makeRequest({
|
|
2875
|
-
path: "/v1/chat/completions",
|
|
2876
|
-
method: "POST",
|
|
2877
|
-
body,
|
|
2878
|
-
selectedModel,
|
|
2879
|
-
baseUrl,
|
|
2880
|
-
mintUrl,
|
|
2881
|
-
token,
|
|
2882
|
-
requiredSats,
|
|
2883
|
-
maxTokens,
|
|
2884
|
-
headers: requestHeaders,
|
|
2885
|
-
baseHeaders
|
|
2886
|
-
});
|
|
2887
|
-
if (!response.body) {
|
|
2888
|
-
throw new Error("Response body is not available");
|
|
2889
|
-
}
|
|
2890
|
-
if (response.status === 200) {
|
|
2891
|
-
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
2892
|
-
const streamingResult = await this.streamProcessor.process(
|
|
2893
|
-
response,
|
|
2894
|
-
{
|
|
2895
|
-
onContent: callbacks.onStreamingUpdate,
|
|
2896
|
-
onThinking: callbacks.onThinkingUpdate
|
|
2897
|
-
},
|
|
2898
|
-
selectedModel.id
|
|
2899
|
-
);
|
|
2900
|
-
if (streamingResult.finish_reason === "content_filter") {
|
|
2901
|
-
callbacks.onMessageAppend({
|
|
2902
|
-
role: "assistant",
|
|
2903
|
-
content: "Your request was denied due to content filtering."
|
|
2904
|
-
});
|
|
2905
|
-
} else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
|
|
2906
|
-
const message = await this._createAssistantMessage(streamingResult);
|
|
2907
|
-
callbacks.onMessageAppend(message);
|
|
2908
|
-
} else {
|
|
2909
|
-
callbacks.onMessageAppend({
|
|
2910
|
-
role: "system",
|
|
2911
|
-
content: "The provider did not respond to this request."
|
|
2912
|
-
});
|
|
2913
|
-
}
|
|
2914
|
-
callbacks.onStreamingUpdate("");
|
|
2915
|
-
callbacks.onThinkingUpdate("");
|
|
2916
|
-
const isApikeysEstimate = this.mode === "apikeys";
|
|
2917
|
-
let satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
2918
|
-
token,
|
|
2919
|
-
baseUrl: baseUrlUsed,
|
|
2920
|
-
initialTokenBalance: tokenBalanceInSats,
|
|
2921
|
-
fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
|
|
2922
|
-
response
|
|
2923
|
-
});
|
|
2924
|
-
const estimatedCosts = this._getEstimatedCosts(
|
|
2925
|
-
selectedModel,
|
|
2926
|
-
streamingResult
|
|
2927
|
-
);
|
|
2928
|
-
const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
|
|
2929
|
-
onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
|
|
2930
|
-
} else {
|
|
2931
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
2932
|
-
}
|
|
2933
|
-
} catch (error) {
|
|
2934
|
-
this._handleError(error, callbacks);
|
|
2935
|
-
} finally {
|
|
2936
|
-
callbacks.onPaymentProcessing?.(false);
|
|
2937
|
-
}
|
|
2938
2890
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
const
|
|
2958
|
-
|
|
2891
|
+
return new Database(dbPath);
|
|
2892
|
+
};
|
|
2893
|
+
var createSqliteDriver = (options = {}) => {
|
|
2894
|
+
const dbPath = options.dbPath || "routstr.sqlite";
|
|
2895
|
+
const tableName = options.tableName || "sdk_storage";
|
|
2896
|
+
const db = createDatabase(dbPath);
|
|
2897
|
+
db.exec(
|
|
2898
|
+
`CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
|
|
2899
|
+
);
|
|
2900
|
+
const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
|
|
2901
|
+
const upsertStmt = db.prepare(
|
|
2902
|
+
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)
|
|
2903
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
2904
|
+
);
|
|
2905
|
+
const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
|
|
2906
|
+
return {
|
|
2907
|
+
async getItem(key, defaultValue) {
|
|
2908
|
+
try {
|
|
2909
|
+
const row = selectStmt.get(key);
|
|
2910
|
+
if (!row || typeof row.value !== "string") return defaultValue;
|
|
2959
2911
|
try {
|
|
2960
|
-
|
|
2961
|
-
} catch (
|
|
2962
|
-
|
|
2912
|
+
return JSON.parse(row.value);
|
|
2913
|
+
} catch (parseError) {
|
|
2914
|
+
if (typeof defaultValue === "string") {
|
|
2915
|
+
return row.value;
|
|
2916
|
+
}
|
|
2917
|
+
throw parseError;
|
|
2963
2918
|
}
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
response.status,
|
|
2968
|
-
requestId,
|
|
2969
|
-
this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
|
|
2970
|
-
bodyText,
|
|
2971
|
-
params.retryCount ?? 0
|
|
2972
|
-
);
|
|
2919
|
+
} catch (error) {
|
|
2920
|
+
console.error(`SQLite getItem failed for key "${key}":`, error);
|
|
2921
|
+
return defaultValue;
|
|
2973
2922
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
);
|
|
2923
|
+
},
|
|
2924
|
+
async setItem(key, value) {
|
|
2925
|
+
try {
|
|
2926
|
+
upsertStmt.run(key, JSON.stringify(value));
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
console.error(`SQLite setItem failed for key "${key}":`, error);
|
|
2929
|
+
}
|
|
2930
|
+
},
|
|
2931
|
+
async removeItem(key) {
|
|
2932
|
+
try {
|
|
2933
|
+
deleteStmt.run(key);
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
console.error(`SQLite removeItem failed for key "${key}":`, error);
|
|
2987
2936
|
}
|
|
2988
|
-
throw error;
|
|
2989
2937
|
}
|
|
2938
|
+
};
|
|
2939
|
+
};
|
|
2940
|
+
|
|
2941
|
+
// storage/drivers/indexedDB.ts
|
|
2942
|
+
var isBrowser = typeof indexedDB !== "undefined";
|
|
2943
|
+
var openDatabase = (dbName, storeName) => {
|
|
2944
|
+
if (!isBrowser) {
|
|
2945
|
+
return Promise.reject(new Error("IndexedDB is not available"));
|
|
2990
2946
|
}
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
let tryNextProvider = false;
|
|
2998
|
-
this._log(
|
|
2999
|
-
"DEBUG",
|
|
3000
|
-
`[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
|
|
3001
|
-
);
|
|
3002
|
-
this._log(
|
|
3003
|
-
"DEBUG",
|
|
3004
|
-
`[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
|
|
3005
|
-
);
|
|
3006
|
-
if (params.token.startsWith("cashu")) {
|
|
3007
|
-
const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
|
|
3008
|
-
params.token
|
|
3009
|
-
);
|
|
3010
|
-
if (tryReceiveTokenResult.success) {
|
|
3011
|
-
this._log(
|
|
3012
|
-
"DEBUG",
|
|
3013
|
-
`[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
|
|
3014
|
-
);
|
|
3015
|
-
tryNextProvider = true;
|
|
3016
|
-
if (this.mode === "lazyrefund")
|
|
3017
|
-
this.storageAdapter.removeToken(baseUrl);
|
|
3018
|
-
} else {
|
|
3019
|
-
this._log(
|
|
3020
|
-
"DEBUG",
|
|
3021
|
-
`[RoutstrClient] _handleErrorResponse: Failed to receive token. `
|
|
3022
|
-
);
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
if (this.mode === "xcashu") {
|
|
3026
|
-
if (xCashuRefundToken) {
|
|
3027
|
-
this._log(
|
|
3028
|
-
"DEBUG",
|
|
3029
|
-
`[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
|
|
3030
|
-
);
|
|
3031
|
-
try {
|
|
3032
|
-
const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
|
|
3033
|
-
if (receiveResult.success) {
|
|
3034
|
-
this._log(
|
|
3035
|
-
"DEBUG",
|
|
3036
|
-
`[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
|
|
3037
|
-
);
|
|
3038
|
-
tryNextProvider = true;
|
|
3039
|
-
} else
|
|
3040
|
-
throw new ProviderError(
|
|
3041
|
-
baseUrl,
|
|
3042
|
-
status,
|
|
3043
|
-
"xcashu refund failed",
|
|
3044
|
-
requestId
|
|
3045
|
-
);
|
|
3046
|
-
} catch (error) {
|
|
3047
|
-
this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
|
|
3048
|
-
throw new ProviderError(
|
|
3049
|
-
baseUrl,
|
|
3050
|
-
status,
|
|
3051
|
-
"[xcashu] Failed to receive refund token",
|
|
3052
|
-
requestId
|
|
3053
|
-
);
|
|
3054
|
-
}
|
|
3055
|
-
} else {
|
|
3056
|
-
if (!tryNextProvider)
|
|
3057
|
-
throw new ProviderError(
|
|
3058
|
-
baseUrl,
|
|
3059
|
-
status,
|
|
3060
|
-
"[xcashu] Failed to receive refund token",
|
|
3061
|
-
requestId
|
|
3062
|
-
);
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
|
|
3066
|
-
this.storageAdapter.getApiKey(baseUrl);
|
|
3067
|
-
let topupAmount = params.requiredSats;
|
|
3068
|
-
try {
|
|
3069
|
-
let currentBalance = 0;
|
|
3070
|
-
if (this.mode === "apikeys") {
|
|
3071
|
-
const currentBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
3072
|
-
params.token,
|
|
3073
|
-
baseUrl
|
|
3074
|
-
);
|
|
3075
|
-
currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
|
|
3076
|
-
} else if (this.mode === "lazyrefund") {
|
|
3077
|
-
const distribution = this.storageAdapter.getCachedTokenDistribution();
|
|
3078
|
-
const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
|
|
3079
|
-
currentBalance = tokenEntry?.amount ?? 0;
|
|
3080
|
-
}
|
|
3081
|
-
const shortfall = Math.max(0, params.requiredSats - currentBalance);
|
|
3082
|
-
topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
|
|
3083
|
-
} catch (e) {
|
|
3084
|
-
this._log(
|
|
3085
|
-
"WARN",
|
|
3086
|
-
"Could not get current token balance for topup calculation:",
|
|
3087
|
-
e
|
|
3088
|
-
);
|
|
3089
|
-
}
|
|
3090
|
-
const topupResult = await this.balanceManager.topUp({
|
|
3091
|
-
mintUrl,
|
|
3092
|
-
baseUrl,
|
|
3093
|
-
amount: topupAmount * TOPUP_MARGIN,
|
|
3094
|
-
token: params.token
|
|
3095
|
-
});
|
|
3096
|
-
this._log(
|
|
3097
|
-
"DEBUG",
|
|
3098
|
-
`[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
|
|
3099
|
-
);
|
|
3100
|
-
if (!topupResult.success) {
|
|
3101
|
-
const message = topupResult.message || "";
|
|
3102
|
-
if (message.includes("Insufficient balance")) {
|
|
3103
|
-
const needMatch = message.match(/need (\d+)/);
|
|
3104
|
-
const haveMatch = message.match(/have (\d+)/);
|
|
3105
|
-
const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
|
|
3106
|
-
const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
|
|
3107
|
-
this._log(
|
|
3108
|
-
"DEBUG",
|
|
3109
|
-
`[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
|
|
3110
|
-
);
|
|
3111
|
-
throw new InsufficientBalanceError(
|
|
3112
|
-
required,
|
|
3113
|
-
available,
|
|
3114
|
-
0,
|
|
3115
|
-
"",
|
|
3116
|
-
message
|
|
3117
|
-
);
|
|
3118
|
-
} else {
|
|
3119
|
-
this._log(
|
|
3120
|
-
"DEBUG",
|
|
3121
|
-
`[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
|
|
3122
|
-
);
|
|
3123
|
-
tryNextProvider = true;
|
|
3124
|
-
}
|
|
3125
|
-
} else {
|
|
3126
|
-
this._log(
|
|
3127
|
-
"DEBUG",
|
|
3128
|
-
`[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
|
|
3129
|
-
);
|
|
3130
|
-
}
|
|
3131
|
-
if (!tryNextProvider) {
|
|
3132
|
-
if (retryCount < MAX_RETRIES_PER_PROVIDER) {
|
|
3133
|
-
this._log(
|
|
3134
|
-
"DEBUG",
|
|
3135
|
-
`[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
|
|
3136
|
-
);
|
|
3137
|
-
return this._makeRequest({
|
|
3138
|
-
...params,
|
|
3139
|
-
token: params.token,
|
|
3140
|
-
headers: this._withAuthHeader(params.baseHeaders, params.token),
|
|
3141
|
-
retryCount: retryCount + 1
|
|
3142
|
-
});
|
|
3143
|
-
} else {
|
|
3144
|
-
this._log(
|
|
3145
|
-
"DEBUG",
|
|
3146
|
-
`[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
|
|
3147
|
-
);
|
|
3148
|
-
tryNextProvider = true;
|
|
3149
|
-
}
|
|
2947
|
+
return new Promise((resolve, reject) => {
|
|
2948
|
+
const request = indexedDB.open(dbName, 1);
|
|
2949
|
+
request.onupgradeneeded = () => {
|
|
2950
|
+
const db = request.result;
|
|
2951
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
2952
|
+
db.createObjectStore(storeName);
|
|
3150
2953
|
}
|
|
2954
|
+
};
|
|
2955
|
+
request.onsuccess = () => resolve(request.result);
|
|
2956
|
+
request.onerror = () => reject(request.error);
|
|
2957
|
+
});
|
|
2958
|
+
};
|
|
2959
|
+
var createIndexedDBDriver = (options = {}) => {
|
|
2960
|
+
const dbName = options.dbName || "routstr-sdk";
|
|
2961
|
+
const storeName = options.storeName || "sdk_storage";
|
|
2962
|
+
let dbPromise = null;
|
|
2963
|
+
const getDb = () => {
|
|
2964
|
+
if (!dbPromise) {
|
|
2965
|
+
dbPromise = openDatabase(dbName, storeName);
|
|
3151
2966
|
}
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
2967
|
+
return dbPromise;
|
|
2968
|
+
};
|
|
2969
|
+
return {
|
|
2970
|
+
async getItem(key, defaultValue) {
|
|
3155
2971
|
try {
|
|
3156
|
-
const
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
2972
|
+
const db = await getDb();
|
|
2973
|
+
return new Promise((resolve, reject) => {
|
|
2974
|
+
const tx = db.transaction(storeName, "readonly");
|
|
2975
|
+
const store = tx.objectStore(storeName);
|
|
2976
|
+
const request = store.get(key);
|
|
2977
|
+
request.onsuccess = () => {
|
|
2978
|
+
const raw = request.result;
|
|
2979
|
+
if (raw === void 0) {
|
|
2980
|
+
resolve(defaultValue);
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
if (typeof raw === "string") {
|
|
2984
|
+
try {
|
|
2985
|
+
resolve(JSON.parse(raw));
|
|
2986
|
+
} catch {
|
|
2987
|
+
if (typeof defaultValue === "string") {
|
|
2988
|
+
resolve(raw);
|
|
2989
|
+
} else {
|
|
2990
|
+
resolve(defaultValue);
|
|
2991
|
+
}
|
|
3174
2992
|
}
|
|
3175
|
-
|
|
2993
|
+
} else {
|
|
2994
|
+
resolve(raw);
|
|
3176
2995
|
}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
this.storageAdapter.updateApiKeyBalance(
|
|
3181
|
-
baseUrl,
|
|
3182
|
-
latestTokenBalance
|
|
3183
|
-
);
|
|
3184
|
-
}
|
|
3185
|
-
}
|
|
2996
|
+
};
|
|
2997
|
+
request.onerror = () => reject(request.error);
|
|
2998
|
+
});
|
|
3186
2999
|
} catch (error) {
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
`[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
|
|
3190
|
-
error
|
|
3191
|
-
);
|
|
3000
|
+
console.error(`IndexedDB getItem failed for key "${key}":`, error);
|
|
3001
|
+
return defaultValue;
|
|
3192
3002
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
)
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3003
|
+
},
|
|
3004
|
+
async setItem(key, value) {
|
|
3005
|
+
try {
|
|
3006
|
+
const db = await getDb();
|
|
3007
|
+
return new Promise((resolve, reject) => {
|
|
3008
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3009
|
+
const store = tx.objectStore(storeName);
|
|
3010
|
+
store.put(JSON.stringify(value), key);
|
|
3011
|
+
tx.oncomplete = () => resolve();
|
|
3012
|
+
tx.onerror = () => reject(tx.error);
|
|
3203
3013
|
});
|
|
3204
|
-
}
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3014
|
+
} catch (error) {
|
|
3015
|
+
console.error(`IndexedDB setItem failed for key "${key}":`, error);
|
|
3016
|
+
}
|
|
3017
|
+
},
|
|
3018
|
+
async removeItem(key) {
|
|
3019
|
+
try {
|
|
3020
|
+
const db = await getDb();
|
|
3021
|
+
return new Promise((resolve, reject) => {
|
|
3022
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3023
|
+
const store = tx.objectStore(storeName);
|
|
3024
|
+
store.delete(key);
|
|
3025
|
+
tx.oncomplete = () => resolve();
|
|
3026
|
+
tx.onerror = () => reject(tx.error);
|
|
3027
|
+
});
|
|
3028
|
+
} catch (error) {
|
|
3029
|
+
console.error(`IndexedDB removeItem failed for key "${key}":`, error);
|
|
3210
3030
|
}
|
|
3211
3031
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
);
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3032
|
+
};
|
|
3033
|
+
};
|
|
3034
|
+
|
|
3035
|
+
// storage/keys.ts
|
|
3036
|
+
var SDK_STORAGE_KEYS = {
|
|
3037
|
+
MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
|
|
3038
|
+
LAST_USED_MODEL: "lastUsedModel",
|
|
3039
|
+
BASE_URLS_LIST: "base_urls_list",
|
|
3040
|
+
DISABLED_PROVIDERS: "disabled_providers",
|
|
3041
|
+
MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
|
|
3042
|
+
INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
|
|
3043
|
+
LAST_MODELS_UPDATE: "lastModelsUpdate",
|
|
3044
|
+
LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
|
|
3045
|
+
LOCAL_CASHU_TOKENS: "local_cashu_tokens",
|
|
3046
|
+
API_KEYS: "api_keys",
|
|
3047
|
+
CHILD_KEYS: "child_keys",
|
|
3048
|
+
ROUTSTR21_MODELS: "routstr21Models",
|
|
3049
|
+
LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
|
|
3050
|
+
CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
|
|
3051
|
+
USAGE_TRACKING: "usage_tracking",
|
|
3052
|
+
CLIENT_IDS: "client_ids"
|
|
3053
|
+
};
|
|
3054
|
+
|
|
3055
|
+
// storage/usageTracking/indexedDB.ts
|
|
3056
|
+
var DEFAULT_DB_NAME = "routstr-sdk";
|
|
3057
|
+
var DEFAULT_STORE_NAME = "usage_tracking";
|
|
3058
|
+
var MIGRATION_MARKER_KEY = "usage_tracking_migration_v1";
|
|
3059
|
+
var isBrowser2 = typeof indexedDB !== "undefined";
|
|
3060
|
+
var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3061
|
+
var openDatabase2 = (dbName, storeName) => {
|
|
3062
|
+
if (!isBrowser2) {
|
|
3063
|
+
return Promise.reject(new Error("IndexedDB is not available"));
|
|
3064
|
+
}
|
|
3065
|
+
return new Promise((resolve, reject) => {
|
|
3066
|
+
const request = indexedDB.open(dbName, 1);
|
|
3067
|
+
request.onupgradeneeded = () => {
|
|
3068
|
+
const db = request.result;
|
|
3069
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
3070
|
+
const store = db.createObjectStore(storeName, { keyPath: "id" });
|
|
3071
|
+
store.createIndex("timestamp", "timestamp", { unique: false });
|
|
3072
|
+
store.createIndex("modelId", "modelId", { unique: false });
|
|
3073
|
+
store.createIndex("baseUrl", "baseUrl", { unique: false });
|
|
3074
|
+
store.createIndex("sessionId", "sessionId", { unique: false });
|
|
3075
|
+
store.createIndex("client", "client", { unique: false });
|
|
3076
|
+
}
|
|
3077
|
+
};
|
|
3078
|
+
request.onsuccess = () => resolve(request.result);
|
|
3079
|
+
request.onerror = () => reject(request.error);
|
|
3080
|
+
});
|
|
3081
|
+
};
|
|
3082
|
+
var matchesFilters = (entry, options = {}) => {
|
|
3083
|
+
if (typeof options.before === "number" && entry.timestamp >= options.before) {
|
|
3084
|
+
return false;
|
|
3085
|
+
}
|
|
3086
|
+
if (typeof options.after === "number" && entry.timestamp <= options.after) {
|
|
3087
|
+
return false;
|
|
3088
|
+
}
|
|
3089
|
+
if (options.modelId && entry.modelId !== options.modelId) {
|
|
3090
|
+
return false;
|
|
3091
|
+
}
|
|
3092
|
+
if (options.baseUrl && normalizeBaseUrl(entry.baseUrl) !== normalizeBaseUrl(options.baseUrl)) {
|
|
3093
|
+
return false;
|
|
3094
|
+
}
|
|
3095
|
+
if (options.sessionId && entry.sessionId !== options.sessionId) {
|
|
3096
|
+
return false;
|
|
3097
|
+
}
|
|
3098
|
+
if (options.client && entry.client !== options.client) {
|
|
3099
|
+
return false;
|
|
3100
|
+
}
|
|
3101
|
+
return true;
|
|
3102
|
+
};
|
|
3103
|
+
var createIndexedDBUsageTrackingDriver = (options = {}) => {
|
|
3104
|
+
const dbName = options.dbName || DEFAULT_DB_NAME;
|
|
3105
|
+
const storeName = options.storeName || DEFAULT_STORE_NAME;
|
|
3106
|
+
const legacyStorageDriver = options.legacyStorageDriver;
|
|
3107
|
+
let dbPromise = null;
|
|
3108
|
+
let migrationPromise = null;
|
|
3109
|
+
const getDb = () => {
|
|
3110
|
+
if (!dbPromise) {
|
|
3111
|
+
dbPromise = openDatabase2(dbName, storeName);
|
|
3112
|
+
}
|
|
3113
|
+
return dbPromise;
|
|
3114
|
+
};
|
|
3115
|
+
const putMany = async (entries) => {
|
|
3116
|
+
if (entries.length === 0) return;
|
|
3117
|
+
const db = await getDb();
|
|
3118
|
+
await new Promise((resolve, reject) => {
|
|
3119
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3120
|
+
const store = tx.objectStore(storeName);
|
|
3121
|
+
for (const entry of entries) {
|
|
3122
|
+
store.put({ ...entry, baseUrl: normalizeBaseUrl(entry.baseUrl) });
|
|
3123
|
+
}
|
|
3124
|
+
tx.oncomplete = () => resolve();
|
|
3125
|
+
tx.onerror = () => reject(tx.error);
|
|
3126
|
+
});
|
|
3127
|
+
};
|
|
3128
|
+
const ensureMigrated = async () => {
|
|
3129
|
+
if (!legacyStorageDriver) return;
|
|
3130
|
+
if (!migrationPromise) {
|
|
3131
|
+
migrationPromise = (async () => {
|
|
3132
|
+
const migrated = await legacyStorageDriver.getItem(
|
|
3133
|
+
MIGRATION_MARKER_KEY,
|
|
3134
|
+
false
|
|
3256
3135
|
);
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
});
|
|
3262
|
-
this._log(
|
|
3263
|
-
"DEBUG",
|
|
3264
|
-
`[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
|
|
3136
|
+
if (migrated) return;
|
|
3137
|
+
const legacyEntries = await legacyStorageDriver.getItem(
|
|
3138
|
+
SDK_STORAGE_KEYS.USAGE_TRACKING,
|
|
3139
|
+
[]
|
|
3265
3140
|
);
|
|
3266
|
-
if (
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
status,
|
|
3270
|
-
refundResult.message ?? "Unknown error"
|
|
3271
|
-
);
|
|
3141
|
+
if (legacyEntries.length > 0) {
|
|
3142
|
+
await putMany(legacyEntries);
|
|
3143
|
+
await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
|
|
3272
3144
|
}
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
this.providerManager.markFailed(baseUrl);
|
|
3276
|
-
this._log(
|
|
3277
|
-
"DEBUG",
|
|
3278
|
-
`[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
|
|
3279
|
-
);
|
|
3280
|
-
if (!selectedModel) {
|
|
3281
|
-
throw new ProviderError(
|
|
3282
|
-
baseUrl,
|
|
3283
|
-
status,
|
|
3284
|
-
"Funny, no selected model. HMM. "
|
|
3285
|
-
);
|
|
3145
|
+
await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY, true);
|
|
3146
|
+
})();
|
|
3286
3147
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
)
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
);
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
)
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
)
|
|
3303
|
-
const
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3148
|
+
await migrationPromise;
|
|
3149
|
+
};
|
|
3150
|
+
return {
|
|
3151
|
+
async migrate() {
|
|
3152
|
+
await ensureMigrated();
|
|
3153
|
+
},
|
|
3154
|
+
async append(entry) {
|
|
3155
|
+
await ensureMigrated();
|
|
3156
|
+
await putMany([entry]);
|
|
3157
|
+
},
|
|
3158
|
+
async appendMany(entries) {
|
|
3159
|
+
await ensureMigrated();
|
|
3160
|
+
await putMany(entries);
|
|
3161
|
+
},
|
|
3162
|
+
async list(options2 = {}) {
|
|
3163
|
+
await ensureMigrated();
|
|
3164
|
+
const db = await getDb();
|
|
3165
|
+
return new Promise((resolve, reject) => {
|
|
3166
|
+
const tx = db.transaction(storeName, "readonly");
|
|
3167
|
+
const store = tx.objectStore(storeName);
|
|
3168
|
+
const index = store.index("timestamp");
|
|
3169
|
+
const direction = "prev";
|
|
3170
|
+
const request = index.openCursor(null, direction);
|
|
3171
|
+
const results = [];
|
|
3172
|
+
const limit = options2.limit;
|
|
3173
|
+
request.onsuccess = () => {
|
|
3174
|
+
const cursor = request.result;
|
|
3175
|
+
if (!cursor) {
|
|
3176
|
+
resolve(results);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
const value = cursor.value;
|
|
3180
|
+
if (matchesFilters(value, options2)) {
|
|
3181
|
+
results.push(value);
|
|
3182
|
+
if (typeof limit === "number" && results.length >= limit) {
|
|
3183
|
+
resolve(results);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
cursor.continue();
|
|
3188
|
+
};
|
|
3189
|
+
request.onerror = () => reject(request.error);
|
|
3316
3190
|
});
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3191
|
+
},
|
|
3192
|
+
async count(options2 = {}) {
|
|
3193
|
+
const results = await this.list(options2);
|
|
3194
|
+
return results.length;
|
|
3195
|
+
},
|
|
3196
|
+
async deleteOlderThan(timestamp) {
|
|
3197
|
+
await ensureMigrated();
|
|
3198
|
+
const db = await getDb();
|
|
3199
|
+
return new Promise((resolve, reject) => {
|
|
3200
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3201
|
+
const store = tx.objectStore(storeName);
|
|
3202
|
+
const index = store.index("timestamp");
|
|
3203
|
+
const range = IDBKeyRange.upperBound(timestamp, true);
|
|
3204
|
+
const request = index.openCursor(range);
|
|
3205
|
+
let deleted = 0;
|
|
3206
|
+
request.onsuccess = () => {
|
|
3207
|
+
const cursor = request.result;
|
|
3208
|
+
if (!cursor) {
|
|
3209
|
+
resolve(deleted);
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
deleted += 1;
|
|
3213
|
+
cursor.delete();
|
|
3214
|
+
cursor.continue();
|
|
3215
|
+
};
|
|
3216
|
+
request.onerror = () => reject(request.error);
|
|
3217
|
+
});
|
|
3218
|
+
},
|
|
3219
|
+
async clear() {
|
|
3220
|
+
await ensureMigrated();
|
|
3221
|
+
const db = await getDb();
|
|
3222
|
+
await new Promise((resolve, reject) => {
|
|
3223
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
3224
|
+
tx.objectStore(storeName).clear();
|
|
3225
|
+
tx.oncomplete = () => resolve();
|
|
3226
|
+
tx.onerror = () => reject(tx.error);
|
|
3328
3227
|
});
|
|
3329
3228
|
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3229
|
+
};
|
|
3230
|
+
};
|
|
3231
|
+
|
|
3232
|
+
// storage/usageTracking/sqlite.ts
|
|
3233
|
+
var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
|
|
3234
|
+
var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3235
|
+
var isBun2 = () => {
|
|
3236
|
+
return typeof process.versions.bun !== "undefined";
|
|
3237
|
+
};
|
|
3238
|
+
var createDatabase2 = (dbPath) => {
|
|
3239
|
+
if (isBun2()) {
|
|
3240
|
+
throw new Error(
|
|
3241
|
+
"SQLite driver not supported in Bun. Use createMemoryDriver() instead."
|
|
3333
3242
|
);
|
|
3334
3243
|
}
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
if (this.mode === "xcashu" && response) {
|
|
3342
|
-
const refundToken = response.headers.get("x-cashu") ?? void 0;
|
|
3343
|
-
if (refundToken) {
|
|
3344
|
-
try {
|
|
3345
|
-
const receiveResult = await this.cashuSpender.receiveToken(refundToken);
|
|
3346
|
-
satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
|
|
3347
|
-
} catch (error) {
|
|
3348
|
-
this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
|
|
3349
|
-
}
|
|
3350
|
-
}
|
|
3351
|
-
} else if (this.mode === "lazyrefund") {
|
|
3352
|
-
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
3353
|
-
token,
|
|
3354
|
-
baseUrl
|
|
3355
|
-
);
|
|
3356
|
-
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
3357
|
-
this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
|
|
3358
|
-
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
3359
|
-
} else if (this.mode === "apikeys") {
|
|
3360
|
-
try {
|
|
3361
|
-
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
3362
|
-
token,
|
|
3363
|
-
baseUrl
|
|
3364
|
-
);
|
|
3365
|
-
this._log(
|
|
3366
|
-
"DEBUG",
|
|
3367
|
-
"LATEST Balance",
|
|
3368
|
-
latestBalanceInfo.amount,
|
|
3369
|
-
latestBalanceInfo.reserved,
|
|
3370
|
-
latestBalanceInfo.apiKey,
|
|
3371
|
-
baseUrl
|
|
3372
|
-
);
|
|
3373
|
-
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
3374
|
-
const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
3375
|
-
if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
|
|
3376
|
-
this.storageAdapter.removeApiKey(baseUrl);
|
|
3377
|
-
this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
|
|
3378
|
-
}
|
|
3379
|
-
this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
|
|
3380
|
-
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
3381
|
-
} catch (e) {
|
|
3382
|
-
this._log("WARN", "Could not get updated API key balance:", e);
|
|
3383
|
-
satsSpent = fallbackSatsSpent ?? initialTokenBalance;
|
|
3384
|
-
}
|
|
3385
|
-
}
|
|
3386
|
-
return satsSpent;
|
|
3387
|
-
}
|
|
3388
|
-
/**
|
|
3389
|
-
* Convert messages for API format
|
|
3390
|
-
*/
|
|
3391
|
-
async _convertMessages(messages) {
|
|
3392
|
-
return Promise.all(
|
|
3393
|
-
messages.filter((m) => m.role !== "system").map(async (m) => ({
|
|
3394
|
-
role: m.role,
|
|
3395
|
-
content: typeof m.content === "string" ? m.content : m.content
|
|
3396
|
-
}))
|
|
3244
|
+
let Database = null;
|
|
3245
|
+
try {
|
|
3246
|
+
Database = __require("better-sqlite3");
|
|
3247
|
+
} catch (error) {
|
|
3248
|
+
throw new Error(
|
|
3249
|
+
`better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
|
|
3397
3250
|
);
|
|
3398
3251
|
}
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
type: "text",
|
|
3408
|
-
text: result.content,
|
|
3409
|
-
thinking: result.thinking,
|
|
3410
|
-
citations: result.citations,
|
|
3411
|
-
annotations: result.annotations
|
|
3412
|
-
});
|
|
3413
|
-
}
|
|
3414
|
-
for (const img of result.images) {
|
|
3415
|
-
content.push({
|
|
3416
|
-
type: "image_url",
|
|
3417
|
-
image_url: {
|
|
3418
|
-
url: img.image_url.url
|
|
3419
|
-
}
|
|
3420
|
-
});
|
|
3421
|
-
}
|
|
3422
|
-
return {
|
|
3423
|
-
role: "assistant",
|
|
3424
|
-
content
|
|
3425
|
-
};
|
|
3426
|
-
}
|
|
3427
|
-
return {
|
|
3428
|
-
role: "assistant",
|
|
3429
|
-
content: result.content || ""
|
|
3430
|
-
};
|
|
3252
|
+
return new Database(dbPath);
|
|
3253
|
+
};
|
|
3254
|
+
var buildWhereClause = (options = {}) => {
|
|
3255
|
+
const clauses = [];
|
|
3256
|
+
const params = [];
|
|
3257
|
+
if (typeof options.before === "number") {
|
|
3258
|
+
clauses.push("timestamp < ?");
|
|
3259
|
+
params.push(options.before);
|
|
3431
3260
|
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
*/
|
|
3436
|
-
async _createChildKey(baseUrl, parentApiKey, options) {
|
|
3437
|
-
const response = await fetch(`${baseUrl}v1/balance/child-key`, {
|
|
3438
|
-
method: "POST",
|
|
3439
|
-
headers: {
|
|
3440
|
-
"Content-Type": "application/json",
|
|
3441
|
-
Authorization: `Bearer ${parentApiKey}`
|
|
3442
|
-
},
|
|
3443
|
-
body: JSON.stringify({
|
|
3444
|
-
count: options?.count ?? 1,
|
|
3445
|
-
balance_limit: options?.balanceLimit,
|
|
3446
|
-
balance_limit_reset: options?.balanceLimitReset,
|
|
3447
|
-
validity_date: options?.validityDate
|
|
3448
|
-
})
|
|
3449
|
-
});
|
|
3450
|
-
if (!response.ok) {
|
|
3451
|
-
throw new Error(
|
|
3452
|
-
`Failed to create child key: ${response.status} ${await response.text()}`
|
|
3453
|
-
);
|
|
3454
|
-
}
|
|
3455
|
-
const data = await response.json();
|
|
3456
|
-
return {
|
|
3457
|
-
childKey: data.api_keys?.[0],
|
|
3458
|
-
balance: data.balance ?? 0,
|
|
3459
|
-
balanceLimit: data.balance_limit,
|
|
3460
|
-
validityDate: data.validity_date
|
|
3461
|
-
};
|
|
3261
|
+
if (typeof options.after === "number") {
|
|
3262
|
+
clauses.push("timestamp > ?");
|
|
3263
|
+
params.push(options.after);
|
|
3462
3264
|
}
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
_getEstimatedCosts(selectedModel, streamingResult) {
|
|
3467
|
-
let estimatedCosts = 0;
|
|
3468
|
-
if (streamingResult.usage) {
|
|
3469
|
-
const { completion_tokens, prompt_tokens } = streamingResult.usage;
|
|
3470
|
-
if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
|
|
3471
|
-
estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
return estimatedCosts;
|
|
3265
|
+
if (options.modelId) {
|
|
3266
|
+
clauses.push("model_id = ?");
|
|
3267
|
+
params.push(options.modelId);
|
|
3475
3268
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
_getPendingCashuTokenAmount() {
|
|
3480
|
-
const distribution = this.storageAdapter.getCachedTokenDistribution();
|
|
3481
|
-
return distribution.reduce((total, item) => total + item.amount, 0);
|
|
3269
|
+
if (options.baseUrl) {
|
|
3270
|
+
clauses.push("base_url = ?");
|
|
3271
|
+
params.push(normalizeBaseUrl2(options.baseUrl));
|
|
3482
3272
|
}
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
_handleError(error, callbacks) {
|
|
3487
|
-
this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
|
|
3488
|
-
if (error instanceof Error) {
|
|
3489
|
-
const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
|
|
3490
|
-
const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
|
|
3491
|
-
this._log(
|
|
3492
|
-
"ERROR",
|
|
3493
|
-
`[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
|
|
3494
|
-
);
|
|
3495
|
-
callbacks.onMessageAppend({
|
|
3496
|
-
role: "system",
|
|
3497
|
-
content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
|
|
3498
|
-
});
|
|
3499
|
-
} else {
|
|
3500
|
-
callbacks.onMessageAppend({
|
|
3501
|
-
role: "system",
|
|
3502
|
-
content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
|
|
3503
|
-
});
|
|
3504
|
-
}
|
|
3273
|
+
if (options.sessionId) {
|
|
3274
|
+
clauses.push("session_id = ?");
|
|
3275
|
+
params.push(options.sessionId);
|
|
3505
3276
|
}
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
async _checkBalance() {
|
|
3510
|
-
const balances = await this.walletAdapter.getBalances();
|
|
3511
|
-
const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
|
|
3512
|
-
if (totalBalance <= 0) {
|
|
3513
|
-
throw new InsufficientBalanceError(1, 0);
|
|
3514
|
-
}
|
|
3277
|
+
if (options.client) {
|
|
3278
|
+
clauses.push("client = ?");
|
|
3279
|
+
params.push(options.client);
|
|
3515
3280
|
}
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3281
|
+
return {
|
|
3282
|
+
sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
|
|
3283
|
+
params
|
|
3284
|
+
};
|
|
3285
|
+
};
|
|
3286
|
+
var createSqliteUsageTrackingDriver = (options = {}) => {
|
|
3287
|
+
const dbPath = options.dbPath || "routstr.sqlite";
|
|
3288
|
+
const tableName = options.tableName || "usage_tracking";
|
|
3289
|
+
const db = createDatabase2(dbPath);
|
|
3290
|
+
const legacyStorageDriver = options.legacyStorageDriver;
|
|
3291
|
+
db.exec(`
|
|
3292
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
3293
|
+
id TEXT PRIMARY KEY,
|
|
3294
|
+
timestamp INTEGER NOT NULL,
|
|
3295
|
+
model_id TEXT NOT NULL,
|
|
3296
|
+
base_url TEXT NOT NULL,
|
|
3297
|
+
request_id TEXT NOT NULL,
|
|
3298
|
+
cost REAL NOT NULL,
|
|
3299
|
+
sats_cost REAL NOT NULL,
|
|
3300
|
+
prompt_tokens INTEGER NOT NULL,
|
|
3301
|
+
completion_tokens INTEGER NOT NULL,
|
|
3302
|
+
total_tokens INTEGER NOT NULL,
|
|
3303
|
+
client TEXT,
|
|
3304
|
+
session_id TEXT,
|
|
3305
|
+
tags TEXT
|
|
3524
3306
|
);
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3307
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
|
|
3308
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
|
|
3309
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
|
|
3310
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
|
|
3311
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
|
|
3312
|
+
`);
|
|
3313
|
+
const insertStmt = db.prepare(`
|
|
3314
|
+
INSERT OR REPLACE INTO ${tableName} (
|
|
3315
|
+
id, timestamp, model_id, base_url, request_id,
|
|
3316
|
+
cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
|
|
3317
|
+
client, session_id, tags
|
|
3318
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3319
|
+
`);
|
|
3320
|
+
let migrationComplete = false;
|
|
3321
|
+
const appendOne = (entry) => {
|
|
3322
|
+
insertStmt.run(
|
|
3323
|
+
entry.id,
|
|
3324
|
+
entry.timestamp,
|
|
3325
|
+
entry.modelId,
|
|
3326
|
+
normalizeBaseUrl2(entry.baseUrl),
|
|
3327
|
+
entry.requestId,
|
|
3328
|
+
entry.cost,
|
|
3329
|
+
entry.satsCost,
|
|
3330
|
+
entry.promptTokens,
|
|
3331
|
+
entry.completionTokens,
|
|
3332
|
+
entry.totalTokens,
|
|
3333
|
+
entry.client ?? null,
|
|
3334
|
+
entry.sessionId ?? null,
|
|
3335
|
+
JSON.stringify(entry.tags ?? [])
|
|
3336
|
+
);
|
|
3337
|
+
};
|
|
3338
|
+
const ensureMigrated = async () => {
|
|
3339
|
+
if (!legacyStorageDriver || migrationComplete) return;
|
|
3340
|
+
const migrated = await legacyStorageDriver.getItem(
|
|
3341
|
+
MIGRATION_MARKER_KEY2,
|
|
3342
|
+
false
|
|
3343
|
+
);
|
|
3344
|
+
if (migrated) {
|
|
3345
|
+
migrationComplete = true;
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
const legacyEntries = await legacyStorageDriver.getItem(
|
|
3349
|
+
SDK_STORAGE_KEYS.USAGE_TRACKING,
|
|
3350
|
+
[]
|
|
3351
|
+
);
|
|
3352
|
+
for (const entry of legacyEntries) {
|
|
3353
|
+
appendOne(entry);
|
|
3354
|
+
}
|
|
3355
|
+
if (legacyEntries.length > 0) {
|
|
3356
|
+
await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
|
|
3357
|
+
}
|
|
3358
|
+
await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
|
|
3359
|
+
migrationComplete = true;
|
|
3360
|
+
};
|
|
3361
|
+
const mapRow = (row) => ({
|
|
3362
|
+
id: row.id,
|
|
3363
|
+
timestamp: row.timestamp,
|
|
3364
|
+
modelId: row.model_id,
|
|
3365
|
+
baseUrl: row.base_url,
|
|
3366
|
+
requestId: row.request_id,
|
|
3367
|
+
cost: row.cost,
|
|
3368
|
+
satsCost: row.sats_cost,
|
|
3369
|
+
promptTokens: row.prompt_tokens,
|
|
3370
|
+
completionTokens: row.completion_tokens,
|
|
3371
|
+
totalTokens: row.total_tokens,
|
|
3372
|
+
client: row.client ?? void 0,
|
|
3373
|
+
sessionId: row.session_id ?? void 0,
|
|
3374
|
+
tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
|
|
3375
|
+
});
|
|
3376
|
+
return {
|
|
3377
|
+
async migrate() {
|
|
3378
|
+
await ensureMigrated();
|
|
3379
|
+
},
|
|
3380
|
+
async append(entry) {
|
|
3381
|
+
await ensureMigrated();
|
|
3382
|
+
appendOne(entry);
|
|
3383
|
+
},
|
|
3384
|
+
async appendMany(entries) {
|
|
3385
|
+
await ensureMigrated();
|
|
3386
|
+
for (const entry of entries) {
|
|
3387
|
+
appendOne(entry);
|
|
3589
3388
|
}
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
const
|
|
3594
|
-
|
|
3389
|
+
},
|
|
3390
|
+
async list(options2 = {}) {
|
|
3391
|
+
await ensureMigrated();
|
|
3392
|
+
const { sql, params } = buildWhereClause(options2);
|
|
3393
|
+
const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
|
|
3394
|
+
const stmt = db.prepare(
|
|
3395
|
+
`SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
|
|
3595
3396
|
);
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
}
|
|
3599
|
-
if (tokenBalance === 0 && parentApiKey) {
|
|
3600
|
-
try {
|
|
3601
|
-
const balanceInfo = await this.balanceManager.getTokenBalance(
|
|
3602
|
-
parentApiKey.key,
|
|
3603
|
-
baseUrl
|
|
3604
|
-
);
|
|
3605
|
-
tokenBalance = balanceInfo.amount;
|
|
3606
|
-
tokenBalanceUnit = balanceInfo.unit;
|
|
3607
|
-
} catch (e) {
|
|
3608
|
-
this._log("WARN", "Could not get initial API key balance:", e);
|
|
3609
|
-
}
|
|
3610
|
-
}
|
|
3611
|
-
this._log(
|
|
3612
|
-
"DEBUG",
|
|
3613
|
-
`[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
|
|
3614
|
-
);
|
|
3615
|
-
return {
|
|
3616
|
-
token: parentApiKey?.key ?? "",
|
|
3617
|
-
tokenBalance,
|
|
3618
|
-
tokenBalanceUnit
|
|
3619
|
-
};
|
|
3620
|
-
}
|
|
3621
|
-
this._log(
|
|
3622
|
-
"DEBUG",
|
|
3623
|
-
`[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
|
|
3624
|
-
);
|
|
3625
|
-
const spendResult = await this.cashuSpender.spend({
|
|
3626
|
-
mintUrl,
|
|
3627
|
-
amount,
|
|
3628
|
-
baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
|
|
3629
|
-
reuseToken: this.mode === "lazyrefund"
|
|
3630
|
-
});
|
|
3631
|
-
if (!spendResult.token) {
|
|
3632
|
-
this._log(
|
|
3633
|
-
"ERROR",
|
|
3634
|
-
`[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
|
|
3635
|
-
spendResult.error
|
|
3636
|
-
);
|
|
3637
|
-
} else {
|
|
3638
|
-
this._log(
|
|
3639
|
-
"DEBUG",
|
|
3640
|
-
`[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
|
|
3397
|
+
const rows = stmt.all(
|
|
3398
|
+
...typeof options2.limit === "number" ? [...params, options2.limit] : params
|
|
3641
3399
|
);
|
|
3400
|
+
return rows.map(mapRow);
|
|
3401
|
+
},
|
|
3402
|
+
async count(options2 = {}) {
|
|
3403
|
+
await ensureMigrated();
|
|
3404
|
+
const { sql, params } = buildWhereClause(options2);
|
|
3405
|
+
const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
|
|
3406
|
+
const row = stmt.get(...params);
|
|
3407
|
+
return Number(row?.count ?? 0);
|
|
3408
|
+
},
|
|
3409
|
+
async deleteOlderThan(timestamp) {
|
|
3410
|
+
await ensureMigrated();
|
|
3411
|
+
const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
|
|
3412
|
+
const result = stmt.run(timestamp);
|
|
3413
|
+
return result.changes;
|
|
3414
|
+
},
|
|
3415
|
+
async clear() {
|
|
3416
|
+
await ensureMigrated();
|
|
3417
|
+
db.prepare(`DELETE FROM ${tableName}`).run();
|
|
3642
3418
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3419
|
+
};
|
|
3420
|
+
};
|
|
3421
|
+
|
|
3422
|
+
// storage/usageTracking/memory.ts
|
|
3423
|
+
var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3424
|
+
var matchesFilters2 = (entry, options = {}) => {
|
|
3425
|
+
if (typeof options.before === "number" && entry.timestamp >= options.before) {
|
|
3426
|
+
return false;
|
|
3648
3427
|
}
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
*/
|
|
3652
|
-
_buildBaseHeaders(additionalHeaders = {}, token) {
|
|
3653
|
-
const headers = {
|
|
3654
|
-
...additionalHeaders,
|
|
3655
|
-
"Content-Type": "application/json"
|
|
3656
|
-
};
|
|
3657
|
-
return headers;
|
|
3428
|
+
if (typeof options.after === "number" && entry.timestamp <= options.after) {
|
|
3429
|
+
return false;
|
|
3658
3430
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
*/
|
|
3662
|
-
_withAuthHeader(headers, token) {
|
|
3663
|
-
const nextHeaders = { ...headers };
|
|
3664
|
-
if (this.mode === "xcashu") {
|
|
3665
|
-
nextHeaders["X-Cashu"] = token;
|
|
3666
|
-
} else {
|
|
3667
|
-
nextHeaders["Authorization"] = `Bearer ${token}`;
|
|
3668
|
-
}
|
|
3669
|
-
return nextHeaders;
|
|
3431
|
+
if (options.modelId && entry.modelId !== options.modelId) {
|
|
3432
|
+
return false;
|
|
3670
3433
|
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
// storage/drivers/localStorage.ts
|
|
3674
|
-
var canUseLocalStorage = () => {
|
|
3675
|
-
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
3676
|
-
};
|
|
3677
|
-
var isQuotaExceeded = (error) => {
|
|
3678
|
-
const e = error;
|
|
3679
|
-
return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
|
|
3680
|
-
};
|
|
3681
|
-
var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
|
|
3682
|
-
var localStorageDriver = {
|
|
3683
|
-
async getItem(key, defaultValue) {
|
|
3684
|
-
if (!canUseLocalStorage()) return defaultValue;
|
|
3685
|
-
try {
|
|
3686
|
-
const item = window.localStorage.getItem(key);
|
|
3687
|
-
if (item === null) return defaultValue;
|
|
3688
|
-
try {
|
|
3689
|
-
return JSON.parse(item);
|
|
3690
|
-
} catch (parseError) {
|
|
3691
|
-
if (typeof defaultValue === "string") {
|
|
3692
|
-
return item;
|
|
3693
|
-
}
|
|
3694
|
-
throw parseError;
|
|
3695
|
-
}
|
|
3696
|
-
} catch (error) {
|
|
3697
|
-
console.error(`Error retrieving item with key "${key}":`, error);
|
|
3698
|
-
if (canUseLocalStorage()) {
|
|
3699
|
-
try {
|
|
3700
|
-
window.localStorage.removeItem(key);
|
|
3701
|
-
} catch (removeError) {
|
|
3702
|
-
console.error(
|
|
3703
|
-
`Error removing corrupted item with key "${key}":`,
|
|
3704
|
-
removeError
|
|
3705
|
-
);
|
|
3706
|
-
}
|
|
3707
|
-
}
|
|
3708
|
-
return defaultValue;
|
|
3709
|
-
}
|
|
3710
|
-
},
|
|
3711
|
-
async setItem(key, value) {
|
|
3712
|
-
if (!canUseLocalStorage()) return;
|
|
3713
|
-
try {
|
|
3714
|
-
window.localStorage.setItem(key, JSON.stringify(value));
|
|
3715
|
-
} catch (error) {
|
|
3716
|
-
if (isQuotaExceeded(error)) {
|
|
3717
|
-
if (NON_CRITICAL_KEYS.has(key)) {
|
|
3718
|
-
console.warn(
|
|
3719
|
-
`Storage quota exceeded; skipping non-critical key "${key}".`
|
|
3720
|
-
);
|
|
3721
|
-
return;
|
|
3722
|
-
}
|
|
3723
|
-
try {
|
|
3724
|
-
window.localStorage.removeItem("modelsFromAllProviders");
|
|
3725
|
-
} catch {
|
|
3726
|
-
}
|
|
3727
|
-
try {
|
|
3728
|
-
window.localStorage.setItem(key, JSON.stringify(value));
|
|
3729
|
-
return;
|
|
3730
|
-
} catch (retryError) {
|
|
3731
|
-
console.warn(
|
|
3732
|
-
`Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
|
|
3733
|
-
retryError
|
|
3734
|
-
);
|
|
3735
|
-
return;
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
console.error(`Error storing item with key "${key}":`, error);
|
|
3739
|
-
}
|
|
3740
|
-
},
|
|
3741
|
-
async removeItem(key) {
|
|
3742
|
-
if (!canUseLocalStorage()) return;
|
|
3743
|
-
try {
|
|
3744
|
-
window.localStorage.removeItem(key);
|
|
3745
|
-
} catch (error) {
|
|
3746
|
-
console.error(`Error removing item with key "${key}":`, error);
|
|
3747
|
-
}
|
|
3434
|
+
if (options.baseUrl && normalizeBaseUrl3(entry.baseUrl) !== normalizeBaseUrl3(options.baseUrl)) {
|
|
3435
|
+
return false;
|
|
3748
3436
|
}
|
|
3437
|
+
if (options.sessionId && entry.sessionId !== options.sessionId) {
|
|
3438
|
+
return false;
|
|
3439
|
+
}
|
|
3440
|
+
if (options.client && entry.client !== options.client) {
|
|
3441
|
+
return false;
|
|
3442
|
+
}
|
|
3443
|
+
return true;
|
|
3749
3444
|
};
|
|
3750
|
-
|
|
3751
|
-
// storage/drivers/memory.ts
|
|
3752
|
-
var createMemoryDriver = (seed) => {
|
|
3445
|
+
var createMemoryUsageTrackingDriver = (seed = []) => {
|
|
3753
3446
|
const store = /* @__PURE__ */ new Map();
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
store.set(key, value);
|
|
3757
|
-
}
|
|
3447
|
+
for (const entry of seed) {
|
|
3448
|
+
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
|
|
3758
3449
|
}
|
|
3759
3450
|
return {
|
|
3760
|
-
async
|
|
3761
|
-
|
|
3762
|
-
if (item === void 0) return defaultValue;
|
|
3763
|
-
try {
|
|
3764
|
-
return JSON.parse(item);
|
|
3765
|
-
} catch (parseError) {
|
|
3766
|
-
if (typeof defaultValue === "string") {
|
|
3767
|
-
return item;
|
|
3768
|
-
}
|
|
3769
|
-
throw parseError;
|
|
3770
|
-
}
|
|
3451
|
+
async migrate() {
|
|
3452
|
+
return;
|
|
3771
3453
|
},
|
|
3772
|
-
async
|
|
3773
|
-
store.set(
|
|
3454
|
+
async append(entry) {
|
|
3455
|
+
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
|
|
3774
3456
|
},
|
|
3775
|
-
async
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
};
|
|
3779
|
-
};
|
|
3780
|
-
|
|
3781
|
-
// storage/drivers/sqlite.ts
|
|
3782
|
-
var isBun = () => {
|
|
3783
|
-
return typeof process.versions.bun !== "undefined";
|
|
3784
|
-
};
|
|
3785
|
-
var createDatabase = (dbPath) => {
|
|
3786
|
-
if (isBun()) {
|
|
3787
|
-
throw new Error(
|
|
3788
|
-
"SQLite driver not supported in Bun. Use createMemoryDriver() instead."
|
|
3789
|
-
);
|
|
3790
|
-
}
|
|
3791
|
-
let Database = null;
|
|
3792
|
-
try {
|
|
3793
|
-
Database = __require("better-sqlite3");
|
|
3794
|
-
} catch (error) {
|
|
3795
|
-
throw new Error(
|
|
3796
|
-
`better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
|
|
3797
|
-
);
|
|
3798
|
-
}
|
|
3799
|
-
return new Database(dbPath);
|
|
3800
|
-
};
|
|
3801
|
-
var createSqliteDriver = (options = {}) => {
|
|
3802
|
-
const dbPath = options.dbPath || "routstr.sqlite";
|
|
3803
|
-
const tableName = options.tableName || "sdk_storage";
|
|
3804
|
-
const db = createDatabase(dbPath);
|
|
3805
|
-
db.exec(
|
|
3806
|
-
`CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
|
|
3807
|
-
);
|
|
3808
|
-
const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
|
|
3809
|
-
const upsertStmt = db.prepare(
|
|
3810
|
-
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)
|
|
3811
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
3812
|
-
);
|
|
3813
|
-
const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
|
|
3814
|
-
return {
|
|
3815
|
-
async getItem(key, defaultValue) {
|
|
3816
|
-
try {
|
|
3817
|
-
const row = selectStmt.get(key);
|
|
3818
|
-
if (!row || typeof row.value !== "string") return defaultValue;
|
|
3819
|
-
try {
|
|
3820
|
-
return JSON.parse(row.value);
|
|
3821
|
-
} catch (parseError) {
|
|
3822
|
-
if (typeof defaultValue === "string") {
|
|
3823
|
-
return row.value;
|
|
3824
|
-
}
|
|
3825
|
-
throw parseError;
|
|
3826
|
-
}
|
|
3827
|
-
} catch (error) {
|
|
3828
|
-
console.error(`SQLite getItem failed for key "${key}":`, error);
|
|
3829
|
-
return defaultValue;
|
|
3457
|
+
async appendMany(entries) {
|
|
3458
|
+
for (const entry of entries) {
|
|
3459
|
+
store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
|
|
3830
3460
|
}
|
|
3831
3461
|
},
|
|
3832
|
-
async
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
console.error(`SQLite setItem failed for key "${key}":`, error);
|
|
3462
|
+
async list(options = {}) {
|
|
3463
|
+
const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options)).sort((a, b) => b.timestamp - a.timestamp);
|
|
3464
|
+
if (typeof options.limit === "number") {
|
|
3465
|
+
return entries.slice(0, options.limit);
|
|
3837
3466
|
}
|
|
3467
|
+
return entries;
|
|
3838
3468
|
},
|
|
3839
|
-
async
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3469
|
+
async count(options = {}) {
|
|
3470
|
+
return (await this.list(options)).length;
|
|
3471
|
+
},
|
|
3472
|
+
async deleteOlderThan(timestamp) {
|
|
3473
|
+
let deleted = 0;
|
|
3474
|
+
for (const [id, entry] of store.entries()) {
|
|
3475
|
+
if (entry.timestamp < timestamp) {
|
|
3476
|
+
store.delete(id);
|
|
3477
|
+
deleted += 1;
|
|
3478
|
+
}
|
|
3844
3479
|
}
|
|
3480
|
+
return deleted;
|
|
3481
|
+
},
|
|
3482
|
+
async clear() {
|
|
3483
|
+
store.clear();
|
|
3845
3484
|
}
|
|
3846
3485
|
};
|
|
3847
3486
|
};
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
db.createObjectStore(storeName);
|
|
3861
|
-
}
|
|
3862
|
-
};
|
|
3863
|
-
request.onsuccess = () => resolve(request.result);
|
|
3864
|
-
request.onerror = () => reject(request.error);
|
|
3865
|
-
});
|
|
3866
|
-
};
|
|
3867
|
-
var createIndexedDBDriver = (options = {}) => {
|
|
3868
|
-
const dbName = options.dbName || "routstr-sdk";
|
|
3869
|
-
const storeName = options.storeName || "sdk_storage";
|
|
3870
|
-
let dbPromise = null;
|
|
3871
|
-
const getDb = () => {
|
|
3872
|
-
if (!dbPromise) {
|
|
3873
|
-
dbPromise = openDatabase(dbName, storeName);
|
|
3874
|
-
}
|
|
3875
|
-
return dbPromise;
|
|
3876
|
-
};
|
|
3877
|
-
return {
|
|
3878
|
-
async getItem(key, defaultValue) {
|
|
3879
|
-
try {
|
|
3880
|
-
const db = await getDb();
|
|
3881
|
-
return new Promise((resolve, reject) => {
|
|
3882
|
-
const tx = db.transaction(storeName, "readonly");
|
|
3883
|
-
const store = tx.objectStore(storeName);
|
|
3884
|
-
const request = store.get(key);
|
|
3885
|
-
request.onsuccess = () => {
|
|
3886
|
-
const raw = request.result;
|
|
3887
|
-
if (raw === void 0) {
|
|
3888
|
-
resolve(defaultValue);
|
|
3889
|
-
return;
|
|
3890
|
-
}
|
|
3891
|
-
if (typeof raw === "string") {
|
|
3892
|
-
try {
|
|
3893
|
-
resolve(JSON.parse(raw));
|
|
3894
|
-
} catch {
|
|
3895
|
-
if (typeof defaultValue === "string") {
|
|
3896
|
-
resolve(raw);
|
|
3897
|
-
} else {
|
|
3898
|
-
resolve(defaultValue);
|
|
3899
|
-
}
|
|
3900
|
-
}
|
|
3901
|
-
} else {
|
|
3902
|
-
resolve(raw);
|
|
3903
|
-
}
|
|
3904
|
-
};
|
|
3905
|
-
request.onerror = () => reject(request.error);
|
|
3906
|
-
});
|
|
3907
|
-
} catch (error) {
|
|
3908
|
-
console.error(`IndexedDB getItem failed for key "${key}":`, error);
|
|
3909
|
-
return defaultValue;
|
|
3910
|
-
}
|
|
3911
|
-
},
|
|
3912
|
-
async setItem(key, value) {
|
|
3913
|
-
try {
|
|
3914
|
-
const db = await getDb();
|
|
3915
|
-
return new Promise((resolve, reject) => {
|
|
3916
|
-
const tx = db.transaction(storeName, "readwrite");
|
|
3917
|
-
const store = tx.objectStore(storeName);
|
|
3918
|
-
store.put(JSON.stringify(value), key);
|
|
3919
|
-
tx.oncomplete = () => resolve();
|
|
3920
|
-
tx.onerror = () => reject(tx.error);
|
|
3921
|
-
});
|
|
3922
|
-
} catch (error) {
|
|
3923
|
-
console.error(`IndexedDB setItem failed for key "${key}":`, error);
|
|
3924
|
-
}
|
|
3925
|
-
},
|
|
3926
|
-
async removeItem(key) {
|
|
3927
|
-
try {
|
|
3928
|
-
const db = await getDb();
|
|
3929
|
-
return new Promise((resolve, reject) => {
|
|
3930
|
-
const tx = db.transaction(storeName, "readwrite");
|
|
3931
|
-
const store = tx.objectStore(storeName);
|
|
3932
|
-
store.delete(key);
|
|
3933
|
-
tx.oncomplete = () => resolve();
|
|
3934
|
-
tx.onerror = () => reject(tx.error);
|
|
3935
|
-
});
|
|
3936
|
-
} catch (error) {
|
|
3937
|
-
console.error(`IndexedDB removeItem failed for key "${key}":`, error);
|
|
3938
|
-
}
|
|
3939
|
-
}
|
|
3940
|
-
};
|
|
3941
|
-
};
|
|
3942
|
-
|
|
3943
|
-
// storage/keys.ts
|
|
3944
|
-
var SDK_STORAGE_KEYS = {
|
|
3945
|
-
MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
|
|
3946
|
-
LAST_USED_MODEL: "lastUsedModel",
|
|
3947
|
-
BASE_URLS_LIST: "base_urls_list",
|
|
3948
|
-
DISABLED_PROVIDERS: "disabled_providers",
|
|
3949
|
-
MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
|
|
3950
|
-
INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
|
|
3951
|
-
LAST_MODELS_UPDATE: "lastModelsUpdate",
|
|
3952
|
-
LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
|
|
3953
|
-
LOCAL_CASHU_TOKENS: "local_cashu_tokens",
|
|
3954
|
-
API_KEYS: "api_keys",
|
|
3955
|
-
CHILD_KEYS: "child_keys",
|
|
3956
|
-
ROUTSTR21_MODELS: "routstr21Models",
|
|
3957
|
-
LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
|
|
3958
|
-
CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
|
|
3959
|
-
USAGE_TRACKING: "usage_tracking",
|
|
3960
|
-
CLIENT_IDS: "client_ids"
|
|
3961
|
-
};
|
|
3962
|
-
|
|
3963
|
-
// storage/store.ts
|
|
3964
|
-
var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3965
|
-
var getCashuTokenBalance = (token) => {
|
|
3966
|
-
try {
|
|
3967
|
-
const decoded = getDecodedToken(token);
|
|
3968
|
-
const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
|
|
3969
|
-
let sum = 0;
|
|
3970
|
-
for (const proof of decoded.proofs) {
|
|
3971
|
-
sum += proof.amount / unitDivisor;
|
|
3972
|
-
}
|
|
3973
|
-
return sum;
|
|
3974
|
-
} catch {
|
|
3975
|
-
return 0;
|
|
3487
|
+
var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3488
|
+
var getCashuTokenBalance = (token) => {
|
|
3489
|
+
try {
|
|
3490
|
+
const decoded = getDecodedToken(token);
|
|
3491
|
+
const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
|
|
3492
|
+
let sum = 0;
|
|
3493
|
+
for (const proof of decoded.proofs) {
|
|
3494
|
+
sum += proof.amount / unitDivisor;
|
|
3495
|
+
}
|
|
3496
|
+
return sum;
|
|
3497
|
+
} catch {
|
|
3498
|
+
return 0;
|
|
3976
3499
|
}
|
|
3977
3500
|
};
|
|
3978
3501
|
var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
@@ -3990,12 +3513,11 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
3990
3513
|
routstr21Models: [],
|
|
3991
3514
|
lastRoutstr21ModelsUpdate: null,
|
|
3992
3515
|
cachedReceiveTokens: [],
|
|
3993
|
-
usageTracking: [],
|
|
3994
3516
|
clientIds: [],
|
|
3995
3517
|
setModelsFromAllProviders: (value) => {
|
|
3996
3518
|
const normalized = {};
|
|
3997
3519
|
for (const [baseUrl, models] of Object.entries(value)) {
|
|
3998
|
-
normalized[
|
|
3520
|
+
normalized[normalizeBaseUrl4(baseUrl)] = models;
|
|
3999
3521
|
}
|
|
4000
3522
|
void driver.setItem(
|
|
4001
3523
|
SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
|
|
@@ -4008,7 +3530,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
4008
3530
|
set({ lastUsedModel: value });
|
|
4009
3531
|
},
|
|
4010
3532
|
setBaseUrlsList: (value) => {
|
|
4011
|
-
const normalized = value.map((url) =>
|
|
3533
|
+
const normalized = value.map((url) => normalizeBaseUrl4(url));
|
|
4012
3534
|
void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
|
|
4013
3535
|
set({ baseUrlsList: normalized });
|
|
4014
3536
|
},
|
|
@@ -4017,14 +3539,14 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
4017
3539
|
set({ lastBaseUrlsUpdate: value });
|
|
4018
3540
|
},
|
|
4019
3541
|
setDisabledProviders: (value) => {
|
|
4020
|
-
const normalized = value.map((url) =>
|
|
3542
|
+
const normalized = value.map((url) => normalizeBaseUrl4(url));
|
|
4021
3543
|
void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
|
|
4022
3544
|
set({ disabledProviders: normalized });
|
|
4023
3545
|
},
|
|
4024
3546
|
setMintsFromAllProviders: (value) => {
|
|
4025
3547
|
const normalized = {};
|
|
4026
3548
|
for (const [baseUrl, mints] of Object.entries(value)) {
|
|
4027
|
-
normalized[
|
|
3549
|
+
normalized[normalizeBaseUrl4(baseUrl)] = mints.map(
|
|
4028
3550
|
(mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
|
|
4029
3551
|
);
|
|
4030
3552
|
}
|
|
@@ -4037,7 +3559,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
4037
3559
|
setInfoFromAllProviders: (value) => {
|
|
4038
3560
|
const normalized = {};
|
|
4039
3561
|
for (const [baseUrl, info] of Object.entries(value)) {
|
|
4040
|
-
normalized[
|
|
3562
|
+
normalized[normalizeBaseUrl4(baseUrl)] = info;
|
|
4041
3563
|
}
|
|
4042
3564
|
void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
|
|
4043
3565
|
set({ infoFromAllProviders: normalized });
|
|
@@ -4045,7 +3567,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
4045
3567
|
setLastModelsUpdate: (value) => {
|
|
4046
3568
|
const normalized = {};
|
|
4047
3569
|
for (const [baseUrl, timestamp] of Object.entries(value)) {
|
|
4048
|
-
normalized[
|
|
3570
|
+
normalized[normalizeBaseUrl4(baseUrl)] = timestamp;
|
|
4049
3571
|
}
|
|
4050
3572
|
void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
|
|
4051
3573
|
set({ lastModelsUpdate: normalized });
|
|
@@ -4055,7 +3577,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
4055
3577
|
const updates = typeof value === "function" ? value(state.cachedTokens) : value;
|
|
4056
3578
|
const normalized = updates.map((entry) => ({
|
|
4057
3579
|
...entry,
|
|
4058
|
-
baseUrl:
|
|
3580
|
+
baseUrl: normalizeBaseUrl4(entry.baseUrl),
|
|
4059
3581
|
balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
|
|
4060
3582
|
lastUsed: entry.lastUsed ?? null
|
|
4061
3583
|
}));
|
|
@@ -4068,7 +3590,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
4068
3590
|
const updates = typeof value === "function" ? value(state.apiKeys) : value;
|
|
4069
3591
|
const normalized = updates.map((entry) => ({
|
|
4070
3592
|
...entry,
|
|
4071
|
-
baseUrl:
|
|
3593
|
+
baseUrl: normalizeBaseUrl4(entry.baseUrl),
|
|
4072
3594
|
balance: entry.balance ?? 0,
|
|
4073
3595
|
lastUsed: entry.lastUsed ?? null
|
|
4074
3596
|
}));
|
|
@@ -4080,7 +3602,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
4080
3602
|
set((state) => {
|
|
4081
3603
|
const updates = typeof value === "function" ? value(state.childKeys) : value;
|
|
4082
3604
|
const normalized = updates.map((entry) => ({
|
|
4083
|
-
parentBaseUrl:
|
|
3605
|
+
parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
|
|
4084
3606
|
childKey: entry.childKey,
|
|
4085
3607
|
balance: entry.balance ?? 0,
|
|
4086
3608
|
balanceLimit: entry.balanceLimit,
|
|
@@ -4109,10 +3631,6 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
|
|
|
4109
3631
|
void driver.setItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, normalized);
|
|
4110
3632
|
set({ cachedReceiveTokens: normalized });
|
|
4111
3633
|
},
|
|
4112
|
-
setUsageTracking: (value) => {
|
|
4113
|
-
void driver.setItem(SDK_STORAGE_KEYS.USAGE_TRACKING, value);
|
|
4114
|
-
set({ usageTracking: value });
|
|
4115
|
-
},
|
|
4116
3634
|
setClientIds: (value) => {
|
|
4117
3635
|
set((state) => {
|
|
4118
3636
|
const updates = typeof value === "function" ? value(state.clientIds) : value;
|
|
@@ -4142,7 +3660,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
|
|
|
4142
3660
|
rawRoutstr21Models,
|
|
4143
3661
|
rawLastRoutstr21ModelsUpdate,
|
|
4144
3662
|
rawCachedReceiveTokens,
|
|
4145
|
-
rawUsageTracking,
|
|
4146
3663
|
rawClientIds
|
|
4147
3664
|
] = await Promise.all([
|
|
4148
3665
|
driver.getItem(
|
|
@@ -4174,51 +3691,50 @@ var hydrateStoreFromDriver = async (store, driver) => {
|
|
|
4174
3691
|
null
|
|
4175
3692
|
),
|
|
4176
3693
|
driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
|
|
4177
|
-
driver.getItem(SDK_STORAGE_KEYS.USAGE_TRACKING, []),
|
|
4178
3694
|
driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
|
|
4179
3695
|
]);
|
|
4180
3696
|
const modelsFromAllProviders = Object.fromEntries(
|
|
4181
3697
|
Object.entries(rawModels).map(([baseUrl, models]) => [
|
|
4182
|
-
|
|
3698
|
+
normalizeBaseUrl4(baseUrl),
|
|
4183
3699
|
models
|
|
4184
3700
|
])
|
|
4185
3701
|
);
|
|
4186
|
-
const baseUrlsList = rawBaseUrls.map((url) =>
|
|
3702
|
+
const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl4(url));
|
|
4187
3703
|
const disabledProviders = rawDisabledProviders.map(
|
|
4188
|
-
(url) =>
|
|
3704
|
+
(url) => normalizeBaseUrl4(url)
|
|
4189
3705
|
);
|
|
4190
3706
|
const mintsFromAllProviders = Object.fromEntries(
|
|
4191
3707
|
Object.entries(rawMints).map(([baseUrl, mints]) => [
|
|
4192
|
-
|
|
3708
|
+
normalizeBaseUrl4(baseUrl),
|
|
4193
3709
|
mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
|
|
4194
3710
|
])
|
|
4195
3711
|
);
|
|
4196
3712
|
const infoFromAllProviders = Object.fromEntries(
|
|
4197
3713
|
Object.entries(rawInfo).map(([baseUrl, info]) => [
|
|
4198
|
-
|
|
3714
|
+
normalizeBaseUrl4(baseUrl),
|
|
4199
3715
|
info
|
|
4200
3716
|
])
|
|
4201
3717
|
);
|
|
4202
3718
|
const lastModelsUpdate = Object.fromEntries(
|
|
4203
3719
|
Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
|
|
4204
|
-
|
|
3720
|
+
normalizeBaseUrl4(baseUrl),
|
|
4205
3721
|
timestamp
|
|
4206
3722
|
])
|
|
4207
3723
|
);
|
|
4208
3724
|
const cachedTokens = rawCachedTokens.map((entry) => ({
|
|
4209
3725
|
...entry,
|
|
4210
|
-
baseUrl:
|
|
3726
|
+
baseUrl: normalizeBaseUrl4(entry.baseUrl),
|
|
4211
3727
|
balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
|
|
4212
3728
|
lastUsed: entry.lastUsed ?? null
|
|
4213
3729
|
}));
|
|
4214
3730
|
const apiKeys = rawApiKeys.map((entry) => ({
|
|
4215
3731
|
...entry,
|
|
4216
|
-
baseUrl:
|
|
3732
|
+
baseUrl: normalizeBaseUrl4(entry.baseUrl),
|
|
4217
3733
|
balance: entry.balance ?? 0,
|
|
4218
3734
|
lastUsed: entry.lastUsed ?? null
|
|
4219
3735
|
}));
|
|
4220
3736
|
const childKeys = rawChildKeys.map((entry) => ({
|
|
4221
|
-
parentBaseUrl:
|
|
3737
|
+
parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
|
|
4222
3738
|
childKey: entry.childKey,
|
|
4223
3739
|
balance: entry.balance ?? 0,
|
|
4224
3740
|
balanceLimit: entry.balanceLimit,
|
|
@@ -4233,7 +3749,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
|
|
|
4233
3749
|
unit: entry.unit || "sat",
|
|
4234
3750
|
createdAt: entry.createdAt ?? Date.now()
|
|
4235
3751
|
}));
|
|
4236
|
-
const usageTracking = rawUsageTracking;
|
|
4237
3752
|
const clientIds = rawClientIds.map((entry) => ({
|
|
4238
3753
|
...entry,
|
|
4239
3754
|
createdAt: entry.createdAt ?? Date.now(),
|
|
@@ -4254,7 +3769,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
|
|
|
4254
3769
|
routstr21Models,
|
|
4255
3770
|
lastRoutstr21ModelsUpdate,
|
|
4256
3771
|
cachedReceiveTokens,
|
|
4257
|
-
usageTracking,
|
|
4258
3772
|
clientIds
|
|
4259
3773
|
});
|
|
4260
3774
|
};
|
|
@@ -4275,12 +3789,12 @@ var createDiscoveryAdapterFromStore = (store) => ({
|
|
|
4275
3789
|
getCachedProviderInfo: () => store.getState().infoFromAllProviders,
|
|
4276
3790
|
setCachedProviderInfo: (info) => store.getState().setInfoFromAllProviders(info),
|
|
4277
3791
|
getProviderLastUpdate: (baseUrl) => {
|
|
4278
|
-
const normalized =
|
|
3792
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4279
3793
|
const timestamps = store.getState().lastModelsUpdate;
|
|
4280
3794
|
return timestamps[normalized] || null;
|
|
4281
3795
|
},
|
|
4282
3796
|
setProviderLastUpdate: (baseUrl, timestamp) => {
|
|
4283
|
-
const normalized =
|
|
3797
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4284
3798
|
const timestamps = { ...store.getState().lastModelsUpdate };
|
|
4285
3799
|
timestamps[normalized] = timestamp;
|
|
4286
3800
|
store.getState().setLastModelsUpdate(timestamps);
|
|
@@ -4299,7 +3813,7 @@ var createDiscoveryAdapterFromStore = (store) => ({
|
|
|
4299
3813
|
});
|
|
4300
3814
|
var createStorageAdapterFromStore = (store) => ({
|
|
4301
3815
|
getToken: (baseUrl) => {
|
|
4302
|
-
const normalized =
|
|
3816
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4303
3817
|
const entry = store.getState().cachedTokens.find((token) => token.baseUrl === normalized);
|
|
4304
3818
|
if (!entry) return null;
|
|
4305
3819
|
const next = store.getState().cachedTokens.map(
|
|
@@ -4309,7 +3823,7 @@ var createStorageAdapterFromStore = (store) => ({
|
|
|
4309
3823
|
return entry.token;
|
|
4310
3824
|
},
|
|
4311
3825
|
setToken: (baseUrl, token) => {
|
|
4312
|
-
const normalized =
|
|
3826
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4313
3827
|
const tokens = store.getState().cachedTokens;
|
|
4314
3828
|
const balance = getCashuTokenBalance(token);
|
|
4315
3829
|
const existingIndex = tokens.findIndex(
|
|
@@ -4318,266 +3832,1532 @@ var createStorageAdapterFromStore = (store) => ({
|
|
|
4318
3832
|
if (existingIndex !== -1) {
|
|
4319
3833
|
throw new Error(`Token already exists for baseUrl: ${normalized}`);
|
|
4320
3834
|
}
|
|
4321
|
-
const next = [...tokens];
|
|
4322
|
-
next.push({
|
|
4323
|
-
baseUrl: normalized,
|
|
3835
|
+
const next = [...tokens];
|
|
3836
|
+
next.push({
|
|
3837
|
+
baseUrl: normalized,
|
|
3838
|
+
token,
|
|
3839
|
+
balance,
|
|
3840
|
+
lastUsed: Date.now()
|
|
3841
|
+
});
|
|
3842
|
+
store.getState().setCachedTokens(next);
|
|
3843
|
+
},
|
|
3844
|
+
removeToken: (baseUrl) => {
|
|
3845
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3846
|
+
const next = store.getState().cachedTokens.filter((entry) => entry.baseUrl !== normalized);
|
|
3847
|
+
store.getState().setCachedTokens(next);
|
|
3848
|
+
},
|
|
3849
|
+
updateTokenBalance: (baseUrl, balance) => {
|
|
3850
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3851
|
+
const tokens = store.getState().cachedTokens;
|
|
3852
|
+
const next = tokens.map(
|
|
3853
|
+
(entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
|
|
3854
|
+
);
|
|
3855
|
+
store.getState().setCachedTokens(next);
|
|
3856
|
+
},
|
|
3857
|
+
getCachedTokenDistribution: () => {
|
|
3858
|
+
const cachedTokens = store.getState().cachedTokens;
|
|
3859
|
+
const distributionMap = {};
|
|
3860
|
+
for (const entry of cachedTokens) {
|
|
3861
|
+
const sum = entry.balance || 0;
|
|
3862
|
+
if (sum > 0) {
|
|
3863
|
+
distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
|
|
3867
|
+
},
|
|
3868
|
+
getApiKeyDistribution: () => {
|
|
3869
|
+
const apiKeys = store.getState().apiKeys;
|
|
3870
|
+
const distributionMap = {};
|
|
3871
|
+
for (const entry of apiKeys) {
|
|
3872
|
+
const sum = entry.balance || 0;
|
|
3873
|
+
if (sum > 0) {
|
|
3874
|
+
distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
|
|
3878
|
+
},
|
|
3879
|
+
saveProviderInfo: (baseUrl, info) => {
|
|
3880
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3881
|
+
const next = { ...store.getState().infoFromAllProviders };
|
|
3882
|
+
next[normalized] = info;
|
|
3883
|
+
store.getState().setInfoFromAllProviders(next);
|
|
3884
|
+
},
|
|
3885
|
+
getProviderInfo: (baseUrl) => {
|
|
3886
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3887
|
+
return store.getState().infoFromAllProviders[normalized] || null;
|
|
3888
|
+
},
|
|
3889
|
+
// ========== API Keys (for apikeys mode) ==========
|
|
3890
|
+
getApiKey: (baseUrl) => {
|
|
3891
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3892
|
+
const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
|
|
3893
|
+
if (!entry) return null;
|
|
3894
|
+
const next = store.getState().apiKeys.map(
|
|
3895
|
+
(key) => key.baseUrl === normalized ? { ...key, lastUsed: Date.now() } : key
|
|
3896
|
+
);
|
|
3897
|
+
store.getState().setApiKeys(next);
|
|
3898
|
+
return entry;
|
|
3899
|
+
},
|
|
3900
|
+
setApiKey: (baseUrl, key) => {
|
|
3901
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3902
|
+
const keys = store.getState().apiKeys;
|
|
3903
|
+
const existingIndex = keys.findIndex(
|
|
3904
|
+
(entry) => entry.baseUrl === normalized
|
|
3905
|
+
);
|
|
3906
|
+
if (existingIndex !== -1) {
|
|
3907
|
+
throw new Error(`ApiKey already exists for baseUrl: ${normalized}`);
|
|
3908
|
+
}
|
|
3909
|
+
const next = [...keys];
|
|
3910
|
+
next.push({
|
|
3911
|
+
baseUrl: normalized,
|
|
3912
|
+
key,
|
|
3913
|
+
balance: 0,
|
|
3914
|
+
lastUsed: Date.now()
|
|
3915
|
+
});
|
|
3916
|
+
store.getState().setApiKeys(next);
|
|
3917
|
+
},
|
|
3918
|
+
updateApiKeyBalance: (baseUrl, balance) => {
|
|
3919
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3920
|
+
const keys = store.getState().apiKeys;
|
|
3921
|
+
const next = keys.map(
|
|
3922
|
+
(entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
|
|
3923
|
+
);
|
|
3924
|
+
store.getState().setApiKeys(next);
|
|
3925
|
+
},
|
|
3926
|
+
removeApiKey: (baseUrl) => {
|
|
3927
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
3928
|
+
const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
|
|
3929
|
+
store.getState().setApiKeys(next);
|
|
3930
|
+
},
|
|
3931
|
+
getAllApiKeys: () => {
|
|
3932
|
+
return store.getState().apiKeys.map((entry) => ({
|
|
3933
|
+
baseUrl: entry.baseUrl,
|
|
3934
|
+
key: entry.key,
|
|
3935
|
+
balance: entry.balance,
|
|
3936
|
+
lastUsed: entry.lastUsed
|
|
3937
|
+
}));
|
|
3938
|
+
},
|
|
3939
|
+
// ========== Child Keys ==========
|
|
3940
|
+
getChildKey: (parentBaseUrl) => {
|
|
3941
|
+
const normalized = normalizeBaseUrl4(parentBaseUrl);
|
|
3942
|
+
const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
|
|
3943
|
+
if (!entry) return null;
|
|
3944
|
+
return {
|
|
3945
|
+
parentBaseUrl: entry.parentBaseUrl,
|
|
3946
|
+
childKey: entry.childKey,
|
|
3947
|
+
balance: entry.balance,
|
|
3948
|
+
balanceLimit: entry.balanceLimit,
|
|
3949
|
+
validityDate: entry.validityDate,
|
|
3950
|
+
createdAt: entry.createdAt
|
|
3951
|
+
};
|
|
3952
|
+
},
|
|
3953
|
+
setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
|
|
3954
|
+
const normalized = normalizeBaseUrl4(parentBaseUrl);
|
|
3955
|
+
const keys = store.getState().childKeys;
|
|
3956
|
+
const existingIndex = keys.findIndex(
|
|
3957
|
+
(entry) => entry.parentBaseUrl === normalized
|
|
3958
|
+
);
|
|
3959
|
+
if (existingIndex !== -1) {
|
|
3960
|
+
const next = keys.map(
|
|
3961
|
+
(entry) => entry.parentBaseUrl === normalized ? {
|
|
3962
|
+
...entry,
|
|
3963
|
+
childKey,
|
|
3964
|
+
balance: balance ?? 0,
|
|
3965
|
+
validityDate,
|
|
3966
|
+
balanceLimit,
|
|
3967
|
+
createdAt: Date.now()
|
|
3968
|
+
} : entry
|
|
3969
|
+
);
|
|
3970
|
+
store.getState().setChildKeys(next);
|
|
3971
|
+
} else {
|
|
3972
|
+
const next = [...keys];
|
|
3973
|
+
next.push({
|
|
3974
|
+
parentBaseUrl: normalized,
|
|
3975
|
+
childKey,
|
|
3976
|
+
balance: balance ?? 0,
|
|
3977
|
+
validityDate,
|
|
3978
|
+
balanceLimit,
|
|
3979
|
+
createdAt: Date.now()
|
|
3980
|
+
});
|
|
3981
|
+
store.getState().setChildKeys(next);
|
|
3982
|
+
}
|
|
3983
|
+
},
|
|
3984
|
+
updateChildKeyBalance: (parentBaseUrl, balance) => {
|
|
3985
|
+
const normalized = normalizeBaseUrl4(parentBaseUrl);
|
|
3986
|
+
const keys = store.getState().childKeys;
|
|
3987
|
+
const next = keys.map(
|
|
3988
|
+
(entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
|
|
3989
|
+
);
|
|
3990
|
+
store.getState().setChildKeys(next);
|
|
3991
|
+
},
|
|
3992
|
+
removeChildKey: (parentBaseUrl) => {
|
|
3993
|
+
const normalized = normalizeBaseUrl4(parentBaseUrl);
|
|
3994
|
+
const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
|
|
3995
|
+
store.getState().setChildKeys(next);
|
|
3996
|
+
},
|
|
3997
|
+
getAllChildKeys: () => {
|
|
3998
|
+
return store.getState().childKeys.map((entry) => ({
|
|
3999
|
+
parentBaseUrl: entry.parentBaseUrl,
|
|
4000
|
+
childKey: entry.childKey,
|
|
4001
|
+
balance: entry.balance,
|
|
4002
|
+
balanceLimit: entry.balanceLimit,
|
|
4003
|
+
validityDate: entry.validityDate,
|
|
4004
|
+
createdAt: entry.createdAt
|
|
4005
|
+
}));
|
|
4006
|
+
},
|
|
4007
|
+
getCachedReceiveTokens: () => {
|
|
4008
|
+
return store.getState().cachedReceiveTokens;
|
|
4009
|
+
},
|
|
4010
|
+
setCachedReceiveTokens: (tokens) => {
|
|
4011
|
+
store.getState().setCachedReceiveTokens(tokens);
|
|
4012
|
+
}
|
|
4013
|
+
});
|
|
4014
|
+
var createProviderRegistryFromStore = (store) => ({
|
|
4015
|
+
getModelsForProvider: (baseUrl) => {
|
|
4016
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4017
|
+
return store.getState().modelsFromAllProviders[normalized] || [];
|
|
4018
|
+
},
|
|
4019
|
+
getDisabledProviders: () => store.getState().disabledProviders,
|
|
4020
|
+
getProviderMints: (baseUrl) => {
|
|
4021
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4022
|
+
return store.getState().mintsFromAllProviders[normalized] || [];
|
|
4023
|
+
},
|
|
4024
|
+
getProviderInfo: async (baseUrl) => {
|
|
4025
|
+
const normalized = normalizeBaseUrl4(baseUrl);
|
|
4026
|
+
const cached = store.getState().infoFromAllProviders[normalized];
|
|
4027
|
+
if (cached) return cached;
|
|
4028
|
+
try {
|
|
4029
|
+
const response = await fetch(`${normalized}v1/info`);
|
|
4030
|
+
if (!response.ok) {
|
|
4031
|
+
throw new Error(`Failed ${response.status}`);
|
|
4032
|
+
}
|
|
4033
|
+
const info = await response.json();
|
|
4034
|
+
const next = { ...store.getState().infoFromAllProviders };
|
|
4035
|
+
next[normalized] = info;
|
|
4036
|
+
store.getState().setInfoFromAllProviders(next);
|
|
4037
|
+
return info;
|
|
4038
|
+
} catch (error) {
|
|
4039
|
+
console.warn(`Failed to fetch provider info from ${normalized}:`, error);
|
|
4040
|
+
return null;
|
|
4041
|
+
}
|
|
4042
|
+
},
|
|
4043
|
+
getAllProvidersModels: () => store.getState().modelsFromAllProviders
|
|
4044
|
+
});
|
|
4045
|
+
|
|
4046
|
+
// storage/index.ts
|
|
4047
|
+
var isBrowser3 = () => {
|
|
4048
|
+
try {
|
|
4049
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
4050
|
+
} catch {
|
|
4051
|
+
return false;
|
|
4052
|
+
}
|
|
4053
|
+
};
|
|
4054
|
+
var isNode = () => {
|
|
4055
|
+
try {
|
|
4056
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
4057
|
+
} catch {
|
|
4058
|
+
return false;
|
|
4059
|
+
}
|
|
4060
|
+
};
|
|
4061
|
+
var defaultDriver = null;
|
|
4062
|
+
var isBun3 = () => {
|
|
4063
|
+
return typeof process.versions.bun !== "undefined";
|
|
4064
|
+
};
|
|
4065
|
+
var getDefaultSdkDriver = () => {
|
|
4066
|
+
if (defaultDriver) return defaultDriver;
|
|
4067
|
+
if (isBrowser3()) {
|
|
4068
|
+
defaultDriver = localStorageDriver;
|
|
4069
|
+
return defaultDriver;
|
|
4070
|
+
}
|
|
4071
|
+
if (isBun3()) {
|
|
4072
|
+
defaultDriver = createMemoryDriver();
|
|
4073
|
+
return defaultDriver;
|
|
4074
|
+
}
|
|
4075
|
+
if (isNode()) {
|
|
4076
|
+
defaultDriver = createSqliteDriver();
|
|
4077
|
+
return defaultDriver;
|
|
4078
|
+
}
|
|
4079
|
+
defaultDriver = createMemoryDriver();
|
|
4080
|
+
return defaultDriver;
|
|
4081
|
+
};
|
|
4082
|
+
var defaultStore = null;
|
|
4083
|
+
var defaultUsageTrackingDriver = null;
|
|
4084
|
+
var getDefaultSdkStore = () => {
|
|
4085
|
+
if (!defaultStore) {
|
|
4086
|
+
defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
|
|
4087
|
+
}
|
|
4088
|
+
return defaultStore.hydrate.then(() => defaultStore.store);
|
|
4089
|
+
};
|
|
4090
|
+
var getDefaultUsageTrackingDriver = () => {
|
|
4091
|
+
if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
|
|
4092
|
+
const storageDriver = getDefaultSdkDriver();
|
|
4093
|
+
if (isBrowser3()) {
|
|
4094
|
+
defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
|
|
4095
|
+
legacyStorageDriver: storageDriver
|
|
4096
|
+
});
|
|
4097
|
+
return defaultUsageTrackingDriver;
|
|
4098
|
+
}
|
|
4099
|
+
if (isBun3()) {
|
|
4100
|
+
defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
|
|
4101
|
+
return defaultUsageTrackingDriver;
|
|
4102
|
+
}
|
|
4103
|
+
if (isNode()) {
|
|
4104
|
+
defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
|
|
4105
|
+
legacyStorageDriver: storageDriver
|
|
4106
|
+
});
|
|
4107
|
+
return defaultUsageTrackingDriver;
|
|
4108
|
+
}
|
|
4109
|
+
defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
|
|
4110
|
+
return defaultUsageTrackingDriver;
|
|
4111
|
+
};
|
|
4112
|
+
var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
|
|
4113
|
+
var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
|
|
4114
|
+
var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
|
|
4115
|
+
function createSSEParserTransform(onUsage, onResponseId) {
|
|
4116
|
+
let buffer = "";
|
|
4117
|
+
const maybeCaptureUsageFromJson = (jsonText) => {
|
|
4118
|
+
try {
|
|
4119
|
+
const data = JSON.parse(jsonText);
|
|
4120
|
+
const responseId = data.id;
|
|
4121
|
+
if (typeof responseId === "string" && responseId.trim().length > 0) {
|
|
4122
|
+
onResponseId?.(responseId.trim());
|
|
4123
|
+
}
|
|
4124
|
+
const usage = extractUsageFromSSEJson(data);
|
|
4125
|
+
if (usage) {
|
|
4126
|
+
onUsage(usage);
|
|
4127
|
+
}
|
|
4128
|
+
} catch {
|
|
4129
|
+
}
|
|
4130
|
+
};
|
|
4131
|
+
const processLine = (self, line) => {
|
|
4132
|
+
const trimmed = line.trim();
|
|
4133
|
+
if (!trimmed) {
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
4136
|
+
if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
|
|
4137
|
+
self.push("data: [DONE]\n\n");
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
if (trimmed.startsWith("data:")) {
|
|
4141
|
+
const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
|
|
4142
|
+
if (dataStr === "[DONE]") {
|
|
4143
|
+
self.push("data: [DONE]\n\n");
|
|
4144
|
+
return;
|
|
4145
|
+
}
|
|
4146
|
+
maybeCaptureUsageFromJson(dataStr);
|
|
4147
|
+
self.push(`data: ${dataStr}
|
|
4148
|
+
|
|
4149
|
+
`);
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
4152
|
+
if (trimmed.startsWith("{")) {
|
|
4153
|
+
maybeCaptureUsageFromJson(trimmed);
|
|
4154
|
+
self.push(`data: ${trimmed}
|
|
4155
|
+
|
|
4156
|
+
`);
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
4159
|
+
self.push(line + "\n");
|
|
4160
|
+
};
|
|
4161
|
+
return new Transform({
|
|
4162
|
+
transform(chunk, encoding, callback) {
|
|
4163
|
+
buffer += chunk.toString();
|
|
4164
|
+
const lines = buffer.split(/\r?\n/);
|
|
4165
|
+
buffer = lines.pop() || "";
|
|
4166
|
+
for (const line of lines) {
|
|
4167
|
+
processLine(this, line);
|
|
4168
|
+
}
|
|
4169
|
+
callback();
|
|
4170
|
+
},
|
|
4171
|
+
flush(callback) {
|
|
4172
|
+
if (buffer.trim()) {
|
|
4173
|
+
processLine(this, buffer);
|
|
4174
|
+
}
|
|
4175
|
+
buffer = "";
|
|
4176
|
+
callback();
|
|
4177
|
+
}
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
var TOPUP_MARGIN = 1.2;
|
|
4181
|
+
var RoutstrClient = class {
|
|
4182
|
+
constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
|
|
4183
|
+
this.walletAdapter = walletAdapter;
|
|
4184
|
+
this.storageAdapter = storageAdapter;
|
|
4185
|
+
this.providerRegistry = providerRegistry;
|
|
4186
|
+
this.balanceManager = new BalanceManager(
|
|
4187
|
+
walletAdapter,
|
|
4188
|
+
storageAdapter,
|
|
4189
|
+
providerRegistry
|
|
4190
|
+
);
|
|
4191
|
+
this.cashuSpender = new CashuSpender(
|
|
4192
|
+
walletAdapter,
|
|
4193
|
+
storageAdapter,
|
|
4194
|
+
providerRegistry,
|
|
4195
|
+
this.balanceManager
|
|
4196
|
+
);
|
|
4197
|
+
this.streamProcessor = new StreamProcessor();
|
|
4198
|
+
this.providerManager = new ProviderManager(providerRegistry);
|
|
4199
|
+
this.alertLevel = alertLevel;
|
|
4200
|
+
if (mode === "lazyrefund") {
|
|
4201
|
+
this.mode = "apikeys";
|
|
4202
|
+
} else if (mode === "apikeys") {
|
|
4203
|
+
this.mode = "lazyrefund";
|
|
4204
|
+
} else {
|
|
4205
|
+
this.mode = mode;
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
cashuSpender;
|
|
4209
|
+
balanceManager;
|
|
4210
|
+
streamProcessor;
|
|
4211
|
+
providerManager;
|
|
4212
|
+
alertLevel;
|
|
4213
|
+
mode;
|
|
4214
|
+
debugLevel = "WARN";
|
|
4215
|
+
/**
|
|
4216
|
+
* Get the current client mode
|
|
4217
|
+
*/
|
|
4218
|
+
getMode() {
|
|
4219
|
+
return this.mode;
|
|
4220
|
+
}
|
|
4221
|
+
getDebugLevel() {
|
|
4222
|
+
return this.debugLevel;
|
|
4223
|
+
}
|
|
4224
|
+
setDebugLevel(level) {
|
|
4225
|
+
this.debugLevel = level;
|
|
4226
|
+
}
|
|
4227
|
+
_log(level, ...args) {
|
|
4228
|
+
const levelPriority = {
|
|
4229
|
+
DEBUG: 0,
|
|
4230
|
+
WARN: 1,
|
|
4231
|
+
ERROR: 2
|
|
4232
|
+
};
|
|
4233
|
+
if (levelPriority[level] >= levelPriority[this.debugLevel]) {
|
|
4234
|
+
switch (level) {
|
|
4235
|
+
case "DEBUG":
|
|
4236
|
+
console.log(...args);
|
|
4237
|
+
break;
|
|
4238
|
+
case "WARN":
|
|
4239
|
+
console.warn(...args);
|
|
4240
|
+
break;
|
|
4241
|
+
case "ERROR":
|
|
4242
|
+
console.error(...args);
|
|
4243
|
+
break;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
/**
|
|
4248
|
+
* Get the CashuSpender instance
|
|
4249
|
+
*/
|
|
4250
|
+
getCashuSpender() {
|
|
4251
|
+
return this.cashuSpender;
|
|
4252
|
+
}
|
|
4253
|
+
/**
|
|
4254
|
+
* Get the BalanceManager instance
|
|
4255
|
+
*/
|
|
4256
|
+
getBalanceManager() {
|
|
4257
|
+
return this.balanceManager;
|
|
4258
|
+
}
|
|
4259
|
+
/**
|
|
4260
|
+
* Get the ProviderManager instance
|
|
4261
|
+
*/
|
|
4262
|
+
getProviderManager() {
|
|
4263
|
+
return this.providerManager;
|
|
4264
|
+
}
|
|
4265
|
+
/**
|
|
4266
|
+
* Check if the client is currently busy (in critical section)
|
|
4267
|
+
*/
|
|
4268
|
+
get isBusy() {
|
|
4269
|
+
return this.cashuSpender.isBusy;
|
|
4270
|
+
}
|
|
4271
|
+
/**
|
|
4272
|
+
* Route an API request to the upstream provider
|
|
4273
|
+
*
|
|
4274
|
+
* This is a simpler alternative to fetchAIResponse that just proxies
|
|
4275
|
+
* the request upstream without the streaming callback machinery.
|
|
4276
|
+
* Useful for daemon-style routing where you just need to forward
|
|
4277
|
+
* requests and get responses back.
|
|
4278
|
+
*/
|
|
4279
|
+
async routeRequest(params) {
|
|
4280
|
+
const prepared = await this._prepareRoutedRequest(params);
|
|
4281
|
+
const satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4282
|
+
token: prepared.tokenUsed,
|
|
4283
|
+
baseUrl: prepared.baseUrlUsed,
|
|
4284
|
+
initialTokenBalance: prepared.tokenBalanceInSats,
|
|
4285
|
+
response: prepared.response,
|
|
4286
|
+
modelId: prepared.modelId,
|
|
4287
|
+
usage: prepared.capturedUsage,
|
|
4288
|
+
requestId: prepared.capturedResponseId
|
|
4289
|
+
});
|
|
4290
|
+
prepared.response.satsSpent = satsSpent;
|
|
4291
|
+
prepared.response.usage = prepared.capturedUsage;
|
|
4292
|
+
prepared.response.requestId = prepared.capturedResponseId;
|
|
4293
|
+
return prepared.response;
|
|
4294
|
+
}
|
|
4295
|
+
async routeRequestToNodeResponse(params) {
|
|
4296
|
+
const { res } = params;
|
|
4297
|
+
const prepared = await this._prepareRoutedRequest(params);
|
|
4298
|
+
res.statusCode = prepared.response.status;
|
|
4299
|
+
prepared.response.headers.forEach((value, key) => {
|
|
4300
|
+
res.setHeader(key, value);
|
|
4301
|
+
});
|
|
4302
|
+
const body = prepared.response.body;
|
|
4303
|
+
if (!body) {
|
|
4304
|
+
const satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4305
|
+
token: prepared.tokenUsed,
|
|
4306
|
+
baseUrl: prepared.baseUrlUsed,
|
|
4307
|
+
initialTokenBalance: prepared.tokenBalanceInSats,
|
|
4308
|
+
response: prepared.response,
|
|
4309
|
+
modelId: prepared.modelId,
|
|
4310
|
+
usage: prepared.capturedUsage,
|
|
4311
|
+
requestId: prepared.capturedResponseId
|
|
4312
|
+
});
|
|
4313
|
+
prepared.response.satsSpent = satsSpent;
|
|
4314
|
+
res.end();
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
const nodeReadable = Readable.fromWeb(body);
|
|
4318
|
+
await new Promise((resolve, reject) => {
|
|
4319
|
+
let settled = false;
|
|
4320
|
+
const finish = async () => {
|
|
4321
|
+
if (settled) return;
|
|
4322
|
+
settled = true;
|
|
4323
|
+
try {
|
|
4324
|
+
const satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4325
|
+
token: prepared.tokenUsed,
|
|
4326
|
+
baseUrl: prepared.baseUrlUsed,
|
|
4327
|
+
initialTokenBalance: prepared.tokenBalanceInSats,
|
|
4328
|
+
response: prepared.response,
|
|
4329
|
+
modelId: prepared.modelId,
|
|
4330
|
+
usage: prepared.capturedUsage,
|
|
4331
|
+
requestId: prepared.capturedResponseId
|
|
4332
|
+
});
|
|
4333
|
+
prepared.response.satsSpent = satsSpent;
|
|
4334
|
+
prepared.response.usage = prepared.capturedUsage;
|
|
4335
|
+
prepared.response.requestId = prepared.capturedResponseId;
|
|
4336
|
+
resolve();
|
|
4337
|
+
} catch (error) {
|
|
4338
|
+
reject(error);
|
|
4339
|
+
}
|
|
4340
|
+
};
|
|
4341
|
+
const fail = (error) => {
|
|
4342
|
+
if (settled) return;
|
|
4343
|
+
settled = true;
|
|
4344
|
+
reject(error);
|
|
4345
|
+
};
|
|
4346
|
+
res.once("finish", finish);
|
|
4347
|
+
res.once("close", finish);
|
|
4348
|
+
res.once("error", fail);
|
|
4349
|
+
nodeReadable.once("error", fail);
|
|
4350
|
+
nodeReadable.pipe(res);
|
|
4351
|
+
});
|
|
4352
|
+
}
|
|
4353
|
+
async _prepareRoutedRequest(params) {
|
|
4354
|
+
const {
|
|
4355
|
+
path,
|
|
4356
|
+
method,
|
|
4357
|
+
body,
|
|
4358
|
+
headers = {},
|
|
4359
|
+
baseUrl,
|
|
4360
|
+
mintUrl,
|
|
4361
|
+
modelId
|
|
4362
|
+
} = params;
|
|
4363
|
+
await this._checkBalance();
|
|
4364
|
+
let requiredSats = 1;
|
|
4365
|
+
let selectedModel;
|
|
4366
|
+
if (modelId) {
|
|
4367
|
+
const providerModel = await this.providerManager.getModelForProvider(
|
|
4368
|
+
baseUrl,
|
|
4369
|
+
modelId
|
|
4370
|
+
);
|
|
4371
|
+
selectedModel = providerModel ?? void 0;
|
|
4372
|
+
if (selectedModel) {
|
|
4373
|
+
requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
4374
|
+
selectedModel,
|
|
4375
|
+
[]
|
|
4376
|
+
);
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
|
|
4380
|
+
mintUrl,
|
|
4381
|
+
amount: requiredSats,
|
|
4382
|
+
baseUrl
|
|
4383
|
+
});
|
|
4384
|
+
this._log("DEBUG", token, baseUrl);
|
|
4385
|
+
let requestBody = body;
|
|
4386
|
+
if (body && typeof body === "object") {
|
|
4387
|
+
const bodyObj = body;
|
|
4388
|
+
if (!bodyObj.stream) {
|
|
4389
|
+
requestBody = { ...bodyObj, stream: false };
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
const baseHeaders = this._buildBaseHeaders(headers);
|
|
4393
|
+
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
4394
|
+
const response = await this._makeRequest({
|
|
4395
|
+
path,
|
|
4396
|
+
method,
|
|
4397
|
+
body: method === "GET" ? void 0 : requestBody,
|
|
4398
|
+
baseUrl,
|
|
4399
|
+
mintUrl,
|
|
4400
|
+
token,
|
|
4401
|
+
requiredSats,
|
|
4402
|
+
headers: requestHeaders,
|
|
4403
|
+
baseHeaders,
|
|
4404
|
+
selectedModel
|
|
4405
|
+
});
|
|
4406
|
+
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
4407
|
+
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
4408
|
+
const tokenUsed = response.token || token;
|
|
4409
|
+
const contentType = response.headers.get("content-type") || "";
|
|
4410
|
+
let processedResponse = response;
|
|
4411
|
+
let capturedUsage;
|
|
4412
|
+
let capturedResponseId;
|
|
4413
|
+
if (contentType.includes("text/event-stream") && response.body) {
|
|
4414
|
+
const nodeReadable = Readable.fromWeb(response.body);
|
|
4415
|
+
const sseParser = createSSEParserTransform(
|
|
4416
|
+
(usage) => {
|
|
4417
|
+
capturedUsage = usage;
|
|
4418
|
+
processedResponse.usage = usage;
|
|
4419
|
+
},
|
|
4420
|
+
(responseId) => {
|
|
4421
|
+
capturedResponseId = responseId;
|
|
4422
|
+
processedResponse.requestId = responseId;
|
|
4423
|
+
}
|
|
4424
|
+
);
|
|
4425
|
+
const transformed = nodeReadable.pipe(sseParser, { end: true });
|
|
4426
|
+
const webStream = Readable.toWeb(
|
|
4427
|
+
transformed
|
|
4428
|
+
);
|
|
4429
|
+
processedResponse = new Response(webStream, {
|
|
4430
|
+
status: response.status,
|
|
4431
|
+
statusText: response.statusText,
|
|
4432
|
+
headers: response.headers
|
|
4433
|
+
});
|
|
4434
|
+
processedResponse.baseUrl = response.baseUrl;
|
|
4435
|
+
processedResponse.token = response.token;
|
|
4436
|
+
}
|
|
4437
|
+
return {
|
|
4438
|
+
response: processedResponse,
|
|
4439
|
+
tokenUsed,
|
|
4440
|
+
baseUrlUsed,
|
|
4441
|
+
tokenBalanceInSats,
|
|
4442
|
+
modelId,
|
|
4443
|
+
capturedUsage,
|
|
4444
|
+
capturedResponseId
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
/**
|
|
4448
|
+
* Fetch AI response with streaming
|
|
4449
|
+
*/
|
|
4450
|
+
async fetchAIResponse(options, callbacks) {
|
|
4451
|
+
const {
|
|
4452
|
+
messageHistory,
|
|
4453
|
+
selectedModel,
|
|
4454
|
+
baseUrl,
|
|
4455
|
+
mintUrl,
|
|
4456
|
+
balance,
|
|
4457
|
+
transactionHistory,
|
|
4458
|
+
maxTokens,
|
|
4459
|
+
headers
|
|
4460
|
+
} = options;
|
|
4461
|
+
const apiMessages = await this._convertMessages(messageHistory);
|
|
4462
|
+
const requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
4463
|
+
selectedModel,
|
|
4464
|
+
apiMessages,
|
|
4465
|
+
maxTokens
|
|
4466
|
+
);
|
|
4467
|
+
try {
|
|
4468
|
+
await this._checkBalance();
|
|
4469
|
+
callbacks.onPaymentProcessing?.(true);
|
|
4470
|
+
const spendResult = await this._spendToken({
|
|
4471
|
+
mintUrl,
|
|
4472
|
+
amount: requiredSats,
|
|
4473
|
+
baseUrl
|
|
4474
|
+
});
|
|
4475
|
+
let token = spendResult.token;
|
|
4476
|
+
let tokenBalance = spendResult.tokenBalance;
|
|
4477
|
+
let tokenBalanceUnit = spendResult.tokenBalanceUnit;
|
|
4478
|
+
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
4479
|
+
callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
|
|
4480
|
+
const baseHeaders = this._buildBaseHeaders(headers);
|
|
4481
|
+
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
4482
|
+
this.providerManager.resetFailedProviders();
|
|
4483
|
+
const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
|
|
4484
|
+
const providerVersion = providerInfo?.version ?? "";
|
|
4485
|
+
let modelIdForRequest = selectedModel.id;
|
|
4486
|
+
if (/^0\.1\./.test(providerVersion)) {
|
|
4487
|
+
const newModel = await this.providerManager.getModelForProvider(
|
|
4488
|
+
baseUrl,
|
|
4489
|
+
selectedModel.id
|
|
4490
|
+
);
|
|
4491
|
+
modelIdForRequest = newModel?.id ?? selectedModel.id;
|
|
4492
|
+
}
|
|
4493
|
+
const body = {
|
|
4494
|
+
model: modelIdForRequest,
|
|
4495
|
+
messages: apiMessages,
|
|
4496
|
+
stream: true
|
|
4497
|
+
};
|
|
4498
|
+
if (maxTokens !== void 0) {
|
|
4499
|
+
body.max_tokens = maxTokens;
|
|
4500
|
+
}
|
|
4501
|
+
if (selectedModel?.name?.startsWith("OpenAI:")) {
|
|
4502
|
+
body.tools = [{ type: "web_search" }];
|
|
4503
|
+
}
|
|
4504
|
+
const response = await this._makeRequest({
|
|
4505
|
+
path: "/v1/chat/completions",
|
|
4506
|
+
method: "POST",
|
|
4507
|
+
body,
|
|
4508
|
+
selectedModel,
|
|
4509
|
+
baseUrl,
|
|
4510
|
+
mintUrl,
|
|
4511
|
+
token,
|
|
4512
|
+
requiredSats,
|
|
4513
|
+
maxTokens,
|
|
4514
|
+
headers: requestHeaders,
|
|
4515
|
+
baseHeaders
|
|
4516
|
+
});
|
|
4517
|
+
if (!response.body) {
|
|
4518
|
+
throw new Error("Response body is not available");
|
|
4519
|
+
}
|
|
4520
|
+
if (response.status === 200) {
|
|
4521
|
+
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
4522
|
+
const streamingResult = await this.streamProcessor.process(
|
|
4523
|
+
response,
|
|
4524
|
+
{
|
|
4525
|
+
onContent: callbacks.onStreamingUpdate,
|
|
4526
|
+
onThinking: callbacks.onThinkingUpdate
|
|
4527
|
+
},
|
|
4528
|
+
selectedModel.id
|
|
4529
|
+
);
|
|
4530
|
+
if (streamingResult.finish_reason === "content_filter") {
|
|
4531
|
+
callbacks.onMessageAppend({
|
|
4532
|
+
role: "assistant",
|
|
4533
|
+
content: "Your request was denied due to content filtering."
|
|
4534
|
+
});
|
|
4535
|
+
} else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
|
|
4536
|
+
const message = await this._createAssistantMessage(streamingResult);
|
|
4537
|
+
callbacks.onMessageAppend(message);
|
|
4538
|
+
} else {
|
|
4539
|
+
callbacks.onMessageAppend({
|
|
4540
|
+
role: "system",
|
|
4541
|
+
content: "The provider did not respond to this request."
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
callbacks.onStreamingUpdate("");
|
|
4545
|
+
callbacks.onThinkingUpdate("");
|
|
4546
|
+
const isApikeysEstimate = this.mode === "apikeys";
|
|
4547
|
+
let satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
4548
|
+
token,
|
|
4549
|
+
baseUrl: baseUrlUsed,
|
|
4550
|
+
initialTokenBalance: tokenBalanceInSats,
|
|
4551
|
+
fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
|
|
4552
|
+
response,
|
|
4553
|
+
modelId: selectedModel.id,
|
|
4554
|
+
usage: streamingResult.usage ? {
|
|
4555
|
+
promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
|
|
4556
|
+
completionTokens: Number(
|
|
4557
|
+
streamingResult.usage.completion_tokens ?? 0
|
|
4558
|
+
),
|
|
4559
|
+
totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
|
|
4560
|
+
cost: Number(streamingResult.usage.cost ?? 0),
|
|
4561
|
+
satsCost: Number(streamingResult.usage.sats_cost ?? 0)
|
|
4562
|
+
} : void 0,
|
|
4563
|
+
requestId: streamingResult.responseId
|
|
4564
|
+
});
|
|
4565
|
+
const estimatedCosts = this._getEstimatedCosts(
|
|
4566
|
+
selectedModel,
|
|
4567
|
+
streamingResult
|
|
4568
|
+
);
|
|
4569
|
+
const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
|
|
4570
|
+
onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
|
|
4571
|
+
} else {
|
|
4572
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
4573
|
+
}
|
|
4574
|
+
} catch (error) {
|
|
4575
|
+
this._handleError(error, callbacks);
|
|
4576
|
+
} finally {
|
|
4577
|
+
callbacks.onPaymentProcessing?.(false);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
/**
|
|
4581
|
+
* Make the API request with failover support
|
|
4582
|
+
*/
|
|
4583
|
+
async _makeRequest(params) {
|
|
4584
|
+
const { path, method, body, baseUrl, token, headers } = params;
|
|
4585
|
+
try {
|
|
4586
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
4587
|
+
if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
|
|
4588
|
+
this._log("DEBUG", "HEADERS,", headers);
|
|
4589
|
+
const response = await fetch(url, {
|
|
4590
|
+
method,
|
|
4591
|
+
headers,
|
|
4592
|
+
body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
|
|
4593
|
+
});
|
|
4594
|
+
if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
|
|
4595
|
+
response.baseUrl = baseUrl;
|
|
4596
|
+
response.token = token;
|
|
4597
|
+
if (!response.ok) {
|
|
4598
|
+
const requestId = response.headers.get("x-routstr-request-id") || void 0;
|
|
4599
|
+
let bodyText;
|
|
4600
|
+
try {
|
|
4601
|
+
bodyText = await response.text();
|
|
4602
|
+
} catch (e) {
|
|
4603
|
+
bodyText = void 0;
|
|
4604
|
+
}
|
|
4605
|
+
return await this._handleErrorResponse(
|
|
4606
|
+
params,
|
|
4607
|
+
token,
|
|
4608
|
+
response.status,
|
|
4609
|
+
requestId,
|
|
4610
|
+
this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
|
|
4611
|
+
bodyText,
|
|
4612
|
+
params.retryCount ?? 0
|
|
4613
|
+
);
|
|
4614
|
+
}
|
|
4615
|
+
return response;
|
|
4616
|
+
} catch (error) {
|
|
4617
|
+
if (isNetworkErrorMessage(error?.message || "")) {
|
|
4618
|
+
return await this._handleErrorResponse(
|
|
4619
|
+
params,
|
|
4620
|
+
token,
|
|
4621
|
+
-1,
|
|
4622
|
+
// just for Network Error to skip all statuses
|
|
4623
|
+
void 0,
|
|
4624
|
+
void 0,
|
|
4625
|
+
void 0,
|
|
4626
|
+
params.retryCount ?? 0
|
|
4627
|
+
);
|
|
4628
|
+
}
|
|
4629
|
+
throw error;
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Handle error responses with failover
|
|
4634
|
+
*/
|
|
4635
|
+
async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
|
|
4636
|
+
const MAX_RETRIES_PER_PROVIDER = 2;
|
|
4637
|
+
const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
|
|
4638
|
+
let tryNextProvider = false;
|
|
4639
|
+
this._log(
|
|
4640
|
+
"DEBUG",
|
|
4641
|
+
`[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
|
|
4642
|
+
);
|
|
4643
|
+
this._log(
|
|
4644
|
+
"DEBUG",
|
|
4645
|
+
`[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
|
|
4646
|
+
);
|
|
4647
|
+
if (params.token.startsWith("cashu")) {
|
|
4648
|
+
const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
|
|
4649
|
+
params.token
|
|
4650
|
+
);
|
|
4651
|
+
if (tryReceiveTokenResult.success) {
|
|
4652
|
+
this._log(
|
|
4653
|
+
"DEBUG",
|
|
4654
|
+
`[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
|
|
4655
|
+
);
|
|
4656
|
+
tryNextProvider = true;
|
|
4657
|
+
if (this.mode === "lazyrefund")
|
|
4658
|
+
this.storageAdapter.removeToken(baseUrl);
|
|
4659
|
+
} else {
|
|
4660
|
+
this._log(
|
|
4661
|
+
"DEBUG",
|
|
4662
|
+
`[RoutstrClient] _handleErrorResponse: Failed to receive token. `
|
|
4663
|
+
);
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
if (this.mode === "xcashu") {
|
|
4667
|
+
if (xCashuRefundToken) {
|
|
4668
|
+
this._log(
|
|
4669
|
+
"DEBUG",
|
|
4670
|
+
`[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
|
|
4671
|
+
);
|
|
4672
|
+
try {
|
|
4673
|
+
const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
|
|
4674
|
+
if (receiveResult.success) {
|
|
4675
|
+
this._log(
|
|
4676
|
+
"DEBUG",
|
|
4677
|
+
`[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
|
|
4678
|
+
);
|
|
4679
|
+
tryNextProvider = true;
|
|
4680
|
+
} else
|
|
4681
|
+
throw new ProviderError(
|
|
4682
|
+
baseUrl,
|
|
4683
|
+
status,
|
|
4684
|
+
"xcashu refund failed",
|
|
4685
|
+
requestId
|
|
4686
|
+
);
|
|
4687
|
+
} catch (error) {
|
|
4688
|
+
this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
|
|
4689
|
+
throw new ProviderError(
|
|
4690
|
+
baseUrl,
|
|
4691
|
+
status,
|
|
4692
|
+
"[xcashu] Failed to receive refund token",
|
|
4693
|
+
requestId
|
|
4694
|
+
);
|
|
4695
|
+
}
|
|
4696
|
+
} else {
|
|
4697
|
+
if (!tryNextProvider)
|
|
4698
|
+
throw new ProviderError(
|
|
4699
|
+
baseUrl,
|
|
4700
|
+
status,
|
|
4701
|
+
"[xcashu] Failed to receive refund token",
|
|
4702
|
+
requestId
|
|
4703
|
+
);
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
|
|
4707
|
+
this.storageAdapter.getApiKey(baseUrl);
|
|
4708
|
+
let topupAmount = params.requiredSats;
|
|
4709
|
+
try {
|
|
4710
|
+
let currentBalance = 0;
|
|
4711
|
+
if (this.mode === "apikeys") {
|
|
4712
|
+
const currentBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4713
|
+
params.token,
|
|
4714
|
+
baseUrl
|
|
4715
|
+
);
|
|
4716
|
+
currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
|
|
4717
|
+
} else if (this.mode === "lazyrefund") {
|
|
4718
|
+
const distribution = this.storageAdapter.getCachedTokenDistribution();
|
|
4719
|
+
const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
|
|
4720
|
+
currentBalance = tokenEntry?.amount ?? 0;
|
|
4721
|
+
}
|
|
4722
|
+
const shortfall = Math.max(0, params.requiredSats - currentBalance);
|
|
4723
|
+
topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
|
|
4724
|
+
} catch (e) {
|
|
4725
|
+
this._log(
|
|
4726
|
+
"WARN",
|
|
4727
|
+
"Could not get current token balance for topup calculation:",
|
|
4728
|
+
e
|
|
4729
|
+
);
|
|
4730
|
+
}
|
|
4731
|
+
const topupResult = await this.balanceManager.topUp({
|
|
4732
|
+
mintUrl,
|
|
4733
|
+
baseUrl,
|
|
4734
|
+
amount: topupAmount * TOPUP_MARGIN,
|
|
4735
|
+
token: params.token
|
|
4736
|
+
});
|
|
4737
|
+
this._log(
|
|
4738
|
+
"DEBUG",
|
|
4739
|
+
`[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
|
|
4740
|
+
);
|
|
4741
|
+
if (!topupResult.success) {
|
|
4742
|
+
const message = topupResult.message || "";
|
|
4743
|
+
if (message.includes("Insufficient balance")) {
|
|
4744
|
+
const needMatch = message.match(/need (\d+)/);
|
|
4745
|
+
const haveMatch = message.match(/have (\d+)/);
|
|
4746
|
+
const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
|
|
4747
|
+
const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
|
|
4748
|
+
this._log(
|
|
4749
|
+
"DEBUG",
|
|
4750
|
+
`[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
|
|
4751
|
+
);
|
|
4752
|
+
throw new InsufficientBalanceError(
|
|
4753
|
+
required,
|
|
4754
|
+
available,
|
|
4755
|
+
0,
|
|
4756
|
+
"",
|
|
4757
|
+
message
|
|
4758
|
+
);
|
|
4759
|
+
} else {
|
|
4760
|
+
this._log(
|
|
4761
|
+
"DEBUG",
|
|
4762
|
+
`[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
|
|
4763
|
+
);
|
|
4764
|
+
tryNextProvider = true;
|
|
4765
|
+
}
|
|
4766
|
+
} else {
|
|
4767
|
+
this._log(
|
|
4768
|
+
"DEBUG",
|
|
4769
|
+
`[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
|
|
4770
|
+
);
|
|
4771
|
+
}
|
|
4772
|
+
if (!tryNextProvider) {
|
|
4773
|
+
if (retryCount < MAX_RETRIES_PER_PROVIDER) {
|
|
4774
|
+
this._log(
|
|
4775
|
+
"DEBUG",
|
|
4776
|
+
`[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
|
|
4777
|
+
);
|
|
4778
|
+
return this._makeRequest({
|
|
4779
|
+
...params,
|
|
4780
|
+
token: params.token,
|
|
4781
|
+
headers: this._withAuthHeader(params.baseHeaders, params.token),
|
|
4782
|
+
retryCount: retryCount + 1
|
|
4783
|
+
});
|
|
4784
|
+
} else {
|
|
4785
|
+
this._log(
|
|
4786
|
+
"DEBUG",
|
|
4787
|
+
`[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
|
|
4788
|
+
);
|
|
4789
|
+
tryNextProvider = true;
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4793
|
+
const isInsufficientBalance413 = status === 413 && responseBody?.includes("Insufficient balance");
|
|
4794
|
+
if (isInsufficientBalance413 && !tryNextProvider && this.mode === "apikeys") {
|
|
4795
|
+
let retryToken = params.token;
|
|
4796
|
+
try {
|
|
4797
|
+
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
4798
|
+
params.token,
|
|
4799
|
+
baseUrl
|
|
4800
|
+
);
|
|
4801
|
+
if (latestBalanceInfo.isInvalidApiKey) {
|
|
4802
|
+
this._log(
|
|
4803
|
+
"DEBUG",
|
|
4804
|
+
`[RoutstrClient] _handleErrorResponse: Invalid API key (proofs already spent), removing for ${baseUrl}`
|
|
4805
|
+
);
|
|
4806
|
+
this.storageAdapter.removeApiKey(baseUrl);
|
|
4807
|
+
tryNextProvider = true;
|
|
4808
|
+
} else {
|
|
4809
|
+
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
4810
|
+
if (latestBalanceInfo.apiKey) {
|
|
4811
|
+
const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
4812
|
+
if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
|
|
4813
|
+
if (storedApiKeyEntry) {
|
|
4814
|
+
this.storageAdapter.removeApiKey(baseUrl);
|
|
4815
|
+
}
|
|
4816
|
+
this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
|
|
4817
|
+
}
|
|
4818
|
+
retryToken = latestBalanceInfo.apiKey;
|
|
4819
|
+
}
|
|
4820
|
+
if (latestTokenBalance >= 0) {
|
|
4821
|
+
this.storageAdapter.updateApiKeyBalance(
|
|
4822
|
+
baseUrl,
|
|
4823
|
+
latestTokenBalance
|
|
4824
|
+
);
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
} catch (error) {
|
|
4828
|
+
this._log(
|
|
4829
|
+
"WARN",
|
|
4830
|
+
`[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
|
|
4831
|
+
error
|
|
4832
|
+
);
|
|
4833
|
+
}
|
|
4834
|
+
if (retryCount < MAX_RETRIES_PER_PROVIDER) {
|
|
4835
|
+
this._log(
|
|
4836
|
+
"DEBUG",
|
|
4837
|
+
`[RoutstrClient] _handleErrorResponse: Retrying 413 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
|
|
4838
|
+
);
|
|
4839
|
+
return this._makeRequest({
|
|
4840
|
+
...params,
|
|
4841
|
+
token: retryToken,
|
|
4842
|
+
headers: this._withAuthHeader(params.baseHeaders, retryToken),
|
|
4843
|
+
retryCount: retryCount + 1
|
|
4844
|
+
});
|
|
4845
|
+
} else {
|
|
4846
|
+
this._log(
|
|
4847
|
+
"DEBUG",
|
|
4848
|
+
`[RoutstrClient] _handleErrorResponse: 413 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
|
|
4849
|
+
);
|
|
4850
|
+
tryNextProvider = true;
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
|
|
4854
|
+
this._log(
|
|
4855
|
+
"DEBUG",
|
|
4856
|
+
`[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`
|
|
4857
|
+
);
|
|
4858
|
+
if (this.mode === "lazyrefund") {
|
|
4859
|
+
try {
|
|
4860
|
+
const refundResult = await this.balanceManager.refund({
|
|
4861
|
+
mintUrl,
|
|
4862
|
+
baseUrl,
|
|
4863
|
+
token: params.token
|
|
4864
|
+
});
|
|
4865
|
+
this._log(
|
|
4866
|
+
"DEBUG",
|
|
4867
|
+
`[RoutstrClient] _handleErrorResponse: Lazyrefund result: success=${refundResult.success}`
|
|
4868
|
+
);
|
|
4869
|
+
if (refundResult.success) this.storageAdapter.removeToken(baseUrl);
|
|
4870
|
+
else
|
|
4871
|
+
throw new ProviderError(
|
|
4872
|
+
baseUrl,
|
|
4873
|
+
status,
|
|
4874
|
+
"refund failed",
|
|
4875
|
+
requestId
|
|
4876
|
+
);
|
|
4877
|
+
} catch (error) {
|
|
4878
|
+
throw new ProviderError(
|
|
4879
|
+
baseUrl,
|
|
4880
|
+
status,
|
|
4881
|
+
"Failed to refund token",
|
|
4882
|
+
requestId
|
|
4883
|
+
);
|
|
4884
|
+
}
|
|
4885
|
+
} else if (this.mode === "apikeys") {
|
|
4886
|
+
this._log(
|
|
4887
|
+
"DEBUG",
|
|
4888
|
+
`[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
|
|
4889
|
+
);
|
|
4890
|
+
const initialBalance = await this.balanceManager.getTokenBalance(
|
|
4891
|
+
token,
|
|
4892
|
+
baseUrl
|
|
4893
|
+
);
|
|
4894
|
+
this._log(
|
|
4895
|
+
"DEBUG",
|
|
4896
|
+
`[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`
|
|
4897
|
+
);
|
|
4898
|
+
const refundResult = await this.balanceManager.refundApiKey({
|
|
4899
|
+
mintUrl,
|
|
4900
|
+
baseUrl,
|
|
4901
|
+
apiKey: token
|
|
4902
|
+
});
|
|
4903
|
+
this._log(
|
|
4904
|
+
"DEBUG",
|
|
4905
|
+
`[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
|
|
4906
|
+
);
|
|
4907
|
+
if (!refundResult.success && initialBalance.amount > 0) {
|
|
4908
|
+
throw new ProviderError(
|
|
4909
|
+
baseUrl,
|
|
4910
|
+
status,
|
|
4911
|
+
refundResult.message ?? "Unknown error"
|
|
4912
|
+
);
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
this.providerManager.markFailed(baseUrl);
|
|
4917
|
+
this._log(
|
|
4918
|
+
"DEBUG",
|
|
4919
|
+
`[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
|
|
4920
|
+
);
|
|
4921
|
+
if (!selectedModel) {
|
|
4922
|
+
throw new ProviderError(
|
|
4923
|
+
baseUrl,
|
|
4924
|
+
status,
|
|
4925
|
+
"Funny, no selected model. HMM. "
|
|
4926
|
+
);
|
|
4927
|
+
}
|
|
4928
|
+
const nextProvider = this.providerManager.findNextBestProvider(
|
|
4929
|
+
selectedModel.id,
|
|
4930
|
+
baseUrl
|
|
4931
|
+
);
|
|
4932
|
+
if (nextProvider) {
|
|
4933
|
+
this._log(
|
|
4934
|
+
"DEBUG",
|
|
4935
|
+
`[RoutstrClient] _handleErrorResponse: Failing over to next provider: ${nextProvider}, model: ${selectedModel.id}`
|
|
4936
|
+
);
|
|
4937
|
+
const newModel = await this.providerManager.getModelForProvider(
|
|
4938
|
+
nextProvider,
|
|
4939
|
+
selectedModel.id
|
|
4940
|
+
) ?? selectedModel;
|
|
4941
|
+
const messagesForPricing = Array.isArray(
|
|
4942
|
+
body?.messages
|
|
4943
|
+
) ? body.messages : [];
|
|
4944
|
+
const newRequiredSats = this.providerManager.getRequiredSatsForModel(
|
|
4945
|
+
newModel,
|
|
4946
|
+
messagesForPricing,
|
|
4947
|
+
params.maxTokens
|
|
4948
|
+
);
|
|
4949
|
+
this._log(
|
|
4950
|
+
"DEBUG",
|
|
4951
|
+
`[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
|
|
4952
|
+
);
|
|
4953
|
+
const spendResult = await this._spendToken({
|
|
4954
|
+
mintUrl,
|
|
4955
|
+
amount: newRequiredSats,
|
|
4956
|
+
baseUrl: nextProvider
|
|
4957
|
+
});
|
|
4958
|
+
return this._makeRequest({
|
|
4959
|
+
...params,
|
|
4960
|
+
path,
|
|
4961
|
+
method,
|
|
4962
|
+
body,
|
|
4963
|
+
baseUrl: nextProvider,
|
|
4964
|
+
selectedModel: newModel,
|
|
4965
|
+
token: spendResult.token,
|
|
4966
|
+
requiredSats: newRequiredSats,
|
|
4967
|
+
headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
|
|
4968
|
+
retryCount: 0
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4971
|
+
throw new FailoverError(
|
|
4972
|
+
baseUrl,
|
|
4973
|
+
Array.from(this.providerManager.getFailedProviders())
|
|
4974
|
+
);
|
|
4975
|
+
}
|
|
4976
|
+
/**
|
|
4977
|
+
* Handle post-response balance update for all modes
|
|
4978
|
+
*/
|
|
4979
|
+
async _handlePostResponseBalanceUpdate(params) {
|
|
4980
|
+
const {
|
|
4981
|
+
token,
|
|
4982
|
+
baseUrl,
|
|
4983
|
+
initialTokenBalance,
|
|
4984
|
+
fallbackSatsSpent,
|
|
4985
|
+
response,
|
|
4986
|
+
modelId,
|
|
4987
|
+
usage,
|
|
4988
|
+
requestId
|
|
4989
|
+
} = params;
|
|
4990
|
+
let satsSpent = initialTokenBalance;
|
|
4991
|
+
if (this.mode === "xcashu" && response) {
|
|
4992
|
+
const refundToken = response.headers.get("x-cashu") ?? void 0;
|
|
4993
|
+
if (refundToken) {
|
|
4994
|
+
try {
|
|
4995
|
+
const receiveResult = await this.cashuSpender.receiveToken(refundToken);
|
|
4996
|
+
satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
|
|
4997
|
+
} catch (error) {
|
|
4998
|
+
this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
} else if (this.mode === "lazyrefund") {
|
|
5002
|
+
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
5003
|
+
token,
|
|
5004
|
+
baseUrl
|
|
5005
|
+
);
|
|
5006
|
+
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
5007
|
+
this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
|
|
5008
|
+
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
5009
|
+
} else if (this.mode === "apikeys") {
|
|
5010
|
+
try {
|
|
5011
|
+
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
5012
|
+
token,
|
|
5013
|
+
baseUrl
|
|
5014
|
+
);
|
|
5015
|
+
this._log(
|
|
5016
|
+
"DEBUG",
|
|
5017
|
+
"LATEST Balance",
|
|
5018
|
+
latestBalanceInfo.amount,
|
|
5019
|
+
latestBalanceInfo.reserved,
|
|
5020
|
+
latestBalanceInfo.apiKey,
|
|
5021
|
+
baseUrl
|
|
5022
|
+
);
|
|
5023
|
+
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
5024
|
+
const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
|
|
5025
|
+
if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
|
|
5026
|
+
this.storageAdapter.removeApiKey(baseUrl);
|
|
5027
|
+
this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
|
|
5028
|
+
}
|
|
5029
|
+
this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
|
|
5030
|
+
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
5031
|
+
} catch (e) {
|
|
5032
|
+
this._log("WARN", "Could not get updated API key balance:", e);
|
|
5033
|
+
satsSpent = fallbackSatsSpent ?? initialTokenBalance;
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
await this._trackResponseUsage({
|
|
4324
5037
|
token,
|
|
4325
|
-
|
|
4326
|
-
|
|
5038
|
+
baseUrl,
|
|
5039
|
+
response,
|
|
5040
|
+
modelId,
|
|
5041
|
+
satsSpent,
|
|
5042
|
+
usage,
|
|
5043
|
+
requestId
|
|
4327
5044
|
});
|
|
4328
|
-
|
|
4329
|
-
}
|
|
4330
|
-
|
|
4331
|
-
const
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
5045
|
+
return satsSpent;
|
|
5046
|
+
}
|
|
5047
|
+
async _trackResponseUsage(params) {
|
|
5048
|
+
const {
|
|
5049
|
+
token,
|
|
5050
|
+
baseUrl,
|
|
5051
|
+
response,
|
|
5052
|
+
modelId,
|
|
5053
|
+
satsSpent,
|
|
5054
|
+
usage: providedUsage,
|
|
5055
|
+
requestId: providedRequestId
|
|
5056
|
+
} = params;
|
|
5057
|
+
if (!response || !modelId) {
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
5060
|
+
try {
|
|
5061
|
+
let usage = providedUsage;
|
|
5062
|
+
let requestId = providedRequestId;
|
|
5063
|
+
if (!usage || !requestId) {
|
|
5064
|
+
const contentType = response.headers.get("content-type") || "";
|
|
5065
|
+
if (contentType.includes("text/event-stream")) {
|
|
5066
|
+
usage = usage ?? response.usage;
|
|
5067
|
+
requestId = requestId ?? response.requestId ?? response.headers.get("x-routstr-request-id") ?? void 0;
|
|
5068
|
+
if (!usage) {
|
|
5069
|
+
return;
|
|
5070
|
+
}
|
|
5071
|
+
} else {
|
|
5072
|
+
const cloned = response.clone();
|
|
5073
|
+
const responseBody = await cloned.json();
|
|
5074
|
+
usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
|
|
5075
|
+
requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
if (!usage) {
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
const finalRequestId = requestId || "unknown";
|
|
5082
|
+
const store = await getDefaultSdkStore();
|
|
5083
|
+
const state = store.getState();
|
|
5084
|
+
const matchingClient = state.clientIds.find(
|
|
5085
|
+
(client) => client.apiKey === token
|
|
5086
|
+
);
|
|
5087
|
+
const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
|
|
5088
|
+
const usageTracking = getDefaultUsageTrackingDriver();
|
|
5089
|
+
const entry = {
|
|
5090
|
+
id: entryId,
|
|
5091
|
+
timestamp: Date.now(),
|
|
5092
|
+
modelId,
|
|
5093
|
+
baseUrl,
|
|
5094
|
+
requestId: finalRequestId,
|
|
5095
|
+
client: matchingClient?.clientId,
|
|
5096
|
+
...usage
|
|
5097
|
+
};
|
|
5098
|
+
if (this.mode === "xcashu") {
|
|
5099
|
+
entry.satsCost = satsSpent;
|
|
5100
|
+
}
|
|
5101
|
+
await usageTracking.append(entry);
|
|
5102
|
+
} catch (error) {
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
/**
|
|
5106
|
+
* Convert messages for API format
|
|
5107
|
+
*/
|
|
5108
|
+
async _convertMessages(messages) {
|
|
5109
|
+
return Promise.all(
|
|
5110
|
+
messages.filter((m) => m.role !== "system").map(async (m) => ({
|
|
5111
|
+
role: m.role,
|
|
5112
|
+
content: typeof m.content === "string" ? m.content : m.content
|
|
5113
|
+
}))
|
|
4340
5114
|
);
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
const
|
|
4348
|
-
if (
|
|
4349
|
-
|
|
5115
|
+
}
|
|
5116
|
+
/**
|
|
5117
|
+
* Create assistant message from streaming result
|
|
5118
|
+
*/
|
|
5119
|
+
async _createAssistantMessage(result) {
|
|
5120
|
+
if (result.images && result.images.length > 0) {
|
|
5121
|
+
const content = [];
|
|
5122
|
+
if (result.content) {
|
|
5123
|
+
content.push({
|
|
5124
|
+
type: "text",
|
|
5125
|
+
text: result.content,
|
|
5126
|
+
thinking: result.thinking,
|
|
5127
|
+
citations: result.citations,
|
|
5128
|
+
annotations: result.annotations
|
|
5129
|
+
});
|
|
5130
|
+
}
|
|
5131
|
+
for (const img of result.images) {
|
|
5132
|
+
content.push({
|
|
5133
|
+
type: "image_url",
|
|
5134
|
+
image_url: {
|
|
5135
|
+
url: img.image_url.url
|
|
5136
|
+
}
|
|
5137
|
+
});
|
|
4350
5138
|
}
|
|
5139
|
+
return {
|
|
5140
|
+
role: "assistant",
|
|
5141
|
+
content
|
|
5142
|
+
};
|
|
4351
5143
|
}
|
|
4352
|
-
return
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
5144
|
+
return {
|
|
5145
|
+
role: "assistant",
|
|
5146
|
+
content: result.content || ""
|
|
5147
|
+
};
|
|
5148
|
+
}
|
|
5149
|
+
/**
|
|
5150
|
+
* Calculate estimated costs from usage
|
|
5151
|
+
*/
|
|
5152
|
+
_getEstimatedCosts(selectedModel, streamingResult) {
|
|
5153
|
+
let estimatedCosts = 0;
|
|
5154
|
+
if (streamingResult.usage) {
|
|
5155
|
+
const { completion_tokens, prompt_tokens } = streamingResult.usage;
|
|
5156
|
+
if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
|
|
5157
|
+
estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
|
|
4361
5158
|
}
|
|
4362
5159
|
}
|
|
4363
|
-
return
|
|
4364
|
-
}
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
5160
|
+
return estimatedCosts;
|
|
5161
|
+
}
|
|
5162
|
+
/**
|
|
5163
|
+
* Get pending cashu token amount
|
|
5164
|
+
*/
|
|
5165
|
+
_getPendingCashuTokenAmount() {
|
|
5166
|
+
const distribution = this.storageAdapter.getCachedTokenDistribution();
|
|
5167
|
+
return distribution.reduce((total, item) => total + item.amount, 0);
|
|
5168
|
+
}
|
|
5169
|
+
/**
|
|
5170
|
+
* Handle errors and notify callbacks
|
|
5171
|
+
*/
|
|
5172
|
+
_handleError(error, callbacks) {
|
|
5173
|
+
this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
|
|
5174
|
+
if (error instanceof Error) {
|
|
5175
|
+
const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
|
|
5176
|
+
const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
|
|
5177
|
+
this._log(
|
|
5178
|
+
"ERROR",
|
|
5179
|
+
`[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
|
|
5180
|
+
);
|
|
5181
|
+
callbacks.onMessageAppend({
|
|
5182
|
+
role: "system",
|
|
5183
|
+
content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
|
|
5184
|
+
});
|
|
5185
|
+
} else {
|
|
5186
|
+
callbacks.onMessageAppend({
|
|
5187
|
+
role: "system",
|
|
5188
|
+
content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
|
|
5189
|
+
});
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
/**
|
|
5193
|
+
* Check wallet balance and throw if insufficient
|
|
5194
|
+
*/
|
|
5195
|
+
async _checkBalance() {
|
|
5196
|
+
const balances = await this.walletAdapter.getBalances();
|
|
5197
|
+
const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
|
|
5198
|
+
if (totalBalance <= 0) {
|
|
5199
|
+
throw new InsufficientBalanceError(1, 0);
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
/**
|
|
5203
|
+
* Spend a token using CashuSpender with standardized error handling
|
|
5204
|
+
*/
|
|
5205
|
+
async _spendToken(params) {
|
|
5206
|
+
const { mintUrl, amount, baseUrl } = params;
|
|
5207
|
+
this._log(
|
|
5208
|
+
"DEBUG",
|
|
5209
|
+
`[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
|
|
4391
5210
|
);
|
|
4392
|
-
if (
|
|
4393
|
-
|
|
5211
|
+
if (this.mode === "apikeys") {
|
|
5212
|
+
let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
5213
|
+
if (!parentApiKey) {
|
|
5214
|
+
this._log(
|
|
5215
|
+
"DEBUG",
|
|
5216
|
+
`[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
|
|
5217
|
+
);
|
|
5218
|
+
const spendResult2 = await this.cashuSpender.spend({
|
|
5219
|
+
mintUrl,
|
|
5220
|
+
amount: amount * TOPUP_MARGIN,
|
|
5221
|
+
baseUrl: "",
|
|
5222
|
+
reuseToken: false
|
|
5223
|
+
});
|
|
5224
|
+
if (!spendResult2.token) {
|
|
5225
|
+
this._log(
|
|
5226
|
+
"ERROR",
|
|
5227
|
+
`[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
|
|
5228
|
+
spendResult2.error
|
|
5229
|
+
);
|
|
5230
|
+
throw new Error(
|
|
5231
|
+
`[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
|
|
5232
|
+
);
|
|
5233
|
+
} else {
|
|
5234
|
+
this._log(
|
|
5235
|
+
"DEBUG",
|
|
5236
|
+
`[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
|
|
5237
|
+
);
|
|
5238
|
+
}
|
|
5239
|
+
this._log(
|
|
5240
|
+
"DEBUG",
|
|
5241
|
+
`[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
|
|
5242
|
+
);
|
|
5243
|
+
try {
|
|
5244
|
+
this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
|
|
5245
|
+
} catch (error) {
|
|
5246
|
+
if (error instanceof Error && error.message.includes("ApiKey already exists")) {
|
|
5247
|
+
const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
|
|
5248
|
+
spendResult2.token
|
|
5249
|
+
);
|
|
5250
|
+
if (tryReceiveTokenResult.success) {
|
|
5251
|
+
this._log(
|
|
5252
|
+
"DEBUG",
|
|
5253
|
+
`[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
|
|
5254
|
+
);
|
|
5255
|
+
} else {
|
|
5256
|
+
this._log(
|
|
5257
|
+
"DEBUG",
|
|
5258
|
+
`[RoutstrClient] _handleErrorResponse: Token restore failed or not needed`
|
|
5259
|
+
);
|
|
5260
|
+
}
|
|
5261
|
+
this._log(
|
|
5262
|
+
"DEBUG",
|
|
5263
|
+
`[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
|
|
5264
|
+
);
|
|
5265
|
+
} else {
|
|
5266
|
+
throw error;
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
5270
|
+
} else {
|
|
5271
|
+
this._log(
|
|
5272
|
+
"DEBUG",
|
|
5273
|
+
`[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
|
|
5274
|
+
);
|
|
5275
|
+
}
|
|
5276
|
+
let tokenBalance = 0;
|
|
5277
|
+
let tokenBalanceUnit = "sat";
|
|
5278
|
+
const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
|
|
5279
|
+
const distributionForBaseUrl = apiKeyDistribution.find(
|
|
5280
|
+
(d) => d.baseUrl === baseUrl
|
|
5281
|
+
);
|
|
5282
|
+
if (distributionForBaseUrl) {
|
|
5283
|
+
tokenBalance = distributionForBaseUrl.amount;
|
|
5284
|
+
}
|
|
5285
|
+
if (tokenBalance === 0 && parentApiKey) {
|
|
5286
|
+
try {
|
|
5287
|
+
const balanceInfo = await this.balanceManager.getTokenBalance(
|
|
5288
|
+
parentApiKey.key,
|
|
5289
|
+
baseUrl
|
|
5290
|
+
);
|
|
5291
|
+
tokenBalance = balanceInfo.amount;
|
|
5292
|
+
tokenBalanceUnit = balanceInfo.unit;
|
|
5293
|
+
} catch (e) {
|
|
5294
|
+
this._log("WARN", "Could not get initial API key balance:", e);
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
this._log(
|
|
5298
|
+
"DEBUG",
|
|
5299
|
+
`[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
|
|
5300
|
+
);
|
|
5301
|
+
return {
|
|
5302
|
+
token: parentApiKey?.key ?? "",
|
|
5303
|
+
tokenBalance,
|
|
5304
|
+
tokenBalanceUnit
|
|
5305
|
+
};
|
|
4394
5306
|
}
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
key,
|
|
4399
|
-
balance: 0,
|
|
4400
|
-
lastUsed: Date.now()
|
|
4401
|
-
});
|
|
4402
|
-
store.getState().setApiKeys(next);
|
|
4403
|
-
},
|
|
4404
|
-
updateApiKeyBalance: (baseUrl, balance) => {
|
|
4405
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4406
|
-
const keys = store.getState().apiKeys;
|
|
4407
|
-
const next = keys.map(
|
|
4408
|
-
(entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
|
|
4409
|
-
);
|
|
4410
|
-
store.getState().setApiKeys(next);
|
|
4411
|
-
},
|
|
4412
|
-
removeApiKey: (baseUrl) => {
|
|
4413
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4414
|
-
const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
|
|
4415
|
-
store.getState().setApiKeys(next);
|
|
4416
|
-
},
|
|
4417
|
-
getAllApiKeys: () => {
|
|
4418
|
-
return store.getState().apiKeys.map((entry) => ({
|
|
4419
|
-
baseUrl: entry.baseUrl,
|
|
4420
|
-
key: entry.key,
|
|
4421
|
-
balance: entry.balance,
|
|
4422
|
-
lastUsed: entry.lastUsed
|
|
4423
|
-
}));
|
|
4424
|
-
},
|
|
4425
|
-
// ========== Child Keys ==========
|
|
4426
|
-
getChildKey: (parentBaseUrl) => {
|
|
4427
|
-
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4428
|
-
const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
|
|
4429
|
-
if (!entry) return null;
|
|
4430
|
-
return {
|
|
4431
|
-
parentBaseUrl: entry.parentBaseUrl,
|
|
4432
|
-
childKey: entry.childKey,
|
|
4433
|
-
balance: entry.balance,
|
|
4434
|
-
balanceLimit: entry.balanceLimit,
|
|
4435
|
-
validityDate: entry.validityDate,
|
|
4436
|
-
createdAt: entry.createdAt
|
|
4437
|
-
};
|
|
4438
|
-
},
|
|
4439
|
-
setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
|
|
4440
|
-
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4441
|
-
const keys = store.getState().childKeys;
|
|
4442
|
-
const existingIndex = keys.findIndex(
|
|
4443
|
-
(entry) => entry.parentBaseUrl === normalized
|
|
5307
|
+
this._log(
|
|
5308
|
+
"DEBUG",
|
|
5309
|
+
`[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
|
|
4444
5310
|
);
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
5311
|
+
const spendResult = await this.cashuSpender.spend({
|
|
5312
|
+
mintUrl,
|
|
5313
|
+
amount,
|
|
5314
|
+
baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
|
|
5315
|
+
reuseToken: this.mode === "lazyrefund"
|
|
5316
|
+
});
|
|
5317
|
+
if (!spendResult.token) {
|
|
5318
|
+
this._log(
|
|
5319
|
+
"ERROR",
|
|
5320
|
+
`[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
|
|
5321
|
+
spendResult.error
|
|
4455
5322
|
);
|
|
4456
|
-
store.getState().setChildKeys(next);
|
|
4457
5323
|
} else {
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
balance: balance ?? 0,
|
|
4463
|
-
validityDate,
|
|
4464
|
-
balanceLimit,
|
|
4465
|
-
createdAt: Date.now()
|
|
4466
|
-
});
|
|
4467
|
-
store.getState().setChildKeys(next);
|
|
4468
|
-
}
|
|
4469
|
-
},
|
|
4470
|
-
updateChildKeyBalance: (parentBaseUrl, balance) => {
|
|
4471
|
-
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4472
|
-
const keys = store.getState().childKeys;
|
|
4473
|
-
const next = keys.map(
|
|
4474
|
-
(entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
|
|
4475
|
-
);
|
|
4476
|
-
store.getState().setChildKeys(next);
|
|
4477
|
-
},
|
|
4478
|
-
removeChildKey: (parentBaseUrl) => {
|
|
4479
|
-
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4480
|
-
const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
|
|
4481
|
-
store.getState().setChildKeys(next);
|
|
4482
|
-
},
|
|
4483
|
-
getAllChildKeys: () => {
|
|
4484
|
-
return store.getState().childKeys.map((entry) => ({
|
|
4485
|
-
parentBaseUrl: entry.parentBaseUrl,
|
|
4486
|
-
childKey: entry.childKey,
|
|
4487
|
-
balance: entry.balance,
|
|
4488
|
-
balanceLimit: entry.balanceLimit,
|
|
4489
|
-
validityDate: entry.validityDate,
|
|
4490
|
-
createdAt: entry.createdAt
|
|
4491
|
-
}));
|
|
4492
|
-
},
|
|
4493
|
-
getCachedReceiveTokens: () => {
|
|
4494
|
-
return store.getState().cachedReceiveTokens;
|
|
4495
|
-
},
|
|
4496
|
-
setCachedReceiveTokens: (tokens) => {
|
|
4497
|
-
store.getState().setCachedReceiveTokens(tokens);
|
|
4498
|
-
}
|
|
4499
|
-
});
|
|
4500
|
-
var createProviderRegistryFromStore = (store) => ({
|
|
4501
|
-
getModelsForProvider: (baseUrl) => {
|
|
4502
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4503
|
-
return store.getState().modelsFromAllProviders[normalized] || [];
|
|
4504
|
-
},
|
|
4505
|
-
getDisabledProviders: () => store.getState().disabledProviders,
|
|
4506
|
-
getProviderMints: (baseUrl) => {
|
|
4507
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4508
|
-
return store.getState().mintsFromAllProviders[normalized] || [];
|
|
4509
|
-
},
|
|
4510
|
-
getProviderInfo: async (baseUrl) => {
|
|
4511
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
4512
|
-
const cached = store.getState().infoFromAllProviders[normalized];
|
|
4513
|
-
if (cached) return cached;
|
|
4514
|
-
try {
|
|
4515
|
-
const response = await fetch(`${normalized}v1/info`);
|
|
4516
|
-
if (!response.ok) {
|
|
4517
|
-
throw new Error(`Failed ${response.status}`);
|
|
4518
|
-
}
|
|
4519
|
-
const info = await response.json();
|
|
4520
|
-
const next = { ...store.getState().infoFromAllProviders };
|
|
4521
|
-
next[normalized] = info;
|
|
4522
|
-
store.getState().setInfoFromAllProviders(next);
|
|
4523
|
-
return info;
|
|
4524
|
-
} catch (error) {
|
|
4525
|
-
console.warn(`Failed to fetch provider info from ${normalized}:`, error);
|
|
4526
|
-
return null;
|
|
5324
|
+
this._log(
|
|
5325
|
+
"DEBUG",
|
|
5326
|
+
`[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
|
|
5327
|
+
);
|
|
4527
5328
|
}
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
var isBrowser2 = () => {
|
|
4534
|
-
try {
|
|
4535
|
-
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
4536
|
-
} catch {
|
|
4537
|
-
return false;
|
|
4538
|
-
}
|
|
4539
|
-
};
|
|
4540
|
-
var isNode = () => {
|
|
4541
|
-
try {
|
|
4542
|
-
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
4543
|
-
} catch {
|
|
4544
|
-
return false;
|
|
4545
|
-
}
|
|
4546
|
-
};
|
|
4547
|
-
var defaultDriver = null;
|
|
4548
|
-
var isBun2 = () => {
|
|
4549
|
-
return typeof process.versions.bun !== "undefined";
|
|
4550
|
-
};
|
|
4551
|
-
var getDefaultSdkDriver = () => {
|
|
4552
|
-
if (defaultDriver) return defaultDriver;
|
|
4553
|
-
if (isBrowser2()) {
|
|
4554
|
-
defaultDriver = localStorageDriver;
|
|
4555
|
-
return defaultDriver;
|
|
4556
|
-
}
|
|
4557
|
-
if (isBun2()) {
|
|
4558
|
-
defaultDriver = createMemoryDriver();
|
|
4559
|
-
return defaultDriver;
|
|
5329
|
+
return {
|
|
5330
|
+
token: spendResult.token,
|
|
5331
|
+
tokenBalance: spendResult.balance,
|
|
5332
|
+
tokenBalanceUnit: spendResult.unit ?? "sat"
|
|
5333
|
+
};
|
|
4560
5334
|
}
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
5335
|
+
/**
|
|
5336
|
+
* Build request headers with common defaults and dev mock controls
|
|
5337
|
+
*/
|
|
5338
|
+
_buildBaseHeaders(additionalHeaders = {}, token) {
|
|
5339
|
+
const headers = {
|
|
5340
|
+
...additionalHeaders,
|
|
5341
|
+
"Content-Type": "application/json"
|
|
5342
|
+
};
|
|
5343
|
+
return headers;
|
|
4564
5344
|
}
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
5345
|
+
/**
|
|
5346
|
+
* Attach auth headers using the active client mode
|
|
5347
|
+
*/
|
|
5348
|
+
_withAuthHeader(headers, token) {
|
|
5349
|
+
const nextHeaders = { ...headers };
|
|
5350
|
+
if (this.mode === "xcashu") {
|
|
5351
|
+
nextHeaders["X-Cashu"] = token;
|
|
5352
|
+
} else {
|
|
5353
|
+
nextHeaders["Authorization"] = `Bearer ${token}`;
|
|
5354
|
+
}
|
|
5355
|
+
return nextHeaders;
|
|
4572
5356
|
}
|
|
4573
|
-
return defaultStore.hydrate.then(() => defaultStore.store);
|
|
4574
5357
|
};
|
|
4575
|
-
var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
|
|
4576
|
-
var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
|
|
4577
|
-
var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
|
|
4578
5358
|
|
|
4579
5359
|
// routeRequests.ts
|
|
4580
|
-
async function
|
|
5360
|
+
async function resolveRouteRequestContext(options) {
|
|
4581
5361
|
const {
|
|
4582
5362
|
modelId,
|
|
4583
5363
|
requestBody,
|
|
@@ -4651,12 +5431,11 @@ async function routeRequests(options) {
|
|
|
4651
5431
|
if (!mintUrl) {
|
|
4652
5432
|
throw new Error("No mint configured in wallet");
|
|
4653
5433
|
}
|
|
4654
|
-
const alertLevel = "min";
|
|
4655
5434
|
const client = new RoutstrClient(
|
|
4656
5435
|
walletAdapter,
|
|
4657
5436
|
storageAdapter,
|
|
4658
5437
|
providerRegistry,
|
|
4659
|
-
|
|
5438
|
+
"min",
|
|
4660
5439
|
mode
|
|
4661
5440
|
);
|
|
4662
5441
|
if (debugLevel) {
|
|
@@ -4664,18 +5443,27 @@ async function routeRequests(options) {
|
|
|
4664
5443
|
}
|
|
4665
5444
|
const maxTokens = extractMaxTokens(requestBody);
|
|
4666
5445
|
const stream = extractStream(requestBody);
|
|
4667
|
-
|
|
5446
|
+
const proxiedBody = requestBody && typeof requestBody === "object" ? { ...requestBody } : {};
|
|
5447
|
+
proxiedBody.model = selectedModel.id;
|
|
5448
|
+
if (stream !== void 0) {
|
|
5449
|
+
proxiedBody.stream = stream;
|
|
5450
|
+
}
|
|
5451
|
+
if (maxTokens !== void 0) {
|
|
5452
|
+
proxiedBody.max_tokens = maxTokens;
|
|
5453
|
+
}
|
|
5454
|
+
return {
|
|
5455
|
+
client,
|
|
5456
|
+
baseUrl,
|
|
5457
|
+
mintUrl,
|
|
5458
|
+
path,
|
|
5459
|
+
modelId,
|
|
5460
|
+
proxiedBody
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5463
|
+
async function routeRequests(options) {
|
|
5464
|
+
const { client, baseUrl, mintUrl, path, modelId, proxiedBody } = await resolveRouteRequestContext(options);
|
|
4668
5465
|
try {
|
|
4669
|
-
const
|
|
4670
|
-
proxiedBody.model = selectedModel.id;
|
|
4671
|
-
if (stream !== void 0) {
|
|
4672
|
-
proxiedBody.stream = stream;
|
|
4673
|
-
}
|
|
4674
|
-
if (maxTokens !== void 0) {
|
|
4675
|
-
proxiedBody.max_tokens = maxTokens;
|
|
4676
|
-
}
|
|
4677
|
-
console.log(modelId);
|
|
4678
|
-
response = await client.routeRequest({
|
|
5466
|
+
const response = await client.routeRequest({
|
|
4679
5467
|
path,
|
|
4680
5468
|
method: "POST",
|
|
4681
5469
|
body: proxiedBody,
|
|
@@ -4694,26 +5482,43 @@ async function routeRequests(options) {
|
|
|
4694
5482
|
throw error;
|
|
4695
5483
|
}
|
|
4696
5484
|
}
|
|
5485
|
+
async function routeRequestsToNodeResponse(options) {
|
|
5486
|
+
const { res } = options;
|
|
5487
|
+
const { client, baseUrl, mintUrl, path, modelId, proxiedBody } = await resolveRouteRequestContext(options);
|
|
5488
|
+
try {
|
|
5489
|
+
await client.routeRequestToNodeResponse({
|
|
5490
|
+
path,
|
|
5491
|
+
method: "POST",
|
|
5492
|
+
body: proxiedBody,
|
|
5493
|
+
baseUrl,
|
|
5494
|
+
mintUrl,
|
|
5495
|
+
modelId,
|
|
5496
|
+
res
|
|
5497
|
+
});
|
|
5498
|
+
} catch (error) {
|
|
5499
|
+
if (error instanceof Error && (error.message.includes("401") || error.message.includes("402") || error.message.includes("403"))) {
|
|
5500
|
+
throw new Error(`Authentication failed: ${error.message}`);
|
|
5501
|
+
}
|
|
5502
|
+
throw error;
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
4697
5505
|
function extractMaxTokens(requestBody) {
|
|
4698
5506
|
if (!requestBody || typeof requestBody !== "object") {
|
|
4699
5507
|
return void 0;
|
|
4700
5508
|
}
|
|
4701
5509
|
const body = requestBody;
|
|
4702
5510
|
const maxTokens = body.max_tokens;
|
|
4703
|
-
|
|
4704
|
-
return maxTokens;
|
|
4705
|
-
}
|
|
4706
|
-
return void 0;
|
|
5511
|
+
return typeof maxTokens === "number" ? maxTokens : void 0;
|
|
4707
5512
|
}
|
|
4708
5513
|
function extractStream(requestBody) {
|
|
4709
5514
|
if (!requestBody || typeof requestBody !== "object") {
|
|
4710
|
-
return
|
|
5515
|
+
return void 0;
|
|
4711
5516
|
}
|
|
4712
5517
|
const body = requestBody;
|
|
4713
5518
|
const stream = body.stream;
|
|
4714
|
-
return stream ===
|
|
5519
|
+
return typeof stream === "boolean" ? stream : void 0;
|
|
4715
5520
|
}
|
|
4716
5521
|
|
|
4717
|
-
export { BalanceManager, CashuSpender, FailoverError, InsufficientBalanceError, MintDiscovery, MintDiscoveryError, MintUnreachableError, ModelManager, ModelNotFoundError, NoProvidersAvailableError, ProviderBootstrapError, ProviderError, ProviderManager, RoutstrClient, SDK_STORAGE_KEYS, StreamProcessor, StreamingError, TokenOperationError, createDiscoveryAdapterFromStore, createIndexedDBDriver, createMemoryDriver, createProviderRegistryFromStore, createSdkStore, createSqliteDriver, createStorageAdapterFromStore, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getProviderEndpoints, isOnionUrl, isTorContext, localStorageDriver, normalizeProviderUrl, routeRequests };
|
|
5522
|
+
export { BalanceManager, CashuSpender, FailoverError, InsufficientBalanceError, MintDiscovery, MintDiscoveryError, MintUnreachableError, ModelManager, ModelNotFoundError, NoProvidersAvailableError, ProviderBootstrapError, ProviderError, ProviderManager, RoutstrClient, SDK_STORAGE_KEYS, StreamProcessor, StreamingError, TokenOperationError, createDiscoveryAdapterFromStore, createIndexedDBDriver, createIndexedDBUsageTrackingDriver, createMemoryDriver, createMemoryUsageTrackingDriver, createProviderRegistryFromStore, createSSEParserTransform, createSdkStore, createSqliteDriver, createSqliteUsageTrackingDriver, createStorageAdapterFromStore, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, isOnionUrl, isTorContext, localStorageDriver, normalizeProviderUrl, routeRequests, routeRequestsToNodeResponse };
|
|
4718
5523
|
//# sourceMappingURL=index.mjs.map
|
|
4719
5524
|
//# sourceMappingURL=index.mjs.map
|