@heventure/model-provider-x 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -56,6 +56,18 @@ node dist/cli/index.js \
56
56
  OpenCode provider entries include `options.setCacheKey=true` by default.
57
57
  This lets OpenCode pass a stable cache key through the OpenAI-compatible provider path so relays or local providers that support prompt caching can route repeated context to the same cache.
58
58
 
59
+ When targeting OpenCode, choose the direct API type interactively or pass `--opencode-api`:
60
+
61
+ ```bash
62
+ node dist/cli/index.js setup --target opencode --provider lmstudio --direct --opencode-api responses
63
+ ```
64
+
65
+ Supported values are:
66
+
67
+ - `chat`: writes `npm: "@ai-sdk/openai-compatible"` for `/v1/chat/completions`.
68
+ - `responses`: writes `npm: "@ai-sdk/openai"` for `/v1/responses`.
69
+ - `messages`: writes `npm: "@ai-sdk/anthropic"` for Anthropic-compatible `/v1/messages`.
70
+
59
71
  ## Claude Code Setup
60
72
 
61
73
  Create or update a provider profile and write Claude Code user settings:
@@ -113,6 +125,9 @@ Proxy mode gives the broadest compatibility:
113
125
  - `/v1/chat/completions` and `/v1/completions` passthrough for OpenAI-compatible clients.
114
126
  - `/v1/messages` for Claude Code.
115
127
 
128
+ When proxy mode is selected interactively, the wizard confirms whether to reuse the current proxy token, generate a new one, or enter your own.
129
+ Non-interactive `--yes` runs keep the existing token or use the generated default.
130
+
116
131
  You can force either mode non-interactively:
117
132
 
118
133
  ```bash
package/dist/cli/args.js CHANGED
@@ -35,6 +35,9 @@ export function parseCliArgs(argv) {
35
35
  case "--provider":
36
36
  options.providerPreset = next();
37
37
  break;
38
+ case "--opencode-api":
39
+ options.opencodeApiType = parseOpenCodeApiType(next());
40
+ break;
38
41
  case "--proxy":
39
42
  options.proxy = true;
40
43
  break;
@@ -100,6 +103,7 @@ Options:
100
103
  --name <name> Provider display name.
101
104
  --id <id> Provider id used under provider.<id>.
102
105
  --provider <id> Use a built-in provider preset, for example lmstudio or openai.
106
+ --opencode-api <api> OpenCode API type: chat, responses, or messages.
103
107
  --proxy Write agent config through the local compatibility proxy.
104
108
  --direct Write agent config directly to the upstream provider.
105
109
  --models <list> Comma-separated model ids. Skips interactive model selection.
@@ -111,6 +115,18 @@ Options:
111
115
  }
112
116
  export class HelpRequested extends Error {
113
117
  }
118
+ function parseOpenCodeApiType(value) {
119
+ if (value === "chat" || value === "chat-completions" || value === "openai-compatible") {
120
+ return "chat";
121
+ }
122
+ if (value === "responses" || value === "openai-responses") {
123
+ return "responses";
124
+ }
125
+ if (value === "messages" || value === "anthropic-messages") {
126
+ return "messages";
127
+ }
128
+ throw new Error(`Unknown OpenCode API type: ${value}`);
129
+ }
114
130
  function addModelByOneBasedIndex(selected, models, index) {
115
131
  const model = models[index - 1];
116
132
  if (!model) {
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@ import { createInterface } from "node:readline/promises";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { readProxyStatus, startProxyProcess, stopProxyProcess } from "../core/proxy-process.js";
6
- import { getDefaultToolConfigPath, readToolConfig, upsertProviderProfile } from "../core/tool-config.js";
6
+ import { createProxyAuthToken, getDefaultToolConfigPath, readToolConfig, upsertProviderProfile, writeToolConfig } from "../core/tool-config.js";
7
7
  import { discoverOpenCodeConfigs, getDefaultConfigPath, writeProviderToConfig } from "../core/config.js";
8
8
  import { buildProviderConfig, detectProviderCapabilities, recommendProxyMode, validateAndFetchModels } from "../core/provider.js";
9
9
  import { startProxyServer } from "../proxy/server.js";
@@ -81,7 +81,8 @@ export async function runCli(options) {
81
81
  providerName,
82
82
  baseURL: fetched.baseURL,
83
83
  apiKey,
84
- models: selectedModels
84
+ models: selectedModels,
85
+ opencodeApiType: options.opencodeApiType ?? "chat"
85
86
  });
86
87
  const provider = fragment.provider[providerId];
87
88
  const json = JSON.stringify(fragment, null, 2);
@@ -118,11 +119,11 @@ async function runSetup(command) {
118
119
  const target = await resolveSetupTarget(rl, command.target);
119
120
  const useProxy = await resolveProxyMode(rl, command.options, capabilities, target);
120
121
  if (target === "opencode") {
121
- await writeOpenCodeSetup(rl, command, selection, useProxy);
122
+ await writeOpenCodeSetup(rl, command, selection, useProxy, capabilities);
122
123
  return;
123
124
  }
124
125
  if (target === "codex") {
125
- await writeCodexSetup(command, selection, useProxy);
126
+ await writeCodexSetup(rl, command, selection, useProxy);
126
127
  return;
127
128
  }
128
129
  await writeClaudeCodeSetup(rl, command, selection, useProxy);
@@ -172,15 +173,20 @@ async function collectProviderSelection(rl, command, providerInput) {
172
173
  toolConfigPath
173
174
  };
174
175
  }
175
- async function writeOpenCodeSetup(rl, command, selection, useProxy) {
176
+ async function writeOpenCodeSetup(rl, command, selection, useProxy, capabilities) {
177
+ if (useProxy) {
178
+ await ensureProxyAuthToken(rl, command, selection);
179
+ }
176
180
  const baseURL = useProxy ? `http://${selection.config.proxy.host}:${selection.config.proxy.port}/v1` : selection.upstreamBaseURL;
177
181
  const apiKey = useProxy ? selection.config.proxy.authToken : selection.apiKey;
182
+ const opencodeApiType = await resolveOpenCodeApiType(rl, command, capabilities, useProxy);
178
183
  const fragment = buildProviderConfig({
179
184
  providerId: selection.providerId,
180
185
  providerName: selection.providerName,
181
186
  baseURL,
182
187
  apiKey,
183
- models: selection.selectedModels
188
+ models: selection.selectedModels,
189
+ opencodeApiType
184
190
  });
185
191
  const provider = fragment.provider[selection.providerId];
186
192
  const targetPath = command.options.configPath ?? (await chooseConfigPath(rl, selection.providerId, command.options.yes));
@@ -205,6 +211,9 @@ async function writeOpenCodeSetup(rl, command, selection, useProxy) {
205
211
  }
206
212
  }
207
213
  async function writeClaudeCodeSetup(rl, command, selection, useProxy) {
214
+ if (useProxy) {
215
+ await ensureProxyAuthToken(rl, command, selection);
216
+ }
208
217
  const proxyBaseURL = useProxy ? `http://${selection.config.proxy.host}:${selection.config.proxy.port}` : selection.upstreamBaseURL;
209
218
  const modelMapping = await resolveClaudeModelMapping(rl, command, selection);
210
219
  const result = await writeClaudeCodeSettings({
@@ -233,7 +242,10 @@ async function writeClaudeCodeSetup(rl, command, selection, useProxy) {
233
242
  output.write(`Started proxy: ${proxy.baseURL}\n`);
234
243
  }
235
244
  }
236
- async function writeCodexSetup(_command, selection, useProxy) {
245
+ async function writeCodexSetup(rl, command, selection, useProxy) {
246
+ if (useProxy) {
247
+ await ensureProxyAuthToken(rl, command, selection);
248
+ }
237
249
  const proxyBaseURL = useProxy
238
250
  ? `http://${selection.config.proxy.host}:${selection.config.proxy.port}/v1`
239
251
  : selection.upstreamBaseURL;
@@ -312,6 +324,48 @@ async function runProxyCommand(command) {
312
324
  process.once("SIGTERM", stop);
313
325
  });
314
326
  }
327
+ async function ensureProxyAuthToken(rl, command, selection) {
328
+ if (command.options.yes || !rl) {
329
+ return;
330
+ }
331
+ const current = selection.config.proxy.authToken;
332
+ const action = canUseTui()
333
+ ? await selectChoice("Proxy token", [
334
+ { label: "Use existing proxy token", value: "use", hint: tokenHint(current) },
335
+ { label: "Generate a new proxy token", value: "generate" },
336
+ { label: "Enter a proxy token", value: "input" }
337
+ ])
338
+ : await promptProxyTokenAction(rl, current);
339
+ if (action === "use") {
340
+ return;
341
+ }
342
+ const nextToken = action === "generate"
343
+ ? createProxyAuthToken()
344
+ : await requiredOption(rl, undefined, "Proxy token");
345
+ selection.config.proxy.authToken = nextToken;
346
+ await writeToolConfig(selection.toolConfigPath, selection.config);
347
+ }
348
+ async function promptProxyTokenAction(rl, current) {
349
+ output.write(`Proxy token found (${tokenHint(current)}).\n`);
350
+ const answer = await rl.question("Use existing, generate new, or enter your own? [use/generate/input] ");
351
+ const value = answer.trim().toLowerCase();
352
+ if (!value || value === "use" || value === "u") {
353
+ return "use";
354
+ }
355
+ if (value === "generate" || value === "g") {
356
+ return "generate";
357
+ }
358
+ if (value === "input" || value === "i" || value === "byok") {
359
+ return "input";
360
+ }
361
+ throw new Error(`Unknown proxy token action: ${answer}`);
362
+ }
363
+ function tokenHint(token) {
364
+ if (token.length <= 10) {
365
+ return token;
366
+ }
367
+ return `${token.slice(0, 8)}...${token.slice(-4)}`;
368
+ }
315
369
  async function requiredOption(rl, value, label) {
316
370
  const answer = value ?? (await rl.question(`${label}: `));
317
371
  if (!answer.trim()) {
@@ -362,6 +416,77 @@ async function resolveProxyMode(rl, options, capabilities, target) {
362
416
  const accepted = !answer.trim() || answer.trim().toLowerCase() === "y";
363
417
  return recommendedProxy ? accepted : !accepted;
364
418
  }
419
+ async function resolveOpenCodeApiType(rl, command, capabilities, useProxy) {
420
+ if (command.options.opencodeApiType) {
421
+ return command.options.opencodeApiType;
422
+ }
423
+ const availableApis = useProxy ? new Set(["openai-compatible", "openai-responses", "anthropic-messages"]) : new Set(capabilities.apis);
424
+ const recommended = recommendedOpenCodeApiType(availableApis);
425
+ if (command.options.yes) {
426
+ return recommended;
427
+ }
428
+ const choices = [
429
+ {
430
+ label: "Responses API",
431
+ value: "responses",
432
+ hint: choiceHint("openai-responses", recommended, "responses", availableApis)
433
+ },
434
+ {
435
+ label: "Chat Completions API",
436
+ value: "chat",
437
+ hint: choiceHint("openai-compatible", recommended, "chat", availableApis)
438
+ },
439
+ {
440
+ label: "Anthropic Messages API",
441
+ value: "messages",
442
+ hint: choiceHint("anthropic-messages", recommended, "messages", availableApis)
443
+ }
444
+ ];
445
+ if (canUseTui()) {
446
+ return selectChoice("OpenCode API type", choices);
447
+ }
448
+ output.write("OpenCode API types:\n");
449
+ choices.forEach((choice, index) => {
450
+ output.write(`${index + 1}. ${choice.label}${choice.hint ? ` - ${choice.hint}` : ""}\n`);
451
+ });
452
+ const answer = await rl.question(`OpenCode API type [${recommended}]: `);
453
+ const value = answer.trim();
454
+ if (!value) {
455
+ return recommended;
456
+ }
457
+ if (value === "1" || value === "responses") {
458
+ return "responses";
459
+ }
460
+ if (value === "2" || value === "chat" || value === "chat-completions") {
461
+ return "chat";
462
+ }
463
+ if (value === "3" || value === "messages") {
464
+ return "messages";
465
+ }
466
+ throw new Error(`Unknown OpenCode API type: ${value}`);
467
+ }
468
+ function recommendedOpenCodeApiType(apis) {
469
+ if (apis.has("openai-responses")) {
470
+ return "responses";
471
+ }
472
+ if (apis.has("openai-compatible")) {
473
+ return "chat";
474
+ }
475
+ if (apis.has("anthropic-messages")) {
476
+ return "messages";
477
+ }
478
+ return "chat";
479
+ }
480
+ function choiceHint(api, recommended, value, availableApis) {
481
+ const hints = [];
482
+ if (value === recommended) {
483
+ hints.push("recommended");
484
+ }
485
+ if (!availableApis.has(api)) {
486
+ hints.push("not detected");
487
+ }
488
+ return hints.join(", ");
489
+ }
365
490
  async function resolveDefaultModel(rl, command, selectedModels) {
366
491
  if (command.defaultModel) {
367
492
  if (!selectedModels.includes(command.defaultModel)) {
@@ -71,15 +71,18 @@ export function recommendProxyMode(capabilities, target) {
71
71
  export function buildProviderConfig(input) {
72
72
  const baseURL = normalizeBaseUrl(input.baseURL);
73
73
  const apiKey = input.apiKey?.trim();
74
+ const opencodeApiType = input.opencodeApiType ?? "chat";
74
75
  const provider = {
75
- npm: "@ai-sdk/openai-compatible",
76
+ npm: npmPackageForOpenCodeApiType(opencodeApiType),
76
77
  name: input.providerName.trim(),
77
78
  options: {
78
- baseURL,
79
- setCacheKey: true
79
+ baseURL
80
80
  },
81
81
  models: Object.fromEntries(input.models.map((model) => [model, { name: model }]))
82
82
  };
83
+ if (opencodeApiType !== "messages") {
84
+ provider.options.setCacheKey = true;
85
+ }
83
86
  if (apiKey) {
84
87
  provider.options.apiKey = apiKey;
85
88
  }
@@ -90,6 +93,15 @@ export function buildProviderConfig(input) {
90
93
  }
91
94
  };
92
95
  }
96
+ export function npmPackageForOpenCodeApiType(apiType) {
97
+ if (apiType === "responses") {
98
+ return "@ai-sdk/openai";
99
+ }
100
+ if (apiType === "messages") {
101
+ return "@ai-sdk/anthropic";
102
+ }
103
+ return "@ai-sdk/openai-compatible";
104
+ }
93
105
  function isModelListResponse(body) {
94
106
  return (typeof body === "object" &&
95
107
  body !== null &&
@@ -40,10 +40,13 @@ function createDefaultToolConfig() {
40
40
  proxy: {
41
41
  host: "127.0.0.1",
42
42
  port: 4141,
43
- authToken: `mpx-${randomBytes(18).toString("base64url")}`
43
+ authToken: createProxyAuthToken()
44
44
  }
45
45
  };
46
46
  }
47
+ export function createProxyAuthToken() {
48
+ return `mpx-${randomBytes(18).toString("base64url")}`;
49
+ }
47
50
  function normalizeToolConfig(config) {
48
51
  const fallback = createDefaultToolConfig();
49
52
  const profiles = isRecord(config.profiles) ? config.profiles : {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heventure/model-provider-x",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",