@heventure/model-provider-x 0.2.8-beta.0 → 0.2.8
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/cli/args.js +3 -3
- package/dist/cli/index.js +178 -0
- package/dist/core/tool-config.js +10 -1
- package/package.json +5 -4
package/dist/cli/args.js
CHANGED
|
@@ -103,11 +103,11 @@ export function parseModelSelection(selection, models) {
|
|
|
103
103
|
return [...selected];
|
|
104
104
|
}
|
|
105
105
|
export function usage() {
|
|
106
|
-
return `model-provider-x
|
|
106
|
+
return `model-provider-x (alias: mpx)
|
|
107
107
|
|
|
108
108
|
Usage:
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
mpx [options]
|
|
110
|
+
mpx setup [options]
|
|
111
111
|
|
|
112
112
|
Options:
|
|
113
113
|
--base-url <url> OpenAI-compatible API base URL, for example http://localhost:8888/v1
|
package/dist/cli/index.js
CHANGED
|
@@ -122,6 +122,24 @@ async function runSetup(command) {
|
|
|
122
122
|
const rl = createInterface({ input, output });
|
|
123
123
|
try {
|
|
124
124
|
output.write(canUseTui() ? renderIntro() : "model-provider-x\n\n");
|
|
125
|
+
// Check for existing profiles
|
|
126
|
+
const config = await readToolConfig();
|
|
127
|
+
const profileIds = Object.keys(config.profiles);
|
|
128
|
+
let selectedProfileId;
|
|
129
|
+
if (profileIds.length > 0 && !command.options.baseURL && !command.options.providerPreset) {
|
|
130
|
+
selectedProfileId = await resolveProfileAction(rl, config, profileIds);
|
|
131
|
+
}
|
|
132
|
+
if (selectedProfileId) {
|
|
133
|
+
const savedProfile = config.profiles[selectedProfileId];
|
|
134
|
+
const wantModify = await askModifyProfile(rl, savedProfile);
|
|
135
|
+
if (!wantModify) {
|
|
136
|
+
// Directly apply saved profile
|
|
137
|
+
await applySavedProfile(rl, savedProfile);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Pre-fill command options with saved profile
|
|
141
|
+
command = mergeProfileIntoCommand(command, savedProfile);
|
|
142
|
+
}
|
|
125
143
|
const providerInput = await collectProviderInput(rl, command);
|
|
126
144
|
output.write("Detecting provider capabilities...\n");
|
|
127
145
|
const capabilities = await detectProviderCapabilities({ baseURL: providerInput.baseURL, apiKey: providerInput.apiKey });
|
|
@@ -142,6 +160,135 @@ async function runSetup(command) {
|
|
|
142
160
|
rl.close();
|
|
143
161
|
}
|
|
144
162
|
}
|
|
163
|
+
async function resolveProfileAction(rl, config, profileIds) {
|
|
164
|
+
const choices = [
|
|
165
|
+
{ label: "Create new profile", value: undefined, hint: "start fresh" },
|
|
166
|
+
...profileIds.map((id) => {
|
|
167
|
+
const p = config.profiles[id];
|
|
168
|
+
const keyHint = p.apiKey ? "key set" : "no key";
|
|
169
|
+
const modelCount = `${p.models.length} models`;
|
|
170
|
+
const targetHint = p.target ?? "no target";
|
|
171
|
+
return {
|
|
172
|
+
label: `${p.name} (${id})`,
|
|
173
|
+
value: id,
|
|
174
|
+
hint: `${p.baseURL} · ${modelCount} · ${keyHint} · ${targetHint}`
|
|
175
|
+
};
|
|
176
|
+
})
|
|
177
|
+
];
|
|
178
|
+
if (canUseTui()) {
|
|
179
|
+
return selectChoice("Select profile", choices);
|
|
180
|
+
}
|
|
181
|
+
output.write("Existing profiles:\n");
|
|
182
|
+
choices.forEach((c, i) => {
|
|
183
|
+
const hint = c.hint ? ` - ${c.hint}` : "";
|
|
184
|
+
output.write(` ${i}. ${c.label}${hint}\n`);
|
|
185
|
+
});
|
|
186
|
+
const answer = await rl.question("Select profile number or press Enter for new: ");
|
|
187
|
+
const idx = Number(answer.trim());
|
|
188
|
+
if (!answer.trim() || !Number.isInteger(idx) || idx === 0)
|
|
189
|
+
return undefined;
|
|
190
|
+
return choices[idx]?.value;
|
|
191
|
+
}
|
|
192
|
+
async function askModifyProfile(rl, profile) {
|
|
193
|
+
output.write(`\nSelected profile: ${profile.name}\n`);
|
|
194
|
+
output.write(` Provider: ${profile.baseURL}\n`);
|
|
195
|
+
output.write(` Models: ${profile.models.length}\n`);
|
|
196
|
+
if (profile.target) {
|
|
197
|
+
output.write(` Target: ${profile.target}\n`);
|
|
198
|
+
}
|
|
199
|
+
if (canUseTui()) {
|
|
200
|
+
return selectChoice("What to do?", [
|
|
201
|
+
{ label: "Use as-is", value: false, hint: "apply directly" },
|
|
202
|
+
{ label: "Modify settings", value: true, hint: "update models, key, etc." }
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
const answer = await rl.question("Use this profile as-is or modify? [use/modify] ");
|
|
206
|
+
return answer.trim().toLowerCase() === "modify";
|
|
207
|
+
}
|
|
208
|
+
async function applySavedProfile(rl, profile) {
|
|
209
|
+
const target = profile.target ?? "opencode";
|
|
210
|
+
const toolConfigPath = getDefaultToolConfigPath();
|
|
211
|
+
if (target === "opencode") {
|
|
212
|
+
const opencodeApiType = profile.opencodeApiType ?? "chat";
|
|
213
|
+
const baseURL = profile.proxy
|
|
214
|
+
? `http://127.0.0.1:4141/v1`
|
|
215
|
+
: profile.baseURL;
|
|
216
|
+
const apiKey = profile.proxy
|
|
217
|
+
? (await readToolConfig()).proxy.authToken
|
|
218
|
+
: profile.apiKey ?? "";
|
|
219
|
+
const fragment = buildProviderConfig({
|
|
220
|
+
providerId: profile.id,
|
|
221
|
+
providerName: profile.name,
|
|
222
|
+
baseURL,
|
|
223
|
+
apiKey,
|
|
224
|
+
models: profile.models,
|
|
225
|
+
opencodeApiType
|
|
226
|
+
});
|
|
227
|
+
const targetPath = await chooseConfigPath(rl, profile.id, false);
|
|
228
|
+
if (targetPath) {
|
|
229
|
+
const result = await writeProviderToConfig({ targetPath, providerId: profile.id, provider: fragment.provider[profile.id] });
|
|
230
|
+
output.write(`Updated ${result.targetPath}\n`);
|
|
231
|
+
if (result.backupPath) {
|
|
232
|
+
output.write(`Backup: ${result.backupPath}\n`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
output.write(`${JSON.stringify(fragment, null, 2)}\n`);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (target === "codex") {
|
|
241
|
+
const proxyBaseURL = profile.proxy
|
|
242
|
+
? `http://127.0.0.1:4141/v1`
|
|
243
|
+
: profile.baseURL;
|
|
244
|
+
const result = await writeCodexConfig({
|
|
245
|
+
targetPath: getDefaultCodexConfigPath(),
|
|
246
|
+
providerId: profile.id,
|
|
247
|
+
providerName: profile.name,
|
|
248
|
+
baseURL: proxyBaseURL,
|
|
249
|
+
authCommand: "model-provider-x",
|
|
250
|
+
authArgs: profile.proxy ? ["proxy", "token"] : ["config", "api-key", "--profile", profile.id],
|
|
251
|
+
model: profile.models[0] ?? ""
|
|
252
|
+
});
|
|
253
|
+
output.write(`Updated Codex config: ${result.targetPath}\n`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// claude-code
|
|
257
|
+
const proxyBaseURL = profile.proxy
|
|
258
|
+
? `http://127.0.0.1:4141`
|
|
259
|
+
: profile.baseURL;
|
|
260
|
+
const modelMapping = defaultClaudeModelMapping(profile.models[0] ?? "");
|
|
261
|
+
const result = await writeClaudeCodeSettings({
|
|
262
|
+
targetPath: getDefaultClaudeSettingsPath(),
|
|
263
|
+
proxy: {
|
|
264
|
+
baseURL: proxyBaseURL,
|
|
265
|
+
authToken: profile.proxy ? (await readToolConfig()).proxy.authToken : profile.apiKey ?? "",
|
|
266
|
+
enableModelDiscovery: profile.proxy ?? false,
|
|
267
|
+
defaultModel: profile.models[0] ?? "",
|
|
268
|
+
models: profile.models,
|
|
269
|
+
modelMapping
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
output.write(`Updated Claude Code settings: ${result.targetPath}\n`);
|
|
273
|
+
}
|
|
274
|
+
function mergeProfileIntoCommand(command, profile) {
|
|
275
|
+
const opts = { ...command.options };
|
|
276
|
+
opts.providerName ??= profile.name;
|
|
277
|
+
opts.providerId ??= profile.id;
|
|
278
|
+
opts.baseURL ??= profile.baseURL;
|
|
279
|
+
opts.apiKey ??= profile.apiKey;
|
|
280
|
+
opts.models ??= profile.models.length > 0 ? profile.models : undefined;
|
|
281
|
+
opts.opencodeApiType ??= profile.opencodeApiType;
|
|
282
|
+
if (profile.proxy !== undefined && opts.proxy === undefined) {
|
|
283
|
+
opts.proxy = profile.proxy;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...command,
|
|
287
|
+
profileId: profile.id,
|
|
288
|
+
target: command.target ?? profile.target,
|
|
289
|
+
options: opts
|
|
290
|
+
};
|
|
291
|
+
}
|
|
145
292
|
async function collectProviderInput(rl, command) {
|
|
146
293
|
const providerDefaults = await resolveProviderDefaults(rl, command.options);
|
|
147
294
|
const providerName = await requiredOption(rl, command.options.providerName ?? providerDefaults.name, "Provider name");
|
|
@@ -212,6 +359,17 @@ async function writeOpenCodeSetup(rl, command, selection, useProxy, capabilities
|
|
|
212
359
|
output.write(`${JSON.stringify(fragment, null, 2)}\n`);
|
|
213
360
|
return;
|
|
214
361
|
}
|
|
362
|
+
// Persist complete profile with target, opencodeApiType, and proxy
|
|
363
|
+
await upsertProviderProfile(selection.toolConfigPath, {
|
|
364
|
+
id: selection.providerId,
|
|
365
|
+
name: selection.providerName,
|
|
366
|
+
baseURL: selection.upstreamBaseURL,
|
|
367
|
+
apiKey: selection.apiKey.trim() || undefined,
|
|
368
|
+
models: selection.selectedModels,
|
|
369
|
+
target: "opencode",
|
|
370
|
+
opencodeApiType,
|
|
371
|
+
proxy: useProxy
|
|
372
|
+
});
|
|
215
373
|
const result = await writeProviderToConfig({ targetPath, providerId: selection.providerId, provider });
|
|
216
374
|
output.write(`Saved profile ${selection.providerId} to ${selection.toolConfigPath}\n`);
|
|
217
375
|
output.write(`Updated ${result.targetPath}\n`);
|
|
@@ -245,6 +403,16 @@ async function writeClaudeCodeSetup(rl, command, selection, useProxy) {
|
|
|
245
403
|
modelMapping
|
|
246
404
|
}
|
|
247
405
|
});
|
|
406
|
+
// Persist complete profile
|
|
407
|
+
await upsertProviderProfile(selection.toolConfigPath, {
|
|
408
|
+
id: selection.providerId,
|
|
409
|
+
name: selection.providerName,
|
|
410
|
+
baseURL: selection.upstreamBaseURL,
|
|
411
|
+
apiKey: selection.apiKey.trim() || undefined,
|
|
412
|
+
models: selection.selectedModels,
|
|
413
|
+
target: "claude-code",
|
|
414
|
+
proxy: useProxy
|
|
415
|
+
});
|
|
248
416
|
output.write(`Saved profile ${selection.providerId} to ${selection.toolConfigPath}\n`);
|
|
249
417
|
output.write(`Updated Claude Code settings: ${result.targetPath}\n`);
|
|
250
418
|
if (result.backupPath) {
|
|
@@ -276,6 +444,16 @@ async function writeCodexSetup(rl, command, selection, useProxy) {
|
|
|
276
444
|
authArgs: useProxy ? ["proxy", "token"] : ["config", "api-key", "--profile", selection.providerId],
|
|
277
445
|
model: selection.defaultModel
|
|
278
446
|
});
|
|
447
|
+
// Persist complete profile
|
|
448
|
+
await upsertProviderProfile(selection.toolConfigPath, {
|
|
449
|
+
id: selection.providerId,
|
|
450
|
+
name: selection.providerName,
|
|
451
|
+
baseURL: selection.upstreamBaseURL,
|
|
452
|
+
apiKey: selection.apiKey.trim() || undefined,
|
|
453
|
+
models: selection.selectedModels,
|
|
454
|
+
target: "codex",
|
|
455
|
+
proxy: useProxy
|
|
456
|
+
});
|
|
279
457
|
output.write(`Saved profile ${selection.providerId} to ${selection.toolConfigPath}\n`);
|
|
280
458
|
output.write(`Updated Codex config: ${result.targetPath}\n`);
|
|
281
459
|
if (result.backupPath) {
|
package/dist/core/tool-config.js
CHANGED
|
@@ -61,7 +61,10 @@ function normalizeToolConfig(config) {
|
|
|
61
61
|
name: String(value.name ?? id),
|
|
62
62
|
baseURL: String(value.baseURL ?? ""),
|
|
63
63
|
apiKey: value.apiKey ? String(value.apiKey) : undefined,
|
|
64
|
-
models: Array.isArray(value.models) ? value.models.map(String) : []
|
|
64
|
+
models: Array.isArray(value.models) ? value.models.map(String) : [],
|
|
65
|
+
target: isValidTarget(value.target) ? value.target : undefined,
|
|
66
|
+
opencodeApiType: isValidOpencodeApiType(value.opencodeApiType) ? value.opencodeApiType : undefined,
|
|
67
|
+
proxy: typeof value.proxy === "boolean" ? value.proxy : undefined
|
|
65
68
|
}
|
|
66
69
|
];
|
|
67
70
|
})),
|
|
@@ -83,3 +86,9 @@ async function fileExists(path) {
|
|
|
83
86
|
function isRecord(value) {
|
|
84
87
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
88
|
}
|
|
89
|
+
function isValidTarget(value) {
|
|
90
|
+
return typeof value === "string" && ["opencode", "codex", "claude-code"].includes(value);
|
|
91
|
+
}
|
|
92
|
+
function isValidOpencodeApiType(value) {
|
|
93
|
+
return typeof value === "string" && ["chat", "responses", "messages"].includes(value);
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heventure/model-provider-x",
|
|
3
|
-
"version": "0.2.8
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "TUI configurator and local API proxy for wiring custom model providers into OpenCode and Claude Code.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
"homepage": "https://github.com/HughesCuit/model-provider-x#readme",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"main": "dist/cli/index.js",
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
"bin": {
|
|
19
|
+
"model-provider-x": "dist/cli/index.js",
|
|
20
|
+
"mpx": "dist/cli/index.js"
|
|
21
|
+
},
|
|
21
22
|
"files": [
|
|
22
23
|
"dist",
|
|
23
24
|
"README.md",
|