@heventure/model-provider-x 0.2.4 → 0.2.5
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 +55 -0
- package/dist/cli/args.js +8 -0
- package/dist/cli/index.js +43 -7
- package/dist/cli/model-choices.js +40 -8
- package/dist/core/model-capabilities.js +4 -1
- package/dist/core/model-registry.js +231 -0
- package/dist/core/provider.js +278 -17
- package/dist/data/models-dev.json +98938 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -135,6 +135,61 @@ 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 metadata (5000+ models from 140+ providers).
|
|
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
|
+
|
|
183
|
+
### Updating Models.dev Data
|
|
184
|
+
|
|
185
|
+
The built-in models.dev data is bundled with the package. To update it to the latest version:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npm run update-models-dev
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This fetches the latest model metadata from [models.dev](https://models.dev) and updates `src/data/models-dev.json`.
|
|
192
|
+
|
|
138
193
|
## Commands
|
|
139
194
|
|
|
140
195
|
```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 "--model-registry":
|
|
33
|
+
if (!options.modelRegistryPaths) {
|
|
34
|
+
options.modelRegistryPaths = [];
|
|
35
|
+
}
|
|
36
|
+
options.modelRegistryPaths.push(next());
|
|
37
|
+
break;
|
|
32
38
|
case "--modalities":
|
|
33
39
|
if (!options.modalities) {
|
|
34
40
|
options.modalities = [];
|
|
@@ -113,6 +119,8 @@ Options:
|
|
|
113
119
|
--proxy Write agent config through the local compatibility proxy.
|
|
114
120
|
--direct Write agent config directly to the upstream provider.
|
|
115
121
|
--models <list> Comma-separated model ids. Skips interactive model selection.
|
|
122
|
+
--model-registry <path>
|
|
123
|
+
JSONC model metadata override. Can be repeated.
|
|
116
124
|
--modalities <spec> Model modalities in format <model>:<input>:<output>.
|
|
117
125
|
Example: --modalities qwen-vl:image,text:text
|
|
118
126
|
--config <path> OpenCode config path to write when targeting OpenCode.
|
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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";
|
|
9
11
|
import { isKnownModel } from "../core/model-capabilities.js";
|
|
10
12
|
import { startProxyServer } from "../proxy/server.js";
|
|
11
13
|
import { defaultClaudeModelMapping, getDefaultClaudeSettingsPath, writeClaudeCodeSettings } from "../targets/claude-code.js";
|
|
@@ -72,12 +74,17 @@ export async function runCli(options) {
|
|
|
72
74
|
const baseURL = await requiredOption(rl, options.baseURL ?? providerDefaults.baseURL, "API base URL");
|
|
73
75
|
const apiKey = await resolveApiKey(rl, options.apiKey, providerDefaults.preset);
|
|
74
76
|
output.write("Fetching models...\n");
|
|
75
|
-
const fetched = await validateAndFetchModels({
|
|
77
|
+
const fetched = await validateAndFetchModels({
|
|
78
|
+
baseURL,
|
|
79
|
+
apiKey,
|
|
80
|
+
providerId,
|
|
81
|
+
modelRegistryPaths: resolveModelRegistryPaths(options)
|
|
82
|
+
});
|
|
76
83
|
const selectedModels = options.models ??
|
|
77
84
|
(canUseTui()
|
|
78
|
-
? await multiSelectChoices("Select models", createModelChoices(fetched.
|
|
85
|
+
? await multiSelectChoices("Select models", createModelChoices(fetched.modelDetails))
|
|
79
86
|
: parseModelSelection(await rl.question(formatModelPrompt(fetched.models)), fetched.models));
|
|
80
|
-
const modelDetails = fetched.modelDetails.filter((m) => selectedModels.includes(m.id));
|
|
87
|
+
const modelDetails = applyModelModalityOverrides(fetched.modelDetails.filter((m) => selectedModels.includes(m.id)), options);
|
|
81
88
|
const fragment = buildProviderConfig({
|
|
82
89
|
providerId,
|
|
83
90
|
providerName,
|
|
@@ -145,10 +152,15 @@ async function collectProviderInput(rl, command) {
|
|
|
145
152
|
}
|
|
146
153
|
async function collectProviderSelection(rl, command, providerInput) {
|
|
147
154
|
output.write("Fetching models...\n");
|
|
148
|
-
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
|
+
});
|
|
149
161
|
const selectedModels = command.options.models ??
|
|
150
162
|
(canUseTui()
|
|
151
|
-
? await multiSelectChoices("Select models", createModelChoices(fetched.
|
|
163
|
+
? await multiSelectChoices("Select models", createModelChoices(fetched.modelDetails))
|
|
152
164
|
: parseModelSelection(await rl.question(formatModelPrompt(fetched.models)), fetched.models));
|
|
153
165
|
const defaultModel = await resolveDefaultModel(rl, command, selectedModels);
|
|
154
166
|
if (!defaultModel) {
|
|
@@ -372,6 +384,30 @@ function tokenHint(token) {
|
|
|
372
384
|
}
|
|
373
385
|
return `${token.slice(0, 8)}...${token.slice(-4)}`;
|
|
374
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
|
+
}
|
|
375
411
|
async function requiredOption(rl, value, label) {
|
|
376
412
|
const answer = value ?? (await rl.question(`${label}: `));
|
|
377
413
|
if (!answer.trim()) {
|
|
@@ -671,14 +707,14 @@ async function resolveModelModalities(rl, command, selectedModels, fetchedModelD
|
|
|
671
707
|
async function promptModelModalities(rl, modelId) {
|
|
672
708
|
output.write(`\nModel: ${modelId} - Capabilities unknown\n`);
|
|
673
709
|
if (canUseTui()) {
|
|
674
|
-
const inputModalities = await multiSelectChoices(
|
|
710
|
+
const inputModalities = await multiSelectChoices(`Input modalities: ${modelId}`, [
|
|
675
711
|
{ label: "text", value: "text", hint: "default" },
|
|
676
712
|
{ label: "image", value: "image" },
|
|
677
713
|
{ label: "audio", value: "audio" },
|
|
678
714
|
{ label: "video", value: "video" },
|
|
679
715
|
{ label: "pdf", value: "pdf" },
|
|
680
716
|
]);
|
|
681
|
-
const outputModalities = await multiSelectChoices(
|
|
717
|
+
const outputModalities = await multiSelectChoices(`Output modalities: ${modelId}`, [
|
|
682
718
|
{ label: "text", value: "text", hint: "default" },
|
|
683
719
|
{ label: "image", value: "image" },
|
|
684
720
|
{ label: "audio", value: "audio" },
|
|
@@ -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
|
+
}
|
|
@@ -113,12 +113,15 @@ export function parseCapabilitiesFromApi(model) {
|
|
|
113
113
|
if (model.capabilities && typeof model.capabilities === "object") {
|
|
114
114
|
const caps = model.capabilities;
|
|
115
115
|
const input = ["text"];
|
|
116
|
-
if (caps.vision || caps.image || caps.multimodal) {
|
|
116
|
+
if (caps.vision || caps.image || caps.multimodal || caps.image_input) {
|
|
117
117
|
input.push("image");
|
|
118
118
|
}
|
|
119
119
|
if (caps.audio || caps.speech) {
|
|
120
120
|
input.push("audio");
|
|
121
121
|
}
|
|
122
|
+
if (caps.video) {
|
|
123
|
+
input.push("video");
|
|
124
|
+
}
|
|
122
125
|
if (input.length > 1) {
|
|
123
126
|
return { input: input, output: ["text"] };
|
|
124
127
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { parse } from "jsonc-parser";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
export const DEFAULT_MODEL_REGISTRY_FILE = "model-provider-x.models.jsonc";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const MODELS_DEV_DATA_PATH = resolve(__dirname, "../data/models-dev.json");
|
|
8
|
+
let modelsDevCache = null;
|
|
9
|
+
async function loadModelsDevRegistry() {
|
|
10
|
+
if (modelsDevCache) {
|
|
11
|
+
return modelsDevCache;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const text = await readFile(MODELS_DEV_DATA_PATH, "utf-8");
|
|
15
|
+
const data = JSON.parse(text);
|
|
16
|
+
modelsDevCache = {
|
|
17
|
+
source: data.source ?? "models-dev",
|
|
18
|
+
providers: data.providers
|
|
19
|
+
};
|
|
20
|
+
return modelsDevCache;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { source: "models-dev", providers: {} };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function loadModelRegistryFile(path) {
|
|
27
|
+
const text = await readFile(path, "utf8");
|
|
28
|
+
const value = parse(text);
|
|
29
|
+
if (!isObject(value)) {
|
|
30
|
+
throw new Error(`Model registry must be an object: ${path}`);
|
|
31
|
+
}
|
|
32
|
+
return normalizeRegistry(value, "local-registry");
|
|
33
|
+
}
|
|
34
|
+
export async function resolveModelRegistryMetadata(input) {
|
|
35
|
+
const builtInRegistry = input.includeBuiltIn === false ? undefined : await loadModelsDevRegistry();
|
|
36
|
+
const registries = [
|
|
37
|
+
...(builtInRegistry ? [builtInRegistry] : []),
|
|
38
|
+
...(input.registries ?? [])
|
|
39
|
+
];
|
|
40
|
+
let result;
|
|
41
|
+
for (const registry of registries) {
|
|
42
|
+
const model = lookupRegistryModel(registry, input.providerId, input.modelId);
|
|
43
|
+
if (!model) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const info = normalizeRegistryModel(input.modelId, model, registry.source);
|
|
47
|
+
result = result ? mergeRegistryInfo(result, info) : info;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function normalizeRegistry(value, fallbackSource) {
|
|
52
|
+
return {
|
|
53
|
+
source: stringValue(value.source) ?? fallbackSource,
|
|
54
|
+
providers: normalizeProviders(value.providers),
|
|
55
|
+
models: normalizeModels(value.models)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function lookupRegistryModel(registry, providerId, modelId) {
|
|
59
|
+
const aliases = modelAliases(modelId);
|
|
60
|
+
// First try the specified provider
|
|
61
|
+
if (providerId) {
|
|
62
|
+
const providerModels = registry.providers?.[providerId]?.models;
|
|
63
|
+
const match = lookupModel(providerModels, aliases);
|
|
64
|
+
if (match) {
|
|
65
|
+
return match;
|
|
66
|
+
}
|
|
67
|
+
const modelsDevProvider = registry[providerId];
|
|
68
|
+
if (isObject(modelsDevProvider)) {
|
|
69
|
+
const provider = normalizeProviders({ [providerId]: modelsDevProvider })?.[providerId];
|
|
70
|
+
const providerMatch = lookupModel(provider?.models, aliases);
|
|
71
|
+
if (providerMatch) {
|
|
72
|
+
return providerMatch;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Search across all providers if not found in specified provider
|
|
77
|
+
if (registry.providers) {
|
|
78
|
+
for (const [, provider] of Object.entries(registry.providers)) {
|
|
79
|
+
const match = lookupModel(provider.models, aliases);
|
|
80
|
+
if (match) {
|
|
81
|
+
return match;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return lookupModel(registry.models, aliases);
|
|
86
|
+
}
|
|
87
|
+
function lookupModel(models, aliases) {
|
|
88
|
+
if (!models) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
for (const alias of aliases) {
|
|
92
|
+
if (models[alias]) {
|
|
93
|
+
return models[alias];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
function normalizeRegistryModel(modelId, model, source) {
|
|
99
|
+
const modalities = normalizeModalities(model);
|
|
100
|
+
const capabilities = model.capabilities;
|
|
101
|
+
const toolCall = booleanValue(capabilities?.toolCall ?? capabilities?.tool_call ?? model.tool_call);
|
|
102
|
+
const reasoning = booleanValue(capabilities?.reasoning ?? model.reasoning);
|
|
103
|
+
return cleanModelInfo({
|
|
104
|
+
id: model.id ?? modelId,
|
|
105
|
+
type: model.type,
|
|
106
|
+
architecture: model.architecture,
|
|
107
|
+
quantization: model.quantization,
|
|
108
|
+
parameterSize: model.parameterSize,
|
|
109
|
+
state: model.state,
|
|
110
|
+
contextLength: model.contextLength ?? model.limit?.context,
|
|
111
|
+
modalities,
|
|
112
|
+
capabilities: toolCall || reasoning ? { toolCall, reasoning } : undefined,
|
|
113
|
+
metadataSources: [source]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function normalizeModalities(model) {
|
|
117
|
+
if (model.modalities) {
|
|
118
|
+
return model.modalities;
|
|
119
|
+
}
|
|
120
|
+
if (model.input_modalities || model.output_modalities) {
|
|
121
|
+
return {
|
|
122
|
+
...(model.input_modalities ? { input: model.input_modalities } : {}),
|
|
123
|
+
...(model.output_modalities ? { output: model.output_modalities } : {})
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
function mergeRegistryInfo(base, next) {
|
|
129
|
+
return cleanModelInfo({
|
|
130
|
+
...base,
|
|
131
|
+
...next,
|
|
132
|
+
id: base.id || next.id,
|
|
133
|
+
type: next.type ?? base.type,
|
|
134
|
+
architecture: next.architecture ?? base.architecture,
|
|
135
|
+
quantization: next.quantization ?? base.quantization,
|
|
136
|
+
parameterSize: next.parameterSize ?? base.parameterSize,
|
|
137
|
+
state: next.state ?? base.state,
|
|
138
|
+
contextLength: next.contextLength ?? base.contextLength,
|
|
139
|
+
modalities: next.modalities ?? base.modalities,
|
|
140
|
+
capabilities: base.capabilities || next.capabilities
|
|
141
|
+
? {
|
|
142
|
+
...base.capabilities,
|
|
143
|
+
...next.capabilities
|
|
144
|
+
}
|
|
145
|
+
: undefined,
|
|
146
|
+
metadataSources: [...new Set([...(base.metadataSources ?? []), ...(next.metadataSources ?? [])])]
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function normalizeProviders(value) {
|
|
150
|
+
if (!isObject(value)) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const providers = {};
|
|
154
|
+
for (const [providerId, providerValue] of Object.entries(value)) {
|
|
155
|
+
if (!isObject(providerValue)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const models = normalizeModels(providerValue.models);
|
|
159
|
+
if (models) {
|
|
160
|
+
providers[providerId] = { models };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return Object.keys(providers).length ? providers : undefined;
|
|
164
|
+
}
|
|
165
|
+
function normalizeModels(value) {
|
|
166
|
+
if (!isObject(value)) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const models = {};
|
|
170
|
+
for (const [modelId, modelValue] of Object.entries(value)) {
|
|
171
|
+
if (isObject(modelValue)) {
|
|
172
|
+
models[modelId] = modelValue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return Object.keys(models).length ? models : undefined;
|
|
176
|
+
}
|
|
177
|
+
function modelAliases(modelId) {
|
|
178
|
+
const normalized = modelId.trim().toLowerCase();
|
|
179
|
+
const withoutPrefix = normalized.includes("/") ? normalized.split("/").pop() : normalized;
|
|
180
|
+
// Strip common suffixes for fuzzy matching
|
|
181
|
+
const suffixes = [
|
|
182
|
+
"-it", "-qat", "-instruct", "-chat", "-gguf", "-gptq", "-awq", "-exl2",
|
|
183
|
+
"-fp16", "-fp32", "-int4", "-int8", "-4bit", "-8bit", "-16bit",
|
|
184
|
+
"-preview", "-latest", "-beta", "-alpha", "-rc", "-snapshot",
|
|
185
|
+
"-mlx", "-mlxc", "-bnb", "-hqq",
|
|
186
|
+
"-ud", "-xl", "-xs", "-small", "-medium", "-large", "-mini", "-nano", "-micro",
|
|
187
|
+
"-turbo", "-fast", "-pro", "-plus", "-max", "-ultra", "-flash", "-lite",
|
|
188
|
+
"-mtp", "-moe", "-a17b", "-a22b", "-a3b", "-a10b", "-a12b", "-a55b",
|
|
189
|
+
"-chat", "-base", "-raw", "-uncensored", "-abliterated"
|
|
190
|
+
];
|
|
191
|
+
let fuzzy = withoutPrefix;
|
|
192
|
+
for (const suffix of suffixes) {
|
|
193
|
+
if (fuzzy.endsWith(suffix)) {
|
|
194
|
+
fuzzy = fuzzy.slice(0, -suffix.length);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Also try removing version suffixes like "-v1", "-v2", etc.
|
|
199
|
+
const versionMatch = fuzzy.match(/-v\d+$/);
|
|
200
|
+
const withoutVersion = versionMatch ? fuzzy.slice(0, -versionMatch[0].length) : fuzzy;
|
|
201
|
+
// Try removing parameter size suffixes like "-12b", "-7b", "-70b", etc.
|
|
202
|
+
const paramMatch = fuzzy.match(/-\d+[bBmMkKtT]$/);
|
|
203
|
+
const withoutParams = paramMatch ? fuzzy.slice(0, -paramMatch[0].length) : fuzzy;
|
|
204
|
+
return [...new Set([normalized, withoutPrefix, fuzzy, withoutVersion, withoutParams])];
|
|
205
|
+
}
|
|
206
|
+
function cleanModelInfo(model) {
|
|
207
|
+
const capabilities = model.capabilities?.toolCall || model.capabilities?.reasoning
|
|
208
|
+
? model.capabilities
|
|
209
|
+
: undefined;
|
|
210
|
+
return {
|
|
211
|
+
id: model.id,
|
|
212
|
+
...(model.type ? { type: model.type } : {}),
|
|
213
|
+
...(model.architecture ? { architecture: model.architecture } : {}),
|
|
214
|
+
...(model.quantization ? { quantization: model.quantization } : {}),
|
|
215
|
+
...(model.parameterSize ? { parameterSize: model.parameterSize } : {}),
|
|
216
|
+
...(model.state ? { state: model.state } : {}),
|
|
217
|
+
...(model.contextLength ? { contextLength: model.contextLength } : {}),
|
|
218
|
+
...(model.modalities ? { modalities: model.modalities } : {}),
|
|
219
|
+
...(capabilities ? { capabilities } : {}),
|
|
220
|
+
...(model.metadataSources?.length ? { metadataSources: model.metadataSources } : {})
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function isObject(value) {
|
|
224
|
+
return typeof value === "object" && value !== null;
|
|
225
|
+
}
|
|
226
|
+
function stringValue(value) {
|
|
227
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
228
|
+
}
|
|
229
|
+
function booleanValue(value) {
|
|
230
|
+
return typeof value === "boolean" ? value : undefined;
|
|
231
|
+
}
|