@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/dist/core/provider.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseCapabilitiesFromApi, mergeModelCapabilities } from "./model-capabilities.js";
|
|
2
|
+
import { loadModelRegistryFile, resolveModelRegistryMetadata } from "./model-registry.js";
|
|
2
3
|
export function normalizeBaseUrl(baseURL) {
|
|
3
4
|
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
4
5
|
if (!normalized) {
|
|
@@ -12,7 +13,7 @@ export function normalizeBaseUrl(baseURL) {
|
|
|
12
13
|
}
|
|
13
14
|
return normalized;
|
|
14
15
|
}
|
|
15
|
-
export async function validateAndFetchModels(input, fetchImpl = globalThis.fetch) {
|
|
16
|
+
export async function validateAndFetchModels(input, fetchImpl = globalThis.fetch, lmStudioSdkLoader = loadLmStudioSdkModels) {
|
|
16
17
|
const baseURL = normalizeBaseUrl(input.baseURL);
|
|
17
18
|
const apiKey = input.apiKey?.trim() ?? "";
|
|
18
19
|
const headers = {};
|
|
@@ -27,26 +28,35 @@ export async function validateAndFetchModels(input, fetchImpl = globalThis.fetch
|
|
|
27
28
|
if (!isModelListResponse(body)) {
|
|
28
29
|
throw new Error("Expected /models to return an object with a data array");
|
|
29
30
|
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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))];
|
|
36
47
|
if (models.length === 0) {
|
|
37
48
|
throw new Error("Provider returned no OpenCode-compatible model ids");
|
|
38
49
|
}
|
|
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
|
-
});
|
|
50
|
+
const modelDetails = models.map((modelId) => withMergedModalities(modelDetailsForId(modelId, modelDetailsById)));
|
|
48
51
|
return { baseURL, models, modelDetails };
|
|
49
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);
|
|
59
|
+
}
|
|
50
60
|
export async function detectProviderCapabilities(input, fetchImpl = globalThis.fetch) {
|
|
51
61
|
const baseURL = normalizeBaseUrl(input.baseURL);
|
|
52
62
|
const apiKey = input.apiKey?.trim() ?? "";
|
|
@@ -111,6 +121,15 @@ function buildModelConfig(modelId, modelInfo) {
|
|
|
111
121
|
if (modelInfo?.modalities) {
|
|
112
122
|
config.modalities = modelInfo.modalities;
|
|
113
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
|
+
}
|
|
114
133
|
return config;
|
|
115
134
|
}
|
|
116
135
|
export function npmPackageForOpenCodeApiType(apiType) {
|
|
@@ -132,7 +151,249 @@ function isOpenCodeCompatibleModel(model) {
|
|
|
132
151
|
if (typeof model.type !== "string") {
|
|
133
152
|
return true;
|
|
134
153
|
}
|
|
135
|
-
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(/\/$/, "");
|
|
136
397
|
}
|
|
137
398
|
async function probe(input, init, fetchImpl) {
|
|
138
399
|
try {
|