@heventure/model-provider-x 0.2.2 → 0.2.4
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 +3 -0
- package/dist/cli/args.js +34 -0
- package/dist/cli/index.js +129 -3
- package/dist/core/model-capabilities.js +174 -0
- package/dist/core/provider.js +24 -4
- package/dist/core/tool-config.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,9 @@ Proxy mode gives the broadest compatibility:
|
|
|
125
125
|
- `/v1/chat/completions` and `/v1/completions` passthrough for OpenAI-compatible clients.
|
|
126
126
|
- `/v1/messages` for Claude Code.
|
|
127
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
|
+
|
|
128
131
|
You can force either mode non-interactively:
|
|
129
132
|
|
|
130
133
|
```bash
|
package/dist/cli/args.js
CHANGED
|
@@ -29,6 +29,12 @@ export function parseCliArgs(argv) {
|
|
|
29
29
|
.map((model) => model.trim())
|
|
30
30
|
.filter(Boolean);
|
|
31
31
|
break;
|
|
32
|
+
case "--modalities":
|
|
33
|
+
if (!options.modalities) {
|
|
34
|
+
options.modalities = [];
|
|
35
|
+
}
|
|
36
|
+
options.modalities.push(parseModalitiesArg(next()));
|
|
37
|
+
break;
|
|
32
38
|
case "--name":
|
|
33
39
|
options.providerName = next();
|
|
34
40
|
break;
|
|
@@ -107,6 +113,8 @@ Options:
|
|
|
107
113
|
--proxy Write agent config through the local compatibility proxy.
|
|
108
114
|
--direct Write agent config directly to the upstream provider.
|
|
109
115
|
--models <list> Comma-separated model ids. Skips interactive model selection.
|
|
116
|
+
--modalities <spec> Model modalities in format <model>:<input>:<output>.
|
|
117
|
+
Example: --modalities qwen-vl:image,text:text
|
|
110
118
|
--config <path> OpenCode config path to write when targeting OpenCode.
|
|
111
119
|
--print Print generated JSON and do not write config.
|
|
112
120
|
--yes, -y Accept defaults in non-interactive prompts.
|
|
@@ -134,3 +142,29 @@ function addModelByOneBasedIndex(selected, models, index) {
|
|
|
134
142
|
}
|
|
135
143
|
selected.add(model);
|
|
136
144
|
}
|
|
145
|
+
function parseModalitiesArg(value) {
|
|
146
|
+
const parts = value.split(":");
|
|
147
|
+
if (parts.length !== 3) {
|
|
148
|
+
throw new Error(`Invalid --modalities format: ${value}. Expected <model>:<input>:<output>`);
|
|
149
|
+
}
|
|
150
|
+
const [modelId, inputStr, outputStr] = parts;
|
|
151
|
+
if (!modelId) {
|
|
152
|
+
throw new Error("Model id is required in --modalities");
|
|
153
|
+
}
|
|
154
|
+
const parseModalityList = (s) => {
|
|
155
|
+
return s.split(",").map((m) => {
|
|
156
|
+
const trimmed = m.trim().toLowerCase();
|
|
157
|
+
if (["text", "image", "audio", "video", "pdf"].includes(trimmed)) {
|
|
158
|
+
return trimmed;
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`Unknown modality: ${trimmed}`);
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
modelId,
|
|
165
|
+
modalities: {
|
|
166
|
+
input: parseModalityList(inputStr),
|
|
167
|
+
output: parseModalityList(outputStr),
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -3,9 +3,10 @@ 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
|
+
import { isKnownModel } from "../core/model-capabilities.js";
|
|
9
10
|
import { startProxyServer } from "../proxy/server.js";
|
|
10
11
|
import { defaultClaudeModelMapping, getDefaultClaudeSettingsPath, writeClaudeCodeSettings } from "../targets/claude-code.js";
|
|
11
12
|
import { getDefaultCodexConfigPath, writeCodexConfig } from "../targets/codex.js";
|
|
@@ -76,12 +77,14 @@ export async function runCli(options) {
|
|
|
76
77
|
(canUseTui()
|
|
77
78
|
? await multiSelectChoices("Select models", createModelChoices(fetched.models))
|
|
78
79
|
: parseModelSelection(await rl.question(formatModelPrompt(fetched.models)), fetched.models));
|
|
80
|
+
const modelDetails = fetched.modelDetails.filter((m) => selectedModels.includes(m.id));
|
|
79
81
|
const fragment = buildProviderConfig({
|
|
80
82
|
providerId,
|
|
81
83
|
providerName,
|
|
82
84
|
baseURL: fetched.baseURL,
|
|
83
85
|
apiKey,
|
|
84
86
|
models: selectedModels,
|
|
87
|
+
modelDetails,
|
|
85
88
|
opencodeApiType: options.opencodeApiType ?? "chat"
|
|
86
89
|
});
|
|
87
90
|
const provider = fragment.provider[providerId];
|
|
@@ -123,7 +126,7 @@ async function runSetup(command) {
|
|
|
123
126
|
return;
|
|
124
127
|
}
|
|
125
128
|
if (target === "codex") {
|
|
126
|
-
await writeCodexSetup(command, selection, useProxy);
|
|
129
|
+
await writeCodexSetup(rl, command, selection, useProxy);
|
|
127
130
|
return;
|
|
128
131
|
}
|
|
129
132
|
await writeClaudeCodeSetup(rl, command, selection, useProxy);
|
|
@@ -151,6 +154,7 @@ async function collectProviderSelection(rl, command, providerInput) {
|
|
|
151
154
|
if (!defaultModel) {
|
|
152
155
|
throw new Error("Select at least one model");
|
|
153
156
|
}
|
|
157
|
+
const modelDetails = await resolveModelModalities(rl, command, selectedModels, fetched.modelDetails);
|
|
154
158
|
const toolConfigPath = getDefaultToolConfigPath();
|
|
155
159
|
const config = await upsertProviderProfile(toolConfigPath, {
|
|
156
160
|
id: providerInput.providerId,
|
|
@@ -168,12 +172,16 @@ async function collectProviderSelection(rl, command, providerInput) {
|
|
|
168
172
|
upstreamBaseURL: fetched.baseURL,
|
|
169
173
|
apiKey: providerInput.apiKey,
|
|
170
174
|
selectedModels,
|
|
175
|
+
modelDetails,
|
|
171
176
|
defaultModel,
|
|
172
177
|
config,
|
|
173
178
|
toolConfigPath
|
|
174
179
|
};
|
|
175
180
|
}
|
|
176
181
|
async function writeOpenCodeSetup(rl, command, selection, useProxy, capabilities) {
|
|
182
|
+
if (useProxy) {
|
|
183
|
+
await ensureProxyAuthToken(rl, command, selection);
|
|
184
|
+
}
|
|
177
185
|
const baseURL = useProxy ? `http://${selection.config.proxy.host}:${selection.config.proxy.port}/v1` : selection.upstreamBaseURL;
|
|
178
186
|
const apiKey = useProxy ? selection.config.proxy.authToken : selection.apiKey;
|
|
179
187
|
const opencodeApiType = await resolveOpenCodeApiType(rl, command, capabilities, useProxy);
|
|
@@ -183,6 +191,7 @@ async function writeOpenCodeSetup(rl, command, selection, useProxy, capabilities
|
|
|
183
191
|
baseURL,
|
|
184
192
|
apiKey,
|
|
185
193
|
models: selection.selectedModels,
|
|
194
|
+
modelDetails: selection.modelDetails,
|
|
186
195
|
opencodeApiType
|
|
187
196
|
});
|
|
188
197
|
const provider = fragment.provider[selection.providerId];
|
|
@@ -208,6 +217,9 @@ async function writeOpenCodeSetup(rl, command, selection, useProxy, capabilities
|
|
|
208
217
|
}
|
|
209
218
|
}
|
|
210
219
|
async function writeClaudeCodeSetup(rl, command, selection, useProxy) {
|
|
220
|
+
if (useProxy) {
|
|
221
|
+
await ensureProxyAuthToken(rl, command, selection);
|
|
222
|
+
}
|
|
211
223
|
const proxyBaseURL = useProxy ? `http://${selection.config.proxy.host}:${selection.config.proxy.port}` : selection.upstreamBaseURL;
|
|
212
224
|
const modelMapping = await resolveClaudeModelMapping(rl, command, selection);
|
|
213
225
|
const result = await writeClaudeCodeSettings({
|
|
@@ -236,7 +248,10 @@ async function writeClaudeCodeSetup(rl, command, selection, useProxy) {
|
|
|
236
248
|
output.write(`Started proxy: ${proxy.baseURL}\n`);
|
|
237
249
|
}
|
|
238
250
|
}
|
|
239
|
-
async function writeCodexSetup(
|
|
251
|
+
async function writeCodexSetup(rl, command, selection, useProxy) {
|
|
252
|
+
if (useProxy) {
|
|
253
|
+
await ensureProxyAuthToken(rl, command, selection);
|
|
254
|
+
}
|
|
240
255
|
const proxyBaseURL = useProxy
|
|
241
256
|
? `http://${selection.config.proxy.host}:${selection.config.proxy.port}/v1`
|
|
242
257
|
: selection.upstreamBaseURL;
|
|
@@ -315,6 +330,48 @@ async function runProxyCommand(command) {
|
|
|
315
330
|
process.once("SIGTERM", stop);
|
|
316
331
|
});
|
|
317
332
|
}
|
|
333
|
+
async function ensureProxyAuthToken(rl, command, selection) {
|
|
334
|
+
if (command.options.yes || !rl) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const current = selection.config.proxy.authToken;
|
|
338
|
+
const action = canUseTui()
|
|
339
|
+
? await selectChoice("Proxy token", [
|
|
340
|
+
{ label: "Use existing proxy token", value: "use", hint: tokenHint(current) },
|
|
341
|
+
{ label: "Generate a new proxy token", value: "generate" },
|
|
342
|
+
{ label: "Enter a proxy token", value: "input" }
|
|
343
|
+
])
|
|
344
|
+
: await promptProxyTokenAction(rl, current);
|
|
345
|
+
if (action === "use") {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const nextToken = action === "generate"
|
|
349
|
+
? createProxyAuthToken()
|
|
350
|
+
: await requiredOption(rl, undefined, "Proxy token");
|
|
351
|
+
selection.config.proxy.authToken = nextToken;
|
|
352
|
+
await writeToolConfig(selection.toolConfigPath, selection.config);
|
|
353
|
+
}
|
|
354
|
+
async function promptProxyTokenAction(rl, current) {
|
|
355
|
+
output.write(`Proxy token found (${tokenHint(current)}).\n`);
|
|
356
|
+
const answer = await rl.question("Use existing, generate new, or enter your own? [use/generate/input] ");
|
|
357
|
+
const value = answer.trim().toLowerCase();
|
|
358
|
+
if (!value || value === "use" || value === "u") {
|
|
359
|
+
return "use";
|
|
360
|
+
}
|
|
361
|
+
if (value === "generate" || value === "g") {
|
|
362
|
+
return "generate";
|
|
363
|
+
}
|
|
364
|
+
if (value === "input" || value === "i" || value === "byok") {
|
|
365
|
+
return "input";
|
|
366
|
+
}
|
|
367
|
+
throw new Error(`Unknown proxy token action: ${answer}`);
|
|
368
|
+
}
|
|
369
|
+
function tokenHint(token) {
|
|
370
|
+
if (token.length <= 10) {
|
|
371
|
+
return token;
|
|
372
|
+
}
|
|
373
|
+
return `${token.slice(0, 8)}...${token.slice(-4)}`;
|
|
374
|
+
}
|
|
318
375
|
async function requiredOption(rl, value, label) {
|
|
319
376
|
const answer = value ?? (await rl.question(`${label}: `));
|
|
320
377
|
if (!answer.trim()) {
|
|
@@ -583,4 +640,73 @@ function slugify(value) {
|
|
|
583
640
|
.replace(/[^a-z0-9]+/g, "-")
|
|
584
641
|
.replace(/^-+|-+$/g, "");
|
|
585
642
|
}
|
|
643
|
+
async function resolveModelModalities(rl, command, selectedModels, fetchedModelDetails) {
|
|
644
|
+
const userModalityOverrides = new Map();
|
|
645
|
+
if (command.options.modalities) {
|
|
646
|
+
for (const override of command.options.modalities) {
|
|
647
|
+
userModalityOverrides.set(override.modelId, override.modalities);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const result = [];
|
|
651
|
+
for (const modelId of selectedModels) {
|
|
652
|
+
const fetched = fetchedModelDetails.find((m) => m.id === modelId);
|
|
653
|
+
const userOverride = userModalityOverrides.get(modelId);
|
|
654
|
+
if (userOverride) {
|
|
655
|
+
result.push({ id: modelId, modalities: userOverride });
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (fetched?.modalities) {
|
|
659
|
+
result.push(fetched);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (command.options.yes || isKnownModel(modelId)) {
|
|
663
|
+
result.push(fetched ?? { id: modelId });
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const modalities = await promptModelModalities(rl, modelId);
|
|
667
|
+
result.push({ id: modelId, modalities });
|
|
668
|
+
}
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
async function promptModelModalities(rl, modelId) {
|
|
672
|
+
output.write(`\nModel: ${modelId} - Capabilities unknown\n`);
|
|
673
|
+
if (canUseTui()) {
|
|
674
|
+
const inputModalities = await multiSelectChoices("Input modalities", [
|
|
675
|
+
{ label: "text", value: "text", hint: "default" },
|
|
676
|
+
{ label: "image", value: "image" },
|
|
677
|
+
{ label: "audio", value: "audio" },
|
|
678
|
+
{ label: "video", value: "video" },
|
|
679
|
+
{ label: "pdf", value: "pdf" },
|
|
680
|
+
]);
|
|
681
|
+
const outputModalities = await multiSelectChoices("Output modalities", [
|
|
682
|
+
{ label: "text", value: "text", hint: "default" },
|
|
683
|
+
{ label: "image", value: "image" },
|
|
684
|
+
{ label: "audio", value: "audio" },
|
|
685
|
+
{ label: "video", value: "video" },
|
|
686
|
+
{ label: "pdf", value: "pdf" },
|
|
687
|
+
]);
|
|
688
|
+
return {
|
|
689
|
+
input: inputModalities,
|
|
690
|
+
output: outputModalities,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const inputAnswer = await rl.question("Input modalities [text]: ");
|
|
694
|
+
const outputAnswer = await rl.question("Output modalities [text]: ");
|
|
695
|
+
const parseModalityList = (answer) => {
|
|
696
|
+
if (!answer.trim()) {
|
|
697
|
+
return ["text"];
|
|
698
|
+
}
|
|
699
|
+
return answer.split(",").map((m) => {
|
|
700
|
+
const trimmed = m.trim().toLowerCase();
|
|
701
|
+
if (["text", "image", "audio", "video", "pdf"].includes(trimmed)) {
|
|
702
|
+
return trimmed;
|
|
703
|
+
}
|
|
704
|
+
throw new Error(`Unknown modality: ${trimmed}`);
|
|
705
|
+
});
|
|
706
|
+
};
|
|
707
|
+
return {
|
|
708
|
+
input: parseModalityList(inputAnswer),
|
|
709
|
+
output: parseModalityList(outputAnswer),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
586
712
|
void main();
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const MODEL_CAPABILITIES = {
|
|
2
|
+
"gpt-4o": { input: ["text", "image"], output: ["text"] },
|
|
3
|
+
"gpt-4o-mini": { input: ["text", "image"], output: ["text"] },
|
|
4
|
+
"gpt-4-turbo": { input: ["text", "image"], output: ["text"] },
|
|
5
|
+
"gpt-4-vision-preview": { input: ["text", "image"], output: ["text"] },
|
|
6
|
+
"gpt-4.1": { input: ["text", "image"], output: ["text"] },
|
|
7
|
+
"gpt-4.1-mini": { input: ["text", "image"], output: ["text"] },
|
|
8
|
+
"gpt-4.1-nano": { input: ["text", "image"], output: ["text"] },
|
|
9
|
+
"gpt-5": { input: ["text", "image"], output: ["text"] },
|
|
10
|
+
"gpt-5-mini": { input: ["text", "image"], output: ["text"] },
|
|
11
|
+
"gpt-5-nano": { input: ["text", "image"], output: ["text"] },
|
|
12
|
+
"o1": { input: ["text", "image"], output: ["text"] },
|
|
13
|
+
"o1-mini": { input: ["text", "image"], output: ["text"] },
|
|
14
|
+
"o1-pro": { input: ["text", "image"], output: ["text"] },
|
|
15
|
+
"o3": { input: ["text", "image"], output: ["text"] },
|
|
16
|
+
"o3-mini": { input: ["text", "image"], output: ["text"] },
|
|
17
|
+
"o3-pro": { input: ["text", "image"], output: ["text"] },
|
|
18
|
+
"o4-mini": { input: ["text", "image"], output: ["text"] },
|
|
19
|
+
"claude-3-opus": { input: ["text", "image"], output: ["text"] },
|
|
20
|
+
"claude-3-sonnet": { input: ["text", "image"], output: ["text"] },
|
|
21
|
+
"claude-3-haiku": { input: ["text", "image"], output: ["text"] },
|
|
22
|
+
"claude-3.5-sonnet": { input: ["text", "image"], output: ["text"] },
|
|
23
|
+
"claude-3.5-haiku": { input: ["text", "image"], output: ["text"] },
|
|
24
|
+
"claude-4-opus": { input: ["text", "image"], output: ["text"] },
|
|
25
|
+
"claude-4-sonnet": { input: ["text", "image"], output: ["text"] },
|
|
26
|
+
"claude-4.5-opus": { input: ["text", "image"], output: ["text"] },
|
|
27
|
+
"claude-4.5-sonnet": { input: ["text", "image"], output: ["text"] },
|
|
28
|
+
"claude-4.5-haiku": { input: ["text", "image"], output: ["text"] },
|
|
29
|
+
"gemini-pro-vision": { input: ["text", "image"], output: ["text"] },
|
|
30
|
+
"gemini-1.5-pro": { input: ["text", "image", "audio", "video"], output: ["text"] },
|
|
31
|
+
"gemini-1.5-flash": { input: ["text", "image", "audio", "video"], output: ["text"] },
|
|
32
|
+
"gemini-2.0-flash": { input: ["text", "image", "audio", "video"], output: ["text"] },
|
|
33
|
+
"gemini-2.5-pro": { input: ["text", "image", "audio", "video"], output: ["text"] },
|
|
34
|
+
"gemini-2.5-flash": { input: ["text", "image", "audio", "video"], output: ["text"] },
|
|
35
|
+
"qwen-vl-plus": { input: ["text", "image"], output: ["text"] },
|
|
36
|
+
"qwen-vl-max": { input: ["text", "image"], output: ["text"] },
|
|
37
|
+
"qwen-vl-chat": { input: ["text", "image"], output: ["text"] },
|
|
38
|
+
"qwen2.5-vl-72b-instruct": { input: ["text", "image"], output: ["text"] },
|
|
39
|
+
"qwen2.5-vl-7b-instruct": { input: ["text", "image"], output: ["text"] },
|
|
40
|
+
"qwen2.5-vl-32b-instruct": { input: ["text", "image"], output: ["text"] },
|
|
41
|
+
"qwen3-vl-235b-a22b": { input: ["text", "image"], output: ["text"] },
|
|
42
|
+
"qwen3-vl-30b-a3b": { input: ["text", "image"], output: ["text"] },
|
|
43
|
+
"llama-3.2-11b-vision-instruct": { input: ["text", "image"], output: ["text"] },
|
|
44
|
+
"llama-3.2-90b-vision-instruct": { input: ["text", "image"], output: ["text"] },
|
|
45
|
+
"llama-4-scout-17b-16e-instruct": { input: ["text", "image"], output: ["text"] },
|
|
46
|
+
"llama-4-maverick-17b-128e-instruct": { input: ["text", "image"], output: ["text"] },
|
|
47
|
+
"deepseek-vl": { input: ["text", "image"], output: ["text"] },
|
|
48
|
+
"deepseek-vl2": { input: ["text", "image"], output: ["text"] },
|
|
49
|
+
"glm-4v": { input: ["text", "image"], output: ["text"] },
|
|
50
|
+
"glm-4.5v": { input: ["text", "image"], output: ["text"] },
|
|
51
|
+
"glm-4.6v": { input: ["text", "image"], output: ["text"] },
|
|
52
|
+
"glm-5v-turbo": { input: ["text", "image"], output: ["text"] },
|
|
53
|
+
"pixtral-large-2411": { input: ["text", "image"], output: ["text"] },
|
|
54
|
+
"pixtral-large-2502": { input: ["text", "image"], output: ["text"] },
|
|
55
|
+
"mistral-small-3.1-24b-instruct": { input: ["text", "image"], output: ["text"] },
|
|
56
|
+
"phi-4-multimodal": { input: ["text", "image", "audio"], output: ["text"] },
|
|
57
|
+
"gemma-3-4b-it": { input: ["text", "image"], output: ["text"] },
|
|
58
|
+
"gemma-3-12b-it": { input: ["text", "image"], output: ["text"] },
|
|
59
|
+
"gemma-3-27b-it": { input: ["text", "image"], output: ["text"] },
|
|
60
|
+
"gemma-4-26b-a4b-it": { input: ["text", "image"], output: ["text"] },
|
|
61
|
+
"gemma-4-31b-it": { input: ["text", "image"], output: ["text"] },
|
|
62
|
+
};
|
|
63
|
+
const VISION_KEYWORDS = [
|
|
64
|
+
"vision",
|
|
65
|
+
"vl",
|
|
66
|
+
"visual",
|
|
67
|
+
"multimodal",
|
|
68
|
+
"pixtral",
|
|
69
|
+
];
|
|
70
|
+
const KNOWN_MODALITY_MODELS = new Set(Object.keys(MODEL_CAPABILITIES));
|
|
71
|
+
export function lookupModelCapabilities(modelId) {
|
|
72
|
+
const normalized = modelId.toLowerCase().trim();
|
|
73
|
+
if (MODEL_CAPABILITIES[normalized]) {
|
|
74
|
+
return MODEL_CAPABILITIES[normalized];
|
|
75
|
+
}
|
|
76
|
+
for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {
|
|
77
|
+
if (normalized.includes(pattern) || pattern.includes(normalized)) {
|
|
78
|
+
return caps;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const withoutPrefix = normalized.includes("/")
|
|
82
|
+
? normalized.split("/").pop()
|
|
83
|
+
: normalized;
|
|
84
|
+
if (MODEL_CAPABILITIES[withoutPrefix]) {
|
|
85
|
+
return MODEL_CAPABILITIES[withoutPrefix];
|
|
86
|
+
}
|
|
87
|
+
for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {
|
|
88
|
+
if (withoutPrefix.includes(pattern) || pattern.includes(withoutPrefix)) {
|
|
89
|
+
return caps;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const keyword of VISION_KEYWORDS) {
|
|
93
|
+
if (normalized.includes(keyword)) {
|
|
94
|
+
return { input: ["text", "image"], output: ["text"] };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
export function parseCapabilitiesFromApi(model) {
|
|
100
|
+
if (model.modalities && typeof model.modalities === "object") {
|
|
101
|
+
const m = model.modalities;
|
|
102
|
+
const result = {};
|
|
103
|
+
if (Array.isArray(m.input)) {
|
|
104
|
+
result.input = m.input.filter((v) => typeof v === "string");
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(m.output)) {
|
|
107
|
+
result.output = m.output.filter((v) => typeof v === "string");
|
|
108
|
+
}
|
|
109
|
+
if (result.input || result.output) {
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (model.capabilities && typeof model.capabilities === "object") {
|
|
114
|
+
const caps = model.capabilities;
|
|
115
|
+
const input = ["text"];
|
|
116
|
+
if (caps.vision || caps.image || caps.multimodal) {
|
|
117
|
+
input.push("image");
|
|
118
|
+
}
|
|
119
|
+
if (caps.audio || caps.speech) {
|
|
120
|
+
input.push("audio");
|
|
121
|
+
}
|
|
122
|
+
if (input.length > 1) {
|
|
123
|
+
return { input: input, output: ["text"] };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
export function mergeModelCapabilities(modelId, apiCapabilities, userOverrides) {
|
|
129
|
+
if (userOverrides && (userOverrides.input || userOverrides.output)) {
|
|
130
|
+
return userOverrides;
|
|
131
|
+
}
|
|
132
|
+
if (apiCapabilities && (apiCapabilities.input || apiCapabilities.output)) {
|
|
133
|
+
return apiCapabilities;
|
|
134
|
+
}
|
|
135
|
+
return lookupModelCapabilities(modelId);
|
|
136
|
+
}
|
|
137
|
+
export function isKnownModel(modelId) {
|
|
138
|
+
const normalized = modelId.toLowerCase().trim();
|
|
139
|
+
if (KNOWN_MODALITY_MODELS.has(normalized)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
const withoutPrefix = normalized.includes("/")
|
|
143
|
+
? normalized.split("/").pop()
|
|
144
|
+
: normalized;
|
|
145
|
+
if (KNOWN_MODALITY_MODELS.has(withoutPrefix)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
for (const keyword of VISION_KEYWORDS) {
|
|
149
|
+
if (normalized.includes(keyword)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
export function parseModalitiesFromString(value) {
|
|
156
|
+
const parts = value.split(":");
|
|
157
|
+
if (parts.length !== 2) {
|
|
158
|
+
throw new Error(`Invalid modalities format: ${value}. Expected <input>:<output>`);
|
|
159
|
+
}
|
|
160
|
+
const [inputStr, outputStr] = parts;
|
|
161
|
+
const parseModalityList = (s) => {
|
|
162
|
+
return s.split(",").map((m) => {
|
|
163
|
+
const trimmed = m.trim().toLowerCase();
|
|
164
|
+
if (["text", "image", "audio", "video", "pdf"].includes(trimmed)) {
|
|
165
|
+
return trimmed;
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Unknown modality: ${trimmed}`);
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
input: parseModalityList(inputStr),
|
|
172
|
+
output: parseModalityList(outputStr),
|
|
173
|
+
};
|
|
174
|
+
}
|
package/dist/core/provider.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseCapabilitiesFromApi, mergeModelCapabilities } from "./model-capabilities.js";
|
|
1
2
|
export function normalizeBaseUrl(baseURL) {
|
|
2
3
|
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
3
4
|
if (!normalized) {
|
|
@@ -26,16 +27,25 @@ export async function validateAndFetchModels(input, fetchImpl = globalThis.fetch
|
|
|
26
27
|
if (!isModelListResponse(body)) {
|
|
27
28
|
throw new Error("Expected /models to return an object with a data array");
|
|
28
29
|
}
|
|
30
|
+
const compatibleModels = body.data.filter(isOpenCodeCompatibleModel);
|
|
29
31
|
const models = [
|
|
30
|
-
...new Set(
|
|
31
|
-
.filter(isOpenCodeCompatibleModel)
|
|
32
|
+
...new Set(compatibleModels
|
|
32
33
|
.map((model) => model.id.trim())
|
|
33
34
|
.filter(Boolean))
|
|
34
35
|
];
|
|
35
36
|
if (models.length === 0) {
|
|
36
37
|
throw new Error("Provider returned no OpenCode-compatible model ids");
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
const modelDetails = models.map((modelId) => {
|
|
40
|
+
const rawModel = compatibleModels.find((m) => m.id.trim() === modelId);
|
|
41
|
+
const apiCapabilities = rawModel ? parseCapabilitiesFromApi(rawModel) : undefined;
|
|
42
|
+
const mergedCapabilities = mergeModelCapabilities(modelId, apiCapabilities);
|
|
43
|
+
return {
|
|
44
|
+
id: modelId,
|
|
45
|
+
modalities: mergedCapabilities
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
return { baseURL, models, modelDetails };
|
|
39
49
|
}
|
|
40
50
|
export async function detectProviderCapabilities(input, fetchImpl = globalThis.fetch) {
|
|
41
51
|
const baseURL = normalizeBaseUrl(input.baseURL);
|
|
@@ -78,7 +88,10 @@ export function buildProviderConfig(input) {
|
|
|
78
88
|
options: {
|
|
79
89
|
baseURL
|
|
80
90
|
},
|
|
81
|
-
models: Object.fromEntries(input.models.map((model) =>
|
|
91
|
+
models: Object.fromEntries(input.models.map((model) => {
|
|
92
|
+
const modelInfo = input.modelDetails?.find((m) => m.id === model);
|
|
93
|
+
return [model, buildModelConfig(model, modelInfo)];
|
|
94
|
+
}))
|
|
82
95
|
};
|
|
83
96
|
if (opencodeApiType !== "messages") {
|
|
84
97
|
provider.options.setCacheKey = true;
|
|
@@ -93,6 +106,13 @@ export function buildProviderConfig(input) {
|
|
|
93
106
|
}
|
|
94
107
|
};
|
|
95
108
|
}
|
|
109
|
+
function buildModelConfig(modelId, modelInfo) {
|
|
110
|
+
const config = { name: modelId };
|
|
111
|
+
if (modelInfo?.modalities) {
|
|
112
|
+
config.modalities = modelInfo.modalities;
|
|
113
|
+
}
|
|
114
|
+
return config;
|
|
115
|
+
}
|
|
96
116
|
export function npmPackageForOpenCodeApiType(apiType) {
|
|
97
117
|
if (apiType === "responses") {
|
|
98
118
|
return "@ai-sdk/openai";
|
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