@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
|
@@ -3,6 +3,9 @@ import * as os from "os";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
export const PROXY_ACCESS_TOKEN_PLACEHOLDER = "tbp_local_17821_d7f4c9a2b8e1";
|
|
5
5
|
const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
|
|
6
|
+
const CODEX_PROVIDER_ID = "TokenBuddy";
|
|
7
|
+
const HERMES_PROVIDER_ID = "TokenBuddy";
|
|
8
|
+
const OPENCLAW_TOKENBUDDY_PROVIDER_IDS = ["tokenbuddy", "tokens-buddy"];
|
|
6
9
|
const CLAUDE_ONE_M_MARKER = "[1M]";
|
|
7
10
|
const CLAUDE_CLIENT_HAIKU_MODEL = "claude-haiku-4-5";
|
|
8
11
|
const CLAUDE_CLIENT_SONNET_MODEL = "claude-sonnet-4-6";
|
|
@@ -206,6 +209,100 @@ function removeTopLevelYamlSection(existing, sectionName) {
|
|
|
206
209
|
...lines.slice(sectionEnd),
|
|
207
210
|
].join("\n").replace(/\n*$/, "")}\n`;
|
|
208
211
|
}
|
|
212
|
+
function upsertTopLevelYamlObjectEntry(existing, sectionName, entryName, entryValue) {
|
|
213
|
+
const current = parseSimpleYamlObject(existing);
|
|
214
|
+
const section = isPlainRecord(current[sectionName])
|
|
215
|
+
? { ...current[sectionName] }
|
|
216
|
+
: {};
|
|
217
|
+
const existingEntry = isPlainRecord(section[entryName])
|
|
218
|
+
? section[entryName]
|
|
219
|
+
: {};
|
|
220
|
+
section[entryName] = {
|
|
221
|
+
...existingEntry,
|
|
222
|
+
...entryValue,
|
|
223
|
+
};
|
|
224
|
+
return replaceTopLevelYamlSection(existing, sectionName, yamlContent(section));
|
|
225
|
+
}
|
|
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
|
+
function isTokenBuddyProxyUrl(value) {
|
|
259
|
+
return typeof value === "string" && /(?:127\.0\.0\.1|localhost):17821\b/.test(value);
|
|
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`;
|
|
305
|
+
}
|
|
209
306
|
function readObjectField(value, key) {
|
|
210
307
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
211
308
|
return undefined;
|
|
@@ -253,8 +350,11 @@ function resolveExecutable(commandName) {
|
|
|
253
350
|
function escapeTomlString(value) {
|
|
254
351
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
255
352
|
}
|
|
353
|
+
function escapeRegex(value) {
|
|
354
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
355
|
+
}
|
|
256
356
|
function replaceTomlSection(existing, sectionName, sectionBody) {
|
|
257
|
-
const sectionPattern = new RegExp(`^\\[${sectionName}\\]\\n[\\s\\S]*?(?=^\\[
|
|
357
|
+
const sectionPattern = new RegExp(`^\\[${escapeRegex(sectionName)}\\]\\n[\\s\\S]*?(?=^\\[|(?![\\s\\S]))`, "m");
|
|
258
358
|
const normalized = existing.trimEnd();
|
|
259
359
|
const nextSection = `[${sectionName}]\n${sectionBody.trimEnd()}\n`;
|
|
260
360
|
if (sectionPattern.test(normalized)) {
|
|
@@ -262,6 +362,65 @@ function replaceTomlSection(existing, sectionName, sectionBody) {
|
|
|
262
362
|
}
|
|
263
363
|
return `${normalized}${normalized ? "\n\n" : ""}${nextSection}`;
|
|
264
364
|
}
|
|
365
|
+
function removeTomlSection(existing, sectionName) {
|
|
366
|
+
const sectionPattern = new RegExp(`^\\[${escapeRegex(sectionName)}\\]\\n[\\s\\S]*?(?=^\\[|(?![\\s\\S]))`, "m");
|
|
367
|
+
return `${existing.trimEnd().replace(sectionPattern, "").trimEnd()}\n`;
|
|
368
|
+
}
|
|
369
|
+
function tomlTopLevelEnd(lines) {
|
|
370
|
+
const sectionStart = lines.findIndex((line) => line.trimStart().startsWith("["));
|
|
371
|
+
return sectionStart >= 0 ? sectionStart : lines.length;
|
|
372
|
+
}
|
|
373
|
+
function upsertTopLevelTomlString(existing, key, value) {
|
|
374
|
+
const lines = existing.trimEnd() ? existing.trimEnd().split(/\r?\n/) : [];
|
|
375
|
+
const keyPattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
|
|
376
|
+
const sectionStart = tomlTopLevelEnd(lines);
|
|
377
|
+
for (let index = 0; index < sectionStart; index += 1) {
|
|
378
|
+
if (keyPattern.test(lines[index])) {
|
|
379
|
+
lines[index] = `${key} = "${escapeTomlString(value)}"`;
|
|
380
|
+
return `${lines.join("\n")}\n`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const entry = `${key} = "${escapeTomlString(value)}"`;
|
|
384
|
+
if (sectionStart < lines.length) {
|
|
385
|
+
const before = lines.slice(0, sectionStart);
|
|
386
|
+
const after = lines.slice(sectionStart);
|
|
387
|
+
if (before.length > 0 && before[before.length - 1].trim()) {
|
|
388
|
+
before.push(entry, "");
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
before.push(entry);
|
|
392
|
+
}
|
|
393
|
+
return `${[...before, ...after].join("\n")}\n`;
|
|
394
|
+
}
|
|
395
|
+
lines.push(entry);
|
|
396
|
+
return `${lines.join("\n")}\n`;
|
|
397
|
+
}
|
|
398
|
+
function removeTopLevelTomlKey(existing, key) {
|
|
399
|
+
const lines = existing.trimEnd() ? existing.trimEnd().split(/\r?\n/) : [];
|
|
400
|
+
const keyPattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
|
|
401
|
+
const sectionStart = tomlTopLevelEnd(lines);
|
|
402
|
+
const nextLines = [
|
|
403
|
+
...lines.slice(0, sectionStart).filter((line) => !keyPattern.test(line)),
|
|
404
|
+
...lines.slice(sectionStart),
|
|
405
|
+
];
|
|
406
|
+
return nextLines.length > 0 ? `${nextLines.join("\n").replace(/\n*$/, "")}\n` : "";
|
|
407
|
+
}
|
|
408
|
+
function readTomlSection(existing, sectionName) {
|
|
409
|
+
const sectionPattern = new RegExp(`^\\[${escapeRegex(sectionName)}\\]\\n([\\s\\S]*?)(?=^\\[|(?![\\s\\S]))`, "m");
|
|
410
|
+
return sectionPattern.exec(existing)?.[1];
|
|
411
|
+
}
|
|
412
|
+
function readTopLevelTomlString(existing, key) {
|
|
413
|
+
const lines = existing.trimEnd() ? existing.trimEnd().split(/\r?\n/) : [];
|
|
414
|
+
const sectionStart = tomlTopLevelEnd(lines);
|
|
415
|
+
const keyPattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*["']([^"']+)["']`);
|
|
416
|
+
for (const line of lines.slice(0, sectionStart)) {
|
|
417
|
+
const match = keyPattern.exec(line);
|
|
418
|
+
if (match) {
|
|
419
|
+
return match[1];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
265
424
|
function stripClaudeOneMMarker(model) {
|
|
266
425
|
const trimmed = model.trimEnd();
|
|
267
426
|
if (!trimmed.toLowerCase().endsWith(CLAUDE_ONE_M_MARKER.toLowerCase())) {
|
|
@@ -303,6 +462,23 @@ function pickConfiguredModel(config) {
|
|
|
303
462
|
const haikuModel = config.roles.haiku?.upstreamModel;
|
|
304
463
|
return sonnetModel || opusModel || haikuModel || config.fallbackModel || "";
|
|
305
464
|
}
|
|
465
|
+
function modelsForProtocol(config, protocol, defaultModel) {
|
|
466
|
+
if (config.selectionKind === "single-model" && config.availableModelsByProtocol) {
|
|
467
|
+
const models = uniqueModelIds(config.availableModelsByProtocol[protocol]);
|
|
468
|
+
if (models.length > 0) {
|
|
469
|
+
return models;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return uniqueModelIds([defaultModel]);
|
|
473
|
+
}
|
|
474
|
+
function orderedModelsForProtocol(config, protocol, defaultModel) {
|
|
475
|
+
const models = modelsForProtocol(config, protocol, defaultModel);
|
|
476
|
+
const trimmedDefault = defaultModel.trim();
|
|
477
|
+
if (!trimmedDefault || !models.includes(trimmedDefault)) {
|
|
478
|
+
return models;
|
|
479
|
+
}
|
|
480
|
+
return [trimmedDefault, ...models.filter((model) => model !== trimmedDefault)];
|
|
481
|
+
}
|
|
306
482
|
function resolveProviderRuntimeConfig(provider, options) {
|
|
307
483
|
const selection = options.providerSelections?.[provider.id];
|
|
308
484
|
if (selection) {
|
|
@@ -317,14 +493,52 @@ function resolveProviderRuntimeConfig(provider, options) {
|
|
|
317
493
|
function codexConfig(home, proxyUrl, config) {
|
|
318
494
|
const model = pickConfiguredModel(config);
|
|
319
495
|
const configPath = path.join(home, ".codex", "config.toml");
|
|
320
|
-
const existing = readText(configPath) || "";
|
|
321
|
-
|
|
322
|
-
`
|
|
323
|
-
`
|
|
324
|
-
`
|
|
496
|
+
const existing = removeTomlSection(readText(configPath) || "", "tokenbuddy");
|
|
497
|
+
let content = replaceTomlSection(existing, `model_providers.${CODEX_PROVIDER_ID}`, [
|
|
498
|
+
`name = "TokenBuddy"`,
|
|
499
|
+
`base_url = "${escapeTomlString(openAiBaseUrl(proxyUrl))}"`,
|
|
500
|
+
`wire_api = "responses"`,
|
|
501
|
+
`requires_openai_auth = true`,
|
|
502
|
+
`experimental_bearer_token = "${PROXY_ACCESS_TOKEN_PLACEHOLDER}"`,
|
|
325
503
|
].join("\n"));
|
|
504
|
+
content = upsertTopLevelTomlString(content, "model_provider", CODEX_PROVIDER_ID);
|
|
505
|
+
content = upsertTopLevelTomlString(content, "model", model);
|
|
326
506
|
return [makeChange("codex", configPath, "configure TokenBuddy proxy for Codex", content)];
|
|
327
507
|
}
|
|
508
|
+
function isCodexTokenBuddyConfigured(filePath) {
|
|
509
|
+
const text = readText(filePath) || "";
|
|
510
|
+
const providerSection = readTomlSection(text, `model_providers.${CODEX_PROVIDER_ID}`);
|
|
511
|
+
const legacySection = readTomlSection(text, "tokenbuddy");
|
|
512
|
+
const topLevelProvider = readTopLevelTomlString(text, "model_provider");
|
|
513
|
+
const hasCurrentProvider = Boolean(providerSection) &&
|
|
514
|
+
topLevelProvider === CODEX_PROVIDER_ID &&
|
|
515
|
+
/wire_api\s*=\s*["']responses["']/.test(providerSection || "") &&
|
|
516
|
+
/base_url\s*=\s*["'][^"']*127\.0\.0\.1[^"']*\/v1["']/.test(providerSection || "") &&
|
|
517
|
+
new RegExp(`experimental_bearer_token\\s*=\\s*["']${escapeRegex(PROXY_ACCESS_TOKEN_PLACEHOLDER)}["']`).test(providerSection || "");
|
|
518
|
+
const hasLegacyProvider = Boolean(legacySection) &&
|
|
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" }];
|
|
541
|
+
}
|
|
328
542
|
function resolveClaudeFallbackAlias(config) {
|
|
329
543
|
if (config.roles.sonnet?.upstreamModel) {
|
|
330
544
|
return setClaudeOneMMarker(CLAUDE_CLIENT_SONNET_MODEL, Boolean(config.roles.sonnet.declareOneM));
|
|
@@ -448,6 +662,7 @@ function cleanupClaudeCodeConfig(home) {
|
|
|
448
662
|
}
|
|
449
663
|
function claudeDesktopConfig(home, proxyUrl, config) {
|
|
450
664
|
const model = pickConfiguredModel(config);
|
|
665
|
+
const models = orderedModelsForProtocol(config, "messages", model);
|
|
451
666
|
const configDir = path.join(home, "Library", "Application Support", "Claude");
|
|
452
667
|
const configPath = path.join(configDir, "claude_desktop_config.json");
|
|
453
668
|
const threepDir = path.join(home, "Library", "Application Support", "Claude-3p");
|
|
@@ -465,7 +680,7 @@ function claudeDesktopConfig(home, proxyUrl, config) {
|
|
|
465
680
|
inferenceGatewayAuthScheme: "bearer",
|
|
466
681
|
inferenceGatewayBaseUrl: proxyUrl,
|
|
467
682
|
inferenceProvider: "gateway",
|
|
468
|
-
inferenceModels:
|
|
683
|
+
inferenceModels: models.map((modelName) => ({ name: modelName })),
|
|
469
684
|
};
|
|
470
685
|
const meta = readJsonObject(metaPath);
|
|
471
686
|
const existingEntries = Array.isArray(meta.entries) ? meta.entries : [];
|
|
@@ -507,12 +722,11 @@ function isClaudeDesktopTokenBuddyConfigured(_filePath, home) {
|
|
|
507
722
|
}
|
|
508
723
|
function cleanupClaudeDesktopConfig(home) {
|
|
509
724
|
const paths = claudeDesktopPaths(home);
|
|
510
|
-
if (!isClaudeDesktopTokenBuddyConfigured(paths.configPath, home)) {
|
|
511
|
-
return [];
|
|
512
|
-
}
|
|
513
725
|
const results = [];
|
|
726
|
+
const meta = readJsonObject(paths.metaPath);
|
|
727
|
+
const tokenBuddyIsActive = meta.appliedId === DESKTOP_PROFILE_ID;
|
|
514
728
|
const primary = readJsonObject(paths.configPath);
|
|
515
|
-
if (primary.deploymentMode === "3p") {
|
|
729
|
+
if (tokenBuddyIsActive && primary.deploymentMode === "3p") {
|
|
516
730
|
delete primary.deploymentMode;
|
|
517
731
|
if (Object.keys(primary).length > 0) {
|
|
518
732
|
fs.writeFileSync(paths.configPath, jsonContent(primary), "utf8");
|
|
@@ -524,7 +738,7 @@ function cleanupClaudeDesktopConfig(home) {
|
|
|
524
738
|
}
|
|
525
739
|
}
|
|
526
740
|
const threep = readJsonObject(paths.threepConfigPath);
|
|
527
|
-
if (threep.deploymentMode === "3p") {
|
|
741
|
+
if (tokenBuddyIsActive && threep.deploymentMode === "3p") {
|
|
528
742
|
delete threep.deploymentMode;
|
|
529
743
|
if (Object.keys(threep).length > 0) {
|
|
530
744
|
fs.writeFileSync(paths.threepConfigPath, jsonContent(threep), "utf8");
|
|
@@ -539,7 +753,6 @@ function cleanupClaudeDesktopConfig(home) {
|
|
|
539
753
|
fs.rmSync(paths.profilePath, { force: true });
|
|
540
754
|
results.push({ providerId: "claude-desktop", path: paths.profilePath, action: "removed" });
|
|
541
755
|
}
|
|
542
|
-
const meta = readJsonObject(paths.metaPath);
|
|
543
756
|
let changedMeta = false;
|
|
544
757
|
if (meta.appliedId === DESKTOP_PROFILE_ID) {
|
|
545
758
|
delete meta.appliedId;
|
|
@@ -547,7 +760,8 @@ function cleanupClaudeDesktopConfig(home) {
|
|
|
547
760
|
}
|
|
548
761
|
if (Array.isArray(meta.entries)) {
|
|
549
762
|
const nextEntries = meta.entries.filter((entry) => {
|
|
550
|
-
return !(isPlainRecord(entry) &&
|
|
763
|
+
return !(isPlainRecord(entry) &&
|
|
764
|
+
(entry.id === DESKTOP_PROFILE_ID || entry.name === "TokenBuddy"));
|
|
551
765
|
});
|
|
552
766
|
if (nextEntries.length !== meta.entries.length) {
|
|
553
767
|
meta.entries = nextEntries;
|
|
@@ -562,23 +776,19 @@ function cleanupClaudeDesktopConfig(home) {
|
|
|
562
776
|
}
|
|
563
777
|
function openclawConfig(home, proxyUrl, config) {
|
|
564
778
|
const model = pickConfiguredModel(config);
|
|
779
|
+
const configuredModels = orderedModelsForProtocol(config, "chat_completions", model);
|
|
780
|
+
const defaultModel = configuredModels.includes(model) ? model : configuredModels[0] || model;
|
|
565
781
|
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
|
566
782
|
const current = readJsonObject(configPath);
|
|
567
783
|
const models = isPlainRecord(current.models) ? current.models : {};
|
|
568
784
|
const providers = isPlainRecord(models.providers) ? models.providers : {};
|
|
569
785
|
const existingProvider = isPlainRecord(providers.tokenbuddy) ? providers.tokenbuddy : {};
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
id: model,
|
|
577
|
-
name: model,
|
|
578
|
-
api: "openai-completions",
|
|
579
|
-
input: ["text", "image"],
|
|
580
|
-
},
|
|
581
|
-
];
|
|
786
|
+
const nextModels = configuredModels.map((modelName) => ({
|
|
787
|
+
id: modelName,
|
|
788
|
+
name: modelName,
|
|
789
|
+
api: "openai-completions",
|
|
790
|
+
input: ["text", "image"],
|
|
791
|
+
}));
|
|
582
792
|
providers.tokenbuddy = {
|
|
583
793
|
...existingProvider,
|
|
584
794
|
baseUrl: openAiBaseUrl(proxyUrl),
|
|
@@ -591,29 +801,43 @@ function openclawConfig(home, proxyUrl, config) {
|
|
|
591
801
|
current.models = models;
|
|
592
802
|
const agents = isPlainRecord(current.agents) ? current.agents : {};
|
|
593
803
|
const defaults = isPlainRecord(agents.defaults) ? agents.defaults : {};
|
|
594
|
-
defaults.model = `tokenbuddy/${
|
|
804
|
+
defaults.model = `tokenbuddy/${defaultModel}`;
|
|
595
805
|
agents.defaults = defaults;
|
|
596
806
|
current.agents = agents;
|
|
597
807
|
return [makeChange("openclaw", configPath, "configure OpenClaw proxy settings", jsonContent(current))];
|
|
598
808
|
}
|
|
809
|
+
function isOpenclawTokenBuddyModelRef(value) {
|
|
810
|
+
if (typeof value !== "string") {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
return isTokenBuddyProviderIdentifier(value.split("/", 1)[0]);
|
|
814
|
+
}
|
|
599
815
|
function isOpenclawTokenBuddyConfigured(filePath) {
|
|
600
816
|
const current = readJsonObject(filePath);
|
|
601
|
-
const
|
|
817
|
+
const providers = readObjectField(readObjectField(current, "models"), "providers");
|
|
602
818
|
const defaults = readObjectField(readObjectField(current, "agents"), "defaults");
|
|
603
|
-
if (
|
|
604
|
-
|
|
819
|
+
if (providers) {
|
|
820
|
+
for (const providerId of OPENCLAW_TOKENBUDDY_PROVIDER_IDS) {
|
|
821
|
+
if (Object.prototype.hasOwnProperty.call(providers, providerId)) {
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (defaults && isOpenclawTokenBuddyModelRef(defaults.model)) {
|
|
827
|
+
return true;
|
|
605
828
|
}
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
});
|
|
613
837
|
}
|
|
614
838
|
function cleanupOpenclawConfig(home) {
|
|
615
839
|
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
|
616
|
-
if (!fs.existsSync(configPath)
|
|
840
|
+
if (!fs.existsSync(configPath)) {
|
|
617
841
|
return [];
|
|
618
842
|
}
|
|
619
843
|
const current = readJsonObject(configPath);
|
|
@@ -623,15 +847,35 @@ function cleanupOpenclawConfig(home) {
|
|
|
623
847
|
const defaults = readObjectField(agents, "defaults");
|
|
624
848
|
let changed = false;
|
|
625
849
|
if (providers) {
|
|
626
|
-
|
|
850
|
+
for (const providerId of OPENCLAW_TOKENBUDDY_PROVIDER_IDS) {
|
|
851
|
+
changed = removeObjectKey(providers, providerId) || changed;
|
|
852
|
+
}
|
|
627
853
|
if (models && Object.keys(providers).length === 0) {
|
|
628
854
|
delete models.providers;
|
|
629
855
|
}
|
|
630
856
|
}
|
|
631
|
-
if (defaults &&
|
|
857
|
+
if (defaults && isOpenclawTokenBuddyModelRef(defaults.model)) {
|
|
632
858
|
delete defaults.model;
|
|
633
859
|
changed = true;
|
|
634
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
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
635
879
|
if (!changed) {
|
|
636
880
|
return [];
|
|
637
881
|
}
|
|
@@ -642,6 +886,77 @@ function openAiBaseUrl(proxyUrl) {
|
|
|
642
886
|
const normalized = proxyUrl.replace(/\/+$/, "");
|
|
643
887
|
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
644
888
|
}
|
|
889
|
+
function anthropicBaseUrl(proxyUrl) {
|
|
890
|
+
const normalized = proxyUrl.replace(/\/+$/, "");
|
|
891
|
+
return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized;
|
|
892
|
+
}
|
|
893
|
+
const OPENCODE_TOKENBUDDY_PROVIDERS = [
|
|
894
|
+
{
|
|
895
|
+
providerId: "tokenbuddy",
|
|
896
|
+
protocol: "chat_completions",
|
|
897
|
+
name: "TokenBuddy",
|
|
898
|
+
npm: "@ai-sdk/openai-compatible",
|
|
899
|
+
baseUrl: openAiBaseUrl,
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
providerId: "tokenbuddy-responses",
|
|
903
|
+
protocol: "responses",
|
|
904
|
+
name: "TokenBuddy Responses",
|
|
905
|
+
npm: "@ai-sdk/openai",
|
|
906
|
+
baseUrl: openAiBaseUrl,
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
providerId: "tokenbuddy-messages",
|
|
910
|
+
protocol: "messages",
|
|
911
|
+
name: "TokenBuddy Messages",
|
|
912
|
+
npm: "@ai-sdk/anthropic",
|
|
913
|
+
baseUrl: anthropicBaseUrl,
|
|
914
|
+
},
|
|
915
|
+
];
|
|
916
|
+
function uniqueModelIds(models) {
|
|
917
|
+
const seen = new Set();
|
|
918
|
+
const output = [];
|
|
919
|
+
for (const model of models ?? []) {
|
|
920
|
+
const trimmed = model.trim();
|
|
921
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
seen.add(trimmed);
|
|
925
|
+
output.push(trimmed);
|
|
926
|
+
}
|
|
927
|
+
return output;
|
|
928
|
+
}
|
|
929
|
+
function opencodeModelsForProtocol(config, protocol, defaultModel) {
|
|
930
|
+
if (config.selectionKind === "single-model" && config.availableModelsByProtocol) {
|
|
931
|
+
return uniqueModelIds(config.availableModelsByProtocol[protocol]);
|
|
932
|
+
}
|
|
933
|
+
return protocol === "chat_completions" ? uniqueModelIds([defaultModel]) : [];
|
|
934
|
+
}
|
|
935
|
+
function opencodeModelConfig(model) {
|
|
936
|
+
return {
|
|
937
|
+
name: model,
|
|
938
|
+
attachment: true,
|
|
939
|
+
tool_call: true,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function opencodeDefaultModelRef(defaultModel, modelsByProvider) {
|
|
943
|
+
for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
|
|
944
|
+
if (modelsByProvider.get(provider.providerId)?.includes(defaultModel)) {
|
|
945
|
+
return `${provider.providerId}/${defaultModel}`;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
|
|
949
|
+
const firstModel = modelsByProvider.get(provider.providerId)?.[0];
|
|
950
|
+
if (firstModel) {
|
|
951
|
+
return `${provider.providerId}/${firstModel}`;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return `tokenbuddy/${defaultModel}`;
|
|
955
|
+
}
|
|
956
|
+
function isOpencodeTokenBuddyModelRef(value) {
|
|
957
|
+
return typeof value === "string" &&
|
|
958
|
+
OPENCODE_TOKENBUDDY_PROVIDERS.some((provider) => value.startsWith(`${provider.providerId}/`));
|
|
959
|
+
}
|
|
645
960
|
function opencodeConfig(home, proxyUrl, config) {
|
|
646
961
|
const model = pickConfiguredModel(config);
|
|
647
962
|
const configPath = path.join(home, ".config", "opencode", "opencode.json");
|
|
@@ -649,63 +964,86 @@ function opencodeConfig(home, proxyUrl, config) {
|
|
|
649
964
|
const providers = current.provider && typeof current.provider === "object" && !Array.isArray(current.provider)
|
|
650
965
|
? current.provider
|
|
651
966
|
: {};
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
967
|
+
for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
|
|
968
|
+
delete providers[provider.providerId];
|
|
969
|
+
}
|
|
970
|
+
const modelsByProvider = new Map();
|
|
971
|
+
for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
|
|
972
|
+
const models = opencodeModelsForProtocol(config, provider.protocol, model);
|
|
973
|
+
if (models.length === 0) {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
modelsByProvider.set(provider.providerId, models);
|
|
977
|
+
providers[provider.providerId] = {
|
|
978
|
+
name: provider.name,
|
|
979
|
+
npm: provider.npm,
|
|
980
|
+
options: {
|
|
981
|
+
apiKey: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
982
|
+
baseURL: provider.baseUrl(proxyUrl),
|
|
664
983
|
},
|
|
665
|
-
|
|
666
|
-
|
|
984
|
+
models: Object.fromEntries(models.map((modelId) => [modelId, opencodeModelConfig(modelId)])),
|
|
985
|
+
};
|
|
986
|
+
}
|
|
667
987
|
current.provider = providers;
|
|
668
988
|
// 写顶层 model / small_model,让 opencode 默认走 tokenbuddy 而不是残留的 openai/qwen-plus 死链
|
|
669
|
-
|
|
670
|
-
current.
|
|
989
|
+
const defaultModelRef = opencodeDefaultModelRef(model, modelsByProvider);
|
|
990
|
+
current.model = defaultModelRef;
|
|
991
|
+
current.small_model = defaultModelRef;
|
|
671
992
|
return [makeChange("opencode", configPath, "configure OpenCode provider for TokenBuddy proxy", jsonContent(current))];
|
|
672
993
|
}
|
|
994
|
+
function isLegacyOpencodeTokenBuddyProvider(providerId, providerConfig) {
|
|
995
|
+
if (providerId !== "openai" || !isPlainRecord(providerConfig)) {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
const options = readObjectField(providerConfig, "options");
|
|
999
|
+
const apiKey = options?.apiKey;
|
|
1000
|
+
const tokenBuddyKey = apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER ||
|
|
1001
|
+
(typeof apiKey === "string" && apiKey.toUpperCase().startsWith("TOKENBUDDY_"));
|
|
1002
|
+
return tokenBuddyKey && isTokenBuddyProxyUrl(options?.baseURL);
|
|
1003
|
+
}
|
|
673
1004
|
function isOpencodeTokenBuddyConfigured(filePath) {
|
|
674
1005
|
const current = readJsonObject(filePath);
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1006
|
+
const providers = readObjectField(current, "provider");
|
|
1007
|
+
if (providers) {
|
|
1008
|
+
for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
|
|
1009
|
+
if (Object.prototype.hasOwnProperty.call(providers, provider.providerId)) {
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
1014
|
+
if (isLegacyOpencodeTokenBuddyProvider(providerId, providerConfig)) {
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
679
1018
|
}
|
|
680
|
-
return
|
|
681
|
-
options.apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
682
|
-
typeof options.baseURL === "string" &&
|
|
683
|
-
options.baseURL.includes("127.0.0.1") &&
|
|
684
|
-
options.baseURL.endsWith("/v1") &&
|
|
685
|
-
typeof current.model === "string" &&
|
|
686
|
-
current.model.startsWith("tokenbuddy/") &&
|
|
687
|
-
typeof current.small_model === "string" &&
|
|
688
|
-
current.small_model.startsWith("tokenbuddy/");
|
|
1019
|
+
return isOpencodeTokenBuddyModelRef(current.model) || isOpencodeTokenBuddyModelRef(current.small_model);
|
|
689
1020
|
}
|
|
690
1021
|
function cleanupOpencodeConfig(home) {
|
|
691
1022
|
const configPath = path.join(home, ".config", "opencode", "opencode.json");
|
|
692
|
-
if (!fs.existsSync(configPath)
|
|
1023
|
+
if (!fs.existsSync(configPath)) {
|
|
693
1024
|
return [];
|
|
694
1025
|
}
|
|
695
1026
|
const current = readJsonObject(configPath);
|
|
696
1027
|
const providers = readObjectField(current, "provider");
|
|
697
1028
|
let changed = false;
|
|
698
1029
|
if (providers) {
|
|
699
|
-
|
|
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
|
+
}
|
|
700
1036
|
if (Object.keys(providers).length === 0) {
|
|
701
1037
|
delete current.provider;
|
|
702
1038
|
}
|
|
703
1039
|
}
|
|
704
|
-
|
|
1040
|
+
const model = current.model;
|
|
1041
|
+
if (isOpencodeTokenBuddyModelRef(model)) {
|
|
705
1042
|
delete current.model;
|
|
706
1043
|
changed = true;
|
|
707
1044
|
}
|
|
708
|
-
|
|
1045
|
+
const smallModel = current.small_model;
|
|
1046
|
+
if (isOpencodeTokenBuddyModelRef(smallModel)) {
|
|
709
1047
|
delete current.small_model;
|
|
710
1048
|
changed = true;
|
|
711
1049
|
}
|
|
@@ -725,39 +1063,117 @@ function hermesConfig(home, proxyUrl, config) {
|
|
|
725
1063
|
const existing = readText(configPath) || "";
|
|
726
1064
|
const current = parseSimpleYamlObject(existing);
|
|
727
1065
|
const modelConfig = isPlainRecord(current.model) ? current.model : {};
|
|
1066
|
+
const baseUrl = openAiBaseUrl(proxyUrl);
|
|
728
1067
|
const nextModelConfig = {
|
|
729
1068
|
...modelConfig,
|
|
730
1069
|
default: model,
|
|
731
|
-
provider:
|
|
732
|
-
base_url:
|
|
1070
|
+
provider: HERMES_PROVIDER_ID,
|
|
1071
|
+
base_url: baseUrl,
|
|
733
1072
|
api_key: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
734
1073
|
api_mode: "chat_completions",
|
|
735
1074
|
};
|
|
736
|
-
const
|
|
1075
|
+
const withModel = replaceTopLevelYamlSection(existing, "model", yamlContent(nextModelConfig));
|
|
1076
|
+
const content = upsertTopLevelYamlObjectEntry(withModel, "providers", HERMES_PROVIDER_ID, {
|
|
1077
|
+
name: HERMES_PROVIDER_ID,
|
|
1078
|
+
base_url: baseUrl,
|
|
1079
|
+
api_key: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
1080
|
+
transport: "chat_completions",
|
|
1081
|
+
default_model: model,
|
|
1082
|
+
});
|
|
737
1083
|
return [makeChange("hermes", configPath, "configure Hermes OpenAI proxy settings", content)];
|
|
738
1084
|
}
|
|
739
|
-
function
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1085
|
+
function isHermesTokenBuddyBaseUrl(value) {
|
|
1086
|
+
return typeof value === "string" &&
|
|
1087
|
+
value.includes("127.0.0.1") &&
|
|
1088
|
+
value.endsWith("/v1");
|
|
1089
|
+
}
|
|
1090
|
+
function isHermesTokenBuddyProviderValue(value) {
|
|
1091
|
+
if (typeof value !== "string") {
|
|
743
1092
|
return false;
|
|
744
1093
|
}
|
|
745
|
-
|
|
1094
|
+
const normalized = value.trim().toLowerCase();
|
|
1095
|
+
return normalized === "custom" ||
|
|
1096
|
+
normalized === HERMES_PROVIDER_ID.toLowerCase() ||
|
|
1097
|
+
normalized === "custom:tokenbuddy";
|
|
1098
|
+
}
|
|
1099
|
+
function isHermesTokenBuddyProviderEntry(entry) {
|
|
1100
|
+
return entry.api_key === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
1101
|
+
isHermesTokenBuddyBaseUrl(entry.base_url) &&
|
|
1102
|
+
(entry.transport === "chat_completions" || entry.api_mode === "chat_completions");
|
|
1103
|
+
}
|
|
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
|
+
function isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured) {
|
|
1140
|
+
return isHermesTokenBuddyProviderValue(modelConfig.provider) &&
|
|
746
1141
|
modelConfig.api_key === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
747
1142
|
modelConfig.api_mode === "chat_completions" &&
|
|
748
|
-
|
|
749
|
-
modelConfig.base_url.includes("127.0.0.1") &&
|
|
750
|
-
modelConfig.base_url.endsWith("/v1") &&
|
|
1143
|
+
(isHermesTokenBuddyBaseUrl(modelConfig.base_url) || namedProviderConfigured) &&
|
|
751
1144
|
typeof modelConfig.default === "string" &&
|
|
752
1145
|
modelConfig.default.length > 0;
|
|
753
1146
|
}
|
|
1147
|
+
function isHermesTokenBuddyConfigured(filePath) {
|
|
1148
|
+
const text = readText(filePath) || "";
|
|
1149
|
+
const current = readYamlObject(filePath);
|
|
1150
|
+
const modelConfig = readObjectField(current, "model");
|
|
1151
|
+
if (!modelConfig) {
|
|
1152
|
+
return hasHermesTokenBuddyCustomProvider(text);
|
|
1153
|
+
}
|
|
1154
|
+
const hasTokenBuddyCustomProvider = hasHermesTokenBuddyCustomProvider(text);
|
|
1155
|
+
const providersConfig = readObjectField(current, "providers");
|
|
1156
|
+
const tokenBuddyProvider = readObjectField(providersConfig, HERMES_PROVIDER_ID);
|
|
1157
|
+
const namedProviderConfigured = Boolean(tokenBuddyProvider && isHermesTokenBuddyProviderEntry(tokenBuddyProvider));
|
|
1158
|
+
const hasTokenBuddyModel = isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured);
|
|
1159
|
+
return hasTokenBuddyModel || hasTokenBuddyCustomProvider;
|
|
1160
|
+
}
|
|
754
1161
|
function cleanupHermesConfig(home) {
|
|
755
1162
|
const configPath = path.join(home, ".hermes", "config.yaml");
|
|
756
1163
|
if (!fs.existsSync(configPath) || !isHermesTokenBuddyConfigured(configPath)) {
|
|
757
1164
|
return [];
|
|
758
1165
|
}
|
|
759
1166
|
const existing = readText(configPath) || "";
|
|
760
|
-
const
|
|
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);
|
|
761
1177
|
if (next.trim()) {
|
|
762
1178
|
fs.writeFileSync(configPath, next, "utf8");
|
|
763
1179
|
return [{ providerId: "hermes", path: configPath, action: "cleaned" }];
|
|
@@ -771,7 +1187,9 @@ const PROVIDERS = [
|
|
|
771
1187
|
name: "Codex CLI",
|
|
772
1188
|
commandName: "codex",
|
|
773
1189
|
configPath: (home) => path.join(home, ".codex", "config.toml"),
|
|
1190
|
+
isConfigured: isCodexTokenBuddyConfigured,
|
|
774
1191
|
changes: codexConfig,
|
|
1192
|
+
cleanup: cleanupCodexConfig,
|
|
775
1193
|
modelSelectionKind: "single-model",
|
|
776
1194
|
protocolPreference: "responses",
|
|
777
1195
|
},
|
|
@@ -982,6 +1400,8 @@ export function applyProviderInstall(options, store) {
|
|
|
982
1400
|
/**
|
|
983
1401
|
* 回滚 provider 安装。
|
|
984
1402
|
* 从 `store` 读取安装前的快照,恢复原文件(如果快照里有原始内容)。
|
|
1403
|
+
* 恢复后仍会执行 provider cleanup,确保 repeated apply 或旧 TokenBuddy
|
|
1404
|
+
* 配置升级后的 disconnect 语义是移除 TokenBuddy,而不是恢复旧 TokenBuddy。
|
|
985
1405
|
* 没有快照的 provider 标记为 `missing_snapshot`。
|
|
986
1406
|
*
|
|
987
1407
|
* @param options 回滚选项
|
|
@@ -1020,6 +1440,7 @@ export function rollbackProviderInstall(options, store) {
|
|
|
1020
1440
|
results.push({ providerId, path: file.path, action: "removed" });
|
|
1021
1441
|
}
|
|
1022
1442
|
}
|
|
1443
|
+
results.push(...(provider.cleanup?.(home) ?? []));
|
|
1023
1444
|
store.removeProviderInstallSnapshot(providerId);
|
|
1024
1445
|
store.removeProviderRuntimeConfig(providerId);
|
|
1025
1446
|
}
|