@heventure/model-provider-x 0.2.3 → 0.2.5-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/README.md +45 -0
- package/dist/cli/args.js +42 -0
- package/dist/cli/index.js +115 -4
- package/dist/cli/model-choices.js +40 -8
- package/dist/core/model-capabilities.js +177 -0
- package/dist/core/model-registry.js +191 -0
- package/dist/core/provider.js +291 -10
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -135,6 +135,51 @@ node dist/cli/index.js setup --target codex --provider lmstudio --proxy
|
|
|
135
135
|
node dist/cli/index.js setup --target opencode --provider lmstudio --direct
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
## Model Metadata Registry
|
|
139
|
+
|
|
140
|
+
Some OpenAI-compatible providers only return minimal `/v1/models` entries with `id`, `object`, and `owned_by`.
|
|
141
|
+
For those providers, `model-provider-x` can enrich model capabilities from registries.
|
|
142
|
+
|
|
143
|
+
The merge order is:
|
|
144
|
+
|
|
145
|
+
1. User overrides such as `--modalities`.
|
|
146
|
+
2. Provider or native runtime metadata.
|
|
147
|
+
3. Project-local registry overrides.
|
|
148
|
+
4. Built-in models.dev-shaped metadata.
|
|
149
|
+
5. Conservative model-name heuristics.
|
|
150
|
+
|
|
151
|
+
By default, the CLI reads `model-provider-x.models.jsonc` from the current working directory when it exists.
|
|
152
|
+
You can also pass one or more registry files explicitly:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
node dist/cli/index.js setup \
|
|
156
|
+
--provider custom \
|
|
157
|
+
--model-registry ./model-provider-x.models.jsonc
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Example registry:
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
{
|
|
164
|
+
"providers": {
|
|
165
|
+
"mimo-pool": {
|
|
166
|
+
"models": {
|
|
167
|
+
"mimo-v2.5-tts": {
|
|
168
|
+
"type": "tts",
|
|
169
|
+
"modalities": {
|
|
170
|
+
"input": ["text"],
|
|
171
|
+
"output": ["audio"]
|
|
172
|
+
},
|
|
173
|
+
"limit": {
|
|
174
|
+
"context": 8192
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
138
183
|
## Commands
|
|
139
184
|
|
|
140
185
|
```bash
|
package/dist/cli/args.js
CHANGED
|
@@ -29,6 +29,18 @@ export function parseCliArgs(argv) {
|
|
|
29
29
|
.map((model) => model.trim())
|
|
30
30
|
.filter(Boolean);
|
|
31
31
|
break;
|
|
32
|
+
case "--model-registry":
|
|
33
|
+
if (!options.modelRegistryPaths) {
|
|
34
|
+
options.modelRegistryPaths = [];
|
|
35
|
+
}
|
|
36
|
+
options.modelRegistryPaths.push(next());
|
|
37
|
+
break;
|
|
38
|
+
case "--modalities":
|
|
39
|
+
if (!options.modalities) {
|
|
40
|
+
options.modalities = [];
|
|
41
|
+
}
|
|
42
|
+
options.modalities.push(parseModalitiesArg(next()));
|
|
43
|
+
break;
|
|
32
44
|
case "--name":
|
|
33
45
|
options.providerName = next();
|
|
34
46
|
break;
|
|
@@ -107,6 +119,10 @@ Options:
|
|
|
107
119
|
--proxy Write agent config through the local compatibility proxy.
|
|
108
120
|
--direct Write agent config directly to the upstream provider.
|
|
109
121
|
--models <list> Comma-separated model ids. Skips interactive model selection.
|
|
122
|
+
--model-registry <path>
|
|
123
|
+
JSONC model metadata override. Can be repeated.
|
|
124
|
+
--modalities <spec> Model modalities in format <model>:<input>:<output>.
|
|
125
|
+
Example: --modalities qwen-vl:image,text:text
|
|
110
126
|
--config <path> OpenCode config path to write when targeting OpenCode.
|
|
111
127
|
--print Print generated JSON and do not write config.
|
|
112
128
|
--yes, -y Accept defaults in non-interactive prompts.
|
|
@@ -134,3 +150,29 @@ function addModelByOneBasedIndex(selected, models, index) {
|
|
|
134
150
|
}
|
|
135
151
|
selected.add(model);
|
|
136
152
|
}
|
|
153
|
+
function parseModalitiesArg(value) {
|
|
154
|
+
const parts = value.split(":");
|
|
155
|
+
if (parts.length !== 3) {
|
|
156
|
+
throw new Error(`Invalid --modalities format: ${value}. Expected <model>:<input>:<output>`);
|
|
157
|
+
}
|
|
158
|
+
const [modelId, inputStr, outputStr] = parts;
|
|
159
|
+
if (!modelId) {
|
|
160
|
+
throw new Error("Model id is required in --modalities");
|
|
161
|
+
}
|
|
162
|
+
const parseModalityList = (s) => {
|
|
163
|
+
return s.split(",").map((m) => {
|
|
164
|
+
const trimmed = m.trim().toLowerCase();
|
|
165
|
+
if (["text", "image", "audio", "video", "pdf"].includes(trimmed)) {
|
|
166
|
+
return trimmed;
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`Unknown modality: ${trimmed}`);
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
modelId,
|
|
173
|
+
modalities: {
|
|
174
|
+
input: parseModalityList(inputStr),
|
|
175
|
+
output: parseModalityList(outputStr),
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { readProxyStatus, startProxyProcess, stopProxyProcess } from "../core/proxy-process.js";
|
|
6
7
|
import { createProxyAuthToken, getDefaultToolConfigPath, readToolConfig, upsertProviderProfile, writeToolConfig } from "../core/tool-config.js";
|
|
7
8
|
import { discoverOpenCodeConfigs, getDefaultConfigPath, writeProviderToConfig } from "../core/config.js";
|
|
8
9
|
import { buildProviderConfig, detectProviderCapabilities, recommendProxyMode, validateAndFetchModels } from "../core/provider.js";
|
|
10
|
+
import { DEFAULT_MODEL_REGISTRY_FILE } from "../core/model-registry.js";
|
|
11
|
+
import { isKnownModel } from "../core/model-capabilities.js";
|
|
9
12
|
import { startProxyServer } from "../proxy/server.js";
|
|
10
13
|
import { defaultClaudeModelMapping, getDefaultClaudeSettingsPath, writeClaudeCodeSettings } from "../targets/claude-code.js";
|
|
11
14
|
import { getDefaultCodexConfigPath, writeCodexConfig } from "../targets/codex.js";
|
|
@@ -71,17 +74,24 @@ export async function runCli(options) {
|
|
|
71
74
|
const baseURL = await requiredOption(rl, options.baseURL ?? providerDefaults.baseURL, "API base URL");
|
|
72
75
|
const apiKey = await resolveApiKey(rl, options.apiKey, providerDefaults.preset);
|
|
73
76
|
output.write("Fetching models...\n");
|
|
74
|
-
const fetched = await validateAndFetchModels({
|
|
77
|
+
const fetched = await validateAndFetchModels({
|
|
78
|
+
baseURL,
|
|
79
|
+
apiKey,
|
|
80
|
+
providerId,
|
|
81
|
+
modelRegistryPaths: resolveModelRegistryPaths(options)
|
|
82
|
+
});
|
|
75
83
|
const selectedModels = options.models ??
|
|
76
84
|
(canUseTui()
|
|
77
|
-
? await multiSelectChoices("Select models", createModelChoices(fetched.
|
|
85
|
+
? await multiSelectChoices("Select models", createModelChoices(fetched.modelDetails))
|
|
78
86
|
: parseModelSelection(await rl.question(formatModelPrompt(fetched.models)), fetched.models));
|
|
87
|
+
const modelDetails = applyModelModalityOverrides(fetched.modelDetails.filter((m) => selectedModels.includes(m.id)), options);
|
|
79
88
|
const fragment = buildProviderConfig({
|
|
80
89
|
providerId,
|
|
81
90
|
providerName,
|
|
82
91
|
baseURL: fetched.baseURL,
|
|
83
92
|
apiKey,
|
|
84
93
|
models: selectedModels,
|
|
94
|
+
modelDetails,
|
|
85
95
|
opencodeApiType: options.opencodeApiType ?? "chat"
|
|
86
96
|
});
|
|
87
97
|
const provider = fragment.provider[providerId];
|
|
@@ -142,15 +152,21 @@ async function collectProviderInput(rl, command) {
|
|
|
142
152
|
}
|
|
143
153
|
async function collectProviderSelection(rl, command, providerInput) {
|
|
144
154
|
output.write("Fetching models...\n");
|
|
145
|
-
const fetched = await validateAndFetchModels({
|
|
155
|
+
const fetched = await validateAndFetchModels({
|
|
156
|
+
baseURL: providerInput.baseURL,
|
|
157
|
+
apiKey: providerInput.apiKey,
|
|
158
|
+
providerId: providerInput.providerId,
|
|
159
|
+
modelRegistryPaths: resolveModelRegistryPaths(command.options)
|
|
160
|
+
});
|
|
146
161
|
const selectedModels = command.options.models ??
|
|
147
162
|
(canUseTui()
|
|
148
|
-
? await multiSelectChoices("Select models", createModelChoices(fetched.
|
|
163
|
+
? await multiSelectChoices("Select models", createModelChoices(fetched.modelDetails))
|
|
149
164
|
: parseModelSelection(await rl.question(formatModelPrompt(fetched.models)), fetched.models));
|
|
150
165
|
const defaultModel = await resolveDefaultModel(rl, command, selectedModels);
|
|
151
166
|
if (!defaultModel) {
|
|
152
167
|
throw new Error("Select at least one model");
|
|
153
168
|
}
|
|
169
|
+
const modelDetails = await resolveModelModalities(rl, command, selectedModels, fetched.modelDetails);
|
|
154
170
|
const toolConfigPath = getDefaultToolConfigPath();
|
|
155
171
|
const config = await upsertProviderProfile(toolConfigPath, {
|
|
156
172
|
id: providerInput.providerId,
|
|
@@ -168,6 +184,7 @@ async function collectProviderSelection(rl, command, providerInput) {
|
|
|
168
184
|
upstreamBaseURL: fetched.baseURL,
|
|
169
185
|
apiKey: providerInput.apiKey,
|
|
170
186
|
selectedModels,
|
|
187
|
+
modelDetails,
|
|
171
188
|
defaultModel,
|
|
172
189
|
config,
|
|
173
190
|
toolConfigPath
|
|
@@ -186,6 +203,7 @@ async function writeOpenCodeSetup(rl, command, selection, useProxy, capabilities
|
|
|
186
203
|
baseURL,
|
|
187
204
|
apiKey,
|
|
188
205
|
models: selection.selectedModels,
|
|
206
|
+
modelDetails: selection.modelDetails,
|
|
189
207
|
opencodeApiType
|
|
190
208
|
});
|
|
191
209
|
const provider = fragment.provider[selection.providerId];
|
|
@@ -366,6 +384,30 @@ function tokenHint(token) {
|
|
|
366
384
|
}
|
|
367
385
|
return `${token.slice(0, 8)}...${token.slice(-4)}`;
|
|
368
386
|
}
|
|
387
|
+
function resolveModelRegistryPaths(options) {
|
|
388
|
+
const paths = [...(options.modelRegistryPaths ?? [])];
|
|
389
|
+
if (existsSync(DEFAULT_MODEL_REGISTRY_FILE) && !paths.includes(DEFAULT_MODEL_REGISTRY_FILE)) {
|
|
390
|
+
paths.push(DEFAULT_MODEL_REGISTRY_FILE);
|
|
391
|
+
}
|
|
392
|
+
return paths;
|
|
393
|
+
}
|
|
394
|
+
function applyModelModalityOverrides(modelDetails, options) {
|
|
395
|
+
if (!options.modalities?.length) {
|
|
396
|
+
return modelDetails;
|
|
397
|
+
}
|
|
398
|
+
const overrides = new Map(options.modalities.map((override) => [override.modelId, override.modalities]));
|
|
399
|
+
return modelDetails.map((model) => {
|
|
400
|
+
const modalities = overrides.get(model.id);
|
|
401
|
+
if (!modalities) {
|
|
402
|
+
return model;
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
...model,
|
|
406
|
+
modalities,
|
|
407
|
+
metadataSources: [...new Set([...(model.metadataSources ?? []), "user"])]
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
}
|
|
369
411
|
async function requiredOption(rl, value, label) {
|
|
370
412
|
const answer = value ?? (await rl.question(`${label}: `));
|
|
371
413
|
if (!answer.trim()) {
|
|
@@ -634,4 +676,73 @@ function slugify(value) {
|
|
|
634
676
|
.replace(/[^a-z0-9]+/g, "-")
|
|
635
677
|
.replace(/^-+|-+$/g, "");
|
|
636
678
|
}
|
|
679
|
+
async function resolveModelModalities(rl, command, selectedModels, fetchedModelDetails) {
|
|
680
|
+
const userModalityOverrides = new Map();
|
|
681
|
+
if (command.options.modalities) {
|
|
682
|
+
for (const override of command.options.modalities) {
|
|
683
|
+
userModalityOverrides.set(override.modelId, override.modalities);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const result = [];
|
|
687
|
+
for (const modelId of selectedModels) {
|
|
688
|
+
const fetched = fetchedModelDetails.find((m) => m.id === modelId);
|
|
689
|
+
const userOverride = userModalityOverrides.get(modelId);
|
|
690
|
+
if (userOverride) {
|
|
691
|
+
result.push({ id: modelId, modalities: userOverride });
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (fetched?.modalities) {
|
|
695
|
+
result.push(fetched);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (command.options.yes || isKnownModel(modelId)) {
|
|
699
|
+
result.push(fetched ?? { id: modelId });
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const modalities = await promptModelModalities(rl, modelId);
|
|
703
|
+
result.push({ id: modelId, modalities });
|
|
704
|
+
}
|
|
705
|
+
return result;
|
|
706
|
+
}
|
|
707
|
+
async function promptModelModalities(rl, modelId) {
|
|
708
|
+
output.write(`\nModel: ${modelId} - Capabilities unknown\n`);
|
|
709
|
+
if (canUseTui()) {
|
|
710
|
+
const inputModalities = await multiSelectChoices(`Input modalities: ${modelId}`, [
|
|
711
|
+
{ label: "text", value: "text", hint: "default" },
|
|
712
|
+
{ label: "image", value: "image" },
|
|
713
|
+
{ label: "audio", value: "audio" },
|
|
714
|
+
{ label: "video", value: "video" },
|
|
715
|
+
{ label: "pdf", value: "pdf" },
|
|
716
|
+
]);
|
|
717
|
+
const outputModalities = await multiSelectChoices(`Output modalities: ${modelId}`, [
|
|
718
|
+
{ label: "text", value: "text", hint: "default" },
|
|
719
|
+
{ label: "image", value: "image" },
|
|
720
|
+
{ label: "audio", value: "audio" },
|
|
721
|
+
{ label: "video", value: "video" },
|
|
722
|
+
{ label: "pdf", value: "pdf" },
|
|
723
|
+
]);
|
|
724
|
+
return {
|
|
725
|
+
input: inputModalities,
|
|
726
|
+
output: outputModalities,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
const inputAnswer = await rl.question("Input modalities [text]: ");
|
|
730
|
+
const outputAnswer = await rl.question("Output modalities [text]: ");
|
|
731
|
+
const parseModalityList = (answer) => {
|
|
732
|
+
if (!answer.trim()) {
|
|
733
|
+
return ["text"];
|
|
734
|
+
}
|
|
735
|
+
return answer.split(",").map((m) => {
|
|
736
|
+
const trimmed = m.trim().toLowerCase();
|
|
737
|
+
if (["text", "image", "audio", "video", "pdf"].includes(trimmed)) {
|
|
738
|
+
return trimmed;
|
|
739
|
+
}
|
|
740
|
+
throw new Error(`Unknown modality: ${trimmed}`);
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
return {
|
|
744
|
+
input: parseModalityList(inputAnswer),
|
|
745
|
+
output: parseModalityList(outputAnswer),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
637
748
|
void main();
|
|
@@ -1,17 +1,49 @@
|
|
|
1
1
|
const likelyUnsupportedModelName = /\b(?:embed|embedding|embeddings|bge|e5|nomic-embed)\b/i;
|
|
2
2
|
export function createModelChoices(models) {
|
|
3
3
|
return models.map((model) => {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const info = typeof model === "string" ? { id: model } : model;
|
|
5
|
+
const unsupported = isLikelyUnsupportedModel(info);
|
|
6
|
+
const hint = modelHint(info, unsupported);
|
|
7
|
+
if (!unsupported) {
|
|
8
|
+
return modelChoice(info.id, hint, true);
|
|
6
9
|
}
|
|
7
|
-
return
|
|
8
|
-
label: model,
|
|
9
|
-
value: model,
|
|
10
|
-
hint: "suspected unsupported model",
|
|
11
|
-
selected: false
|
|
12
|
-
};
|
|
10
|
+
return modelChoice(info.id, hint, false);
|
|
13
11
|
});
|
|
14
12
|
}
|
|
15
13
|
export function isLikelyUnsupportedModelName(model) {
|
|
16
14
|
return likelyUnsupportedModelName.test(model);
|
|
17
15
|
}
|
|
16
|
+
function isLikelyUnsupportedModel(model) {
|
|
17
|
+
return model.type === "embedding" || isLikelyUnsupportedModelName(model.id);
|
|
18
|
+
}
|
|
19
|
+
function modelHint(model, unsupported) {
|
|
20
|
+
const hints = [
|
|
21
|
+
model.type,
|
|
22
|
+
model.architecture,
|
|
23
|
+
model.quantization,
|
|
24
|
+
model.contextLength ? `${formatContextLength(model.contextLength)} ctx` : undefined,
|
|
25
|
+
model.modalities?.input?.includes("image") ? "vision" : undefined,
|
|
26
|
+
model.capabilities?.toolCall ? "tools" : undefined,
|
|
27
|
+
model.capabilities?.reasoning ? "reasoning" : undefined,
|
|
28
|
+
model.state && model.state !== "loaded" ? model.state : undefined,
|
|
29
|
+
unsupported ? "suspected unsupported model" : undefined
|
|
30
|
+
].filter((hint) => Boolean(hint));
|
|
31
|
+
return hints.length ? hints.join(", ") : undefined;
|
|
32
|
+
}
|
|
33
|
+
function formatContextLength(value) {
|
|
34
|
+
if (value >= 1000 && value % 1000 === 0) {
|
|
35
|
+
return `${value / 1000}k`;
|
|
36
|
+
}
|
|
37
|
+
if (value >= 1024 && value % 1024 === 0) {
|
|
38
|
+
return `${value / 1024}k`;
|
|
39
|
+
}
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
function modelChoice(modelId, hint, selected) {
|
|
43
|
+
return {
|
|
44
|
+
label: modelId,
|
|
45
|
+
value: modelId,
|
|
46
|
+
...(hint ? { hint } : {}),
|
|
47
|
+
selected
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
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 || caps.image_input) {
|
|
117
|
+
input.push("image");
|
|
118
|
+
}
|
|
119
|
+
if (caps.audio || caps.speech) {
|
|
120
|
+
input.push("audio");
|
|
121
|
+
}
|
|
122
|
+
if (caps.video) {
|
|
123
|
+
input.push("video");
|
|
124
|
+
}
|
|
125
|
+
if (input.length > 1) {
|
|
126
|
+
return { input: input, output: ["text"] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
export function mergeModelCapabilities(modelId, apiCapabilities, userOverrides) {
|
|
132
|
+
if (userOverrides && (userOverrides.input || userOverrides.output)) {
|
|
133
|
+
return userOverrides;
|
|
134
|
+
}
|
|
135
|
+
if (apiCapabilities && (apiCapabilities.input || apiCapabilities.output)) {
|
|
136
|
+
return apiCapabilities;
|
|
137
|
+
}
|
|
138
|
+
return lookupModelCapabilities(modelId);
|
|
139
|
+
}
|
|
140
|
+
export function isKnownModel(modelId) {
|
|
141
|
+
const normalized = modelId.toLowerCase().trim();
|
|
142
|
+
if (KNOWN_MODALITY_MODELS.has(normalized)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
const withoutPrefix = normalized.includes("/")
|
|
146
|
+
? normalized.split("/").pop()
|
|
147
|
+
: normalized;
|
|
148
|
+
if (KNOWN_MODALITY_MODELS.has(withoutPrefix)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
for (const keyword of VISION_KEYWORDS) {
|
|
152
|
+
if (normalized.includes(keyword)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
export function parseModalitiesFromString(value) {
|
|
159
|
+
const parts = value.split(":");
|
|
160
|
+
if (parts.length !== 2) {
|
|
161
|
+
throw new Error(`Invalid modalities format: ${value}. Expected <input>:<output>`);
|
|
162
|
+
}
|
|
163
|
+
const [inputStr, outputStr] = parts;
|
|
164
|
+
const parseModalityList = (s) => {
|
|
165
|
+
return s.split(",").map((m) => {
|
|
166
|
+
const trimmed = m.trim().toLowerCase();
|
|
167
|
+
if (["text", "image", "audio", "video", "pdf"].includes(trimmed)) {
|
|
168
|
+
return trimmed;
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`Unknown modality: ${trimmed}`);
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
input: parseModalityList(inputStr),
|
|
175
|
+
output: parseModalityList(outputStr),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { parse } from "jsonc-parser";
|
|
3
|
+
export const DEFAULT_MODEL_REGISTRY_FILE = "model-provider-x.models.jsonc";
|
|
4
|
+
const BUILT_IN_MODELS_DEV_REGISTRY = {
|
|
5
|
+
source: "models-dev",
|
|
6
|
+
providers: {
|
|
7
|
+
openai: {
|
|
8
|
+
models: {
|
|
9
|
+
"gpt-oss-20b": {
|
|
10
|
+
type: "llm",
|
|
11
|
+
contextLength: 131072,
|
|
12
|
+
capabilities: {
|
|
13
|
+
reasoning: true,
|
|
14
|
+
toolCall: true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export async function loadModelRegistryFile(path) {
|
|
22
|
+
const text = await readFile(path, "utf8");
|
|
23
|
+
const value = parse(text);
|
|
24
|
+
if (!isObject(value)) {
|
|
25
|
+
throw new Error(`Model registry must be an object: ${path}`);
|
|
26
|
+
}
|
|
27
|
+
return normalizeRegistry(value, "local-registry");
|
|
28
|
+
}
|
|
29
|
+
export async function resolveModelRegistryMetadata(input) {
|
|
30
|
+
const registries = [
|
|
31
|
+
...(input.includeBuiltIn === false ? [] : [BUILT_IN_MODELS_DEV_REGISTRY]),
|
|
32
|
+
...(input.registries ?? [])
|
|
33
|
+
];
|
|
34
|
+
let result;
|
|
35
|
+
for (const registry of registries) {
|
|
36
|
+
const model = lookupRegistryModel(registry, input.providerId, input.modelId);
|
|
37
|
+
if (!model) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const info = normalizeRegistryModel(input.modelId, model, registry.source);
|
|
41
|
+
result = result ? mergeRegistryInfo(result, info) : info;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function normalizeRegistry(value, fallbackSource) {
|
|
46
|
+
return {
|
|
47
|
+
source: stringValue(value.source) ?? fallbackSource,
|
|
48
|
+
providers: normalizeProviders(value.providers),
|
|
49
|
+
models: normalizeModels(value.models)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function lookupRegistryModel(registry, providerId, modelId) {
|
|
53
|
+
const aliases = modelAliases(modelId);
|
|
54
|
+
if (providerId) {
|
|
55
|
+
const providerModels = registry.providers?.[providerId]?.models;
|
|
56
|
+
const match = lookupModel(providerModels, aliases);
|
|
57
|
+
if (match) {
|
|
58
|
+
return match;
|
|
59
|
+
}
|
|
60
|
+
const modelsDevProvider = registry[providerId];
|
|
61
|
+
if (isObject(modelsDevProvider)) {
|
|
62
|
+
const provider = normalizeProviders({ [providerId]: modelsDevProvider })?.[providerId];
|
|
63
|
+
const providerMatch = lookupModel(provider?.models, aliases);
|
|
64
|
+
if (providerMatch) {
|
|
65
|
+
return providerMatch;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return lookupModel(registry.models, aliases);
|
|
70
|
+
}
|
|
71
|
+
function lookupModel(models, aliases) {
|
|
72
|
+
if (!models) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
for (const alias of aliases) {
|
|
76
|
+
if (models[alias]) {
|
|
77
|
+
return models[alias];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
function normalizeRegistryModel(modelId, model, source) {
|
|
83
|
+
const modalities = normalizeModalities(model);
|
|
84
|
+
const capabilities = model.capabilities;
|
|
85
|
+
const toolCall = booleanValue(capabilities?.toolCall ?? capabilities?.tool_call ?? model.tool_call);
|
|
86
|
+
const reasoning = booleanValue(capabilities?.reasoning ?? model.reasoning);
|
|
87
|
+
return cleanModelInfo({
|
|
88
|
+
id: model.id ?? modelId,
|
|
89
|
+
type: model.type,
|
|
90
|
+
architecture: model.architecture,
|
|
91
|
+
quantization: model.quantization,
|
|
92
|
+
parameterSize: model.parameterSize,
|
|
93
|
+
state: model.state,
|
|
94
|
+
contextLength: model.contextLength ?? model.limit?.context,
|
|
95
|
+
modalities,
|
|
96
|
+
capabilities: toolCall || reasoning ? { toolCall, reasoning } : undefined,
|
|
97
|
+
metadataSources: [source]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function normalizeModalities(model) {
|
|
101
|
+
if (model.modalities) {
|
|
102
|
+
return model.modalities;
|
|
103
|
+
}
|
|
104
|
+
if (model.input_modalities || model.output_modalities) {
|
|
105
|
+
return {
|
|
106
|
+
...(model.input_modalities ? { input: model.input_modalities } : {}),
|
|
107
|
+
...(model.output_modalities ? { output: model.output_modalities } : {})
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
function mergeRegistryInfo(base, next) {
|
|
113
|
+
return cleanModelInfo({
|
|
114
|
+
...base,
|
|
115
|
+
...next,
|
|
116
|
+
id: base.id || next.id,
|
|
117
|
+
type: next.type ?? base.type,
|
|
118
|
+
architecture: next.architecture ?? base.architecture,
|
|
119
|
+
quantization: next.quantization ?? base.quantization,
|
|
120
|
+
parameterSize: next.parameterSize ?? base.parameterSize,
|
|
121
|
+
state: next.state ?? base.state,
|
|
122
|
+
contextLength: next.contextLength ?? base.contextLength,
|
|
123
|
+
modalities: next.modalities ?? base.modalities,
|
|
124
|
+
capabilities: base.capabilities || next.capabilities
|
|
125
|
+
? {
|
|
126
|
+
...base.capabilities,
|
|
127
|
+
...next.capabilities
|
|
128
|
+
}
|
|
129
|
+
: undefined,
|
|
130
|
+
metadataSources: [...new Set([...(base.metadataSources ?? []), ...(next.metadataSources ?? [])])]
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function normalizeProviders(value) {
|
|
134
|
+
if (!isObject(value)) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const providers = {};
|
|
138
|
+
for (const [providerId, providerValue] of Object.entries(value)) {
|
|
139
|
+
if (!isObject(providerValue)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const models = normalizeModels(providerValue.models);
|
|
143
|
+
if (models) {
|
|
144
|
+
providers[providerId] = { models };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return Object.keys(providers).length ? providers : undefined;
|
|
148
|
+
}
|
|
149
|
+
function normalizeModels(value) {
|
|
150
|
+
if (!isObject(value)) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const models = {};
|
|
154
|
+
for (const [modelId, modelValue] of Object.entries(value)) {
|
|
155
|
+
if (isObject(modelValue)) {
|
|
156
|
+
models[modelId] = modelValue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return Object.keys(models).length ? models : undefined;
|
|
160
|
+
}
|
|
161
|
+
function modelAliases(modelId) {
|
|
162
|
+
const normalized = modelId.trim();
|
|
163
|
+
const withoutPrefix = normalized.includes("/") ? normalized.split("/").pop() : normalized;
|
|
164
|
+
return [...new Set([normalized, withoutPrefix])];
|
|
165
|
+
}
|
|
166
|
+
function cleanModelInfo(model) {
|
|
167
|
+
const capabilities = model.capabilities?.toolCall || model.capabilities?.reasoning
|
|
168
|
+
? model.capabilities
|
|
169
|
+
: undefined;
|
|
170
|
+
return {
|
|
171
|
+
id: model.id,
|
|
172
|
+
...(model.type ? { type: model.type } : {}),
|
|
173
|
+
...(model.architecture ? { architecture: model.architecture } : {}),
|
|
174
|
+
...(model.quantization ? { quantization: model.quantization } : {}),
|
|
175
|
+
...(model.parameterSize ? { parameterSize: model.parameterSize } : {}),
|
|
176
|
+
...(model.state ? { state: model.state } : {}),
|
|
177
|
+
...(model.contextLength ? { contextLength: model.contextLength } : {}),
|
|
178
|
+
...(model.modalities ? { modalities: model.modalities } : {}),
|
|
179
|
+
...(capabilities ? { capabilities } : {}),
|
|
180
|
+
...(model.metadataSources?.length ? { metadataSources: model.metadataSources } : {})
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function isObject(value) {
|
|
184
|
+
return typeof value === "object" && value !== null;
|
|
185
|
+
}
|
|
186
|
+
function stringValue(value) {
|
|
187
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
188
|
+
}
|
|
189
|
+
function booleanValue(value) {
|
|
190
|
+
return typeof value === "boolean" ? value : undefined;
|
|
191
|
+
}
|
package/dist/core/provider.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseCapabilitiesFromApi, mergeModelCapabilities } from "./model-capabilities.js";
|
|
2
|
+
import { loadModelRegistryFile, resolveModelRegistryMetadata } from "./model-registry.js";
|
|
1
3
|
export function normalizeBaseUrl(baseURL) {
|
|
2
4
|
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
3
5
|
if (!normalized) {
|
|
@@ -11,7 +13,7 @@ export function normalizeBaseUrl(baseURL) {
|
|
|
11
13
|
}
|
|
12
14
|
return normalized;
|
|
13
15
|
}
|
|
14
|
-
export async function validateAndFetchModels(input, fetchImpl = globalThis.fetch) {
|
|
16
|
+
export async function validateAndFetchModels(input, fetchImpl = globalThis.fetch, lmStudioSdkLoader = loadLmStudioSdkModels) {
|
|
15
17
|
const baseURL = normalizeBaseUrl(input.baseURL);
|
|
16
18
|
const apiKey = input.apiKey?.trim() ?? "";
|
|
17
19
|
const headers = {};
|
|
@@ -26,16 +28,34 @@ export async function validateAndFetchModels(input, fetchImpl = globalThis.fetch
|
|
|
26
28
|
if (!isModelListResponse(body)) {
|
|
27
29
|
throw new Error("Expected /models to return an object with a data array");
|
|
28
30
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const sourceModels = body.data.map((model) => normalizeModelInfo(model, "openai-models"));
|
|
32
|
+
const nativeModels = await fetchNativeRestModelDetails(baseURL, headers, fetchImpl);
|
|
33
|
+
const sdkModels = nativeModels.length > 0 && (lmStudioSdkLoader !== loadLmStudioSdkModels || fetchImpl === globalThis.fetch)
|
|
34
|
+
? await safeLoadLmStudioSdkModels(lmStudioSdkLoader, baseURL)
|
|
35
|
+
: [];
|
|
36
|
+
const registries = await loadRegistries(input);
|
|
37
|
+
const registryModels = await Promise.all(sourceModels.map((model) => resolveModelRegistryMetadata({
|
|
38
|
+
providerId: input.providerId,
|
|
39
|
+
modelId: model.id,
|
|
40
|
+
registries
|
|
41
|
+
})));
|
|
42
|
+
const modelDetailsById = mergeModelDetails([...sourceModels, ...registryModels.filter(isModelInfo), ...nativeModels, ...sdkModels]);
|
|
43
|
+
const compatibleModels = sourceModels
|
|
44
|
+
.map((model) => mergeModelInfo(model, modelDetailsForId(model.id, modelDetailsById)))
|
|
45
|
+
.filter(isOpenCodeCompatibleModel);
|
|
46
|
+
const models = [...new Set(compatibleModels.map((model) => model.id.trim()).filter(Boolean))];
|
|
35
47
|
if (models.length === 0) {
|
|
36
48
|
throw new Error("Provider returned no OpenCode-compatible model ids");
|
|
37
49
|
}
|
|
38
|
-
|
|
50
|
+
const modelDetails = models.map((modelId) => withMergedModalities(modelDetailsForId(modelId, modelDetailsById)));
|
|
51
|
+
return { baseURL, models, modelDetails };
|
|
52
|
+
}
|
|
53
|
+
async function loadRegistries(input) {
|
|
54
|
+
const loaded = await Promise.all((input.modelRegistryPaths ?? []).map((path) => loadModelRegistryFile(path)));
|
|
55
|
+
return [...(input.registries ?? []), ...loaded];
|
|
56
|
+
}
|
|
57
|
+
function isModelInfo(value) {
|
|
58
|
+
return Boolean(value);
|
|
39
59
|
}
|
|
40
60
|
export async function detectProviderCapabilities(input, fetchImpl = globalThis.fetch) {
|
|
41
61
|
const baseURL = normalizeBaseUrl(input.baseURL);
|
|
@@ -78,7 +98,10 @@ export function buildProviderConfig(input) {
|
|
|
78
98
|
options: {
|
|
79
99
|
baseURL
|
|
80
100
|
},
|
|
81
|
-
models: Object.fromEntries(input.models.map((model) =>
|
|
101
|
+
models: Object.fromEntries(input.models.map((model) => {
|
|
102
|
+
const modelInfo = input.modelDetails?.find((m) => m.id === model);
|
|
103
|
+
return [model, buildModelConfig(model, modelInfo)];
|
|
104
|
+
}))
|
|
82
105
|
};
|
|
83
106
|
if (opencodeApiType !== "messages") {
|
|
84
107
|
provider.options.setCacheKey = true;
|
|
@@ -93,6 +116,22 @@ export function buildProviderConfig(input) {
|
|
|
93
116
|
}
|
|
94
117
|
};
|
|
95
118
|
}
|
|
119
|
+
function buildModelConfig(modelId, modelInfo) {
|
|
120
|
+
const config = { name: modelId };
|
|
121
|
+
if (modelInfo?.modalities) {
|
|
122
|
+
config.modalities = modelInfo.modalities;
|
|
123
|
+
}
|
|
124
|
+
if (modelInfo?.capabilities?.reasoning) {
|
|
125
|
+
config.reasoning = true;
|
|
126
|
+
}
|
|
127
|
+
if (modelInfo?.capabilities?.toolCall) {
|
|
128
|
+
config.tool_call = true;
|
|
129
|
+
}
|
|
130
|
+
if (modelInfo?.contextLength) {
|
|
131
|
+
config.limit = { context: modelInfo.contextLength };
|
|
132
|
+
}
|
|
133
|
+
return config;
|
|
134
|
+
}
|
|
96
135
|
export function npmPackageForOpenCodeApiType(apiType) {
|
|
97
136
|
if (apiType === "responses") {
|
|
98
137
|
return "@ai-sdk/openai";
|
|
@@ -112,7 +151,249 @@ function isOpenCodeCompatibleModel(model) {
|
|
|
112
151
|
if (typeof model.type !== "string") {
|
|
113
152
|
return true;
|
|
114
153
|
}
|
|
115
|
-
return ["
|
|
154
|
+
return !["embedding", "embed", "rerank", "reranker"].includes(model.type.trim().toLowerCase());
|
|
155
|
+
}
|
|
156
|
+
async function fetchNativeRestModelDetails(baseURL, headers, fetchImpl) {
|
|
157
|
+
if (!isLikelyLmStudioBaseUrl(baseURL)) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
for (const endpoint of nativeLmStudioModelEndpoints(baseURL)) {
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetchImpl(endpoint, { headers });
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const body = await response.json();
|
|
167
|
+
const rawModels = modelListItems(body);
|
|
168
|
+
if (!rawModels) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
return rawModels.map((model) => normalizeModelInfo(model, "lmstudio-rest"));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
function nativeLmStudioModelEndpoints(baseURL) {
|
|
180
|
+
const url = new URL(baseURL);
|
|
181
|
+
url.pathname = "/api/v1/models";
|
|
182
|
+
url.search = "";
|
|
183
|
+
url.hash = "";
|
|
184
|
+
const v1 = url.toString();
|
|
185
|
+
url.pathname = "/api/v0/models";
|
|
186
|
+
const v0 = url.toString();
|
|
187
|
+
return [v1, v0];
|
|
188
|
+
}
|
|
189
|
+
function isLikelyLmStudioBaseUrl(baseURL) {
|
|
190
|
+
const url = new URL(baseURL);
|
|
191
|
+
return ["localhost", "127.0.0.1", "::1"].includes(url.hostname) && url.port === "1234";
|
|
192
|
+
}
|
|
193
|
+
function normalizeModelInfo(raw, source) {
|
|
194
|
+
const id = String(raw.id ?? raw.key ?? raw.model ?? "").trim();
|
|
195
|
+
const capabilitiesObject = objectValue(raw.capabilities);
|
|
196
|
+
const modalities = parseCapabilitiesFromApi(raw);
|
|
197
|
+
const type = stringValue(raw.type);
|
|
198
|
+
const contextLength = numberValue(raw.max_context_length ?? raw.context_length ?? raw.contextLength);
|
|
199
|
+
const toolCall = capabilityFlag(raw.tool_call ??
|
|
200
|
+
raw.tool_calls ??
|
|
201
|
+
raw.toolCall ??
|
|
202
|
+
raw.supports_tool_calls ??
|
|
203
|
+
raw.supportsToolCalls ??
|
|
204
|
+
capabilitiesObject?.toolUse ??
|
|
205
|
+
capabilitiesObject?.tool_use ??
|
|
206
|
+
capabilitiesObject?.toolCall ??
|
|
207
|
+
capabilitiesObject?.tool_call ??
|
|
208
|
+
capabilitiesObject?.trainedForToolUse ??
|
|
209
|
+
capabilitiesObject?.trained_for_tool_use);
|
|
210
|
+
const reasoning = capabilityFlag(raw.reasoning ?? raw.supports_reasoning ?? capabilitiesObject?.reasoning);
|
|
211
|
+
return cleanModelInfo({
|
|
212
|
+
id,
|
|
213
|
+
type,
|
|
214
|
+
architecture: stringValue(raw.arch ?? raw.architecture),
|
|
215
|
+
quantization: stringValue(raw.quantization) ?? stringValue(objectValue(raw.quantization)?.name),
|
|
216
|
+
parameterSize: stringValue(raw.params_string ?? raw.paramsString ?? raw.parameterSize),
|
|
217
|
+
state: stringValue(raw.state) ?? (Array.isArray(raw.loaded_instances) && raw.loaded_instances.length > 0 ? "loaded" : undefined),
|
|
218
|
+
contextLength,
|
|
219
|
+
modalities,
|
|
220
|
+
capabilities: toolCall || reasoning ? { toolCall, reasoning } : undefined,
|
|
221
|
+
metadataSources: [source]
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function mergeModelDetails(models) {
|
|
225
|
+
const result = new Map();
|
|
226
|
+
for (const model of models) {
|
|
227
|
+
if (!model.id) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
for (const alias of modelAliases(model.id)) {
|
|
231
|
+
const current = result.get(alias);
|
|
232
|
+
result.set(alias, current ? mergeModelInfo(current, model) : model);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function modelDetailsForId(modelId, models) {
|
|
238
|
+
return modelAliases(modelId).reduce((merged, alias) => {
|
|
239
|
+
const next = models.get(alias);
|
|
240
|
+
return next ? mergeModelInfo(merged, next) : merged;
|
|
241
|
+
}, { id: modelId });
|
|
242
|
+
}
|
|
243
|
+
function mergeModelInfo(base, next) {
|
|
244
|
+
return cleanModelInfo({
|
|
245
|
+
...base,
|
|
246
|
+
...next,
|
|
247
|
+
id: base.id || next.id,
|
|
248
|
+
type: next.type ?? base.type,
|
|
249
|
+
architecture: next.architecture ?? base.architecture,
|
|
250
|
+
quantization: next.quantization ?? base.quantization,
|
|
251
|
+
parameterSize: next.parameterSize ?? base.parameterSize,
|
|
252
|
+
state: next.state ?? base.state,
|
|
253
|
+
contextLength: next.contextLength ?? base.contextLength,
|
|
254
|
+
modalities: next.modalities ?? base.modalities,
|
|
255
|
+
capabilities: base.capabilities || next.capabilities
|
|
256
|
+
? {
|
|
257
|
+
...base.capabilities,
|
|
258
|
+
...next.capabilities
|
|
259
|
+
}
|
|
260
|
+
: undefined,
|
|
261
|
+
metadataSources: [...new Set([...(base.metadataSources ?? []), ...(next.metadataSources ?? [])])]
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function withMergedModalities(model) {
|
|
265
|
+
const modalities = mergeModelCapabilities(model.id, model.modalities);
|
|
266
|
+
return cleanModelInfo({
|
|
267
|
+
...model,
|
|
268
|
+
modalities,
|
|
269
|
+
metadataSources: modalities
|
|
270
|
+
? [...new Set([...(model.metadataSources ?? []), model.modalities ? undefined : "heuristics"].filter((v) => Boolean(v)))]
|
|
271
|
+
: model.metadataSources
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function modelAliases(modelId) {
|
|
275
|
+
const normalized = modelId.trim();
|
|
276
|
+
const withoutPrefix = normalized.includes("/") ? normalized.split("/").pop() : normalized;
|
|
277
|
+
return [...new Set([normalized, withoutPrefix])];
|
|
278
|
+
}
|
|
279
|
+
function cleanModelInfo(model) {
|
|
280
|
+
const capabilities = model.capabilities?.toolCall || model.capabilities?.reasoning
|
|
281
|
+
? model.capabilities
|
|
282
|
+
: undefined;
|
|
283
|
+
return {
|
|
284
|
+
id: model.id,
|
|
285
|
+
...(model.type ? { type: model.type } : {}),
|
|
286
|
+
...(model.architecture ? { architecture: model.architecture } : {}),
|
|
287
|
+
...(model.quantization ? { quantization: model.quantization } : {}),
|
|
288
|
+
...(model.parameterSize ? { parameterSize: model.parameterSize } : {}),
|
|
289
|
+
...(model.state ? { state: model.state } : {}),
|
|
290
|
+
...(model.contextLength ? { contextLength: model.contextLength } : {}),
|
|
291
|
+
...(model.modalities ? { modalities: model.modalities } : {}),
|
|
292
|
+
...(capabilities ? { capabilities } : {}),
|
|
293
|
+
...(model.metadataSources?.length ? { metadataSources: model.metadataSources } : {})
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function objectValue(value) {
|
|
297
|
+
return typeof value === "object" && value !== null ? value : undefined;
|
|
298
|
+
}
|
|
299
|
+
function stringValue(value) {
|
|
300
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
301
|
+
}
|
|
302
|
+
function numberValue(value) {
|
|
303
|
+
const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
304
|
+
return Number.isFinite(number) && number > 0 ? number : undefined;
|
|
305
|
+
}
|
|
306
|
+
function booleanValue(value) {
|
|
307
|
+
if (typeof value === "boolean") {
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
if (typeof value === "string") {
|
|
311
|
+
if (["true", "yes", "1"].includes(value.toLowerCase())) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
if (["false", "no", "0"].includes(value.toLowerCase())) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
function capabilityFlag(value) {
|
|
321
|
+
if (typeof value === "object" && value !== null) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
return booleanValue(value);
|
|
325
|
+
}
|
|
326
|
+
function modelListItems(body) {
|
|
327
|
+
if (typeof body !== "object" || body === null) {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
const data = body.data;
|
|
331
|
+
const models = body.models;
|
|
332
|
+
const items = Array.isArray(data) ? data : Array.isArray(models) ? models : undefined;
|
|
333
|
+
if (!items) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
if (!items.every((model) => typeof model === "object" && model !== null)) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
return items;
|
|
340
|
+
}
|
|
341
|
+
async function safeLoadLmStudioSdkModels(loader, baseURL) {
|
|
342
|
+
try {
|
|
343
|
+
return await loader(baseURL);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function loadLmStudioSdkModels(baseURL) {
|
|
350
|
+
const sdk = await import("@lmstudio/sdk");
|
|
351
|
+
const Client = sdk.LMStudioClient;
|
|
352
|
+
if (!Client) {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
const wsBaseURL = httpBaseUrlToWs(baseURL);
|
|
356
|
+
const connectedClient = new Client({ baseUrl: wsBaseURL, logger: silentLogger });
|
|
357
|
+
try {
|
|
358
|
+
const models = await connectedClient.system?.listDownloadedModels?.();
|
|
359
|
+
if (!Array.isArray(models)) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
return models.map((model) => {
|
|
363
|
+
const raw = model;
|
|
364
|
+
const id = stringValue(raw.modelKey ?? raw.path ?? raw.id ?? raw.displayName) ?? "";
|
|
365
|
+
const vision = booleanValue(raw.vision);
|
|
366
|
+
const trainedForToolUse = booleanValue(raw.trainedForToolUse ?? raw.trained_for_tool_use);
|
|
367
|
+
return cleanModelInfo({
|
|
368
|
+
id,
|
|
369
|
+
type: stringValue(raw.type) ?? "llm",
|
|
370
|
+
architecture: stringValue(raw.architecture),
|
|
371
|
+
quantization: stringValue(raw.quantization) ?? stringValue(objectValue(raw.quantization)?.name),
|
|
372
|
+
parameterSize: stringValue(raw.paramsString),
|
|
373
|
+
contextLength: numberValue(raw.maxContextLength ?? raw.max_context_length),
|
|
374
|
+
modalities: vision ? { input: ["text", "image"], output: ["text"] } : undefined,
|
|
375
|
+
capabilities: trainedForToolUse ? { toolCall: true } : undefined,
|
|
376
|
+
metadataSources: ["lmstudio-sdk"]
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
await connectedClient[Symbol.asyncDispose]?.();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const silentLogger = {
|
|
385
|
+
debug() { },
|
|
386
|
+
info() { },
|
|
387
|
+
warn() { },
|
|
388
|
+
error() { }
|
|
389
|
+
};
|
|
390
|
+
function httpBaseUrlToWs(baseURL) {
|
|
391
|
+
const url = new URL(baseURL);
|
|
392
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
393
|
+
url.pathname = "";
|
|
394
|
+
url.search = "";
|
|
395
|
+
url.hash = "";
|
|
396
|
+
return url.toString().replace(/\/$/, "");
|
|
116
397
|
}
|
|
117
398
|
async function probe(input, init, fetchImpl) {
|
|
118
399
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heventure/model-provider-x",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5-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",
|
|
@@ -46,5 +46,8 @@
|
|
|
46
46
|
"typescript": "^5.9.0",
|
|
47
47
|
"typescript-eslint": "^8.0.0",
|
|
48
48
|
"vitest": "^3.0.0"
|
|
49
|
+
},
|
|
50
|
+
"optionalDependencies": {
|
|
51
|
+
"@lmstudio/sdk": "^1.5.0"
|
|
49
52
|
}
|
|
50
53
|
}
|