@heventure/model-provider-x 0.2.8-beta.0 → 0.2.9-beta.0

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 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
- model-provider-x [options]
110
- model-provider-x setup [options]
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) {
@@ -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-beta.0",
3
+ "version": "0.2.9-beta.0",
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
- "bin": {
19
- "model-provider-x": "dist/cli/index.js"
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",