@prestyj/core 5.9.0 → 5.10.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/dist/index.cjs CHANGED
@@ -31,19 +31,30 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AuthStorage: () => AuthStorage,
34
+ DEFAULT_LOCAL_ENDPOINTS: () => DEFAULT_LOCAL_ENDPOINTS,
34
35
  DEFAULT_MAX_VIDEO_BYTES: () => DEFAULT_MAX_VIDEO_BYTES,
36
+ FALLBACK_CONTEXT_WINDOW: () => FALLBACK_CONTEXT_WINDOW,
37
+ LOCAL_API_KEY_PLACEHOLDER: () => LOCAL_API_KEY_PLACEHOLDER,
38
+ LOCAL_AUTH_KEY_PREFIX: () => LOCAL_AUTH_KEY_PREFIX,
35
39
  MODELS: () => MODELS,
36
40
  MOONSHOT_OAUTH_KEY: () => MOONSHOT_OAUTH_KEY,
37
41
  NotLoggedInError: () => NotLoggedInError,
38
42
  SubscriptionUsageError: () => SubscriptionUsageError,
39
43
  TelegramBot: () => TelegramBot,
40
44
  XIAOMI_CREDITS_KEY: () => XIAOMI_CREDITS_KEY,
45
+ clearLocalDiscoveryCache: () => clearLocalDiscoveryCache,
46
+ clearRuntimeModels: () => clearRuntimeModels,
41
47
  closeLogger: () => closeLogger,
42
48
  createAutoUpdater: () => createAutoUpdater,
43
49
  decodeOggOpus: () => decodeOggOpus,
50
+ discoverLocalModels: () => discoverLocalModels,
44
51
  downmixToMono: () => downmixToMono,
52
+ endpointRoot: () => endpointRoot,
45
53
  fetchSubscriptionUsage: () => fetchSubscriptionUsage,
54
+ findProbedModel: () => findProbedModel,
55
+ formatLocalModelId: () => formatLocalModelId,
46
56
  generatePKCE: () => generatePKCE,
57
+ getAllModels: () => getAllModels,
47
58
  getAppPaths: () => getAppPaths,
48
59
  getAuthStorageKey: () => getAuthStorageKey,
49
60
  getAuthStorageKeys: () => getAuthStorageKeys,
@@ -63,25 +74,31 @@ __export(index_exports, {
63
74
  getToolResultCharLimit: () => getToolResultCharLimit,
64
75
  getVideoByteLimit: () => getVideoByteLimit,
65
76
  isKimiCodingEndpoint: () => isKimiCodingEndpoint,
77
+ isLocalModelId: () => isLocalModelId,
66
78
  isLoggerOpen: () => isLoggerOpen,
67
79
  isModelLoaded: () => isModelLoaded,
68
80
  isThinkingLevelSupported: () => isThinkingLevelSupported,
69
81
  kimiCodeBaseUrl: () => kimiCodeBaseUrl,
70
82
  kimiCodingHeaders: () => kimiCodingHeaders,
83
+ localAuthStorageKey: () => localAuthStorageKey,
71
84
  log: () => log,
72
85
  loginAnthropic: () => loginAnthropic,
73
86
  loginGemini: () => loginGemini,
74
87
  loginKimi: () => loginKimi,
75
88
  loginOpenAI: () => loginOpenAI,
76
89
  openLog: () => openLog,
90
+ parseLocalModelId: () => parseLocalModelId,
91
+ probeEndpoint: () => probeEndpoint,
77
92
  readStoredBaseUrlSync: () => readStoredBaseUrlSync,
78
93
  refreshAnthropicToken: () => refreshAnthropicToken,
79
94
  refreshGeminiToken: () => refreshGeminiToken,
80
95
  refreshKimiToken: () => refreshKimiToken,
81
96
  refreshOpenAIToken: () => refreshOpenAIToken,
82
97
  registerLogCleanup: () => registerLogCleanup,
98
+ registerRuntimeModels: () => registerRuntimeModels,
83
99
  resample: () => resample,
84
100
  setProgressCallback: () => setProgressCallback,
101
+ toModelInfo: () => toModelInfo,
85
102
  transcribeVoice: () => transcribeVoice,
86
103
  usesOpenAICodexTransport: () => usesOpenAICodexTransport,
87
104
  withFileLock: () => withFileLock
@@ -239,6 +256,7 @@ function credsFromTokenResponse(data, opts) {
239
256
  accessToken,
240
257
  refreshToken,
241
258
  expiresAt: Date.now() + expiresIn * 1e3,
259
+ expiresIn,
242
260
  baseUrl: kimiCodeBaseUrl()
243
261
  };
244
262
  }
@@ -1271,6 +1289,8 @@ function isAlive(pid) {
1271
1289
  // src/auth-storage.ts
1272
1290
  var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
1273
1291
  var XIAOMI_CREDITS_KEY = "xiaomi-credits";
1292
+ var LOCAL_AUTH_KEY_PREFIX = "local:";
1293
+ var LOCAL_CREDENTIAL_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1e3;
1274
1294
  function activeBaseUrlEntry(data, provider) {
1275
1295
  if (provider === "moonshot") {
1276
1296
  const oauth = data[MOONSHOT_OAUTH_KEY];
@@ -1291,7 +1311,12 @@ function readStoredBaseUrlSync(authFile, provider) {
1291
1311
  return void 0;
1292
1312
  }
1293
1313
  }
1294
- var REFRESH_SKEW_MS = 6e4;
1314
+ var MIN_REFRESH_THRESHOLD_MS = 3e5;
1315
+ var REFRESH_THRESHOLD_RATIO = 0.5;
1316
+ function refreshThresholdMs(creds) {
1317
+ const lifetimeMs = (creds.expiresIn ?? 0) * 1e3;
1318
+ return Math.max(MIN_REFRESH_THRESHOLD_MS, lifetimeMs * REFRESH_THRESHOLD_RATIO);
1319
+ }
1295
1320
  var USAGE_EXHAUSTED_DEFAULT_MS = 15 * 60 * 1e3;
1296
1321
  var STATIC_API_KEY_PROVIDERS = /* @__PURE__ */ new Set([
1297
1322
  "glm",
@@ -1301,7 +1326,9 @@ var STATIC_API_KEY_PROVIDERS = /* @__PURE__ */ new Set([
1301
1326
  "deepseek",
1302
1327
  "openrouter",
1303
1328
  "sakana",
1304
- "xai"
1329
+ "xai",
1330
+ // Local endpoints: a fixed (usually placeholder) key, never refreshable.
1331
+ "local"
1305
1332
  ]);
1306
1333
  var AuthStorage = class {
1307
1334
  data = {};
@@ -1350,8 +1377,33 @@ var AuthStorage = class {
1350
1377
  if (provider === "xiaomi") {
1351
1378
  return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
1352
1379
  }
1380
+ if (provider === "local") {
1381
+ return Object.keys(this.data).some((key) => key.startsWith(LOCAL_AUTH_KEY_PREFIX));
1382
+ }
1353
1383
  return Boolean(this.data[provider]);
1354
1384
  }
1385
+ /** Endpoint ids that currently have a `local:<id>` credential stored. */
1386
+ async listLocalEndpointIds() {
1387
+ await this.ensureLoaded();
1388
+ return Object.keys(this.data).filter((key) => key.startsWith(LOCAL_AUTH_KEY_PREFIX)).map((key) => key.slice(LOCAL_AUTH_KEY_PREFIX.length));
1389
+ }
1390
+ /**
1391
+ * Write (or refresh) the credential for one local endpoint. The `baseUrl` is
1392
+ * what `effectiveBaseUrl` later picks up, and `accessToken` is the endpoint's
1393
+ * key — a placeholder for the servers that ignore it.
1394
+ */
1395
+ async setLocalEndpoint(endpointId, baseUrl, apiKey) {
1396
+ await this.setCredentials(`${LOCAL_AUTH_KEY_PREFIX}${endpointId}`, {
1397
+ accessToken: apiKey && apiKey.length > 0 ? apiKey : "local",
1398
+ refreshToken: "",
1399
+ expiresAt: Date.now() + LOCAL_CREDENTIAL_LIFETIME_MS,
1400
+ baseUrl
1401
+ });
1402
+ }
1403
+ /** Remove one local endpoint's credential. No-op when it isn't stored. */
1404
+ async removeLocalEndpoint(endpointId) {
1405
+ await this.clearCredentials(`${LOCAL_AUTH_KEY_PREFIX}${endpointId}`);
1406
+ }
1355
1407
  /**
1356
1408
  * True if the active credential for `provider` is a static API key with no
1357
1409
  * refresh mechanism. For `moonshot` this is only true when the Kimi OAuth
@@ -1534,7 +1586,7 @@ var AuthStorage = class {
1534
1586
  if (STATIC_API_KEY_PROVIDERS.has(provider)) {
1535
1587
  return creds;
1536
1588
  }
1537
- if (!opts?.forceRefresh && Date.now() < creds.expiresAt - REFRESH_SKEW_MS) {
1589
+ if (!opts?.forceRefresh && Date.now() < creds.expiresAt - refreshThresholdMs(creds)) {
1538
1590
  return creds;
1539
1591
  }
1540
1592
  const existing = this.refreshLocks.get(provider);
@@ -1547,7 +1599,7 @@ var AuthStorage = class {
1547
1599
  throw new NotLoggedInError(provider);
1548
1600
  }
1549
1601
  const credentialWasReplaced = latestCreds.accessToken !== creds.accessToken || latestCreds.refreshToken !== creds.refreshToken || latestCreds.expiresAt !== creds.expiresAt;
1550
- if (credentialWasReplaced || !opts?.forceRefresh && Date.now() < latestCreds.expiresAt - REFRESH_SKEW_MS) {
1602
+ if (credentialWasReplaced || !opts?.forceRefresh && Date.now() < latestCreds.expiresAt - refreshThresholdMs(latestCreds)) {
1551
1603
  this.data = latest;
1552
1604
  return latestCreds;
1553
1605
  }
@@ -1659,8 +1711,12 @@ var MODELS = [
1659
1711
  // maxThinkingLevel: "max",
1660
1712
  // },
1661
1713
  {
1662
- id: "claude-opus-4-8",
1663
- name: "Claude Opus 4.8",
1714
+ // Released 2026-07-24 — "For complex agentic coding and enterprise work".
1715
+ // Near-Fable capability at half the price ($5/$25 vs $10/$50). Adaptive
1716
+ // thinking with the full effort ladder (low→max, xhigh included); dateless
1717
+ // ID is the canonical pinned snapshot (post-4.6 naming scheme).
1718
+ id: "claude-opus-5",
1719
+ name: "Claude Opus 5",
1664
1720
  provider: "anthropic",
1665
1721
  contextWindow: 1e6,
1666
1722
  maxOutputTokens: 128e3,
@@ -2046,14 +2102,30 @@ var MODELS = [
2046
2102
  maxThinkingLevel: "high"
2047
2103
  }
2048
2104
  ];
2105
+ var runtimeModels = /* @__PURE__ */ new Map();
2106
+ function registerRuntimeModels(models) {
2107
+ for (const model of models) runtimeModels.set(model.id, model);
2108
+ }
2109
+ function clearRuntimeModels(predicate) {
2110
+ if (!predicate) {
2111
+ runtimeModels.clear();
2112
+ return;
2113
+ }
2114
+ for (const [id, model] of runtimeModels) {
2115
+ if (predicate(model)) runtimeModels.delete(id);
2116
+ }
2117
+ }
2118
+ function getAllModels() {
2119
+ return [...MODELS, ...runtimeModels.values()];
2120
+ }
2049
2121
  function getModel(id) {
2050
- return MODELS.find((m) => m.id === id);
2122
+ return MODELS.find((m) => m.id === id) ?? runtimeModels.get(id);
2051
2123
  }
2052
2124
  function getModelsForProvider(provider) {
2053
- return MODELS.filter((m) => m.provider === provider);
2125
+ return getAllModels().filter((m) => m.provider === provider);
2054
2126
  }
2055
2127
  function getAuthStorageKeys(provider, modelId) {
2056
- const model = MODELS.find((m) => m.id === modelId && m.provider === provider);
2128
+ const model = getAllModels().find((m) => m.id === modelId && m.provider === provider);
2057
2129
  return model?.authStorageKeys ?? [provider];
2058
2130
  }
2059
2131
  function getAuthStorageKey(provider, modelId) {
@@ -2076,8 +2148,23 @@ function getDefaultModel(provider) {
2076
2148
  if (provider === "openrouter") return MODELS.find((m) => m.id === "qwen/qwen3.6-plus");
2077
2149
  if (provider === "sakana") return MODELS.find((m) => m.id === "fugu");
2078
2150
  if (provider === "xai") return MODELS.find((m) => m.id === "grok-4.5");
2151
+ if (provider === "local") {
2152
+ return getModelsForProvider("local")[0] ?? PLACEHOLDER_LOCAL_MODEL;
2153
+ }
2079
2154
  return MODELS.find((m) => m.id === "claude-sonnet-5");
2080
2155
  }
2156
+ var PLACEHOLDER_LOCAL_MODEL = {
2157
+ id: "local/none/none",
2158
+ name: "No local model discovered",
2159
+ provider: "local",
2160
+ contextWindow: 8192,
2161
+ maxOutputTokens: 2048,
2162
+ supportsThinking: false,
2163
+ supportsImages: false,
2164
+ supportsVideo: false,
2165
+ costTier: "low",
2166
+ maxThinkingLevel: "high"
2167
+ };
2081
2168
  function usesOpenAICodexTransport(options) {
2082
2169
  return options?.provider === "openai" && Boolean(options.accountId);
2083
2170
  }
@@ -2127,7 +2214,7 @@ var OPENAI_GPT_56_THINKING_LEVELS = [
2127
2214
  ];
2128
2215
  var SAKANA_THINKING_LEVELS = ["high", "xhigh"];
2129
2216
  var XAI_THINKING_LEVELS = ["low", "medium", "high"];
2130
- var ANTHROPIC_OPUS_48_47_THINKING_LEVELS = [
2217
+ var ANTHROPIC_XHIGH_THINKING_LEVELS = [
2131
2218
  "low",
2132
2219
  "medium",
2133
2220
  "high",
@@ -2141,6 +2228,7 @@ var ANTHROPIC_ADAPTIVE_THINKING_LEVELS = [
2141
2228
  "max"
2142
2229
  ];
2143
2230
  var MOONSHOT_K3_THINKING_LEVELS = ["low", "high", "max"];
2231
+ var LOCAL_THINKING_LEVELS = ["low", "medium", "high", "max"];
2144
2232
  function isOpenAIGptModel(provider, model) {
2145
2233
  return provider === "openai" && model.startsWith("gpt-");
2146
2234
  }
@@ -2153,16 +2241,22 @@ function isXaiModel(provider) {
2153
2241
  function isMoonshotK3Model(provider, model) {
2154
2242
  return provider === "moonshot" && model === "kimi-k3";
2155
2243
  }
2156
- function isAnthropicOpus48Or47Model(provider, model) {
2157
- return provider === "anthropic" && /opus-4-8|opus-4-7/.test(model);
2244
+ function isAnthropicXhighModel(provider, model) {
2245
+ return provider === "anthropic" && /opus-5|opus-4-8|opus-4-7/.test(model);
2158
2246
  }
2159
2247
  function isAnthropicAdaptiveModel(provider, model) {
2160
- return provider === "anthropic" && /opus-4-8|opus-4-7|opus-4-6|sonnet-5|fable-5|mythos-5/.test(model);
2248
+ return provider === "anthropic" && /opus-5|opus-4-8|opus-4-7|opus-4-6|sonnet-5|fable-5|mythos-5/.test(model);
2161
2249
  }
2162
2250
  function getSupportedThinkingLevels(provider, model) {
2251
+ if (provider === "local") {
2252
+ const info = getModel(model);
2253
+ if (!info?.supportsThinking) return [];
2254
+ const maxIndex2 = LOCAL_THINKING_LEVELS.indexOf(info.maxThinkingLevel);
2255
+ return maxIndex2 === -1 ? LOCAL_THINKING_LEVELS.slice(0, 3) : LOCAL_THINKING_LEVELS.slice(0, maxIndex2 + 1);
2256
+ }
2163
2257
  const maxLevel = getMaxThinkingLevel(model);
2164
2258
  if (isAnthropicAdaptiveModel(provider, model)) {
2165
- const levels2 = isAnthropicOpus48Or47Model(provider, model) ? ANTHROPIC_OPUS_48_47_THINKING_LEVELS : ANTHROPIC_ADAPTIVE_THINKING_LEVELS;
2259
+ const levels2 = isAnthropicXhighModel(provider, model) ? ANTHROPIC_XHIGH_THINKING_LEVELS : ANTHROPIC_ADAPTIVE_THINKING_LEVELS;
2166
2260
  const maxIndex2 = levels2.indexOf(maxLevel);
2167
2261
  if (maxIndex2 === -1) return ["low", "medium", "high"];
2168
2262
  return levels2.slice(0, maxIndex2 + 1);
@@ -2189,7 +2283,11 @@ function isThinkingLevelSupported(provider, model, level) {
2189
2283
  }
2190
2284
  function getNextThinkingLevel(provider, model, current) {
2191
2285
  const supportedLevels = getSupportedThinkingLevels(provider, model);
2192
- const shouldCycleLevels = isOpenAIGptModel(provider, model) || isAnthropicAdaptiveModel(provider, model) || isSakanaModel(provider) || isXaiModel(provider) || isMoonshotK3Model(provider, model);
2286
+ const shouldCycleLevels = isOpenAIGptModel(provider, model) || isAnthropicAdaptiveModel(provider, model) || isSakanaModel(provider) || isXaiModel(provider) || isMoonshotK3Model(provider, model) || // Local servers take a real effort level, not just on/off: Ollama accepts
2287
+ // low/medium/high on `reasoning_effort` (verified against 0.32) and the
2288
+ // other OpenAI-compatible servers use the same three. A model that can't
2289
+ // reason at all already has no supported levels, so it never gets here.
2290
+ provider === "local";
2193
2291
  if (!shouldCycleLevels) {
2194
2292
  return current ? void 0 : supportedLevels[0];
2195
2293
  }
@@ -2199,6 +2297,253 @@ function getNextThinkingLevel(provider, model, current) {
2199
2297
  return supportedLevels[index + 1];
2200
2298
  }
2201
2299
 
2300
+ // src/local-models.ts
2301
+ var DEFAULT_LOCAL_ENDPOINTS = [
2302
+ { id: "ollama", label: "Ollama", baseUrl: "http://127.0.0.1:11434/v1", kind: "ollama" },
2303
+ { id: "lmstudio", label: "LM Studio", baseUrl: "http://127.0.0.1:1234/v1", kind: "lmstudio" },
2304
+ { id: "llamacpp", label: "llama.cpp", baseUrl: "http://127.0.0.1:8080/v1", kind: "llamacpp" },
2305
+ { id: "vllm", label: "vLLM", baseUrl: "http://127.0.0.1:8000/v1", kind: "vllm" }
2306
+ ];
2307
+ var FALLBACK_CONTEXT_WINDOW = 8192;
2308
+ var LOCAL_API_KEY_PLACEHOLDER = "local";
2309
+ var DEFAULT_PROBE_TIMEOUT_MS = 1200;
2310
+ var ENRICH_CONCURRENCY = 6;
2311
+ var CACHE_TTL_MS2 = 3e4;
2312
+ var NON_CHAT_ID_PATTERN = /(?:^|[-_/])(?:embed|embedding|rerank|reranker|bge|nomic-embed)/i;
2313
+ var LOCAL_ID_PREFIX = "local/";
2314
+ function formatLocalModelId(endpointId, rawId) {
2315
+ return `${LOCAL_ID_PREFIX}${endpointId}/${rawId}`;
2316
+ }
2317
+ function parseLocalModelId(id) {
2318
+ if (!id.startsWith(LOCAL_ID_PREFIX)) return void 0;
2319
+ const rest = id.slice(LOCAL_ID_PREFIX.length);
2320
+ const slash = rest.indexOf("/");
2321
+ if (slash <= 0 || slash === rest.length - 1) return void 0;
2322
+ return { endpointId: rest.slice(0, slash), rawId: rest.slice(slash + 1) };
2323
+ }
2324
+ function isLocalModelId(id) {
2325
+ return parseLocalModelId(id) !== void 0;
2326
+ }
2327
+ function localAuthStorageKey(endpointId) {
2328
+ return `local:${endpointId}`;
2329
+ }
2330
+ function endpointRoot(baseUrl) {
2331
+ return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
2332
+ }
2333
+ function authHeaders(endpoint) {
2334
+ return {
2335
+ Authorization: `Bearer ${endpoint.apiKey ?? LOCAL_API_KEY_PLACEHOLDER}`,
2336
+ Accept: "application/json"
2337
+ };
2338
+ }
2339
+ async function fetchJson(url, endpoint, options) {
2340
+ try {
2341
+ const res = await fetchJsonOrThrow(url, endpoint, options);
2342
+ return res;
2343
+ } catch {
2344
+ return void 0;
2345
+ }
2346
+ }
2347
+ async function fetchJsonOrThrow(url, endpoint, { timeoutMs, signal, method = "GET", body }) {
2348
+ const controller = new AbortController();
2349
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2350
+ const onAbort = () => controller.abort();
2351
+ signal?.addEventListener("abort", onAbort, { once: true });
2352
+ try {
2353
+ const res = await fetch(url, {
2354
+ method,
2355
+ signal: controller.signal,
2356
+ headers: body ? { ...authHeaders(endpoint), "Content-Type": "application/json" } : authHeaders(endpoint),
2357
+ ...body ? { body: JSON.stringify(body) } : {}
2358
+ });
2359
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2360
+ return await res.json();
2361
+ } finally {
2362
+ clearTimeout(timer);
2363
+ signal?.removeEventListener("abort", onAbort);
2364
+ }
2365
+ }
2366
+ function unreachableReason(endpoint, err) {
2367
+ const message = err instanceof Error ? err.message : String(err);
2368
+ if (/abort/i.test(message)) return `No response from ${endpoint.baseUrl} (timed out)`;
2369
+ if (/HTTP 401|HTTP 403/.test(message)) {
2370
+ return `${endpoint.baseUrl} rejected the API key (HTTP ${message.includes("401") ? 401 : 403})`;
2371
+ }
2372
+ if (/HTTP \d+/.test(message)) return `${endpoint.baseUrl} returned ${message}`;
2373
+ return `Not running at ${endpoint.baseUrl}`;
2374
+ }
2375
+ async function probeEndpoint(endpoint, { timeoutMs = DEFAULT_PROBE_TIMEOUT_MS, signal } = {}) {
2376
+ const listUrl = `${endpoint.baseUrl.replace(/\/+$/, "")}/models`;
2377
+ let list;
2378
+ try {
2379
+ list = await fetchJsonOrThrow(listUrl, endpoint, { timeoutMs, signal });
2380
+ } catch (err) {
2381
+ return { endpoint, reachable: false, reason: unreachableReason(endpoint, err), models: [] };
2382
+ }
2383
+ const entries = (list.data ?? []).filter(
2384
+ (entry) => typeof entry.id === "string" && entry.id.length > 0 && !NON_CHAT_ID_PATTERN.test(entry.id)
2385
+ );
2386
+ const models = await enrich(endpoint, entries, { timeoutMs, signal });
2387
+ log("INFO", "local-models", `Probed ${endpoint.label}`, {
2388
+ baseUrl: endpoint.baseUrl,
2389
+ models: String(models.length)
2390
+ });
2391
+ return { endpoint, reachable: true, models };
2392
+ }
2393
+ async function enrich(endpoint, entries, options) {
2394
+ if (endpoint.kind === "lmstudio") return enrichLmStudio(endpoint, entries, options);
2395
+ if (endpoint.kind === "ollama") return enrichOllama(endpoint, entries, options);
2396
+ if (endpoint.kind === "llamacpp") return enrichLlamaCpp(endpoint, entries, options);
2397
+ return entries.map((entry) => genericModel(endpoint, entry));
2398
+ }
2399
+ function genericModel(endpoint, entry) {
2400
+ const declared = typeof entry.max_model_len === "number" ? entry.max_model_len : void 0;
2401
+ return {
2402
+ rawId: entry.id,
2403
+ endpointId: endpoint.id,
2404
+ contextWindow: declared ?? FALLBACK_CONTEXT_WINDOW,
2405
+ contextWindowKnown: declared !== void 0,
2406
+ supportsTools: true,
2407
+ supportsImages: false,
2408
+ supportsThinking: false
2409
+ };
2410
+ }
2411
+ async function enrichOllama(endpoint, entries, options) {
2412
+ const showUrl = `${endpointRoot(endpoint.baseUrl)}/api/show`;
2413
+ const enriched = await mapLimited(entries, ENRICH_CONCURRENCY, async (entry) => {
2414
+ const show = await fetchJson(showUrl, endpoint, {
2415
+ ...options,
2416
+ method: "POST",
2417
+ body: { model: entry.id }
2418
+ });
2419
+ if (!show) return genericModel(endpoint, entry);
2420
+ const caps = show.capabilities ?? [];
2421
+ if (caps.includes("embedding") && !caps.includes("completion")) return void 0;
2422
+ const ctx = ollamaContextLength(show.model_info);
2423
+ return {
2424
+ rawId: entry.id,
2425
+ endpointId: endpoint.id,
2426
+ contextWindow: ctx ?? FALLBACK_CONTEXT_WINDOW,
2427
+ contextWindowKnown: ctx !== void 0,
2428
+ // Ollama reports capabilities honestly, so trust it here rather than
2429
+ // using the optimistic generic default.
2430
+ supportsTools: caps.includes("tools"),
2431
+ supportsImages: caps.includes("vision"),
2432
+ supportsThinking: caps.includes("thinking")
2433
+ };
2434
+ });
2435
+ return enriched.filter((model) => model !== void 0);
2436
+ }
2437
+ function ollamaContextLength(info) {
2438
+ if (!info) return void 0;
2439
+ for (const [key, value] of Object.entries(info)) {
2440
+ if (key.endsWith(".context_length") && typeof value === "number" && value > 0) return value;
2441
+ }
2442
+ return void 0;
2443
+ }
2444
+ async function enrichLmStudio(endpoint, entries, options) {
2445
+ const detail = await fetchJson(
2446
+ `${endpointRoot(endpoint.baseUrl)}/api/v0/models`,
2447
+ endpoint,
2448
+ options
2449
+ );
2450
+ if (!detail?.data) return entries.map((entry) => genericModel(endpoint, entry));
2451
+ const byId = new Map(detail.data.filter((m) => m.id).map((m) => [m.id, m]));
2452
+ const models = [];
2453
+ for (const entry of entries) {
2454
+ const info = byId.get(entry.id);
2455
+ if (info && info.type !== "llm" && info.type !== "vlm") continue;
2456
+ const ctx = info?.max_context_length;
2457
+ models.push({
2458
+ rawId: entry.id,
2459
+ endpointId: endpoint.id,
2460
+ contextWindow: typeof ctx === "number" && ctx > 0 ? ctx : FALLBACK_CONTEXT_WINDOW,
2461
+ contextWindowKnown: typeof ctx === "number" && ctx > 0,
2462
+ // LM Studio doesn't report tool support; it gates per-model at request time.
2463
+ supportsTools: true,
2464
+ supportsImages: info?.type === "vlm",
2465
+ supportsThinking: false,
2466
+ loaded: info?.state === "loaded"
2467
+ });
2468
+ }
2469
+ return models;
2470
+ }
2471
+ async function enrichLlamaCpp(endpoint, entries, options) {
2472
+ const props = await fetchJson(
2473
+ `${endpointRoot(endpoint.baseUrl)}/props`,
2474
+ endpoint,
2475
+ options
2476
+ );
2477
+ const nCtx = props?.default_generation_settings?.n_ctx;
2478
+ const known = typeof nCtx === "number" && nCtx > 0;
2479
+ return entries.map((entry) => ({
2480
+ ...genericModel(endpoint, entry),
2481
+ contextWindow: known ? nCtx : FALLBACK_CONTEXT_WINDOW,
2482
+ contextWindowKnown: known
2483
+ }));
2484
+ }
2485
+ async function mapLimited(items, limit, fn) {
2486
+ const results = new Array(items.length);
2487
+ let next = 0;
2488
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
2489
+ while (next < items.length) {
2490
+ const index = next++;
2491
+ results[index] = await fn(items[index]);
2492
+ }
2493
+ });
2494
+ await Promise.all(workers);
2495
+ return results;
2496
+ }
2497
+ function maxThinkingLevelFor(endpoint) {
2498
+ return endpoint.kind === "ollama" ? "max" : "high";
2499
+ }
2500
+ function toModelInfo(model, endpoint) {
2501
+ return {
2502
+ id: formatLocalModelId(model.endpointId, model.rawId),
2503
+ name: `${model.rawId} (${endpoint.label})`,
2504
+ provider: "local",
2505
+ contextWindow: model.contextWindow,
2506
+ // Leave real headroom for the prompt on small local windows.
2507
+ maxOutputTokens: Math.max(512, Math.min(4096, Math.floor(model.contextWindow / 4))),
2508
+ supportsThinking: model.supportsThinking,
2509
+ supportsImages: model.supportsImages,
2510
+ supportsVideo: false,
2511
+ costTier: "low",
2512
+ maxThinkingLevel: maxThinkingLevelFor(endpoint),
2513
+ authStorageKeys: [localAuthStorageKey(model.endpointId)]
2514
+ };
2515
+ }
2516
+ var cache;
2517
+ function cacheKey(endpoints) {
2518
+ return endpoints.map((e) => `${e.id}@${e.baseUrl}`).join("|");
2519
+ }
2520
+ async function discoverLocalModels(endpoints = DEFAULT_LOCAL_ENDPOINTS, options = {}) {
2521
+ const key = cacheKey(endpoints);
2522
+ if (!options.force && cache && cache.key === key && Date.now() - cache.at < CACHE_TTL_MS2) {
2523
+ return cache.result;
2524
+ }
2525
+ const probes = await Promise.all(endpoints.map((endpoint) => probeEndpoint(endpoint, options)));
2526
+ const models = probes.flatMap(
2527
+ (probe) => probe.models.map((model) => toModelInfo(model, probe.endpoint))
2528
+ );
2529
+ const result = { probes, models };
2530
+ cache = { key, at: Date.now(), result };
2531
+ return result;
2532
+ }
2533
+ function clearLocalDiscoveryCache() {
2534
+ cache = void 0;
2535
+ }
2536
+ function findProbedModel(probes, modelId) {
2537
+ const parsed = parseLocalModelId(modelId);
2538
+ if (!parsed) return void 0;
2539
+ for (const probe of probes) {
2540
+ if (probe.endpoint.id !== parsed.endpointId) continue;
2541
+ const model = probe.models.find((m) => m.rawId === parsed.rawId);
2542
+ if (model) return { model, endpoint: probe.endpoint };
2543
+ }
2544
+ return void 0;
2545
+ }
2546
+
2202
2547
  // src/provider-usage.ts
2203
2548
  var SubscriptionUsageError = class extends Error {
2204
2549
  constructor(message, status, retryAfterMs2) {
@@ -2916,19 +3261,30 @@ function createAutoUpdater(config) {
2916
3261
  // Annotate the CommonJS export names for ESM import in node:
2917
3262
  0 && (module.exports = {
2918
3263
  AuthStorage,
3264
+ DEFAULT_LOCAL_ENDPOINTS,
2919
3265
  DEFAULT_MAX_VIDEO_BYTES,
3266
+ FALLBACK_CONTEXT_WINDOW,
3267
+ LOCAL_API_KEY_PLACEHOLDER,
3268
+ LOCAL_AUTH_KEY_PREFIX,
2920
3269
  MODELS,
2921
3270
  MOONSHOT_OAUTH_KEY,
2922
3271
  NotLoggedInError,
2923
3272
  SubscriptionUsageError,
2924
3273
  TelegramBot,
2925
3274
  XIAOMI_CREDITS_KEY,
3275
+ clearLocalDiscoveryCache,
3276
+ clearRuntimeModels,
2926
3277
  closeLogger,
2927
3278
  createAutoUpdater,
2928
3279
  decodeOggOpus,
3280
+ discoverLocalModels,
2929
3281
  downmixToMono,
3282
+ endpointRoot,
2930
3283
  fetchSubscriptionUsage,
3284
+ findProbedModel,
3285
+ formatLocalModelId,
2931
3286
  generatePKCE,
3287
+ getAllModels,
2932
3288
  getAppPaths,
2933
3289
  getAuthStorageKey,
2934
3290
  getAuthStorageKeys,
@@ -2948,25 +3304,31 @@ function createAutoUpdater(config) {
2948
3304
  getToolResultCharLimit,
2949
3305
  getVideoByteLimit,
2950
3306
  isKimiCodingEndpoint,
3307
+ isLocalModelId,
2951
3308
  isLoggerOpen,
2952
3309
  isModelLoaded,
2953
3310
  isThinkingLevelSupported,
2954
3311
  kimiCodeBaseUrl,
2955
3312
  kimiCodingHeaders,
3313
+ localAuthStorageKey,
2956
3314
  log,
2957
3315
  loginAnthropic,
2958
3316
  loginGemini,
2959
3317
  loginKimi,
2960
3318
  loginOpenAI,
2961
3319
  openLog,
3320
+ parseLocalModelId,
3321
+ probeEndpoint,
2962
3322
  readStoredBaseUrlSync,
2963
3323
  refreshAnthropicToken,
2964
3324
  refreshGeminiToken,
2965
3325
  refreshKimiToken,
2966
3326
  refreshOpenAIToken,
2967
3327
  registerLogCleanup,
3328
+ registerRuntimeModels,
2968
3329
  resample,
2969
3330
  setProgressCallback,
3331
+ toModelInfo,
2970
3332
  transcribeVoice,
2971
3333
  usesOpenAICodexTransport,
2972
3334
  withFileLock