@tokenbuddy/tokenbuddy 1.0.38 → 1.0.40

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.
@@ -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
- const startBootstrap = this.config.clawtipWalletBootstrapStarter || startClawtipWalletBootstrap;
629
- const activation = await startBootstrap(payment, { home: this.config.clawtipHomeDir });
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: (promptTokens + completionTokens) * 4,
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
- if (baseUrl.endsWith("/v1") && endpoint.startsWith("/v1/")) {
2678
- return `${baseUrl}${endpoint.slice(3)}`;
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}${endpoint}`;
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 ?? 3000);
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 ?? 3000;
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",