@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 +15 -0
- package/dist/cli/args.js +16 -0
- package/dist/cli/index.js +132 -7
- package/dist/core/provider.js +15 -3
- package/dist/core/tool-config.js +4 -1
- package/package.json +1 -1
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(
|
|
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)) {
|
package/dist/core/provider.js
CHANGED
|
@@ -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:
|
|
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 &&
|
package/dist/core/tool-config.js
CHANGED
|
@@ -40,10 +40,13 @@ function createDefaultToolConfig() {
|
|
|
40
40
|
proxy: {
|
|
41
41
|
host: "127.0.0.1",
|
|
42
42
|
port: 4141,
|
|
43
|
-
authToken:
|
|
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