@tokenbuddy/tokenbuddy 1.0.37 → 1.0.39
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/dist/src/buyer-store.d.ts +1 -1
- package/dist/src/buyer-store.js +3 -3
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +66 -5
- package/dist/src/daemon.d.ts +23 -5
- package/dist/src/daemon.js +606 -9
- package/dist/src/provider-install.d.ts +3 -0
- package/dist/src/provider-install.js +506 -85
- package/dist/src/workdir.d.ts +10 -0
- package/dist/src/workdir.js +26 -0
- package/package.json +2 -2
- package/static/ui/assets/index-BAwWDK4H.js +271 -0
- package/static/ui/assets/index-DM9SnAfj.css +1 -0
- package/static/ui/index.html +2 -2
- package/static/ui/assets/index-Djfl9tw5.js +0 -271
- package/static/ui/assets/index-DkfztCkn.css +0 -1
package/dist/src/daemon.js
CHANGED
|
@@ -34,6 +34,9 @@ const CLAWTIP_STATIC_ROUTE = "/static/clawtip";
|
|
|
34
34
|
const CLAWTIP_RECHARGE_QR_FILE = "recharge.png";
|
|
35
35
|
const MANUAL_PROVIDER_SECRET_CONFIG_KEY = "manual-provider-secrets";
|
|
36
36
|
const USER_INFERENCE_TEST_TIMEOUT_MS = 60_000;
|
|
37
|
+
const MANUAL_PROVIDER_PROBE_TIMEOUT_MS = 30_000;
|
|
38
|
+
const RECONNECT_OPENCODE_PROTOCOLS = ["chat_completions", "responses", "messages"];
|
|
39
|
+
const RECONNECT_PROVIDER_IDS = ["codex", "claude-code", "claude-desktop", "openclaw", "opencode", "hermes"];
|
|
37
40
|
class BuyerPaymentSetupError extends Error {
|
|
38
41
|
errorCode;
|
|
39
42
|
statusCode;
|
|
@@ -143,6 +146,39 @@ function nonNegativeIntegerField(value) {
|
|
|
143
146
|
function nonNegativeFiniteField(value) {
|
|
144
147
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
145
148
|
}
|
|
149
|
+
function nonNegativeUsdMicrosField(value) {
|
|
150
|
+
if (typeof value === "number") {
|
|
151
|
+
return Number.isFinite(value) && value >= 0 ? Math.round(value * 1_000_000) : undefined;
|
|
152
|
+
}
|
|
153
|
+
if (typeof value !== "string") {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const normalized = value.trim().replace(/^\$/, "");
|
|
157
|
+
if (!normalized) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const parsed = Number(normalized);
|
|
161
|
+
return Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed * 1_000_000) : undefined;
|
|
162
|
+
}
|
|
163
|
+
function extractUpstreamCostMicros(data, usage) {
|
|
164
|
+
const costDetails = usageRecord(data?.cost_details) ?? usageRecord(usage?.cost_details);
|
|
165
|
+
return nonNegativeIntegerField(usage?.cost_micros)
|
|
166
|
+
?? nonNegativeIntegerField(usage?.costMicros)
|
|
167
|
+
?? nonNegativeIntegerField(data?.cost_micros)
|
|
168
|
+
?? nonNegativeIntegerField(data?.costMicros)
|
|
169
|
+
?? nonNegativeUsdMicrosField(usage?.cost)
|
|
170
|
+
?? nonNegativeUsdMicrosField(usage?.total_cost)
|
|
171
|
+
?? nonNegativeUsdMicrosField(usage?.totalCost)
|
|
172
|
+
?? nonNegativeUsdMicrosField(usage?.cost_usd)
|
|
173
|
+
?? nonNegativeUsdMicrosField(usage?.costUsd)
|
|
174
|
+
?? nonNegativeUsdMicrosField(data?.cost)
|
|
175
|
+
?? nonNegativeUsdMicrosField(data?.total_cost)
|
|
176
|
+
?? nonNegativeUsdMicrosField(data?.totalCost)
|
|
177
|
+
?? nonNegativeUsdMicrosField(data?.cost_usd)
|
|
178
|
+
?? nonNegativeUsdMicrosField(data?.costUsd)
|
|
179
|
+
?? nonNegativeUsdMicrosField(costDetails?.upstream_inference_cost)
|
|
180
|
+
?? nonNegativeUsdMicrosField(costDetails?.upstreamInferenceCost);
|
|
181
|
+
}
|
|
146
182
|
function usageRecord(value) {
|
|
147
183
|
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
148
184
|
}
|
|
@@ -625,8 +661,19 @@ export class TokenbuddyDaemon {
|
|
|
625
661
|
const fetchBootstrap = this.config.clawtipBootstrapFetcher || fetchClawtipBootstrap;
|
|
626
662
|
const bootstrap = await fetchBootstrap(bootstrapUrl);
|
|
627
663
|
const payment = normalizeClawtipActivationPayment(bootstrap);
|
|
628
|
-
|
|
629
|
-
|
|
664
|
+
let activation;
|
|
665
|
+
try {
|
|
666
|
+
activation = await this.startClawtipActivationWithRebindFallback(payment);
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
if (isClawtipBalanceInsufficientError(error)) {
|
|
670
|
+
logger.info("control.payment.clawtip.activation_recharge_required", "ClawTip activation returned recharge QR after insufficient balance", {
|
|
671
|
+
failureReason: error instanceof Error ? error.message : String(error)
|
|
672
|
+
});
|
|
673
|
+
return this.clawtipRechargeQr();
|
|
674
|
+
}
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
630
677
|
if (!activation.parsedOutput.mediaPath) {
|
|
631
678
|
throw new Error("ClawTip activation did not return a QR image.");
|
|
632
679
|
}
|
|
@@ -679,6 +726,34 @@ export class TokenbuddyDaemon {
|
|
|
679
726
|
payCredentialWritten: Boolean(activation.payCredential)
|
|
680
727
|
};
|
|
681
728
|
}
|
|
729
|
+
async startClawtipActivationWithRebindFallback(payment) {
|
|
730
|
+
const startBootstrap = this.config.clawtipWalletBootstrapStarter || startClawtipWalletBootstrap;
|
|
731
|
+
try {
|
|
732
|
+
return await startBootstrap(payment, { home: this.config.clawtipHomeDir });
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
if (isClawtipBalanceInsufficientError(error)) {
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
const walletConfig = inspectOpenClawWalletConfig(this.config.clawtipHomeDir);
|
|
739
|
+
if (!walletConfig.exists) {
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
const backupPath = backupOpenClawWalletConfigForRebind(walletConfig.expectedPath);
|
|
743
|
+
logger.info("control.payment.clawtip.rebind_retry", "ClawTip activation retrying with wallet rebind QR", {
|
|
744
|
+
walletConfigPath: walletConfig.expectedPath,
|
|
745
|
+
backupPath,
|
|
746
|
+
failureReason: error instanceof Error ? error.message : String(error)
|
|
747
|
+
});
|
|
748
|
+
try {
|
|
749
|
+
return await startBootstrap(payment, { home: this.config.clawtipHomeDir });
|
|
750
|
+
}
|
|
751
|
+
catch (retryError) {
|
|
752
|
+
restoreOpenClawWalletConfigBackup(walletConfig.expectedPath, backupPath);
|
|
753
|
+
throw retryError;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
682
757
|
scheduleClawtipActivationWait(clawtipId) {
|
|
683
758
|
if (this.clawtipActivationWait) {
|
|
684
759
|
return;
|
|
@@ -1107,6 +1182,322 @@ export class TokenbuddyDaemon {
|
|
|
1107
1182
|
}
|
|
1108
1183
|
};
|
|
1109
1184
|
}
|
|
1185
|
+
isReconnectProviderId(value) {
|
|
1186
|
+
return typeof value === "string" && RECONNECT_PROVIDER_IDS.includes(value);
|
|
1187
|
+
}
|
|
1188
|
+
uniqueModelIds(modelIds) {
|
|
1189
|
+
const seen = new Set();
|
|
1190
|
+
const output = [];
|
|
1191
|
+
for (const modelId of modelIds) {
|
|
1192
|
+
const trimmed = modelId.trim();
|
|
1193
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
seen.add(trimmed);
|
|
1197
|
+
output.push(trimmed);
|
|
1198
|
+
}
|
|
1199
|
+
return output;
|
|
1200
|
+
}
|
|
1201
|
+
reconnectProtocolForProvider(providerId) {
|
|
1202
|
+
if (providerId === "codex") {
|
|
1203
|
+
return "responses";
|
|
1204
|
+
}
|
|
1205
|
+
if (providerId === "claude-code" || providerId === "claude-desktop") {
|
|
1206
|
+
return "messages";
|
|
1207
|
+
}
|
|
1208
|
+
return "chat_completions";
|
|
1209
|
+
}
|
|
1210
|
+
reconnectProtocolsForProvider(providerId) {
|
|
1211
|
+
if (providerId === "opencode") {
|
|
1212
|
+
return [...RECONNECT_OPENCODE_PROTOCOLS];
|
|
1213
|
+
}
|
|
1214
|
+
return [this.reconnectProtocolForProvider(providerId)];
|
|
1215
|
+
}
|
|
1216
|
+
catalogModelSupportsProtocol(model, protocol) {
|
|
1217
|
+
if (protocol === "messages") {
|
|
1218
|
+
return model.supportedProtocols.includes("messages") || model.supportedProtocols.includes("anthropic_messages");
|
|
1219
|
+
}
|
|
1220
|
+
return model.supportedProtocols.includes(protocol);
|
|
1221
|
+
}
|
|
1222
|
+
catalogModelsByProtocol(models, protocols) {
|
|
1223
|
+
const result = {};
|
|
1224
|
+
for (const protocol of protocols) {
|
|
1225
|
+
const modelIds = this.uniqueModelIds(models
|
|
1226
|
+
.filter((model) => this.catalogModelSupportsProtocol(model, protocol))
|
|
1227
|
+
.map((model) => model.id));
|
|
1228
|
+
if (modelIds.length > 0) {
|
|
1229
|
+
result[protocol] = modelIds;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return result;
|
|
1233
|
+
}
|
|
1234
|
+
previousProviderDefaultModel(providerId) {
|
|
1235
|
+
const runtime = this.tokenStore.getProviderRuntimeConfig(providerId)?.config;
|
|
1236
|
+
if (!runtime) {
|
|
1237
|
+
return undefined;
|
|
1238
|
+
}
|
|
1239
|
+
if (runtime.selectionKind === "claude-role-mapping") {
|
|
1240
|
+
return runtime.fallbackModel ||
|
|
1241
|
+
runtime.roles.sonnet?.upstreamModel ||
|
|
1242
|
+
runtime.roles.opus?.upstreamModel ||
|
|
1243
|
+
runtime.roles.haiku?.upstreamModel;
|
|
1244
|
+
}
|
|
1245
|
+
return runtime.defaultModel;
|
|
1246
|
+
}
|
|
1247
|
+
pickReconnectDefaultModel(providerId, models) {
|
|
1248
|
+
const protocols = this.reconnectProtocolsForProvider(providerId);
|
|
1249
|
+
const compatible = this.uniqueModelIds(models
|
|
1250
|
+
.filter((model) => protocols.some((protocol) => this.catalogModelSupportsProtocol(model, protocol)))
|
|
1251
|
+
.map((model) => model.id));
|
|
1252
|
+
if (compatible.length === 0) {
|
|
1253
|
+
return undefined;
|
|
1254
|
+
}
|
|
1255
|
+
const previous = this.previousProviderDefaultModel(providerId);
|
|
1256
|
+
if (previous && compatible.includes(previous)) {
|
|
1257
|
+
return previous;
|
|
1258
|
+
}
|
|
1259
|
+
return compatible.find((model) => model === "gpt-5.4") ||
|
|
1260
|
+
compatible.find((model) => model.toLowerCase().includes("gpt")) ||
|
|
1261
|
+
compatible[0];
|
|
1262
|
+
}
|
|
1263
|
+
reconnectProviderSelection(providerId, models) {
|
|
1264
|
+
const defaultModel = this.pickReconnectDefaultModel(providerId, models);
|
|
1265
|
+
if (!defaultModel) {
|
|
1266
|
+
throw new Error(`no compatible models available for ${providerId}`);
|
|
1267
|
+
}
|
|
1268
|
+
if (providerId === "claude-code") {
|
|
1269
|
+
return {
|
|
1270
|
+
selectionKind: "claude-role-mapping",
|
|
1271
|
+
protocolPreference: "messages",
|
|
1272
|
+
fallbackModel: defaultModel,
|
|
1273
|
+
roles: {
|
|
1274
|
+
sonnet: {
|
|
1275
|
+
upstreamModel: defaultModel,
|
|
1276
|
+
displayName: defaultModel,
|
|
1277
|
+
declareOneM: true
|
|
1278
|
+
},
|
|
1279
|
+
opus: {
|
|
1280
|
+
upstreamModel: defaultModel,
|
|
1281
|
+
displayName: defaultModel,
|
|
1282
|
+
declareOneM: true
|
|
1283
|
+
},
|
|
1284
|
+
haiku: {
|
|
1285
|
+
upstreamModel: defaultModel,
|
|
1286
|
+
displayName: defaultModel
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
selectionKind: "single-model",
|
|
1293
|
+
protocolPreference: this.reconnectProtocolForProvider(providerId),
|
|
1294
|
+
defaultModel,
|
|
1295
|
+
availableModelsByProtocol: this.catalogModelsByProtocol(models, this.reconnectProtocolsForProvider(providerId))
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
reconnectProviderSelections(providerIds, models) {
|
|
1299
|
+
const selections = {};
|
|
1300
|
+
for (const providerId of providerIds) {
|
|
1301
|
+
selections[providerId] = this.reconnectProviderSelection(providerId, models);
|
|
1302
|
+
}
|
|
1303
|
+
return selections;
|
|
1304
|
+
}
|
|
1305
|
+
manualProviderModelCatalog() {
|
|
1306
|
+
const manualConfig = this.currentManualProviders();
|
|
1307
|
+
const observations = new Map(this.currentManualProviderObservations().observations.map((entry) => [entry.providerId, entry]));
|
|
1308
|
+
const providers = manualConfig.routing.policy === "locked"
|
|
1309
|
+
? manualConfig.providers.filter((provider) => provider.enabled && provider.id === manualConfig.routing.lockedProviderId)
|
|
1310
|
+
: manualConfig.providers.filter((provider) => provider.enabled);
|
|
1311
|
+
return {
|
|
1312
|
+
models: providers.flatMap((provider) => provider.models.map((modelId) => ({
|
|
1313
|
+
id: modelId,
|
|
1314
|
+
sellerId: provider.id,
|
|
1315
|
+
sellerName: provider.name,
|
|
1316
|
+
sellerUrl: provider.baseUrl,
|
|
1317
|
+
supportedProtocols: provider.supportedProtocols,
|
|
1318
|
+
paymentMethods: ["provider_key"]
|
|
1319
|
+
}))),
|
|
1320
|
+
sellers: providers.map((provider) => {
|
|
1321
|
+
const observation = observations.get(provider.id);
|
|
1322
|
+
return {
|
|
1323
|
+
id: provider.id,
|
|
1324
|
+
name: provider.name,
|
|
1325
|
+
url: provider.baseUrl,
|
|
1326
|
+
status: observation?.status ?? "active",
|
|
1327
|
+
routeState: this.routeStateFromCatalogStatus(observation?.status ?? "active"),
|
|
1328
|
+
ttftMs: observation?.ttftMs,
|
|
1329
|
+
avgTokensPerSecond: observation?.avgTokensPerSecond
|
|
1330
|
+
};
|
|
1331
|
+
})
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
async discoverSellerModelCatalog() {
|
|
1335
|
+
if (this.forceRegistrySnapshotForTest && this.lastRegistrySnapshot) {
|
|
1336
|
+
const snapshot = catalogSnapshotFromRegistry(this.lastRegistrySnapshot);
|
|
1337
|
+
return {
|
|
1338
|
+
models: snapshot.models,
|
|
1339
|
+
sellers: this.sellerCatalogWithRuntimeMetrics(snapshot.sellers)
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
|
|
1344
|
+
this.lastRegistrySnapshot = catalog.registry ?? sellerCatalogResultToRegistrySnapshot(catalog);
|
|
1345
|
+
if (catalog.registry && catalog.registryJson && catalog.registryTrust) {
|
|
1346
|
+
this.saveTrustedRegistryCache({
|
|
1347
|
+
registry: catalog.registry,
|
|
1348
|
+
registryJson: catalog.registryJson,
|
|
1349
|
+
trust: catalog.registryTrust
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
return {
|
|
1353
|
+
models: catalog.models,
|
|
1354
|
+
sellers: this.sellerCatalogWithRuntimeMetrics(catalog.sellers)
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
catch (error) {
|
|
1358
|
+
const cached = this.loadTrustedRegistryCache(error);
|
|
1359
|
+
if (!cached) {
|
|
1360
|
+
throw error;
|
|
1361
|
+
}
|
|
1362
|
+
const snapshot = catalogSnapshotFromRegistry(cached);
|
|
1363
|
+
return {
|
|
1364
|
+
models: snapshot.models,
|
|
1365
|
+
sellers: this.sellerCatalogWithRuntimeMetrics(snapshot.sellers)
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
async refreshEnabledManualProviderModels() {
|
|
1370
|
+
const config = this.currentManualProviders();
|
|
1371
|
+
const now = new Date().toISOString();
|
|
1372
|
+
const refreshedProviders = [];
|
|
1373
|
+
const warnings = [];
|
|
1374
|
+
const providers = [];
|
|
1375
|
+
for (const provider of config.providers) {
|
|
1376
|
+
if (!provider.enabled) {
|
|
1377
|
+
providers.push(provider);
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
try {
|
|
1381
|
+
const probe = await this.probeManualProviderModels({
|
|
1382
|
+
baseUrl: provider.baseUrl,
|
|
1383
|
+
apiKey: this.manualProviderApiKey(provider)
|
|
1384
|
+
});
|
|
1385
|
+
providers.push({
|
|
1386
|
+
...provider,
|
|
1387
|
+
models: probe.modelIds,
|
|
1388
|
+
supportedProtocols: probe.supportedProtocols,
|
|
1389
|
+
updatedAt: now
|
|
1390
|
+
});
|
|
1391
|
+
refreshedProviders.push(provider.id);
|
|
1392
|
+
this.recordManualProviderProbeResult({
|
|
1393
|
+
providerId: provider.id,
|
|
1394
|
+
status: "healthy"
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1399
|
+
providers.push(provider);
|
|
1400
|
+
warnings.push({
|
|
1401
|
+
code: "manual_provider_refresh_failed",
|
|
1402
|
+
providerId: provider.id,
|
|
1403
|
+
message: errorMessage
|
|
1404
|
+
});
|
|
1405
|
+
this.recordManualProviderProbeResult({
|
|
1406
|
+
providerId: provider.id,
|
|
1407
|
+
status: "unhealthy",
|
|
1408
|
+
errorClass: "models_refresh_failed",
|
|
1409
|
+
errorMessage
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (refreshedProviders.length > 0) {
|
|
1414
|
+
this.saveManualProviders({
|
|
1415
|
+
version: 1,
|
|
1416
|
+
providers,
|
|
1417
|
+
routing: config.routing,
|
|
1418
|
+
updatedAt: now
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
return { refreshedProviders, warnings };
|
|
1422
|
+
}
|
|
1423
|
+
filterAutoEffectiveCatalog(catalog) {
|
|
1424
|
+
const config = this.currentAutoProviderConfig();
|
|
1425
|
+
const warnings = [];
|
|
1426
|
+
if (!config.enabled) {
|
|
1427
|
+
warnings.push({
|
|
1428
|
+
code: "auto_provider_disabled",
|
|
1429
|
+
message: "auto provider is disabled"
|
|
1430
|
+
});
|
|
1431
|
+
return {
|
|
1432
|
+
mode: "auto",
|
|
1433
|
+
source: "auto",
|
|
1434
|
+
models: [],
|
|
1435
|
+
sellers: [],
|
|
1436
|
+
warnings,
|
|
1437
|
+
refreshedProviders: [],
|
|
1438
|
+
unavailableModelIds: config.modelIds
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
if (config.modelIds.length === 0) {
|
|
1442
|
+
warnings.push({
|
|
1443
|
+
code: "auto_provider_models_empty",
|
|
1444
|
+
message: "auto provider has no selected models"
|
|
1445
|
+
});
|
|
1446
|
+
return {
|
|
1447
|
+
mode: "auto",
|
|
1448
|
+
source: "auto",
|
|
1449
|
+
models: [],
|
|
1450
|
+
sellers: [],
|
|
1451
|
+
warnings,
|
|
1452
|
+
refreshedProviders: [],
|
|
1453
|
+
unavailableModelIds: []
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
const selectedModels = new Set(config.modelIds);
|
|
1457
|
+
const selectedSellers = new Set(config.sellerIds);
|
|
1458
|
+
const models = catalog.models.filter((model) => {
|
|
1459
|
+
return selectedModels.has(model.id) &&
|
|
1460
|
+
(config.range !== "custom" || selectedSellers.has(model.sellerId));
|
|
1461
|
+
});
|
|
1462
|
+
const sellerIds = new Set(models.map((model) => model.sellerId));
|
|
1463
|
+
const sellers = catalog.sellers.filter((seller) => sellerIds.has(seller.id));
|
|
1464
|
+
const availableModelIds = new Set(models.map((model) => model.id));
|
|
1465
|
+
const unavailableModelIds = config.modelIds.filter((modelId) => !availableModelIds.has(modelId));
|
|
1466
|
+
if (unavailableModelIds.length > 0) {
|
|
1467
|
+
warnings.push({
|
|
1468
|
+
code: "auto_provider_selected_models_unavailable",
|
|
1469
|
+
message: `selected models unavailable in current auto provider catalog: ${unavailableModelIds.join(", ")}`
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
mode: "auto",
|
|
1474
|
+
source: "auto",
|
|
1475
|
+
models,
|
|
1476
|
+
sellers,
|
|
1477
|
+
warnings,
|
|
1478
|
+
refreshedProviders: [],
|
|
1479
|
+
unavailableModelIds
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
async effectiveModelCatalog(options) {
|
|
1483
|
+
const mode = this.currentProviderMode().mode;
|
|
1484
|
+
if (mode === "manual") {
|
|
1485
|
+
const refresh = options.refreshModels
|
|
1486
|
+
? await this.refreshEnabledManualProviderModels()
|
|
1487
|
+
: { refreshedProviders: [], warnings: [] };
|
|
1488
|
+
const catalog = this.manualProviderModelCatalog();
|
|
1489
|
+
return {
|
|
1490
|
+
mode,
|
|
1491
|
+
source: "manual",
|
|
1492
|
+
models: catalog.models,
|
|
1493
|
+
sellers: catalog.sellers,
|
|
1494
|
+
warnings: refresh.warnings,
|
|
1495
|
+
refreshedProviders: refresh.refreshedProviders,
|
|
1496
|
+
unavailableModelIds: []
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
return this.filterAutoEffectiveCatalog(await this.discoverSellerModelCatalog());
|
|
1500
|
+
}
|
|
1110
1501
|
clientToolsSummary() {
|
|
1111
1502
|
const providerStatuses = detectProviders({ home: this.config.providerHomeDir }).map(clientToolStatusFromProvider);
|
|
1112
1503
|
const clients = [
|
|
@@ -2041,6 +2432,8 @@ export class TokenbuddyDaemon {
|
|
|
2041
2432
|
?? nonNegativeIntegerField(usage?.cache_read_input_tokens)
|
|
2042
2433
|
?? nonNegativeIntegerField(usage?.cache_read_tokens)
|
|
2043
2434
|
?? 0;
|
|
2435
|
+
const upstreamCostMicros = extractUpstreamCostMicros(data, usage);
|
|
2436
|
+
const billablePromptTokens = Math.max(0, promptTokens - cacheReadTokens);
|
|
2044
2437
|
const imageMetadata = endpoint === "/v1/images/generations"
|
|
2045
2438
|
? imageUsageMetadata(data, usageRecord(requestBody))
|
|
2046
2439
|
: {};
|
|
@@ -2048,7 +2441,8 @@ export class TokenbuddyDaemon {
|
|
|
2048
2441
|
promptTokens,
|
|
2049
2442
|
completionTokens,
|
|
2050
2443
|
cacheReadTokens,
|
|
2051
|
-
billedMicros: (
|
|
2444
|
+
billedMicros: upstreamCostMicros ?? (billablePromptTokens + completionTokens) * 4,
|
|
2445
|
+
...(upstreamCostMicros === undefined ? {} : { upstreamCostMicros }),
|
|
2052
2446
|
...imageMetadata
|
|
2053
2447
|
};
|
|
2054
2448
|
}
|
|
@@ -2674,10 +3068,13 @@ export class TokenbuddyDaemon {
|
|
|
2674
3068
|
}
|
|
2675
3069
|
manualProviderEndpointUrl(provider, endpoint) {
|
|
2676
3070
|
const baseUrl = provider.baseUrl.replace(/\/+$/, "");
|
|
2677
|
-
|
|
2678
|
-
|
|
3071
|
+
const upstreamEndpoint = endpoint === "/messages" && !baseUrl.endsWith("/v1")
|
|
3072
|
+
? "/v1/messages"
|
|
3073
|
+
: endpoint;
|
|
3074
|
+
if (baseUrl.endsWith("/v1") && upstreamEndpoint.startsWith("/v1/")) {
|
|
3075
|
+
return `${baseUrl}${upstreamEndpoint.slice(3)}`;
|
|
2679
3076
|
}
|
|
2680
|
-
return `${baseUrl}${
|
|
3077
|
+
return `${baseUrl}${upstreamEndpoint}`;
|
|
2681
3078
|
}
|
|
2682
3079
|
manualProviderEndpointUrlFromBase(baseUrl, endpoint) {
|
|
2683
3080
|
const normalized = baseUrl.replace(/\/+$/, "");
|
|
@@ -2702,7 +3099,7 @@ export class TokenbuddyDaemon {
|
|
|
2702
3099
|
}
|
|
2703
3100
|
const startedAt = Date.now();
|
|
2704
3101
|
const ac = new AbortController();
|
|
2705
|
-
const timer = setTimeout(() => ac.abort(new Error("manual provider model probe timeout")), this.config.warmupProbeTimeoutMs ??
|
|
3102
|
+
const timer = setTimeout(() => ac.abort(new Error("manual provider model probe timeout")), this.config.warmupProbeTimeoutMs ?? MANUAL_PROVIDER_PROBE_TIMEOUT_MS);
|
|
2706
3103
|
let modelIds = [];
|
|
2707
3104
|
try {
|
|
2708
3105
|
const response = await fetch(this.manualProviderEndpointUrlFromBase(parsed.toString(), "/v1/models"), {
|
|
@@ -2752,9 +3149,10 @@ export class TokenbuddyDaemon {
|
|
|
2752
3149
|
const protocols = [
|
|
2753
3150
|
{ protocol: "chat_completions", endpoint: "/v1/chat/completions" },
|
|
2754
3151
|
{ protocol: "responses", endpoint: "/v1/responses" },
|
|
3152
|
+
{ protocol: "messages", endpoint: "/v1/messages" },
|
|
2755
3153
|
{ protocol: "images_generations", endpoint: "/v1/images/generations" }
|
|
2756
3154
|
];
|
|
2757
|
-
const timeoutMs = this.config.warmupProbeTimeoutMs ??
|
|
3155
|
+
const timeoutMs = this.config.warmupProbeTimeoutMs ?? MANUAL_PROVIDER_PROBE_TIMEOUT_MS;
|
|
2758
3156
|
return Promise.all(protocols.map((protocol) => this.probeManualProviderProtocol(input, protocol, timeoutMs)));
|
|
2759
3157
|
}
|
|
2760
3158
|
async probeManualProviderProtocol(input, protocol, timeoutMs) {
|
|
@@ -2780,7 +3178,8 @@ export class TokenbuddyDaemon {
|
|
|
2780
3178
|
method: "POST",
|
|
2781
3179
|
headers: {
|
|
2782
3180
|
"Content-Type": "application/json",
|
|
2783
|
-
"Authorization": `Bearer ${input.apiKey}
|
|
3181
|
+
"Authorization": `Bearer ${input.apiKey}`,
|
|
3182
|
+
...(protocol.protocol === "messages" ? { "anthropic-version": "2023-06-01" } : {})
|
|
2784
3183
|
},
|
|
2785
3184
|
body: JSON.stringify(manualProviderProtocolProbeBody(protocol.protocol, modelId)),
|
|
2786
3185
|
signal: ac.signal
|
|
@@ -2983,6 +3382,7 @@ export class TokenbuddyDaemon {
|
|
|
2983
3382
|
headers: {
|
|
2984
3383
|
"Content-Type": "application/json",
|
|
2985
3384
|
"Authorization": `Bearer ${this.manualProviderApiKey(provider)}`,
|
|
3385
|
+
...(this.endpointProtocol(endpoint) === "messages" ? { "anthropic-version": "2023-06-01" } : {}),
|
|
2986
3386
|
"X-Request-Id": attemptContext.requestId,
|
|
2987
3387
|
"Idempotency-Key": attemptContext.idempotencyKey,
|
|
2988
3388
|
"X-TokenBuddy-Deadline-Ms": String(deadlineMs)
|
|
@@ -4069,6 +4469,44 @@ export class TokenbuddyDaemon {
|
|
|
4069
4469
|
});
|
|
4070
4470
|
}
|
|
4071
4471
|
});
|
|
4472
|
+
controlApp.post("/models/refresh", async (req, res) => {
|
|
4473
|
+
try {
|
|
4474
|
+
const catalog = await this.effectiveModelCatalog({ refreshModels: true });
|
|
4475
|
+
logger.info("models.effective_refresh.succeeded", "effective provider model catalog refreshed", {
|
|
4476
|
+
mode: catalog.mode,
|
|
4477
|
+
source: catalog.source,
|
|
4478
|
+
sellerCount: catalog.sellers.length,
|
|
4479
|
+
modelCount: catalog.models.length,
|
|
4480
|
+
refreshedProviderCount: catalog.refreshedProviders.length,
|
|
4481
|
+
warningCount: catalog.warnings.length
|
|
4482
|
+
});
|
|
4483
|
+
res.status(200).json({
|
|
4484
|
+
ok: true,
|
|
4485
|
+
object: "list",
|
|
4486
|
+
mode: catalog.mode,
|
|
4487
|
+
source: catalog.source,
|
|
4488
|
+
registryUrl: this.config.sellerRegistryUrl,
|
|
4489
|
+
data: catalog.models,
|
|
4490
|
+
sellers: catalog.sellers,
|
|
4491
|
+
refreshedProviders: catalog.refreshedProviders,
|
|
4492
|
+
unavailableModelIds: catalog.unavailableModelIds,
|
|
4493
|
+
warnings: catalog.warnings
|
|
4494
|
+
});
|
|
4495
|
+
}
|
|
4496
|
+
catch (error) {
|
|
4497
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4498
|
+
logger.warn("models.effective_refresh.failed", "effective provider model catalog refresh failed", {
|
|
4499
|
+
registryUrl: this.config.sellerRegistryUrl,
|
|
4500
|
+
errorMessage
|
|
4501
|
+
});
|
|
4502
|
+
res.status(502).json({
|
|
4503
|
+
error: {
|
|
4504
|
+
code: "models_refresh_failed",
|
|
4505
|
+
message: errorMessage
|
|
4506
|
+
}
|
|
4507
|
+
});
|
|
4508
|
+
}
|
|
4509
|
+
});
|
|
4072
4510
|
controlApp.post("/providers/detect", (req, res) => {
|
|
4073
4511
|
try {
|
|
4074
4512
|
const providers = detectProviders({ home: typeof req.body?.home === "string" ? req.body.home : this.config.providerHomeDir });
|
|
@@ -4110,6 +4548,98 @@ export class TokenbuddyDaemon {
|
|
|
4110
4548
|
});
|
|
4111
4549
|
}
|
|
4112
4550
|
});
|
|
4551
|
+
controlApp.post("/providers/reconnect", async (req, res) => {
|
|
4552
|
+
try {
|
|
4553
|
+
const rawProviders = Array.isArray(req.body?.providers) ? req.body.providers : [];
|
|
4554
|
+
if (rawProviders.length === 0) {
|
|
4555
|
+
res.status(400).json({
|
|
4556
|
+
error: {
|
|
4557
|
+
code: "provider_reconnect_failed",
|
|
4558
|
+
message: "providers must include at least one provider id"
|
|
4559
|
+
}
|
|
4560
|
+
});
|
|
4561
|
+
return;
|
|
4562
|
+
}
|
|
4563
|
+
const providerIds = [];
|
|
4564
|
+
for (const provider of rawProviders) {
|
|
4565
|
+
if (!this.isReconnectProviderId(provider)) {
|
|
4566
|
+
res.status(400).json({
|
|
4567
|
+
error: {
|
|
4568
|
+
code: "provider_reconnect_failed",
|
|
4569
|
+
message: `unsupported provider: ${String(provider)}`
|
|
4570
|
+
}
|
|
4571
|
+
});
|
|
4572
|
+
return;
|
|
4573
|
+
}
|
|
4574
|
+
if (!providerIds.includes(provider)) {
|
|
4575
|
+
providerIds.push(provider);
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
const catalog = await this.effectiveModelCatalog({ refreshModels: req.body?.refreshModels !== false });
|
|
4579
|
+
if (catalog.models.length === 0) {
|
|
4580
|
+
res.status(409).json({
|
|
4581
|
+
error: {
|
|
4582
|
+
code: "provider_reconnect_no_models",
|
|
4583
|
+
message: `no models available for current ${catalog.mode} provider mode`
|
|
4584
|
+
},
|
|
4585
|
+
mode: catalog.mode,
|
|
4586
|
+
source: catalog.source,
|
|
4587
|
+
warnings: catalog.warnings,
|
|
4588
|
+
unavailableModelIds: catalog.unavailableModelIds
|
|
4589
|
+
});
|
|
4590
|
+
return;
|
|
4591
|
+
}
|
|
4592
|
+
const providerSelections = this.reconnectProviderSelections(providerIds, catalog.models);
|
|
4593
|
+
const firstSelection = providerSelections[providerIds[0]];
|
|
4594
|
+
const model = firstSelection?.selectionKind === "claude-role-mapping"
|
|
4595
|
+
? firstSelection.fallbackModel
|
|
4596
|
+
: firstSelection?.defaultModel;
|
|
4597
|
+
const proxyUrl = `http://127.0.0.1:${this.activeProxyPort()}`;
|
|
4598
|
+
const applied = applyProviderInstall({
|
|
4599
|
+
providers: providerIds,
|
|
4600
|
+
proxyUrl,
|
|
4601
|
+
model,
|
|
4602
|
+
providerSelections,
|
|
4603
|
+
home: typeof req.body?.home === "string" ? req.body.home : this.config.providerHomeDir
|
|
4604
|
+
}, this.tokenStore);
|
|
4605
|
+
const protocolCounts = catalog.models.reduce((counts, entry) => {
|
|
4606
|
+
for (const protocol of entry.supportedProtocols) {
|
|
4607
|
+
counts[protocol] = (counts[protocol] ?? 0) + 1;
|
|
4608
|
+
}
|
|
4609
|
+
return counts;
|
|
4610
|
+
}, {});
|
|
4611
|
+
logger.info("provider.reconnect.applied", "provider reconnect applied with refreshed models", {
|
|
4612
|
+
providerCount: providerIds.length,
|
|
4613
|
+
changeCount: applied.length,
|
|
4614
|
+
mode: catalog.mode,
|
|
4615
|
+
source: catalog.source,
|
|
4616
|
+
modelCount: catalog.models.length,
|
|
4617
|
+
warningCount: catalog.warnings.length
|
|
4618
|
+
});
|
|
4619
|
+
res.status(200).json({
|
|
4620
|
+
ok: true,
|
|
4621
|
+
mode: catalog.mode,
|
|
4622
|
+
source: catalog.source,
|
|
4623
|
+
modelCount: catalog.models.length,
|
|
4624
|
+
sellerCount: catalog.sellers.length,
|
|
4625
|
+
protocolCounts,
|
|
4626
|
+
refreshedProviders: catalog.refreshedProviders,
|
|
4627
|
+
unavailableModelIds: catalog.unavailableModelIds,
|
|
4628
|
+
warnings: catalog.warnings,
|
|
4629
|
+
applied
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
4632
|
+
catch (error) {
|
|
4633
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4634
|
+
logger.warn("provider.reconnect.failed", "provider reconnect failed", { errorMessage });
|
|
4635
|
+
res.status(400).json({
|
|
4636
|
+
error: {
|
|
4637
|
+
code: "provider_reconnect_failed",
|
|
4638
|
+
message: errorMessage
|
|
4639
|
+
}
|
|
4640
|
+
});
|
|
4641
|
+
}
|
|
4642
|
+
});
|
|
4113
4643
|
controlApp.post("/providers/install/preview", (req, res) => {
|
|
4114
4644
|
try {
|
|
4115
4645
|
const changes = previewProviderInstall({
|
|
@@ -5176,6 +5706,37 @@ function safeQrExtension(filePath) {
|
|
|
5176
5706
|
function safeStaticFileSegment(value) {
|
|
5177
5707
|
return value.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "clawtip";
|
|
5178
5708
|
}
|
|
5709
|
+
function backupOpenClawWalletConfigForRebind(configPath) {
|
|
5710
|
+
const backupPath = nextOpenClawWalletBackupPath(configPath);
|
|
5711
|
+
fs.renameSync(configPath, backupPath);
|
|
5712
|
+
return backupPath;
|
|
5713
|
+
}
|
|
5714
|
+
function restoreOpenClawWalletConfigBackup(configPath, backupPath) {
|
|
5715
|
+
if (fs.existsSync(configPath) || !fs.existsSync(backupPath)) {
|
|
5716
|
+
return;
|
|
5717
|
+
}
|
|
5718
|
+
fs.renameSync(backupPath, configPath);
|
|
5719
|
+
}
|
|
5720
|
+
function nextOpenClawWalletBackupPath(configPath) {
|
|
5721
|
+
const stamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
|
5722
|
+
let candidate = `${configPath}.before-tokenbuddy-rebind-${stamp}`;
|
|
5723
|
+
let counter = 1;
|
|
5724
|
+
while (fs.existsSync(candidate)) {
|
|
5725
|
+
counter += 1;
|
|
5726
|
+
candidate = `${configPath}.before-tokenbuddy-rebind-${stamp}-${counter}`;
|
|
5727
|
+
}
|
|
5728
|
+
return candidate;
|
|
5729
|
+
}
|
|
5730
|
+
function isClawtipBalanceInsufficientError(error) {
|
|
5731
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5732
|
+
const lower = message.toLowerCase();
|
|
5733
|
+
return message.includes("余额不足")
|
|
5734
|
+
|| message.includes("余额不够")
|
|
5735
|
+
|| message.includes("账户余额")
|
|
5736
|
+
|| lower.includes("insufficient balance")
|
|
5737
|
+
|| lower.includes("not enough balance")
|
|
5738
|
+
|| lower.includes("not enough funds");
|
|
5739
|
+
}
|
|
5179
5740
|
function readConfigString(config, key) {
|
|
5180
5741
|
const value = config?.[key];
|
|
5181
5742
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
@@ -5313,6 +5874,9 @@ function manualProviderProbeModelCandidates(modelIds, protocol) {
|
|
|
5313
5874
|
if (protocol === "images_generations") {
|
|
5314
5875
|
return imageProbeModelCandidates(uniqueIds);
|
|
5315
5876
|
}
|
|
5877
|
+
if (protocol === "messages") {
|
|
5878
|
+
return messagesProbeModelCandidates(uniqueIds);
|
|
5879
|
+
}
|
|
5316
5880
|
const preferredExactIds = [
|
|
5317
5881
|
"openai/gpt-chat-latest",
|
|
5318
5882
|
"openai/gpt-5.5",
|
|
@@ -5346,6 +5910,39 @@ function manualProviderProbeModelCandidates(modelIds, protocol) {
|
|
|
5346
5910
|
}
|
|
5347
5911
|
return candidates.slice(0, 6);
|
|
5348
5912
|
}
|
|
5913
|
+
function messagesProbeModelCandidates(uniqueIds) {
|
|
5914
|
+
const preferredExactIds = [
|
|
5915
|
+
"claude-sonnet-4-6",
|
|
5916
|
+
"claude-sonnet-4-5",
|
|
5917
|
+
"claude-opus-4-7",
|
|
5918
|
+
"claude-opus-4-6",
|
|
5919
|
+
"claude-haiku-4-5",
|
|
5920
|
+
"claude-3-7-sonnet-20250219",
|
|
5921
|
+
"claude-3-5-sonnet-20241022",
|
|
5922
|
+
"claude-3-5-haiku-20241022"
|
|
5923
|
+
];
|
|
5924
|
+
const candidates = [];
|
|
5925
|
+
const push = (id) => {
|
|
5926
|
+
if (id && !candidates.includes(id)) {
|
|
5927
|
+
candidates.push(id);
|
|
5928
|
+
}
|
|
5929
|
+
};
|
|
5930
|
+
for (const id of preferredExactIds) {
|
|
5931
|
+
push(uniqueIds.find((candidate) => candidate === id));
|
|
5932
|
+
}
|
|
5933
|
+
for (const id of uniqueIds) {
|
|
5934
|
+
if (id.startsWith("claude-") || id.includes("/claude-")) {
|
|
5935
|
+
push(id);
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
for (const id of uniqueIds) {
|
|
5939
|
+
push(id);
|
|
5940
|
+
if (candidates.length >= 6) {
|
|
5941
|
+
break;
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
return candidates.slice(0, 6);
|
|
5945
|
+
}
|
|
5349
5946
|
function imageProbeModelCandidates(uniqueIds) {
|
|
5350
5947
|
const preferredExactIds = [
|
|
5351
5948
|
"gpt-image-2",
|