@tokenbuddy/tokenbuddy 1.0.5 → 1.0.7
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 +48 -1
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +144 -17
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +17 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +560 -63
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +11 -5
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +574 -161
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
- package/dist/src/doctor-clawtip-wallet.js +54 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -0
- package/dist/src/doctor-diagnostics.d.ts +99 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -0
- package/dist/src/doctor-diagnostics.js +552 -0
- package/dist/src/doctor-diagnostics.js.map +1 -0
- package/dist/src/init-clawtip-activation.d.ts +48 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -0
- package/dist/src/init-clawtip-activation.js +395 -0
- package/dist/src/init-clawtip-activation.js.map +1 -0
- package/dist/src/init-payment-options.d.ts +56 -0
- package/dist/src/init-payment-options.d.ts.map +1 -0
- package/dist/src/init-payment-options.js +165 -0
- package/dist/src/init-payment-options.js.map +1 -0
- package/dist/src/provider-install.d.ts +37 -2
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +317 -67
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +79 -0
- package/dist/src/seller-catalog.d.ts.map +1 -0
- package/dist/src/seller-catalog.js +126 -0
- package/dist/src/seller-catalog.js.map +1 -0
- package/dist/src/tb-proxyd.js +13 -2
- package/dist/src/tb-proxyd.js.map +1 -1
- package/dist/src/terminal-image.d.ts +22 -0
- package/dist/src/terminal-image.d.ts.map +1 -0
- package/dist/src/terminal-image.js +135 -0
- package/dist/src/terminal-image.js.map +1 -0
- package/package.json +1 -1
- package/src/buyer-store.ts +253 -18
- package/src/cli.ts +709 -68
- package/src/daemon.ts +651 -167
- package/src/doctor-clawtip-wallet.ts +70 -0
- package/src/doctor-diagnostics.ts +861 -0
- package/src/init-clawtip-activation.ts +487 -0
- package/src/init-payment-options.ts +249 -0
- package/src/provider-install.ts +426 -76
- package/src/seller-catalog.ts +222 -0
- package/src/tb-proxyd.ts +14 -2
- package/src/terminal-image.ts +187 -0
- package/tests/e2e.test.ts +88 -5
- package/tests/tokenbuddy.test.ts +1362 -27
package/src/provider-install.ts
CHANGED
|
@@ -1,29 +1,75 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
BuyerStore,
|
|
6
|
+
ProviderInstallSnapshot,
|
|
7
|
+
} from "./buyer-store.js";
|
|
8
|
+
import {
|
|
9
|
+
ProtocolPreference,
|
|
10
|
+
SellerRoutingPreference,
|
|
11
|
+
} from "./seller-catalog.js";
|
|
12
|
+
|
|
13
|
+
export const PROXY_ACCESS_TOKEN_PLACEHOLDER = "TOKENBUDDY_PROXY";
|
|
7
14
|
const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
|
|
15
|
+
const CLAUDE_ONE_M_MARKER = "[1M]";
|
|
16
|
+
const CLAUDE_CLIENT_HAIKU_MODEL = "claude-haiku-4-5";
|
|
17
|
+
const CLAUDE_CLIENT_SONNET_MODEL = "claude-sonnet-4-6";
|
|
18
|
+
const CLAUDE_CLIENT_OPUS_MODEL = "claude-opus-4-7";
|
|
19
|
+
const ROUTING_CONFIG_KEY = "routing";
|
|
8
20
|
|
|
9
21
|
export const SUPPORTED_PROVIDER_IDS = [
|
|
10
22
|
"codex",
|
|
11
23
|
"claude-code",
|
|
12
24
|
"claude-desktop",
|
|
13
25
|
"openclaw",
|
|
14
|
-
"
|
|
26
|
+
"opencode",
|
|
27
|
+
"hermes",
|
|
15
28
|
] as const;
|
|
16
29
|
|
|
17
30
|
export type ProviderId = typeof SUPPORTED_PROVIDER_IDS[number];
|
|
31
|
+
export type ModelSelectionKind = "single-model" | "claude-role-mapping";
|
|
18
32
|
|
|
19
33
|
export interface ProviderDetectOptions {
|
|
20
34
|
home?: string;
|
|
21
35
|
}
|
|
22
36
|
|
|
37
|
+
export interface SingleModelProviderRuntimeConfig {
|
|
38
|
+
selectionKind: "single-model";
|
|
39
|
+
protocolPreference?: ProtocolPreference;
|
|
40
|
+
defaultModel: string;
|
|
41
|
+
sellerId?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ClaudeRoleBinding {
|
|
45
|
+
upstreamModel: string;
|
|
46
|
+
displayName?: string;
|
|
47
|
+
declareOneM?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ClaudeCodeModelMappingConfig {
|
|
51
|
+
selectionKind: "claude-role-mapping";
|
|
52
|
+
protocolPreference?: ProtocolPreference;
|
|
53
|
+
fallbackModel?: string;
|
|
54
|
+
roles: {
|
|
55
|
+
haiku?: ClaudeRoleBinding;
|
|
56
|
+
sonnet?: ClaudeRoleBinding;
|
|
57
|
+
opus?: ClaudeRoleBinding;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type ProviderRuntimeConfig =
|
|
62
|
+
| SingleModelProviderRuntimeConfig
|
|
63
|
+
| ClaudeCodeModelMappingConfig;
|
|
64
|
+
|
|
65
|
+
export type ProviderSelections = Partial<Record<ProviderId, ProviderRuntimeConfig>>;
|
|
66
|
+
|
|
23
67
|
export interface ProviderInstallOptions extends ProviderDetectOptions {
|
|
24
68
|
providers: string[];
|
|
25
69
|
proxyUrl: string;
|
|
26
|
-
model
|
|
70
|
+
model?: string;
|
|
71
|
+
providerSelections?: ProviderSelections;
|
|
72
|
+
sellerRouting?: SellerRoutingPreference;
|
|
27
73
|
}
|
|
28
74
|
|
|
29
75
|
export interface ProviderRollbackOptions extends ProviderDetectOptions {
|
|
@@ -34,7 +80,12 @@ export interface ProviderCandidate {
|
|
|
34
80
|
id: ProviderId;
|
|
35
81
|
name: string;
|
|
36
82
|
detected: boolean;
|
|
83
|
+
configured: boolean;
|
|
84
|
+
status: "configured" | "installed" | "missing";
|
|
37
85
|
configPath: string;
|
|
86
|
+
commandName?: string;
|
|
87
|
+
executablePath?: string;
|
|
88
|
+
observedPaths?: string[];
|
|
38
89
|
reason: string;
|
|
39
90
|
}
|
|
40
91
|
|
|
@@ -63,7 +114,11 @@ interface ProviderDefinition {
|
|
|
63
114
|
id: ProviderId;
|
|
64
115
|
name: string;
|
|
65
116
|
configPath(home: string): string;
|
|
66
|
-
|
|
117
|
+
commandName?: string;
|
|
118
|
+
observedPaths?(home: string): string[];
|
|
119
|
+
changes(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[];
|
|
120
|
+
modelSelectionKind: ModelSelectionKind;
|
|
121
|
+
protocolPreference?: ProtocolPreference;
|
|
67
122
|
}
|
|
68
123
|
|
|
69
124
|
function resolveHome(home?: string): string {
|
|
@@ -105,7 +160,7 @@ function readJsonObject(filePath: string): Record<string, unknown> {
|
|
|
105
160
|
try {
|
|
106
161
|
const parsed = JSON.parse(text) as unknown;
|
|
107
162
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
108
|
-
? parsed as Record<string, unknown>
|
|
163
|
+
? (parsed as Record<string, unknown>)
|
|
109
164
|
: {};
|
|
110
165
|
} catch {
|
|
111
166
|
return {};
|
|
@@ -116,6 +171,33 @@ function jsonContent(value: unknown): string {
|
|
|
116
171
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
117
172
|
}
|
|
118
173
|
|
|
174
|
+
function displayPath(home: string, filePath: string): string {
|
|
175
|
+
return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveExecutable(commandName: string): string | undefined {
|
|
179
|
+
const pathValue = process.env.PATH || "";
|
|
180
|
+
const pathEntries = pathValue.split(path.delimiter).filter(Boolean);
|
|
181
|
+
const windowsExts = process.platform === "win32"
|
|
182
|
+
? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM")
|
|
183
|
+
.split(";")
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
: [""];
|
|
186
|
+
|
|
187
|
+
for (const entry of pathEntries) {
|
|
188
|
+
for (const ext of windowsExts) {
|
|
189
|
+
const candidate = path.join(entry, process.platform === "win32" ? `${commandName}${ext}` : commandName);
|
|
190
|
+
try {
|
|
191
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
192
|
+
return candidate;
|
|
193
|
+
} catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
119
201
|
function escapeTomlString(value: string): string {
|
|
120
202
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
121
203
|
}
|
|
@@ -130,34 +212,201 @@ function replaceTomlSection(existing: string, sectionName: string, sectionBody:
|
|
|
130
212
|
return `${normalized}${normalized ? "\n\n" : ""}${nextSection}`;
|
|
131
213
|
}
|
|
132
214
|
|
|
133
|
-
function
|
|
215
|
+
function stripClaudeOneMMarker(model: string): string {
|
|
216
|
+
const trimmed = model.trimEnd();
|
|
217
|
+
if (!trimmed.toLowerCase().endsWith(CLAUDE_ONE_M_MARKER.toLowerCase())) {
|
|
218
|
+
return model;
|
|
219
|
+
}
|
|
220
|
+
return trimmed.slice(0, -CLAUDE_ONE_M_MARKER.length).trimEnd();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function setClaudeOneMMarker(model: string, enabled: boolean): string {
|
|
224
|
+
const base = stripClaudeOneMMarker(model).trim();
|
|
225
|
+
if (!base) {
|
|
226
|
+
return "";
|
|
227
|
+
}
|
|
228
|
+
return enabled ? `${base}${CLAUDE_ONE_M_MARKER}` : base;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function makeChange(providerId: ProviderId, filePath: string, summary: string, content: string): ProviderFileChange {
|
|
232
|
+
const existed = fs.existsSync(filePath);
|
|
233
|
+
return {
|
|
234
|
+
providerId,
|
|
235
|
+
path: filePath,
|
|
236
|
+
action: existed ? "update" : "create",
|
|
237
|
+
existed,
|
|
238
|
+
summary,
|
|
239
|
+
content,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function makeSingleModelRuntimeConfig(
|
|
244
|
+
provider: ProviderDefinition,
|
|
245
|
+
model: string,
|
|
246
|
+
sellerId?: string,
|
|
247
|
+
): SingleModelProviderRuntimeConfig {
|
|
248
|
+
return {
|
|
249
|
+
selectionKind: "single-model",
|
|
250
|
+
protocolPreference: provider.protocolPreference,
|
|
251
|
+
defaultModel: model,
|
|
252
|
+
sellerId,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function pickConfiguredModel(config: ProviderRuntimeConfig): string {
|
|
257
|
+
if (config.selectionKind === "single-model") {
|
|
258
|
+
return config.defaultModel;
|
|
259
|
+
}
|
|
260
|
+
const sonnetModel = config.roles.sonnet?.upstreamModel;
|
|
261
|
+
const opusModel = config.roles.opus?.upstreamModel;
|
|
262
|
+
const haikuModel = config.roles.haiku?.upstreamModel;
|
|
263
|
+
return sonnetModel || opusModel || haikuModel || config.fallbackModel || "";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function resolveProviderRuntimeConfig(
|
|
267
|
+
provider: ProviderDefinition,
|
|
268
|
+
options: ProviderInstallOptions,
|
|
269
|
+
): ProviderRuntimeConfig {
|
|
270
|
+
const selection = options.providerSelections?.[provider.id];
|
|
271
|
+
if (selection) {
|
|
272
|
+
return selection;
|
|
273
|
+
}
|
|
274
|
+
const model = options.model?.trim();
|
|
275
|
+
if (!model) {
|
|
276
|
+
throw new Error(`model is required for provider ${provider.id}`);
|
|
277
|
+
}
|
|
278
|
+
return makeSingleModelRuntimeConfig(
|
|
279
|
+
provider,
|
|
280
|
+
model,
|
|
281
|
+
options.sellerRouting?.mode === "fixed" ? options.sellerRouting.sellerId : undefined,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function codexConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
|
|
286
|
+
const model = pickConfiguredModel(config);
|
|
134
287
|
const configPath = path.join(home, ".codex", "config.toml");
|
|
135
288
|
const existing = readText(configPath) || "";
|
|
136
|
-
const content = replaceTomlSection(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
289
|
+
const content = replaceTomlSection(
|
|
290
|
+
existing,
|
|
291
|
+
"tokenbuddy",
|
|
292
|
+
[
|
|
293
|
+
`proxy_url = "${escapeTomlString(proxyUrl)}"`,
|
|
294
|
+
`api_key = "${PROXY_ACCESS_TOKEN_PLACEHOLDER}"`,
|
|
295
|
+
`model = "${escapeTomlString(model)}"`,
|
|
296
|
+
].join("\n"),
|
|
297
|
+
);
|
|
141
298
|
return [makeChange("codex", configPath, "configure TokenBuddy proxy for Codex", content)];
|
|
142
299
|
}
|
|
143
300
|
|
|
144
|
-
function
|
|
301
|
+
function resolveClaudeFallbackAlias(config: ClaudeCodeModelMappingConfig): string {
|
|
302
|
+
if (config.roles.sonnet?.upstreamModel) {
|
|
303
|
+
return setClaudeOneMMarker(
|
|
304
|
+
CLAUDE_CLIENT_SONNET_MODEL,
|
|
305
|
+
Boolean(config.roles.sonnet.declareOneM),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (config.roles.opus?.upstreamModel) {
|
|
309
|
+
return setClaudeOneMMarker(
|
|
310
|
+
CLAUDE_CLIENT_OPUS_MODEL,
|
|
311
|
+
Boolean(config.roles.opus.declareOneM),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (config.roles.haiku?.upstreamModel) {
|
|
315
|
+
return CLAUDE_CLIENT_HAIKU_MODEL;
|
|
316
|
+
}
|
|
317
|
+
return CLAUDE_CLIENT_SONNET_MODEL;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildClaudeRoleEnv(config: ClaudeCodeModelMappingConfig): Record<string, string> {
|
|
321
|
+
const env: Record<string, string> = {};
|
|
322
|
+
const roleConfigs: Array<{
|
|
323
|
+
binding?: ClaudeRoleBinding;
|
|
324
|
+
modelKey: string;
|
|
325
|
+
displayNameKey: string;
|
|
326
|
+
alias: string;
|
|
327
|
+
allowOneM: boolean;
|
|
328
|
+
}> = [
|
|
329
|
+
{
|
|
330
|
+
binding: config.roles.haiku,
|
|
331
|
+
modelKey: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
332
|
+
displayNameKey: "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
|
|
333
|
+
alias: CLAUDE_CLIENT_HAIKU_MODEL,
|
|
334
|
+
allowOneM: false,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
binding: config.roles.sonnet,
|
|
338
|
+
modelKey: "ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
339
|
+
displayNameKey: "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
|
340
|
+
alias: CLAUDE_CLIENT_SONNET_MODEL,
|
|
341
|
+
allowOneM: true,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
binding: config.roles.opus,
|
|
345
|
+
modelKey: "ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
346
|
+
displayNameKey: "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
|
347
|
+
alias: CLAUDE_CLIENT_OPUS_MODEL,
|
|
348
|
+
allowOneM: true,
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
for (const role of roleConfigs) {
|
|
353
|
+
if (!role.binding?.upstreamModel?.trim()) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
env[role.modelKey] = role.allowOneM
|
|
357
|
+
? setClaudeOneMMarker(role.alias, Boolean(role.binding.declareOneM))
|
|
358
|
+
: role.alias;
|
|
359
|
+
const displayName = role.binding.displayName?.trim() || stripClaudeOneMMarker(role.binding.upstreamModel).trim();
|
|
360
|
+
if (displayName) {
|
|
361
|
+
env[role.displayNameKey] = displayName;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
env.ANTHROPIC_MODEL = resolveClaudeFallbackAlias(config);
|
|
366
|
+
return env;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function claudeCodeConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
|
|
145
370
|
const configPath = path.join(home, ".claude", "settings.json");
|
|
146
|
-
const
|
|
147
|
-
const env =
|
|
148
|
-
?
|
|
371
|
+
const current = readJsonObject(configPath);
|
|
372
|
+
const env = current.env && typeof current.env === "object" && !Array.isArray(current.env)
|
|
373
|
+
? (current.env as Record<string, unknown>)
|
|
149
374
|
: {};
|
|
150
|
-
|
|
375
|
+
|
|
376
|
+
const nextEnv: Record<string, unknown> = {
|
|
151
377
|
...env,
|
|
152
378
|
ANTHROPIC_BASE_URL: proxyUrl,
|
|
153
|
-
ANTHROPIC_AUTH_TOKEN:
|
|
154
|
-
ANTHROPIC_MODEL: model,
|
|
155
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: model
|
|
379
|
+
ANTHROPIC_AUTH_TOKEN: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
156
380
|
};
|
|
157
|
-
|
|
381
|
+
|
|
382
|
+
delete nextEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
383
|
+
delete nextEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME;
|
|
384
|
+
delete nextEnv.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
385
|
+
delete nextEnv.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME;
|
|
386
|
+
delete nextEnv.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
387
|
+
delete nextEnv.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME;
|
|
388
|
+
delete nextEnv.ANTHROPIC_MODEL;
|
|
389
|
+
|
|
390
|
+
if (config.selectionKind === "claude-role-mapping") {
|
|
391
|
+
Object.assign(nextEnv, buildClaudeRoleEnv(config));
|
|
392
|
+
} else {
|
|
393
|
+
nextEnv.ANTHROPIC_MODEL = config.defaultModel;
|
|
394
|
+
nextEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = config.defaultModel;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
current.env = nextEnv;
|
|
398
|
+
return [
|
|
399
|
+
makeChange(
|
|
400
|
+
"claude-code",
|
|
401
|
+
configPath,
|
|
402
|
+
"configure Anthropic proxy env for Claude Code",
|
|
403
|
+
jsonContent(current),
|
|
404
|
+
),
|
|
405
|
+
];
|
|
158
406
|
}
|
|
159
407
|
|
|
160
|
-
function claudeDesktopConfig(home: string, proxyUrl: string,
|
|
408
|
+
function claudeDesktopConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
|
|
409
|
+
const model = pickConfiguredModel(config);
|
|
161
410
|
const configDir = path.join(home, "Library", "Application Support", "Claude");
|
|
162
411
|
const configPath = path.join(configDir, "claude_desktop_config.json");
|
|
163
412
|
const threepDir = path.join(home, "Library", "Application Support", "Claude-3p");
|
|
@@ -173,134 +422,223 @@ function claudeDesktopConfig(home: string, proxyUrl: string, model: string): Pro
|
|
|
173
422
|
|
|
174
423
|
const profile = {
|
|
175
424
|
disableDeploymentModeChooser: true,
|
|
176
|
-
inferenceGatewayApiKey:
|
|
425
|
+
inferenceGatewayApiKey: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
177
426
|
inferenceGatewayAuthScheme: "bearer",
|
|
178
427
|
inferenceGatewayBaseUrl: proxyUrl,
|
|
179
428
|
inferenceProvider: "gateway",
|
|
180
|
-
inferenceModels: [{ name: model }]
|
|
429
|
+
inferenceModels: [{ name: model }],
|
|
181
430
|
};
|
|
182
431
|
|
|
183
432
|
const meta = readJsonObject(metaPath);
|
|
184
433
|
const existingEntries = Array.isArray(meta.entries) ? meta.entries : [];
|
|
185
434
|
meta.appliedId = DESKTOP_PROFILE_ID;
|
|
186
435
|
meta.entries = [
|
|
187
|
-
...existingEntries.filter((entry) =>
|
|
188
|
-
|
|
189
|
-
}),
|
|
190
|
-
{ id: DESKTOP_PROFILE_ID, name: "TokenBuddy" }
|
|
436
|
+
...existingEntries.filter((entry) => !(entry && typeof entry === "object" && "id" in entry && entry.id === DESKTOP_PROFILE_ID)),
|
|
437
|
+
{ id: DESKTOP_PROFILE_ID, name: "TokenBuddy" },
|
|
191
438
|
];
|
|
192
439
|
|
|
193
440
|
return [
|
|
194
441
|
makeChange("claude-desktop", configPath, "enable Claude Desktop 3p deployment mode", jsonContent(primary)),
|
|
195
442
|
makeChange("claude-desktop", threepConfigPath, "enable Claude Desktop 3p config", jsonContent(threep)),
|
|
196
443
|
makeChange("claude-desktop", profilePath, "write TokenBuddy Claude Desktop profile", jsonContent(profile)),
|
|
197
|
-
makeChange("claude-desktop", metaPath, "select TokenBuddy Claude Desktop profile", jsonContent(meta))
|
|
444
|
+
makeChange("claude-desktop", metaPath, "select TokenBuddy Claude Desktop profile", jsonContent(meta)),
|
|
198
445
|
];
|
|
199
446
|
}
|
|
200
447
|
|
|
201
|
-
function openclawConfig(home: string, proxyUrl: string,
|
|
448
|
+
function openclawConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
|
|
449
|
+
const model = pickConfiguredModel(config);
|
|
202
450
|
const configPath = path.join(home, ".openclaw", "config.json");
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return [makeChange("openclaw", configPath, "configure OpenClaw proxy settings", jsonContent(
|
|
451
|
+
const current = readJsonObject(configPath);
|
|
452
|
+
current.api_url = proxyUrl;
|
|
453
|
+
current.api_key = PROXY_ACCESS_TOKEN_PLACEHOLDER;
|
|
454
|
+
current.model = model;
|
|
455
|
+
return [makeChange("openclaw", configPath, "configure OpenClaw proxy settings", jsonContent(current))];
|
|
208
456
|
}
|
|
209
457
|
|
|
210
|
-
function
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
458
|
+
function openAiBaseUrl(proxyUrl: string): string {
|
|
459
|
+
const normalized = proxyUrl.replace(/\/+$/, "");
|
|
460
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function opencodeConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
|
|
464
|
+
const model = pickConfiguredModel(config);
|
|
465
|
+
const configPath = path.join(home, ".config", "opencode", "opencode.json");
|
|
466
|
+
const current = readJsonObject(configPath);
|
|
467
|
+
const providers = current.provider && typeof current.provider === "object" && !Array.isArray(current.provider)
|
|
468
|
+
? (current.provider as Record<string, unknown>)
|
|
215
469
|
: {};
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
470
|
+
providers.tokenbuddy = {
|
|
471
|
+
name: "TokenBuddy",
|
|
472
|
+
npm: "@ai-sdk/openai",
|
|
473
|
+
options: {
|
|
474
|
+
apiKey: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
475
|
+
baseURL: openAiBaseUrl(proxyUrl),
|
|
476
|
+
},
|
|
477
|
+
models: {
|
|
478
|
+
[model]: {
|
|
479
|
+
name: model,
|
|
480
|
+
attachment: true,
|
|
481
|
+
tool_call: true,
|
|
482
|
+
},
|
|
483
|
+
},
|
|
221
484
|
};
|
|
222
|
-
|
|
485
|
+
current.provider = providers;
|
|
486
|
+
return [makeChange("opencode", configPath, "configure OpenCode provider for TokenBuddy proxy", jsonContent(current))];
|
|
223
487
|
}
|
|
224
488
|
|
|
225
|
-
function
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
489
|
+
function hermesConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
|
|
490
|
+
const model = pickConfiguredModel(config);
|
|
491
|
+
const configPath = path.join(home, ".hermes", "settings.json");
|
|
492
|
+
const current = readJsonObject(configPath);
|
|
493
|
+
const openai = current.openai && typeof current.openai === "object" && !Array.isArray(current.openai)
|
|
494
|
+
? (current.openai as Record<string, unknown>)
|
|
495
|
+
: {};
|
|
496
|
+
current.openai = {
|
|
497
|
+
...openai,
|
|
498
|
+
base_url: proxyUrl,
|
|
499
|
+
api_key: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
500
|
+
model,
|
|
234
501
|
};
|
|
502
|
+
return [makeChange("hermes", configPath, "configure Hermes OpenAI proxy settings", jsonContent(current))];
|
|
235
503
|
}
|
|
236
504
|
|
|
237
505
|
const PROVIDERS: ProviderDefinition[] = [
|
|
238
506
|
{
|
|
239
507
|
id: "codex",
|
|
240
508
|
name: "Codex CLI",
|
|
509
|
+
commandName: "codex",
|
|
241
510
|
configPath: (home) => path.join(home, ".codex", "config.toml"),
|
|
242
|
-
changes: codexConfig
|
|
511
|
+
changes: codexConfig,
|
|
512
|
+
modelSelectionKind: "single-model",
|
|
513
|
+
protocolPreference: "responses",
|
|
243
514
|
},
|
|
244
515
|
{
|
|
245
516
|
id: "claude-code",
|
|
246
517
|
name: "Claude Code CLI",
|
|
518
|
+
commandName: "claude",
|
|
247
519
|
configPath: (home) => path.join(home, ".claude", "settings.json"),
|
|
248
|
-
changes: claudeCodeConfig
|
|
520
|
+
changes: claudeCodeConfig,
|
|
521
|
+
modelSelectionKind: "claude-role-mapping",
|
|
522
|
+
protocolPreference: "messages",
|
|
249
523
|
},
|
|
250
524
|
{
|
|
251
525
|
id: "claude-desktop",
|
|
252
526
|
name: "Claude Desktop App",
|
|
253
527
|
configPath: (home) => path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
254
|
-
changes: claudeDesktopConfig
|
|
528
|
+
changes: claudeDesktopConfig,
|
|
529
|
+
modelSelectionKind: "single-model",
|
|
530
|
+
protocolPreference: "messages",
|
|
255
531
|
},
|
|
256
532
|
{
|
|
257
533
|
id: "openclaw",
|
|
258
534
|
name: "OpenClaw Agent",
|
|
535
|
+
commandName: "openclaw",
|
|
259
536
|
configPath: (home) => path.join(home, ".openclaw", "config.json"),
|
|
260
|
-
|
|
537
|
+
observedPaths: (home) => [
|
|
538
|
+
path.join(home, ".openclaw", "openclaw.json"),
|
|
539
|
+
path.join(home, ".openclaw", "configs"),
|
|
540
|
+
],
|
|
541
|
+
changes: openclawConfig,
|
|
542
|
+
modelSelectionKind: "single-model",
|
|
543
|
+
protocolPreference: "chat_completions",
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
id: "opencode",
|
|
547
|
+
name: "OpenCode",
|
|
548
|
+
commandName: "opencode",
|
|
549
|
+
configPath: (home) => path.join(home, ".config", "opencode", "opencode.json"),
|
|
550
|
+
changes: opencodeConfig,
|
|
551
|
+
modelSelectionKind: "single-model",
|
|
552
|
+
protocolPreference: "responses",
|
|
261
553
|
},
|
|
262
554
|
{
|
|
263
555
|
id: "hermes",
|
|
264
556
|
name: "Hermes Terminal",
|
|
557
|
+
commandName: "hermes",
|
|
265
558
|
configPath: (home) => path.join(home, ".hermes", "settings.json"),
|
|
266
|
-
|
|
267
|
-
|
|
559
|
+
observedPaths: (home) => [
|
|
560
|
+
path.join(home, ".hermes", "config.yaml"),
|
|
561
|
+
path.join(home, ".hermes", "auth.json"),
|
|
562
|
+
],
|
|
563
|
+
changes: hermesConfig,
|
|
564
|
+
modelSelectionKind: "single-model",
|
|
565
|
+
protocolPreference: "chat_completions",
|
|
566
|
+
},
|
|
268
567
|
];
|
|
269
568
|
|
|
569
|
+
function getProviderDefinition(providerId: ProviderId): ProviderDefinition {
|
|
570
|
+
const provider = PROVIDERS.find((entry) => entry.id === providerId);
|
|
571
|
+
if (!provider) {
|
|
572
|
+
throw new Error(`unsupported provider: ${providerId}`);
|
|
573
|
+
}
|
|
574
|
+
return provider;
|
|
575
|
+
}
|
|
576
|
+
|
|
270
577
|
export function detectProviders(options: ProviderDetectOptions = {}): ProviderCandidate[] {
|
|
271
578
|
const home = resolveHome(options.home);
|
|
272
579
|
return PROVIDERS.map((provider) => {
|
|
273
580
|
const configPath = provider.configPath(home);
|
|
274
|
-
const
|
|
581
|
+
const configured = fs.existsSync(configPath);
|
|
582
|
+
const executablePath = provider.commandName ? resolveExecutable(provider.commandName) : undefined;
|
|
583
|
+
const observedPaths = provider.observedPaths?.(home).filter((entry) => fs.existsSync(entry)) || [];
|
|
584
|
+
const installed = Boolean(executablePath) || observedPaths.length > 0;
|
|
585
|
+
const status: ProviderCandidate["status"] = configured
|
|
586
|
+
? "configured"
|
|
587
|
+
: installed
|
|
588
|
+
? "installed"
|
|
589
|
+
: "missing";
|
|
590
|
+
const reasonParts: string[] = [];
|
|
591
|
+
if (configured) {
|
|
592
|
+
reasonParts.push(`Configured at ${displayPath(home, configPath)}`);
|
|
593
|
+
} else if (installed) {
|
|
594
|
+
reasonParts.push(`Installed, TokenBuddy config missing at ${displayPath(home, configPath)}`);
|
|
595
|
+
} else {
|
|
596
|
+
reasonParts.push(`Missing ${displayPath(home, configPath)}`);
|
|
597
|
+
}
|
|
598
|
+
if (executablePath) {
|
|
599
|
+
reasonParts.push(`CLI ${displayPath(home, executablePath)}`);
|
|
600
|
+
}
|
|
601
|
+
if (observedPaths.length > 0) {
|
|
602
|
+
reasonParts.push(`Native files ${observedPaths.map((entry) => displayPath(home, entry)).join(", ")}`);
|
|
603
|
+
}
|
|
275
604
|
return {
|
|
276
605
|
id: provider.id,
|
|
277
606
|
name: provider.name,
|
|
278
|
-
detected,
|
|
607
|
+
detected: status !== "missing",
|
|
608
|
+
configured,
|
|
609
|
+
status,
|
|
279
610
|
configPath,
|
|
280
|
-
|
|
611
|
+
commandName: provider.commandName,
|
|
612
|
+
executablePath,
|
|
613
|
+
observedPaths,
|
|
614
|
+
reason: reasonParts.join(" · "),
|
|
281
615
|
};
|
|
282
616
|
});
|
|
283
617
|
}
|
|
284
618
|
|
|
619
|
+
export function getProviderProtocolPreference(providerId: ProviderId): ProtocolPreference | undefined {
|
|
620
|
+
return getProviderDefinition(providerId).protocolPreference;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function getProviderModelSelectionKind(providerId: ProviderId): ModelSelectionKind {
|
|
624
|
+
return getProviderDefinition(providerId).modelSelectionKind;
|
|
625
|
+
}
|
|
626
|
+
|
|
285
627
|
export function previewProviderInstall(options: ProviderInstallOptions): ProviderFileChange[] {
|
|
286
628
|
const home = resolveHome(options.home);
|
|
287
629
|
const providerIds = assertProviderIds(options.providers);
|
|
288
630
|
if (!options.proxyUrl || !options.proxyUrl.trim()) {
|
|
289
631
|
throw new Error("proxyUrl is required");
|
|
290
632
|
}
|
|
291
|
-
if (!options.model || !options.model.trim()) {
|
|
292
|
-
throw new Error("model is required");
|
|
293
|
-
}
|
|
294
633
|
return providerIds.flatMap((providerId) => {
|
|
295
|
-
const provider =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
return provider.changes(home, options.proxyUrl, options.model);
|
|
634
|
+
const provider = getProviderDefinition(providerId);
|
|
635
|
+
const runtimeConfig = resolveProviderRuntimeConfig(provider, options);
|
|
636
|
+
return provider.changes(home, options.proxyUrl, runtimeConfig);
|
|
300
637
|
});
|
|
301
638
|
}
|
|
302
639
|
|
|
303
640
|
export function applyProviderInstall(options: ProviderInstallOptions, store: BuyerStore): ProviderApplyResult[] {
|
|
641
|
+
const providerIds = assertProviderIds(options.providers);
|
|
304
642
|
const changes = previewProviderInstall(options);
|
|
305
643
|
const byProvider = new Map<ProviderId, ProviderFileChange[]>();
|
|
306
644
|
for (const change of changes) {
|
|
@@ -313,12 +651,22 @@ export function applyProviderInstall(options: ProviderInstallOptions, store: Buy
|
|
|
313
651
|
files: providerChanges.map((change) => ({
|
|
314
652
|
path: change.path,
|
|
315
653
|
existed: fs.existsSync(change.path),
|
|
316
|
-
content: readText(change.path)
|
|
317
|
-
}))
|
|
654
|
+
content: readText(change.path),
|
|
655
|
+
})),
|
|
318
656
|
};
|
|
319
657
|
store.saveProviderInstallSnapshot(snapshot);
|
|
320
658
|
}
|
|
321
659
|
|
|
660
|
+
for (const providerId of providerIds) {
|
|
661
|
+
const provider = getProviderDefinition(providerId);
|
|
662
|
+
const runtimeConfig = resolveProviderRuntimeConfig(provider, options);
|
|
663
|
+
store.saveProviderRuntimeConfig(providerId, runtimeConfig);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (options.sellerRouting) {
|
|
667
|
+
store.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, options.sellerRouting);
|
|
668
|
+
}
|
|
669
|
+
|
|
322
670
|
const applied: ProviderApplyResult[] = [];
|
|
323
671
|
for (const change of changes) {
|
|
324
672
|
const dir = path.dirname(change.path);
|
|
@@ -329,7 +677,7 @@ export function applyProviderInstall(options: ProviderInstallOptions, store: Buy
|
|
|
329
677
|
applied.push({
|
|
330
678
|
providerId: change.providerId,
|
|
331
679
|
path: change.path,
|
|
332
|
-
action: change.existed ? "updated" : "created"
|
|
680
|
+
action: change.existed ? "updated" : "created",
|
|
333
681
|
});
|
|
334
682
|
}
|
|
335
683
|
return applied;
|
|
@@ -358,6 +706,8 @@ export function rollbackProviderInstall(options: ProviderRollbackOptions, store:
|
|
|
358
706
|
}
|
|
359
707
|
}
|
|
360
708
|
store.removeProviderInstallSnapshot(providerId);
|
|
709
|
+
store.removeProviderRuntimeConfig(providerId);
|
|
361
710
|
}
|
|
711
|
+
store.removeDaemonRuntimeConfig(ROUTING_CONFIG_KEY);
|
|
362
712
|
return results;
|
|
363
713
|
}
|