min-agent 0.5.0 → 0.5.1
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 +14 -0
- package/dist/agent.js +53 -7
- package/dist/cli/commands/ctx.js +7 -0
- package/dist/cli/commands/index.js +10 -2
- package/dist/cli/commands/setup.js +55 -3
- package/dist/cli/commands/shared.js +10 -1
- package/dist/cli/program.js +7 -1
- package/dist/cli/setup/detect.js +17 -0
- package/dist/cli/setup/flags.js +12 -0
- package/dist/cli/setup/flow.js +108 -0
- package/dist/cli/setup/provider-form.js +102 -0
- package/dist/cli/setup/ui.js +534 -0
- package/dist/config.js +52 -159
- package/dist/context-window.js +33 -23
- package/dist/ctx-cli.js +30 -0
- package/dist/ctx.js +80 -0
- package/dist/ollama-model.js +234 -0
- package/dist/ollama-openai-bridge.js +383 -0
- package/dist/serve/routes-meta.js +35 -0
- package/dist/thinking-wire.js +15 -4
- package/dist/thinking.js +26 -2
- package/dist/tui/App.js +18 -6
- package/dist/tui/CtxPicker.js +68 -0
- package/dist/tui/InputBar.js +4 -2
- package/dist/tui/ThinkPicker.js +4 -6
- package/dist/tui/index.js +7 -1
- package/dist/tui/slash-commands.js +6 -0
- package/dist/tui/slash-handler.js +27 -1
- package/dist/tui-chat.js +25 -3
- package/docs/API.md +19 -2
- package/docs/superpowers/plans/2026-08-23-cli-setup.md +501 -0
- package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +282 -0
- package/package.json +1 -1
- package/skills/self-config/SKILL.md +3 -1
- package/skills/self-config/reference.md +4 -3
package/dist/config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFileSync, mkdirSync, existsSync, statSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from "os";
|
|
4
|
-
import readline from "readline";
|
|
5
4
|
import { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
6
5
|
import { getEffectiveSandboxPolicy, mergeSandboxConfig, parseSandboxConfig, sandboxLaunchSource, sandboxStatusLabel, } from "./sandbox.js";
|
|
7
6
|
const DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".min-agent");
|
|
@@ -287,184 +286,78 @@ async function fetchModelsFromURL(url, apiKey) {
|
|
|
287
286
|
signal: AbortSignal.timeout(10000),
|
|
288
287
|
});
|
|
289
288
|
if (!response.ok)
|
|
290
|
-
return [];
|
|
291
|
-
const data =
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
289
|
+
return { models: [], ok: false, status: response.status };
|
|
290
|
+
const data = await response.json();
|
|
291
|
+
return { models: parseModelIds(data), ok: true, status: response.status };
|
|
292
|
+
}
|
|
293
|
+
function parseModelIds(data) {
|
|
294
|
+
const list = Array.isArray(data)
|
|
295
|
+
? data
|
|
296
|
+
: data !== null && typeof data === "object" && "data" in data && Array.isArray(data.data)
|
|
297
|
+
? data.data
|
|
298
|
+
: [];
|
|
299
|
+
return list
|
|
300
|
+
.map((item) => item !== null && typeof item === "object" && "id" in item && typeof item.id === "string" ? item.id : null)
|
|
301
|
+
.filter((id) => id != null)
|
|
302
|
+
.sort();
|
|
302
303
|
}
|
|
303
304
|
const MODELS_CACHE_FILE = "models-cache.json";
|
|
304
305
|
function modelsCachePath() {
|
|
305
306
|
return path.join(getConfigDir(), MODELS_CACHE_FILE);
|
|
306
307
|
}
|
|
307
|
-
function
|
|
308
|
+
function modelsCacheKey(baseURL) {
|
|
309
|
+
return baseURL.replace(/\/$/, "");
|
|
310
|
+
}
|
|
311
|
+
function loadModelsCacheMap() {
|
|
308
312
|
const file = modelsCachePath();
|
|
309
313
|
if (!existsSync(file))
|
|
310
|
-
return
|
|
314
|
+
return {};
|
|
311
315
|
try {
|
|
312
|
-
|
|
316
|
+
const parsed = JSON.parse(readFileSync(file, "utf-8"));
|
|
317
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
318
|
+
return {};
|
|
319
|
+
return Object.fromEntries(Object.entries(parsed).filter((entry) => Array.isArray(entry[1]) && entry[1].every((item) => typeof item === "string")));
|
|
313
320
|
}
|
|
314
321
|
catch {
|
|
315
|
-
return
|
|
322
|
+
return {};
|
|
316
323
|
}
|
|
317
324
|
}
|
|
318
|
-
function
|
|
325
|
+
function loadModelsCacheBucket(baseURL) {
|
|
326
|
+
return loadModelsCacheMap()[modelsCacheKey(baseURL)] ?? [];
|
|
327
|
+
}
|
|
328
|
+
function saveModelsCacheBucket(baseURL, models) {
|
|
329
|
+
const map = loadModelsCacheMap();
|
|
330
|
+
map[modelsCacheKey(baseURL)] = models;
|
|
319
331
|
mkdirSync(getConfigDir(), { recursive: true });
|
|
320
|
-
atomicWriteFileSync(modelsCachePath(), JSON.stringify(
|
|
332
|
+
atomicWriteFileSync(modelsCachePath(), JSON.stringify(map));
|
|
321
333
|
}
|
|
322
|
-
export async function
|
|
334
|
+
export async function fetchModelsLive(baseURL, apiKey) {
|
|
323
335
|
try {
|
|
324
|
-
const trimmed = baseURL
|
|
325
|
-
let
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
models = await fetchModelsFromURL(`${trimmed}/v1/models`, apiKey);
|
|
336
|
+
const trimmed = modelsCacheKey(baseURL);
|
|
337
|
+
let result = await fetchModelsFromURL(`${trimmed}/models`, apiKey);
|
|
338
|
+
if (result.models.length === 0 && !trimmed.endsWith("/v1")) {
|
|
339
|
+
result = await fetchModelsFromURL(`${trimmed}/v1/models`, apiKey);
|
|
329
340
|
}
|
|
330
|
-
if (models.length > 0) {
|
|
331
|
-
|
|
332
|
-
return models;
|
|
341
|
+
if (result.ok && result.models.length > 0) {
|
|
342
|
+
saveModelsCacheBucket(trimmed, result.models);
|
|
343
|
+
return { models: result.models, ok: true, status: result.status };
|
|
333
344
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
console.log("\x1b[90m (using cached model list)\x1b[0m");
|
|
338
|
-
}
|
|
339
|
-
return cached;
|
|
345
|
+
if (!result.ok)
|
|
346
|
+
return { models: [], ok: false, status: result.status };
|
|
347
|
+
return { models: [], ok: true, status: result.status };
|
|
340
348
|
}
|
|
341
349
|
catch {
|
|
342
|
-
|
|
343
|
-
const cached = loadModelsCache();
|
|
344
|
-
if (cached.length > 0) {
|
|
345
|
-
console.log("\x1b[90m (using cached model list — network unavailable)\x1b[0m");
|
|
346
|
-
}
|
|
347
|
-
return cached;
|
|
350
|
+
return { models: [], ok: false };
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
|
-
export async function
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const marker = p.name === config.activeProvider ? " ← 当前" : "";
|
|
359
|
-
console.log(` ${i + 1}. ${p.name ?? "(未命名)"} ${p.baseURL} (${p.defaultModel ?? "未设置模型"})${marker}`);
|
|
360
|
-
});
|
|
361
|
-
console.log();
|
|
362
|
-
console.log("选项:");
|
|
363
|
-
console.log(" 1. 添加新 Provider");
|
|
364
|
-
console.log(" 2. 切换当前 Provider");
|
|
365
|
-
console.log(" 3. 修改当前 Provider");
|
|
366
|
-
console.log(" 4. 完成");
|
|
367
|
-
const choice = await ask(rl, "选择 (1/2/3/4)", "4");
|
|
368
|
-
if (choice === "1") {
|
|
369
|
-
const provider = await collectProvider(rl, undefined);
|
|
370
|
-
provider.name =
|
|
371
|
-
(await ask(rl, "Provider 名称", `provider-${providers.length + 1}`)).trim() ||
|
|
372
|
-
`provider-${providers.length + 1}`;
|
|
373
|
-
providers.push(provider);
|
|
374
|
-
config.activeProvider = provider.name;
|
|
375
|
-
config.providers = providers;
|
|
376
|
-
saveConfig(config);
|
|
377
|
-
console.log(`\n✓ Provider "${provider.name}" 已添加并设为当前`);
|
|
378
|
-
}
|
|
379
|
-
else if (choice === "2") {
|
|
380
|
-
const sel = await ask(rl, "输入序号选择当前 Provider");
|
|
381
|
-
const idx = parseInt(sel, 10) - 1;
|
|
382
|
-
const target = providers[idx];
|
|
383
|
-
if (!target?.name) {
|
|
384
|
-
console.error("无效序号");
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
config.activeProvider = target.name;
|
|
388
|
-
saveConfig(config);
|
|
389
|
-
console.log(`\n✓ 当前 Provider 已切换为: ${target.name}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
else if (choice === "3") {
|
|
393
|
-
const name = getActiveProvider(config)?.name ?? providers[0]?.name;
|
|
394
|
-
if (!name) {
|
|
395
|
-
console.error("无可用 Provider");
|
|
396
|
-
rl.close();
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
const idx = providers.findIndex((p) => p.name === name);
|
|
400
|
-
providers[idx] = { ...(await collectProvider(rl, providers[idx])), name };
|
|
401
|
-
config.providers = providers;
|
|
402
|
-
saveConfig(config);
|
|
403
|
-
console.log(`\n✓ Provider "${name}" 已更新`);
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
console.log("\n配置未变更");
|
|
407
|
-
}
|
|
408
|
-
rl.close();
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
// 首次配置
|
|
412
|
-
const provider = await collectProvider(rl, undefined);
|
|
413
|
-
provider.name = "default";
|
|
414
|
-
config.providers = [provider];
|
|
415
|
-
config.activeProvider = "default";
|
|
416
|
-
saveConfig(config);
|
|
417
|
-
rl.close();
|
|
418
|
-
console.log(`\n✓ 配置已保存到 ${path.join(getConfigDir(), CONFIG_FILE_NAME)}`);
|
|
419
|
-
}
|
|
420
|
-
async function collectProvider(rl, existing) {
|
|
421
|
-
console.log("Provider 类型:");
|
|
422
|
-
console.log(" 1. openai-compatible (默认,兼容 OpenAI API 的任意服务)");
|
|
423
|
-
console.log(" 2. openai (OpenAI 官方)");
|
|
424
|
-
console.log(" 3. ollama (本地 Ollama)");
|
|
425
|
-
console.log();
|
|
426
|
-
const typeChoice = await ask(rl, "选择 Provider (1/2/3)", existing?.type === "ollama" ? "3" : existing?.type === "openai" ? "2" : "1");
|
|
427
|
-
const providerType = typeChoice === "3" ? "ollama" : typeChoice === "2" ? "openai" : "openai-compatible";
|
|
428
|
-
let baseURL;
|
|
429
|
-
let apiKey;
|
|
430
|
-
if (providerType === "ollama") {
|
|
431
|
-
baseURL = await ask(rl, "Ollama API URL", existing?.baseURL || "http://localhost:11434/v1");
|
|
432
|
-
baseURL = normalizeOllamaBaseURL(baseURL);
|
|
433
|
-
apiKey = "ollama";
|
|
434
|
-
}
|
|
435
|
-
else if (providerType === "openai") {
|
|
436
|
-
baseURL = "https://api.openai.com/v1";
|
|
437
|
-
apiKey = await ask(rl, "OpenAI API Key", existing?.apiKey);
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
baseURL = await ask(rl, "API Base URL", existing?.baseURL || "https://api.openai.com/v1");
|
|
441
|
-
apiKey = await ask(rl, "API Key", existing?.apiKey);
|
|
442
|
-
}
|
|
443
|
-
if (!baseURL || (!apiKey && providerType !== "ollama")) {
|
|
444
|
-
console.error("URL 和 Key 不能为空");
|
|
445
|
-
process.exit(1);
|
|
446
|
-
}
|
|
447
|
-
console.log("\n正在获取模型列表...");
|
|
448
|
-
const models = await fetchModels(baseURL, apiKey);
|
|
449
|
-
let defaultModel = existing?.defaultModel ?? "";
|
|
450
|
-
if (models.length > 0) {
|
|
451
|
-
console.log(`\n可用模型 (${models.length}):`);
|
|
452
|
-
models.forEach((m, i) => {
|
|
453
|
-
const marker = m === defaultModel ? " ← 当前默认" : "";
|
|
454
|
-
console.log(` ${i + 1}. ${m}${marker}`);
|
|
455
|
-
});
|
|
456
|
-
console.log();
|
|
457
|
-
const choice = await ask(rl, "选择默认模型 (输入序号或模型名)", defaultModel);
|
|
458
|
-
const idx = parseInt(choice, 10) - 1;
|
|
459
|
-
if (idx >= 0 && idx < models.length)
|
|
460
|
-
defaultModel = models[idx];
|
|
461
|
-
else if (choice)
|
|
462
|
-
defaultModel = choice;
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
console.log(" ⚠ 无法获取模型列表,请手动输入模型名");
|
|
466
|
-
const hint = providerType === "ollama" ? "llama3" : providerType === "openai" ? "gpt-4o" : "";
|
|
467
|
-
defaultModel = await ask(rl, "默认模型", defaultModel || hint);
|
|
353
|
+
export async function fetchModels(baseURL, apiKey) {
|
|
354
|
+
const live = await fetchModelsLive(baseURL, apiKey);
|
|
355
|
+
if (live.models.length > 0)
|
|
356
|
+
return live.models;
|
|
357
|
+
const cached = loadModelsCacheBucket(baseURL);
|
|
358
|
+
if (cached.length > 0) {
|
|
359
|
+
const suffix = live.status == null && !live.ok ? " — network unavailable" : "";
|
|
360
|
+
console.log(`\x1b[90m (using cached model list${suffix})\x1b[0m`);
|
|
468
361
|
}
|
|
469
|
-
return
|
|
362
|
+
return cached;
|
|
470
363
|
}
|
package/dist/context-window.js
CHANGED
|
@@ -3,14 +3,17 @@ import path from "path";
|
|
|
3
3
|
import { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
4
4
|
import { getConfigDir, getEffectiveConfig, getActiveProvider } from "./config.js";
|
|
5
5
|
import { getCachedModelCatalog, getModelCatalog } from "./model-catalog.js";
|
|
6
|
+
import { getCachedOllamaModel, getOllamaModel, OLLAMA_AGENT_CONTEXT_CAP, OLLAMA_DEFAULT_CONTEXT_WINDOW, } from "./ollama-model.js";
|
|
6
7
|
/**
|
|
7
8
|
* Auto-detect context window size for the current model.
|
|
8
9
|
*
|
|
9
10
|
* Resolution order:
|
|
10
11
|
* 1. User config: provider.contextWindow (explicit override)
|
|
11
|
-
* 2.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* 2. Ollama providers (`type: "ollama"`): native POST /api/show operational window
|
|
13
|
+
* (Modelfile num_ctx, else architecture length capped at 32k) — never Lonae
|
|
14
|
+
* 3. Provider-specific API (OpenRouter, vLLM, hostname-based Ollama, OpenAI-compatible /models)
|
|
15
|
+
* 4. Lonae model catalog (context + thinking options)
|
|
16
|
+
* 5. Fallback: 512000 (memory only — never persisted as a detected value)
|
|
14
17
|
*/
|
|
15
18
|
export const DEFAULT_CONTEXT_WINDOW = 512000;
|
|
16
19
|
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -164,25 +167,23 @@ async function tryProviderModels(baseURL, apiKey, modelId) {
|
|
|
164
167
|
async function tryOllama(baseURL, modelId) {
|
|
165
168
|
if (!baseURL.includes("localhost") && !baseURL.includes("127.0.0.1") && !baseURL.includes("ollama"))
|
|
166
169
|
return null;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
});
|
|
176
|
-
if (!response.ok)
|
|
177
|
-
return null;
|
|
178
|
-
const data = (await response.json());
|
|
179
|
-
// Ollama returns model_info with context length
|
|
180
|
-
const ctxLength = data.model_info?.["general.context_length"] ?? data.model_info?.context_length ?? data.parameters?.num_ctx;
|
|
181
|
-
return typeof ctxLength === "number" ? ctxLength : null;
|
|
170
|
+
const info = await getOllamaModel(modelId, baseURL);
|
|
171
|
+
return info?.contextWindow ?? null;
|
|
172
|
+
}
|
|
173
|
+
async function detectOllamaWindow(baseURL, id) {
|
|
174
|
+
const shown = await getOllamaModel(id, baseURL);
|
|
175
|
+
if (shown?.contextWindow) {
|
|
176
|
+
setCache(id, shown.contextWindow, "detected");
|
|
177
|
+
return { tokens: shown.contextWindow, source: "detected" };
|
|
182
178
|
}
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
const listed = await tryProviderModels(baseURL, "ollama", id);
|
|
180
|
+
if (listed) {
|
|
181
|
+
const tokens = Math.min(listed, OLLAMA_AGENT_CONTEXT_CAP);
|
|
182
|
+
setCache(id, tokens, "detected");
|
|
183
|
+
return { tokens, source: "detected" };
|
|
185
184
|
}
|
|
185
|
+
setCache(id, OLLAMA_DEFAULT_CONTEXT_WINDOW, "fallback");
|
|
186
|
+
return { tokens: OLLAMA_DEFAULT_CONTEXT_WINDOW, source: "fallback" };
|
|
186
187
|
}
|
|
187
188
|
/**
|
|
188
189
|
* Detect context window size for a model by probing all sources in parallel.
|
|
@@ -192,6 +193,8 @@ async function detectContextWindow(config, id) {
|
|
|
192
193
|
const provider = getActiveProvider(config);
|
|
193
194
|
const baseURL = provider?.baseURL ?? "";
|
|
194
195
|
const apiKey = provider?.apiKey ?? "";
|
|
196
|
+
if (provider?.type === "ollama")
|
|
197
|
+
return detectOllamaWindow(baseURL, id);
|
|
195
198
|
const catalog = await getModelCatalog(id, baseURL);
|
|
196
199
|
if (catalog?.contextWindow)
|
|
197
200
|
return { tokens: catalog.contextWindow, source: "detected" };
|
|
@@ -221,9 +224,16 @@ export async function getContextWindowInfo(modelId) {
|
|
|
221
224
|
const id = modelId ?? provider?.defaultModel;
|
|
222
225
|
if (!id)
|
|
223
226
|
return { tokens: DEFAULT_CONTEXT_WINDOW, source: "fallback" };
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
+
if (provider?.type === "ollama") {
|
|
228
|
+
const shown = getCachedOllamaModel(id, provider.baseURL);
|
|
229
|
+
if (shown?.contextWindow)
|
|
230
|
+
return { tokens: shown.contextWindow, source: "detected" };
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const cached = getCached(id);
|
|
234
|
+
if (cached)
|
|
235
|
+
return cached;
|
|
236
|
+
}
|
|
227
237
|
const pending = inFlight.get(id);
|
|
228
238
|
if (pending)
|
|
229
239
|
return pending;
|
package/dist/ctx-cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { activeProviderIsOllama, configuredCtxChoice, ctxChoiceLabel, parseCtxChoice, setOllamaContextChoice, } from "./ctx.js";
|
|
2
|
+
export const CTX_CLI_USAGE = [
|
|
3
|
+
"Usage: min-agent ctx [2k|4k|8k|12k|16k|32k|64k|128k|256k|auto]",
|
|
4
|
+
" Context levels are only available for an Ollama provider.",
|
|
5
|
+
].join("\n");
|
|
6
|
+
export function runCtxCli(input) {
|
|
7
|
+
if (input.positionals[0] === "--help" || input.positionals[0] === "-h") {
|
|
8
|
+
return { ok: true, lines: [CTX_CLI_USAGE] };
|
|
9
|
+
}
|
|
10
|
+
if (!activeProviderIsOllama()) {
|
|
11
|
+
return { ok: false, lines: ["Context levels are only available for an Ollama provider.", CTX_CLI_USAGE] };
|
|
12
|
+
}
|
|
13
|
+
if (input.positionals.length === 0) {
|
|
14
|
+
const current = configuredCtxChoice();
|
|
15
|
+
const label = current.choice === "auto" && current.tokens != null ? `${current.tokens}` : current.choice;
|
|
16
|
+
return { ok: true, lines: [`Current context window: ${label}`, CTX_CLI_USAGE] };
|
|
17
|
+
}
|
|
18
|
+
if (input.positionals.length > 1)
|
|
19
|
+
return { ok: false, lines: [CTX_CLI_USAGE] };
|
|
20
|
+
const parsed = parseCtxChoice(input.positionals[0]);
|
|
21
|
+
if (!parsed)
|
|
22
|
+
return { ok: false, lines: [CTX_CLI_USAGE] };
|
|
23
|
+
const result = setOllamaContextChoice(parsed);
|
|
24
|
+
if (!result.ok)
|
|
25
|
+
return { ok: false, lines: ["Context levels are only available for an Ollama provider.", CTX_CLI_USAGE] };
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
lines: [`✓ Context window set to ${result.choice === "auto" ? "auto" : ctxChoiceLabel(result.choice)}`],
|
|
29
|
+
};
|
|
30
|
+
}
|
package/dist/ctx.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getActiveProvider, loadConfig, saveConfig } from "./config.js";
|
|
2
|
+
export const CTX_LEVELS = ["2k", "4k", "8k", "12k", "16k", "32k", "64k", "128k", "256k"];
|
|
3
|
+
export const CTX_AUTO = "auto";
|
|
4
|
+
export const CTX_CHOICES = [...CTX_LEVELS, CTX_AUTO];
|
|
5
|
+
const CTX_TOKENS = {
|
|
6
|
+
"2k": 2048,
|
|
7
|
+
"4k": 4096,
|
|
8
|
+
"8k": 8192,
|
|
9
|
+
"12k": 12288,
|
|
10
|
+
"16k": 16384,
|
|
11
|
+
"32k": 32768,
|
|
12
|
+
"64k": 65536,
|
|
13
|
+
"128k": 131072,
|
|
14
|
+
"256k": 262144,
|
|
15
|
+
};
|
|
16
|
+
const TOKEN_TO_LEVEL = new Map(CTX_LEVELS.map((level) => [CTX_TOKENS[level], level]));
|
|
17
|
+
export function isCtxLevel(value) {
|
|
18
|
+
return CTX_LEVELS.includes(value);
|
|
19
|
+
}
|
|
20
|
+
export function isCtxChoice(value) {
|
|
21
|
+
return value === CTX_AUTO || isCtxLevel(value);
|
|
22
|
+
}
|
|
23
|
+
export function tokensForCtxLevel(level) {
|
|
24
|
+
return CTX_TOKENS[level];
|
|
25
|
+
}
|
|
26
|
+
export function matchCtxLevel(tokens) {
|
|
27
|
+
return TOKEN_TO_LEVEL.get(Math.floor(tokens)) ?? null;
|
|
28
|
+
}
|
|
29
|
+
export function parseCtxChoice(raw) {
|
|
30
|
+
if (raw == null)
|
|
31
|
+
return null;
|
|
32
|
+
const text = raw.trim().toLowerCase();
|
|
33
|
+
if (!text)
|
|
34
|
+
return null;
|
|
35
|
+
if (text === "auto" || text === "default")
|
|
36
|
+
return CTX_AUTO;
|
|
37
|
+
if (isCtxLevel(text))
|
|
38
|
+
return text;
|
|
39
|
+
if (!/^\d+$/.test(text))
|
|
40
|
+
return null;
|
|
41
|
+
return matchCtxLevel(Number(text));
|
|
42
|
+
}
|
|
43
|
+
export function ctxChoiceLabel(choice) {
|
|
44
|
+
return choice === CTX_AUTO ? "自动" : choice;
|
|
45
|
+
}
|
|
46
|
+
export function activeProviderIsOllama() {
|
|
47
|
+
return getActiveProvider(loadConfig())?.type === "ollama";
|
|
48
|
+
}
|
|
49
|
+
export function configuredCtxChoice() {
|
|
50
|
+
const tokens = getActiveProvider(loadConfig())?.contextWindow;
|
|
51
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0)
|
|
52
|
+
return { choice: CTX_AUTO };
|
|
53
|
+
const level = matchCtxLevel(tokens);
|
|
54
|
+
if (level)
|
|
55
|
+
return { choice: level, tokens: tokensForCtxLevel(level) };
|
|
56
|
+
return { choice: CTX_AUTO, tokens: Math.floor(tokens) };
|
|
57
|
+
}
|
|
58
|
+
export function setOllamaContextChoice(choice) {
|
|
59
|
+
const cfg = loadConfig();
|
|
60
|
+
const provider = getActiveProvider(cfg);
|
|
61
|
+
if (!provider)
|
|
62
|
+
return { ok: false, reason: "no-provider" };
|
|
63
|
+
if (provider.type !== "ollama")
|
|
64
|
+
return { ok: false, reason: "not-ollama" };
|
|
65
|
+
if (choice === CTX_AUTO)
|
|
66
|
+
delete provider.contextWindow;
|
|
67
|
+
else
|
|
68
|
+
provider.contextWindow = tokensForCtxLevel(choice);
|
|
69
|
+
saveConfig(cfg);
|
|
70
|
+
return choice === CTX_AUTO ? { ok: true, choice } : { ok: true, choice, tokens: tokensForCtxLevel(choice) };
|
|
71
|
+
}
|
|
72
|
+
export function ctxPayload() {
|
|
73
|
+
const provider = getActiveProvider(loadConfig());
|
|
74
|
+
if (provider?.type !== "ollama")
|
|
75
|
+
return { configurable: false, level: null };
|
|
76
|
+
const tokens = provider.contextWindow;
|
|
77
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0)
|
|
78
|
+
return { configurable: true, level: CTX_AUTO };
|
|
79
|
+
return { configurable: true, level: matchCtxLevel(tokens) };
|
|
80
|
+
}
|