@tokenbuddy/tokenbuddy 1.0.39 → 1.0.41
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 +22 -0
- package/dist/src/buyer-store.js +46 -7
- package/dist/src/daemon.d.ts +16 -0
- package/dist/src/daemon.js +421 -17
- package/dist/src/init-clawtip-activation.js +16 -2
- package/dist/src/provider-install.d.ts +3 -5
- package/dist/src/provider-install.js +33 -425
- package/dist/src/route-failover.js +10 -0
- package/dist/src/seller-pool.d.ts +5 -5
- package/dist/src/seller-pool.js +5 -7
- package/dist/src/seller-route-recommendations.d.ts +110 -0
- package/dist/src/seller-route-recommendations.js +334 -0
- package/package.json +2 -2
- package/static/ui/assets/index-Ca_IcEY6.js +271 -0
- package/static/ui/assets/index-DAq0t0SA.css +1 -0
- package/static/ui/index.html +2 -2
- package/static/ui/assets/index-BAwWDK4H.js +0 -271
- package/static/ui/assets/index-DM9SnAfj.css +0 -1
|
@@ -5,22 +5,10 @@ export const PROXY_ACCESS_TOKEN_PLACEHOLDER = "tbp_local_17821_d7f4c9a2b8e1";
|
|
|
5
5
|
const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
|
|
6
6
|
const CODEX_PROVIDER_ID = "TokenBuddy";
|
|
7
7
|
const HERMES_PROVIDER_ID = "TokenBuddy";
|
|
8
|
-
const OPENCLAW_TOKENBUDDY_PROVIDER_IDS = ["tokenbuddy", "tokens-buddy"];
|
|
9
8
|
const CLAUDE_ONE_M_MARKER = "[1M]";
|
|
10
9
|
const CLAUDE_CLIENT_HAIKU_MODEL = "claude-haiku-4-5";
|
|
11
10
|
const CLAUDE_CLIENT_SONNET_MODEL = "claude-sonnet-4-6";
|
|
12
11
|
const CLAUDE_CLIENT_OPUS_MODEL = "claude-opus-4-7";
|
|
13
|
-
const CLAUDE_CODE_TOKENBUDDY_ENV_KEYS = [
|
|
14
|
-
"ANTHROPIC_BASE_URL",
|
|
15
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
16
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
17
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
|
|
18
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
19
|
-
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
|
20
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
21
|
-
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
|
22
|
-
"ANTHROPIC_MODEL",
|
|
23
|
-
];
|
|
24
12
|
export const SUPPORTED_PROVIDER_IDS = [
|
|
25
13
|
"codex",
|
|
26
14
|
"claude-code",
|
|
@@ -223,85 +211,8 @@ function upsertTopLevelYamlObjectEntry(existing, sectionName, entryName, entryVa
|
|
|
223
211
|
};
|
|
224
212
|
return replaceTopLevelYamlSection(existing, sectionName, yamlContent(section));
|
|
225
213
|
}
|
|
226
|
-
function removeTopLevelYamlObjectEntry(existing, sectionName, entryName, shouldRemove) {
|
|
227
|
-
const current = parseSimpleYamlObject(existing);
|
|
228
|
-
const section = isPlainRecord(current[sectionName])
|
|
229
|
-
? { ...current[sectionName] }
|
|
230
|
-
: {};
|
|
231
|
-
const entry = section[entryName];
|
|
232
|
-
if (!isPlainRecord(entry) || !shouldRemove(entry)) {
|
|
233
|
-
return existing;
|
|
234
|
-
}
|
|
235
|
-
delete section[entryName];
|
|
236
|
-
if (Object.keys(section).length === 0) {
|
|
237
|
-
return removeTopLevelYamlSection(existing, sectionName);
|
|
238
|
-
}
|
|
239
|
-
return replaceTopLevelYamlSection(existing, sectionName, yamlContent(section));
|
|
240
|
-
}
|
|
241
|
-
function unquoteYamlScalar(value) {
|
|
242
|
-
const trimmed = value.trim();
|
|
243
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
244
|
-
return trimmed.slice(1, -1);
|
|
245
|
-
}
|
|
246
|
-
return trimmed;
|
|
247
|
-
}
|
|
248
|
-
function normalizedProviderName(value) {
|
|
249
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
250
|
-
}
|
|
251
|
-
function isTokenBuddyProviderIdentifier(value) {
|
|
252
|
-
if (typeof value !== "string") {
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
const normalized = normalizedProviderName(value);
|
|
256
|
-
return normalized === "tokenbuddy" || normalized === "tokensbuddy";
|
|
257
|
-
}
|
|
258
214
|
function isTokenBuddyProxyUrl(value) {
|
|
259
|
-
return typeof value === "string" && /(?:127\.0\.0\.1|localhost)
|
|
260
|
-
}
|
|
261
|
-
function yamlTopLevelListSectionEnd(lines, sectionStart) {
|
|
262
|
-
let index = sectionStart + 1;
|
|
263
|
-
while (index < lines.length) {
|
|
264
|
-
const line = lines[index];
|
|
265
|
-
if (line.trim() && /^[A-Za-z_][A-Za-z0-9_-]*:/.test(line)) {
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
index += 1;
|
|
269
|
-
}
|
|
270
|
-
return index;
|
|
271
|
-
}
|
|
272
|
-
function removeTopLevelYamlListItems(existing, sectionName, shouldRemove) {
|
|
273
|
-
const lines = existing.split(/\r?\n/);
|
|
274
|
-
const sectionStart = lines.findIndex((line) => line === `${sectionName}:` || line.startsWith(`${sectionName}: `));
|
|
275
|
-
if (sectionStart < 0) {
|
|
276
|
-
return existing;
|
|
277
|
-
}
|
|
278
|
-
const sectionEnd = yamlTopLevelListSectionEnd(lines, sectionStart);
|
|
279
|
-
const sectionLines = lines.slice(sectionStart + 1, sectionEnd);
|
|
280
|
-
const keptLines = [];
|
|
281
|
-
let index = 0;
|
|
282
|
-
while (index < sectionLines.length) {
|
|
283
|
-
const line = sectionLines[index];
|
|
284
|
-
if (!line.startsWith("- ")) {
|
|
285
|
-
keptLines.push(line);
|
|
286
|
-
index += 1;
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
const itemStart = index;
|
|
290
|
-
index += 1;
|
|
291
|
-
while (index < sectionLines.length && !sectionLines[index].startsWith("- ")) {
|
|
292
|
-
index += 1;
|
|
293
|
-
}
|
|
294
|
-
const itemLines = sectionLines.slice(itemStart, index);
|
|
295
|
-
if (!shouldRemove(itemLines)) {
|
|
296
|
-
keptLines.push(...itemLines);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
const before = lines.slice(0, sectionStart);
|
|
300
|
-
const after = lines.slice(sectionEnd);
|
|
301
|
-
const nextLines = keptLines.some((line) => line.trim())
|
|
302
|
-
? [...before, `${sectionName}:`, ...keptLines, ...after]
|
|
303
|
-
: [...before, ...after];
|
|
304
|
-
return `${nextLines.join("\n").replace(/\n*$/, "")}\n`;
|
|
215
|
+
return typeof value === "string" && /(?:127\.0\.0\.1|localhost):\d+\b/.test(value);
|
|
305
216
|
}
|
|
306
217
|
function readObjectField(value, key) {
|
|
307
218
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -312,13 +223,6 @@ function readObjectField(value, key) {
|
|
|
312
223
|
? field
|
|
313
224
|
: undefined;
|
|
314
225
|
}
|
|
315
|
-
function removeObjectKey(value, key) {
|
|
316
|
-
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
delete value[key];
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
226
|
function jsonContent(value) {
|
|
323
227
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
324
228
|
}
|
|
@@ -508,36 +412,13 @@ function codexConfig(home, proxyUrl, config) {
|
|
|
508
412
|
function isCodexTokenBuddyConfigured(filePath) {
|
|
509
413
|
const text = readText(filePath) || "";
|
|
510
414
|
const providerSection = readTomlSection(text, `model_providers.${CODEX_PROVIDER_ID}`);
|
|
511
|
-
const legacySection = readTomlSection(text, "tokenbuddy");
|
|
512
415
|
const topLevelProvider = readTopLevelTomlString(text, "model_provider");
|
|
513
416
|
const hasCurrentProvider = Boolean(providerSection) &&
|
|
514
417
|
topLevelProvider === CODEX_PROVIDER_ID &&
|
|
515
418
|
/wire_api\s*=\s*["']responses["']/.test(providerSection || "") &&
|
|
516
419
|
/base_url\s*=\s*["'][^"']*127\.0\.0\.1[^"']*\/v1["']/.test(providerSection || "") &&
|
|
517
420
|
new RegExp(`experimental_bearer_token\\s*=\\s*["']${escapeRegex(PROXY_ACCESS_TOKEN_PLACEHOLDER)}["']`).test(providerSection || "");
|
|
518
|
-
|
|
519
|
-
/proxy_url\s*=\s*["'][^"']*127\.0\.0\.1/.test(legacySection || "") &&
|
|
520
|
-
new RegExp(`api_key\\s*=\\s*["']${escapeRegex(PROXY_ACCESS_TOKEN_PLACEHOLDER)}["']`).test(legacySection || "");
|
|
521
|
-
return hasCurrentProvider || hasLegacyProvider;
|
|
522
|
-
}
|
|
523
|
-
function cleanupCodexConfig(home) {
|
|
524
|
-
const configPath = path.join(home, ".codex", "config.toml");
|
|
525
|
-
if (!fs.existsSync(configPath) || !isCodexTokenBuddyConfigured(configPath)) {
|
|
526
|
-
return [];
|
|
527
|
-
}
|
|
528
|
-
let next = readText(configPath) || "";
|
|
529
|
-
next = removeTomlSection(next, `model_providers.${CODEX_PROVIDER_ID}`);
|
|
530
|
-
next = removeTomlSection(next, "tokenbuddy");
|
|
531
|
-
if (readTopLevelTomlString(next, "model_provider") === CODEX_PROVIDER_ID) {
|
|
532
|
-
next = removeTopLevelTomlKey(next, "model_provider");
|
|
533
|
-
next = removeTopLevelTomlKey(next, "model");
|
|
534
|
-
}
|
|
535
|
-
if (next.trim()) {
|
|
536
|
-
fs.writeFileSync(configPath, next, "utf8");
|
|
537
|
-
return [{ providerId: "codex", path: configPath, action: "cleaned" }];
|
|
538
|
-
}
|
|
539
|
-
fs.rmSync(configPath, { force: true });
|
|
540
|
-
return [{ providerId: "codex", path: configPath, action: "removed" }];
|
|
421
|
+
return hasCurrentProvider;
|
|
541
422
|
}
|
|
542
423
|
function resolveClaudeFallbackAlias(config) {
|
|
543
424
|
if (config.roles.sonnet?.upstreamModel) {
|
|
@@ -630,36 +511,6 @@ function isClaudeCodeTokenBuddyConfigured(filePath) {
|
|
|
630
511
|
typeof env.ANTHROPIC_BASE_URL === "string" &&
|
|
631
512
|
env.ANTHROPIC_BASE_URL.trim().length > 0;
|
|
632
513
|
}
|
|
633
|
-
function cleanupClaudeCodeConfig(home) {
|
|
634
|
-
const configPath = path.join(home, ".claude", "settings.json");
|
|
635
|
-
if (!fs.existsSync(configPath) || !isClaudeCodeTokenBuddyConfigured(configPath)) {
|
|
636
|
-
return [];
|
|
637
|
-
}
|
|
638
|
-
const current = readJsonObject(configPath);
|
|
639
|
-
const env = readObjectField(current, "env");
|
|
640
|
-
if (!env) {
|
|
641
|
-
return [];
|
|
642
|
-
}
|
|
643
|
-
let changed = false;
|
|
644
|
-
for (const key of CLAUDE_CODE_TOKENBUDDY_ENV_KEYS) {
|
|
645
|
-
changed = removeObjectKey(env, key) || changed;
|
|
646
|
-
}
|
|
647
|
-
if (!changed) {
|
|
648
|
-
return [];
|
|
649
|
-
}
|
|
650
|
-
if (Object.keys(env).length > 0) {
|
|
651
|
-
current.env = env;
|
|
652
|
-
}
|
|
653
|
-
else {
|
|
654
|
-
delete current.env;
|
|
655
|
-
}
|
|
656
|
-
if (Object.keys(current).length === 0) {
|
|
657
|
-
fs.rmSync(configPath, { force: true });
|
|
658
|
-
return [{ providerId: "claude-code", path: configPath, action: "removed" }];
|
|
659
|
-
}
|
|
660
|
-
fs.writeFileSync(configPath, jsonContent(current), "utf8");
|
|
661
|
-
return [{ providerId: "claude-code", path: configPath, action: "cleaned" }];
|
|
662
|
-
}
|
|
663
514
|
function claudeDesktopConfig(home, proxyUrl, config) {
|
|
664
515
|
const model = pickConfiguredModel(config);
|
|
665
516
|
const models = orderedModelsForProtocol(config, "messages", model);
|
|
@@ -720,60 +571,6 @@ function isClaudeDesktopTokenBuddyConfigured(_filePath, home) {
|
|
|
720
571
|
return meta.appliedId === DESKTOP_PROFILE_ID &&
|
|
721
572
|
isClaudeDesktopProfileTokenBuddyConfigured(paths.profilePath);
|
|
722
573
|
}
|
|
723
|
-
function cleanupClaudeDesktopConfig(home) {
|
|
724
|
-
const paths = claudeDesktopPaths(home);
|
|
725
|
-
const results = [];
|
|
726
|
-
const meta = readJsonObject(paths.metaPath);
|
|
727
|
-
const tokenBuddyIsActive = meta.appliedId === DESKTOP_PROFILE_ID;
|
|
728
|
-
const primary = readJsonObject(paths.configPath);
|
|
729
|
-
if (tokenBuddyIsActive && primary.deploymentMode === "3p") {
|
|
730
|
-
delete primary.deploymentMode;
|
|
731
|
-
if (Object.keys(primary).length > 0) {
|
|
732
|
-
fs.writeFileSync(paths.configPath, jsonContent(primary), "utf8");
|
|
733
|
-
results.push({ providerId: "claude-desktop", path: paths.configPath, action: "cleaned" });
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
fs.rmSync(paths.configPath, { force: true });
|
|
737
|
-
results.push({ providerId: "claude-desktop", path: paths.configPath, action: "removed" });
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
const threep = readJsonObject(paths.threepConfigPath);
|
|
741
|
-
if (tokenBuddyIsActive && threep.deploymentMode === "3p") {
|
|
742
|
-
delete threep.deploymentMode;
|
|
743
|
-
if (Object.keys(threep).length > 0) {
|
|
744
|
-
fs.writeFileSync(paths.threepConfigPath, jsonContent(threep), "utf8");
|
|
745
|
-
results.push({ providerId: "claude-desktop", path: paths.threepConfigPath, action: "cleaned" });
|
|
746
|
-
}
|
|
747
|
-
else {
|
|
748
|
-
fs.rmSync(paths.threepConfigPath, { force: true });
|
|
749
|
-
results.push({ providerId: "claude-desktop", path: paths.threepConfigPath, action: "removed" });
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
if (fs.existsSync(paths.profilePath)) {
|
|
753
|
-
fs.rmSync(paths.profilePath, { force: true });
|
|
754
|
-
results.push({ providerId: "claude-desktop", path: paths.profilePath, action: "removed" });
|
|
755
|
-
}
|
|
756
|
-
let changedMeta = false;
|
|
757
|
-
if (meta.appliedId === DESKTOP_PROFILE_ID) {
|
|
758
|
-
delete meta.appliedId;
|
|
759
|
-
changedMeta = true;
|
|
760
|
-
}
|
|
761
|
-
if (Array.isArray(meta.entries)) {
|
|
762
|
-
const nextEntries = meta.entries.filter((entry) => {
|
|
763
|
-
return !(isPlainRecord(entry) &&
|
|
764
|
-
(entry.id === DESKTOP_PROFILE_ID || entry.name === "TokenBuddy"));
|
|
765
|
-
});
|
|
766
|
-
if (nextEntries.length !== meta.entries.length) {
|
|
767
|
-
meta.entries = nextEntries;
|
|
768
|
-
changedMeta = true;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
if (changedMeta) {
|
|
772
|
-
fs.writeFileSync(paths.metaPath, jsonContent(meta), "utf8");
|
|
773
|
-
results.push({ providerId: "claude-desktop", path: paths.metaPath, action: "cleaned" });
|
|
774
|
-
}
|
|
775
|
-
return results;
|
|
776
|
-
}
|
|
777
574
|
function openclawConfig(home, proxyUrl, config) {
|
|
778
575
|
const model = pickConfiguredModel(config);
|
|
779
576
|
const configuredModels = orderedModelsForProtocol(config, "chat_completions", model);
|
|
@@ -806,81 +603,19 @@ function openclawConfig(home, proxyUrl, config) {
|
|
|
806
603
|
current.agents = agents;
|
|
807
604
|
return [makeChange("openclaw", configPath, "configure OpenClaw proxy settings", jsonContent(current))];
|
|
808
605
|
}
|
|
809
|
-
function isOpenclawTokenBuddyModelRef(value) {
|
|
810
|
-
if (typeof value !== "string") {
|
|
811
|
-
return false;
|
|
812
|
-
}
|
|
813
|
-
return isTokenBuddyProviderIdentifier(value.split("/", 1)[0]);
|
|
814
|
-
}
|
|
815
606
|
function isOpenclawTokenBuddyConfigured(filePath) {
|
|
816
607
|
const current = readJsonObject(filePath);
|
|
817
608
|
const providers = readObjectField(readObjectField(current, "models"), "providers");
|
|
818
|
-
const defaults = readObjectField(readObjectField(current, "agents"), "defaults");
|
|
819
609
|
if (providers) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (defaults && isOpenclawTokenBuddyModelRef(defaults.model)) {
|
|
827
|
-
return true;
|
|
828
|
-
}
|
|
829
|
-
const defaultModels = readObjectField(defaults, "models");
|
|
830
|
-
if (defaultModels && Object.keys(defaultModels).some((modelRef) => isOpenclawTokenBuddyModelRef(modelRef))) {
|
|
831
|
-
return true;
|
|
832
|
-
}
|
|
833
|
-
const agentsList = readObjectField(current, "agents")?.list;
|
|
834
|
-
return Array.isArray(agentsList) && agentsList.some((agent) => {
|
|
835
|
-
return isPlainRecord(agent) && isOpenclawTokenBuddyModelRef(agent.model);
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
function cleanupOpenclawConfig(home) {
|
|
839
|
-
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
|
840
|
-
if (!fs.existsSync(configPath)) {
|
|
841
|
-
return [];
|
|
842
|
-
}
|
|
843
|
-
const current = readJsonObject(configPath);
|
|
844
|
-
const models = readObjectField(current, "models");
|
|
845
|
-
const providers = readObjectField(models, "providers");
|
|
846
|
-
const agents = readObjectField(current, "agents");
|
|
847
|
-
const defaults = readObjectField(agents, "defaults");
|
|
848
|
-
let changed = false;
|
|
849
|
-
if (providers) {
|
|
850
|
-
for (const providerId of OPENCLAW_TOKENBUDDY_PROVIDER_IDS) {
|
|
851
|
-
changed = removeObjectKey(providers, providerId) || changed;
|
|
852
|
-
}
|
|
853
|
-
if (models && Object.keys(providers).length === 0) {
|
|
854
|
-
delete models.providers;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
if (defaults && isOpenclawTokenBuddyModelRef(defaults.model)) {
|
|
858
|
-
delete defaults.model;
|
|
859
|
-
changed = true;
|
|
860
|
-
}
|
|
861
|
-
const defaultModels = readObjectField(defaults, "models");
|
|
862
|
-
if (defaultModels) {
|
|
863
|
-
for (const modelRef of Object.keys(defaultModels)) {
|
|
864
|
-
changed = (isOpenclawTokenBuddyModelRef(modelRef) && removeObjectKey(defaultModels, modelRef)) || changed;
|
|
865
|
-
}
|
|
866
|
-
if (Object.keys(defaultModels).length === 0 && defaults) {
|
|
867
|
-
delete defaults.models;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
const agentsList = agents?.list;
|
|
871
|
-
if (Array.isArray(agentsList)) {
|
|
872
|
-
for (const agent of agentsList) {
|
|
873
|
-
if (isPlainRecord(agent) && isOpenclawTokenBuddyModelRef(agent.model)) {
|
|
874
|
-
delete agent.model;
|
|
875
|
-
changed = true;
|
|
876
|
-
}
|
|
610
|
+
const provider = readObjectField(providers, "tokenbuddy");
|
|
611
|
+
if (provider?.apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
612
|
+
isTokenBuddyProxyUrl(provider.baseUrl) &&
|
|
613
|
+
provider.auth === "api-key" &&
|
|
614
|
+
provider.api === "openai-completions") {
|
|
615
|
+
return true;
|
|
877
616
|
}
|
|
878
617
|
}
|
|
879
|
-
|
|
880
|
-
return [];
|
|
881
|
-
}
|
|
882
|
-
fs.writeFileSync(configPath, jsonContent(current), "utf8");
|
|
883
|
-
return [{ providerId: "openclaw", path: configPath, action: "cleaned" }];
|
|
618
|
+
return false;
|
|
884
619
|
}
|
|
885
620
|
function openAiBaseUrl(proxyUrl) {
|
|
886
621
|
const normalized = proxyUrl.replace(/\/+$/, "");
|
|
@@ -953,10 +688,6 @@ function opencodeDefaultModelRef(defaultModel, modelsByProvider) {
|
|
|
953
688
|
}
|
|
954
689
|
return `tokenbuddy/${defaultModel}`;
|
|
955
690
|
}
|
|
956
|
-
function isOpencodeTokenBuddyModelRef(value) {
|
|
957
|
-
return typeof value === "string" &&
|
|
958
|
-
OPENCODE_TOKENBUDDY_PROVIDERS.some((provider) => value.startsWith(`${provider.providerId}/`));
|
|
959
|
-
}
|
|
960
691
|
function opencodeConfig(home, proxyUrl, config) {
|
|
961
692
|
const model = pickConfiguredModel(config);
|
|
962
693
|
const configPath = path.join(home, ".config", "opencode", "opencode.json");
|
|
@@ -991,71 +722,25 @@ function opencodeConfig(home, proxyUrl, config) {
|
|
|
991
722
|
current.small_model = defaultModelRef;
|
|
992
723
|
return [makeChange("opencode", configPath, "configure OpenCode provider for TokenBuddy proxy", jsonContent(current))];
|
|
993
724
|
}
|
|
994
|
-
function
|
|
995
|
-
if (
|
|
725
|
+
function isCurrentOpencodeTokenBuddyProvider(providerConfig) {
|
|
726
|
+
if (!isPlainRecord(providerConfig)) {
|
|
996
727
|
return false;
|
|
997
728
|
}
|
|
998
729
|
const options = readObjectField(providerConfig, "options");
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
(typeof apiKey === "string" && apiKey.toUpperCase().startsWith("TOKENBUDDY_"));
|
|
1002
|
-
return tokenBuddyKey && isTokenBuddyProxyUrl(options?.baseURL);
|
|
730
|
+
return options?.apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
731
|
+
isTokenBuddyProxyUrl(options.baseURL);
|
|
1003
732
|
}
|
|
1004
733
|
function isOpencodeTokenBuddyConfigured(filePath) {
|
|
1005
734
|
const current = readJsonObject(filePath);
|
|
1006
735
|
const providers = readObjectField(current, "provider");
|
|
1007
736
|
if (providers) {
|
|
1008
737
|
for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
|
|
1009
|
-
if (
|
|
1010
|
-
return true;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
1014
|
-
if (isLegacyOpencodeTokenBuddyProvider(providerId, providerConfig)) {
|
|
738
|
+
if (isCurrentOpencodeTokenBuddyProvider(providers[provider.providerId])) {
|
|
1015
739
|
return true;
|
|
1016
740
|
}
|
|
1017
741
|
}
|
|
1018
742
|
}
|
|
1019
|
-
return
|
|
1020
|
-
}
|
|
1021
|
-
function cleanupOpencodeConfig(home) {
|
|
1022
|
-
const configPath = path.join(home, ".config", "opencode", "opencode.json");
|
|
1023
|
-
if (!fs.existsSync(configPath)) {
|
|
1024
|
-
return [];
|
|
1025
|
-
}
|
|
1026
|
-
const current = readJsonObject(configPath);
|
|
1027
|
-
const providers = readObjectField(current, "provider");
|
|
1028
|
-
let changed = false;
|
|
1029
|
-
if (providers) {
|
|
1030
|
-
for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
|
|
1031
|
-
changed = removeObjectKey(providers, provider.providerId) || changed;
|
|
1032
|
-
}
|
|
1033
|
-
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
1034
|
-
changed = (isLegacyOpencodeTokenBuddyProvider(providerId, providerConfig) && removeObjectKey(providers, providerId)) || changed;
|
|
1035
|
-
}
|
|
1036
|
-
if (Object.keys(providers).length === 0) {
|
|
1037
|
-
delete current.provider;
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
const model = current.model;
|
|
1041
|
-
if (isOpencodeTokenBuddyModelRef(model)) {
|
|
1042
|
-
delete current.model;
|
|
1043
|
-
changed = true;
|
|
1044
|
-
}
|
|
1045
|
-
const smallModel = current.small_model;
|
|
1046
|
-
if (isOpencodeTokenBuddyModelRef(smallModel)) {
|
|
1047
|
-
delete current.small_model;
|
|
1048
|
-
changed = true;
|
|
1049
|
-
}
|
|
1050
|
-
if (!changed) {
|
|
1051
|
-
return [];
|
|
1052
|
-
}
|
|
1053
|
-
if (Object.keys(current).length === 0) {
|
|
1054
|
-
fs.rmSync(configPath, { force: true });
|
|
1055
|
-
return [{ providerId: "opencode", path: configPath, action: "removed" }];
|
|
1056
|
-
}
|
|
1057
|
-
fs.writeFileSync(configPath, jsonContent(current), "utf8");
|
|
1058
|
-
return [{ providerId: "opencode", path: configPath, action: "cleaned" }];
|
|
743
|
+
return false;
|
|
1059
744
|
}
|
|
1060
745
|
function hermesConfig(home, proxyUrl, config) {
|
|
1061
746
|
const model = pickConfiguredModel(config);
|
|
@@ -1092,50 +777,13 @@ function isHermesTokenBuddyProviderValue(value) {
|
|
|
1092
777
|
return false;
|
|
1093
778
|
}
|
|
1094
779
|
const normalized = value.trim().toLowerCase();
|
|
1095
|
-
return normalized ===
|
|
1096
|
-
normalized === HERMES_PROVIDER_ID.toLowerCase() ||
|
|
1097
|
-
normalized === "custom:tokenbuddy";
|
|
780
|
+
return normalized === HERMES_PROVIDER_ID.toLowerCase();
|
|
1098
781
|
}
|
|
1099
782
|
function isHermesTokenBuddyProviderEntry(entry) {
|
|
1100
783
|
return entry.api_key === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
1101
784
|
isHermesTokenBuddyBaseUrl(entry.base_url) &&
|
|
1102
785
|
(entry.transport === "chat_completions" || entry.api_mode === "chat_completions");
|
|
1103
786
|
}
|
|
1104
|
-
function isHermesTokenBuddyCustomProviderItem(itemLines) {
|
|
1105
|
-
let name = "";
|
|
1106
|
-
let baseUrl = "";
|
|
1107
|
-
let apiKey = "";
|
|
1108
|
-
for (const line of itemLines) {
|
|
1109
|
-
const nameMatch = /^\s*-\s*name:\s*(.+)\s*$/.exec(line) ?? /^\s*name:\s*(.+)\s*$/.exec(line);
|
|
1110
|
-
if (nameMatch) {
|
|
1111
|
-
name = unquoteYamlScalar(nameMatch[1]);
|
|
1112
|
-
continue;
|
|
1113
|
-
}
|
|
1114
|
-
const baseUrlMatch = /^\s*base_url:\s*(.+)\s*$/.exec(line);
|
|
1115
|
-
if (baseUrlMatch) {
|
|
1116
|
-
baseUrl = unquoteYamlScalar(baseUrlMatch[1]);
|
|
1117
|
-
continue;
|
|
1118
|
-
}
|
|
1119
|
-
const apiKeyMatch = /^\s*api_key:\s*(.+)\s*$/.exec(line);
|
|
1120
|
-
if (apiKeyMatch) {
|
|
1121
|
-
apiKey = unquoteYamlScalar(apiKeyMatch[1]);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
const normalizedName = normalizedProviderName(name);
|
|
1125
|
-
return normalizedName === "tokenbuddy" ||
|
|
1126
|
-
normalizedName === "tokensbuddy" ||
|
|
1127
|
-
(isHermesTokenBuddyBaseUrl(baseUrl) && apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER);
|
|
1128
|
-
}
|
|
1129
|
-
function hasHermesTokenBuddyCustomProvider(text) {
|
|
1130
|
-
let found = false;
|
|
1131
|
-
removeTopLevelYamlListItems(text, "custom_providers", (itemLines) => {
|
|
1132
|
-
if (isHermesTokenBuddyCustomProviderItem(itemLines)) {
|
|
1133
|
-
found = true;
|
|
1134
|
-
}
|
|
1135
|
-
return false;
|
|
1136
|
-
});
|
|
1137
|
-
return found;
|
|
1138
|
-
}
|
|
1139
787
|
function isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured) {
|
|
1140
788
|
return isHermesTokenBuddyProviderValue(modelConfig.provider) &&
|
|
1141
789
|
modelConfig.api_key === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
@@ -1145,41 +793,16 @@ function isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured) {
|
|
|
1145
793
|
modelConfig.default.length > 0;
|
|
1146
794
|
}
|
|
1147
795
|
function isHermesTokenBuddyConfigured(filePath) {
|
|
1148
|
-
const text = readText(filePath) || "";
|
|
1149
796
|
const current = readYamlObject(filePath);
|
|
1150
797
|
const modelConfig = readObjectField(current, "model");
|
|
1151
798
|
if (!modelConfig) {
|
|
1152
|
-
return
|
|
799
|
+
return false;
|
|
1153
800
|
}
|
|
1154
|
-
const hasTokenBuddyCustomProvider = hasHermesTokenBuddyCustomProvider(text);
|
|
1155
801
|
const providersConfig = readObjectField(current, "providers");
|
|
1156
802
|
const tokenBuddyProvider = readObjectField(providersConfig, HERMES_PROVIDER_ID);
|
|
1157
803
|
const namedProviderConfigured = Boolean(tokenBuddyProvider && isHermesTokenBuddyProviderEntry(tokenBuddyProvider));
|
|
1158
804
|
const hasTokenBuddyModel = isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured);
|
|
1159
|
-
return hasTokenBuddyModel
|
|
1160
|
-
}
|
|
1161
|
-
function cleanupHermesConfig(home) {
|
|
1162
|
-
const configPath = path.join(home, ".hermes", "config.yaml");
|
|
1163
|
-
if (!fs.existsSync(configPath) || !isHermesTokenBuddyConfigured(configPath)) {
|
|
1164
|
-
return [];
|
|
1165
|
-
}
|
|
1166
|
-
const existing = readText(configPath) || "";
|
|
1167
|
-
const current = parseSimpleYamlObject(existing);
|
|
1168
|
-
const modelConfig = readObjectField(current, "model");
|
|
1169
|
-
const providersConfig = readObjectField(current, "providers");
|
|
1170
|
-
const tokenBuddyProvider = readObjectField(providersConfig, HERMES_PROVIDER_ID);
|
|
1171
|
-
const namedProviderConfigured = Boolean(tokenBuddyProvider && isHermesTokenBuddyProviderEntry(tokenBuddyProvider));
|
|
1172
|
-
const withoutModel = modelConfig && isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured)
|
|
1173
|
-
? removeTopLevelYamlSection(existing, "model")
|
|
1174
|
-
: existing;
|
|
1175
|
-
const withoutProvider = removeTopLevelYamlObjectEntry(withoutModel, "providers", HERMES_PROVIDER_ID, isHermesTokenBuddyProviderEntry);
|
|
1176
|
-
const next = removeTopLevelYamlListItems(withoutProvider, "custom_providers", isHermesTokenBuddyCustomProviderItem);
|
|
1177
|
-
if (next.trim()) {
|
|
1178
|
-
fs.writeFileSync(configPath, next, "utf8");
|
|
1179
|
-
return [{ providerId: "hermes", path: configPath, action: "cleaned" }];
|
|
1180
|
-
}
|
|
1181
|
-
fs.rmSync(configPath, { force: true });
|
|
1182
|
-
return [{ providerId: "hermes", path: configPath, action: "removed" }];
|
|
805
|
+
return hasTokenBuddyModel;
|
|
1183
806
|
}
|
|
1184
807
|
const PROVIDERS = [
|
|
1185
808
|
{
|
|
@@ -1189,7 +812,6 @@ const PROVIDERS = [
|
|
|
1189
812
|
configPath: (home) => path.join(home, ".codex", "config.toml"),
|
|
1190
813
|
isConfigured: isCodexTokenBuddyConfigured,
|
|
1191
814
|
changes: codexConfig,
|
|
1192
|
-
cleanup: cleanupCodexConfig,
|
|
1193
815
|
modelSelectionKind: "single-model",
|
|
1194
816
|
protocolPreference: "responses",
|
|
1195
817
|
},
|
|
@@ -1200,7 +822,6 @@ const PROVIDERS = [
|
|
|
1200
822
|
configPath: (home) => path.join(home, ".claude", "settings.json"),
|
|
1201
823
|
isConfigured: isClaudeCodeTokenBuddyConfigured,
|
|
1202
824
|
changes: claudeCodeConfig,
|
|
1203
|
-
cleanup: cleanupClaudeCodeConfig,
|
|
1204
825
|
modelSelectionKind: "claude-role-mapping",
|
|
1205
826
|
protocolPreference: "messages",
|
|
1206
827
|
},
|
|
@@ -1210,7 +831,6 @@ const PROVIDERS = [
|
|
|
1210
831
|
configPath: (home) => path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
1211
832
|
isConfigured: isClaudeDesktopTokenBuddyConfigured,
|
|
1212
833
|
changes: claudeDesktopConfig,
|
|
1213
|
-
cleanup: cleanupClaudeDesktopConfig,
|
|
1214
834
|
modelSelectionKind: "single-model",
|
|
1215
835
|
protocolPreference: "messages",
|
|
1216
836
|
},
|
|
@@ -1225,7 +845,6 @@ const PROVIDERS = [
|
|
|
1225
845
|
path.join(home, ".openclaw", "config.json"),
|
|
1226
846
|
],
|
|
1227
847
|
changes: openclawConfig,
|
|
1228
|
-
cleanup: cleanupOpenclawConfig,
|
|
1229
848
|
modelSelectionKind: "single-model",
|
|
1230
849
|
protocolPreference: "chat_completions",
|
|
1231
850
|
},
|
|
@@ -1236,7 +855,6 @@ const PROVIDERS = [
|
|
|
1236
855
|
configPath: (home) => path.join(home, ".config", "opencode", "opencode.json"),
|
|
1237
856
|
isConfigured: isOpencodeTokenBuddyConfigured,
|
|
1238
857
|
changes: opencodeConfig,
|
|
1239
|
-
cleanup: cleanupOpencodeConfig,
|
|
1240
858
|
modelSelectionKind: "single-model",
|
|
1241
859
|
protocolPreference: "chat_completions",
|
|
1242
860
|
},
|
|
@@ -1251,7 +869,6 @@ const PROVIDERS = [
|
|
|
1251
869
|
path.join(home, ".hermes", "auth.json"),
|
|
1252
870
|
],
|
|
1253
871
|
changes: hermesConfig,
|
|
1254
|
-
cleanup: cleanupHermesConfig,
|
|
1255
872
|
modelSelectionKind: "single-model",
|
|
1256
873
|
protocolPreference: "chat_completions",
|
|
1257
874
|
},
|
|
@@ -1367,15 +984,17 @@ export function applyProviderInstall(options, store) {
|
|
|
1367
984
|
byProvider.set(change.providerId, [...(byProvider.get(change.providerId) || []), change]);
|
|
1368
985
|
}
|
|
1369
986
|
for (const [providerId, providerChanges] of byProvider) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
987
|
+
if (!store.getProviderInstallSnapshot(providerId)) {
|
|
988
|
+
const snapshot = {
|
|
989
|
+
providerId,
|
|
990
|
+
files: providerChanges.map((change) => ({
|
|
991
|
+
path: change.path,
|
|
992
|
+
existed: fs.existsSync(change.path),
|
|
993
|
+
content: readText(change.path),
|
|
994
|
+
})),
|
|
995
|
+
};
|
|
996
|
+
store.saveProviderInstallSnapshot(snapshot);
|
|
997
|
+
}
|
|
1379
998
|
}
|
|
1380
999
|
for (const providerId of providerIds) {
|
|
1381
1000
|
const provider = getProviderDefinition(providerId);
|
|
@@ -1398,31 +1017,21 @@ export function applyProviderInstall(options, store) {
|
|
|
1398
1017
|
return applied;
|
|
1399
1018
|
}
|
|
1400
1019
|
/**
|
|
1401
|
-
*
|
|
1402
|
-
* 从 `store`
|
|
1403
|
-
*
|
|
1404
|
-
* 配置升级后的 disconnect 语义是移除 TokenBuddy,而不是恢复旧 TokenBuddy。
|
|
1405
|
-
* 没有快照的 provider 标记为 `missing_snapshot`。
|
|
1020
|
+
* 断开 provider 安装。
|
|
1021
|
+
* 从 `store` 读取首次 connect 前的快照,精确恢复原文件或删除当次创建的文件。
|
|
1022
|
+
* 没有快照时不猜测、不清理第三方配置,只返回 `missing_snapshot`。
|
|
1406
1023
|
*
|
|
1407
1024
|
* @param options 回滚选项
|
|
1408
1025
|
* @param store buyer store
|
|
1409
1026
|
* @returns 回滚结果列表
|
|
1410
1027
|
*/
|
|
1411
1028
|
export function rollbackProviderInstall(options, store) {
|
|
1412
|
-
const home = resolveHome(options.home);
|
|
1413
1029
|
const providerIds = assertProviderIds(options.providers);
|
|
1414
1030
|
const results = [];
|
|
1415
1031
|
for (const providerId of providerIds) {
|
|
1416
|
-
const provider = getProviderDefinition(providerId);
|
|
1417
1032
|
const snapshot = store.getProviderInstallSnapshot(providerId);
|
|
1418
1033
|
if (!snapshot) {
|
|
1419
|
-
|
|
1420
|
-
if (cleanupResults.length > 0) {
|
|
1421
|
-
results.push(...cleanupResults);
|
|
1422
|
-
}
|
|
1423
|
-
else {
|
|
1424
|
-
results.push({ providerId, path: "", action: "missing_snapshot" });
|
|
1425
|
-
}
|
|
1034
|
+
results.push({ providerId, path: "", action: "missing_snapshot" });
|
|
1426
1035
|
store.removeProviderRuntimeConfig(providerId);
|
|
1427
1036
|
continue;
|
|
1428
1037
|
}
|
|
@@ -1440,7 +1049,6 @@ export function rollbackProviderInstall(options, store) {
|
|
|
1440
1049
|
results.push({ providerId, path: file.path, action: "removed" });
|
|
1441
1050
|
}
|
|
1442
1051
|
}
|
|
1443
|
-
results.push(...(provider.cleanup?.(home) ?? []));
|
|
1444
1052
|
store.removeProviderInstallSnapshot(providerId);
|
|
1445
1053
|
store.removeProviderRuntimeConfig(providerId);
|
|
1446
1054
|
}
|
|
@@ -74,6 +74,7 @@ export class RouteFailover {
|
|
|
74
74
|
const isSoft = context.errorKind === "soft_5xx" || context.errorKind === "deadline";
|
|
75
75
|
const isBusyCapacity = context.errorKind === "busy_capacity";
|
|
76
76
|
const isPurchaseFailure = context.errorKind === "purchase_failed";
|
|
77
|
+
const isStreamAborted = context.errorKind === "stream_aborted";
|
|
77
78
|
const info = this.pool.inspect(context.sellerId);
|
|
78
79
|
const freshPurchase = info.freshPurchase;
|
|
79
80
|
const budgetExceeded = !this.creditTracker.canAutoPurchase(this.now());
|
|
@@ -91,6 +92,15 @@ export class RouteFailover {
|
|
|
91
92
|
budgetExceeded
|
|
92
93
|
};
|
|
93
94
|
}
|
|
95
|
+
if (isStreamAborted) {
|
|
96
|
+
return {
|
|
97
|
+
action: "abort",
|
|
98
|
+
reason: "stream_aborted_after_response_started",
|
|
99
|
+
freshPurchase,
|
|
100
|
+
retryAttemptsBeforeFailover: context.attempt,
|
|
101
|
+
budgetExceeded
|
|
102
|
+
};
|
|
103
|
+
}
|
|
94
104
|
if (isPurchaseFailure) {
|
|
95
105
|
return {
|
|
96
106
|
action: "failover_next",
|