@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.
@@ -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 compatibleModels = body.data.filter(isOpenCodeCompatibleModel);
31
- const models = [
32
- ...new Set(compatibleModels
33
- .map((model) => model.id.trim())
34
- .filter(Boolean))
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 ["llm", "chat", "completion", "text-generation"].includes(model.type.trim().toLowerCase());
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 {