min-agent 0.3.0 → 0.4.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/README.md +111 -28
- package/dist/agent.js +1119 -256
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/config.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { readFileSync,
|
|
1
|
+
import { readFileSync, mkdirSync, existsSync, statSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from "os";
|
|
4
4
|
import readline from "readline";
|
|
5
|
+
import { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
6
|
+
import { getEffectiveSandboxPolicy, mergeSandboxConfig, parseSandboxConfig, sandboxLaunchSource, sandboxStatusLabel, } from "./sandbox.js";
|
|
5
7
|
const DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".min-agent");
|
|
6
8
|
const CONFIG_FILE_NAME = "config.json";
|
|
7
9
|
const RULES_FILE_NAME = "rules.md";
|
|
@@ -47,11 +49,11 @@ export function saveProjectConfig(config) {
|
|
|
47
49
|
mkdirSync(dir, { recursive: true });
|
|
48
50
|
// Keep unknown keys, drop empty skill arrays so the file stays minimal.
|
|
49
51
|
const { disabledSkills, enabledSkills, ...rest } = config;
|
|
50
|
-
|
|
52
|
+
atomicWriteFileSync(getProjectConfigFile(), JSON.stringify({
|
|
51
53
|
...rest,
|
|
52
54
|
...(disabledSkills?.length ? { disabledSkills } : {}),
|
|
53
55
|
...(enabledSkills?.length ? { enabledSkills } : {}),
|
|
54
|
-
}, null, 2)
|
|
56
|
+
}, null, 2));
|
|
55
57
|
projectCache.delete(getProjectConfigFile());
|
|
56
58
|
}
|
|
57
59
|
export function loadConfigFile(configFile) {
|
|
@@ -62,7 +64,10 @@ export function loadConfigFile(configFile) {
|
|
|
62
64
|
catch {
|
|
63
65
|
return {};
|
|
64
66
|
}
|
|
65
|
-
if (cachedConfig &&
|
|
67
|
+
if (cachedConfig &&
|
|
68
|
+
cachedConfig.file === configFile &&
|
|
69
|
+
cachedConfig.mtimeMs === st.mtimeMs &&
|
|
70
|
+
cachedConfig.size === st.size) {
|
|
66
71
|
return cachedConfig.config;
|
|
67
72
|
}
|
|
68
73
|
let config;
|
|
@@ -97,6 +102,110 @@ export function migrateLegacyConfig(config) {
|
|
|
97
102
|
migrated: true,
|
|
98
103
|
};
|
|
99
104
|
}
|
|
105
|
+
export function mergeProjectOverGlobal(global, project) {
|
|
106
|
+
const activeProvider = project.activeProvider ?? global.activeProvider;
|
|
107
|
+
const sampling = project.sampling ? { ...global.sampling, ...project.sampling } : global.sampling;
|
|
108
|
+
const budget = project.budget ? { ...global.budget, ...project.budget } : global.budget;
|
|
109
|
+
const permission = parsePermissionMode(project.permission) ?? parsePermissionMode(global.permission);
|
|
110
|
+
const sandbox = mergeSandboxConfig(parseSandboxConfig(global.sandbox), parseSandboxConfig(project.sandbox));
|
|
111
|
+
const compaction = project.compaction ? { ...global.compaction, ...project.compaction } : global.compaction;
|
|
112
|
+
const agent = project.agent ? { ...global.agent, ...project.agent } : global.agent;
|
|
113
|
+
let providers = global.providers;
|
|
114
|
+
if (project.defaultModel && providers && providers.length > 0) {
|
|
115
|
+
const idx = activeProvider ? providers.findIndex((p) => p.name === activeProvider) : 0;
|
|
116
|
+
const target = idx >= 0 ? idx : 0;
|
|
117
|
+
providers = providers.map((p, i) => (i === target ? { ...p, defaultModel: project.defaultModel } : p));
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...global,
|
|
121
|
+
activeProvider,
|
|
122
|
+
sampling,
|
|
123
|
+
budget,
|
|
124
|
+
permission,
|
|
125
|
+
...(sandbox ? { sandbox } : {}),
|
|
126
|
+
compaction,
|
|
127
|
+
agent,
|
|
128
|
+
...(providers ? { providers } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function getEffectiveConfig() {
|
|
132
|
+
return mergeProjectOverGlobal(loadConfig(), loadProjectConfig());
|
|
133
|
+
}
|
|
134
|
+
export function getBudgetSnapshot() {
|
|
135
|
+
const global = loadConfig();
|
|
136
|
+
const project = loadProjectConfig();
|
|
137
|
+
const maxCostUSD = mergeProjectOverGlobal(global, project).budget?.maxCostUSD;
|
|
138
|
+
const source = project.budget?.maxCostUSD != null ? "project" : global.budget?.maxCostUSD != null ? "global" : null;
|
|
139
|
+
return { maxCostUSD, source };
|
|
140
|
+
}
|
|
141
|
+
export function parsePermissionMode(value) {
|
|
142
|
+
if (value === "ask" || value === "accept-edits" || value === "allow-all")
|
|
143
|
+
return value;
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
export function permissionModeLabel(mode) {
|
|
147
|
+
if (mode === "allow-all")
|
|
148
|
+
return "allow all";
|
|
149
|
+
if (mode === "accept-edits")
|
|
150
|
+
return "accept edits";
|
|
151
|
+
return "ask";
|
|
152
|
+
}
|
|
153
|
+
export function getPermissionSnapshot() {
|
|
154
|
+
const global = loadConfig();
|
|
155
|
+
const project = loadProjectConfig();
|
|
156
|
+
const permission = parsePermissionMode(project.permission) ?? parsePermissionMode(global.permission) ?? "ask";
|
|
157
|
+
const source = parsePermissionMode(project.permission) != null
|
|
158
|
+
? "project"
|
|
159
|
+
: parsePermissionMode(global.permission) != null
|
|
160
|
+
? "global"
|
|
161
|
+
: null;
|
|
162
|
+
return { permission, source };
|
|
163
|
+
}
|
|
164
|
+
export function setPermissionMode(permission, scope) {
|
|
165
|
+
if (scope === "project") {
|
|
166
|
+
const project = loadProjectConfig();
|
|
167
|
+
saveProjectConfig({ ...project, permission });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
saveConfig({ ...loadConfig(), permission });
|
|
171
|
+
}
|
|
172
|
+
export function setSandboxConfig(partial, scope) {
|
|
173
|
+
if (scope === "project") {
|
|
174
|
+
const project = loadProjectConfig();
|
|
175
|
+
const current = parseSandboxConfig(project.sandbox) ?? {};
|
|
176
|
+
saveProjectConfig({ ...project, sandbox: { ...current, ...partial } });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const cfg = loadConfig();
|
|
180
|
+
const current = parseSandboxConfig(cfg.sandbox) ?? {};
|
|
181
|
+
saveConfig({ ...cfg, sandbox: { ...current, ...partial } });
|
|
182
|
+
}
|
|
183
|
+
export function getSandboxSnapshot() {
|
|
184
|
+
const policy = getEffectiveSandboxPolicy();
|
|
185
|
+
const launch = sandboxLaunchSource();
|
|
186
|
+
const project = parseSandboxConfig(loadProjectConfig().sandbox);
|
|
187
|
+
const global = parseSandboxConfig(loadConfig().sandbox);
|
|
188
|
+
const projectSet = Boolean(project?.mode || project?.network || project?.extraWriteRoots?.length || project?.extraReadRoots?.length);
|
|
189
|
+
const globalSet = Boolean(global?.mode || global?.network || global?.extraWriteRoots?.length || global?.extraReadRoots?.length);
|
|
190
|
+
const source = launch ?? (projectSet ? "project" : globalSet ? "global" : null);
|
|
191
|
+
return {
|
|
192
|
+
mode: policy.mode,
|
|
193
|
+
network: policy.network,
|
|
194
|
+
extraWriteRoots: policy.extraWriteRoots,
|
|
195
|
+
extraReadRoots: policy.extraReadRoots,
|
|
196
|
+
source,
|
|
197
|
+
label: sandboxStatusLabel(policy),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function setBudgetMaxCost(maxCostUSD, scope) {
|
|
201
|
+
if (scope === "project") {
|
|
202
|
+
const project = loadProjectConfig();
|
|
203
|
+
saveProjectConfig({ ...project, budget: { ...project.budget, maxCostUSD } });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const cfg = loadConfig();
|
|
207
|
+
saveConfig({ ...cfg, budget: { ...cfg.budget, maxCostUSD } });
|
|
208
|
+
}
|
|
100
209
|
export function getActiveProvider(config) {
|
|
101
210
|
if (!config.providers || config.providers.length === 0)
|
|
102
211
|
return undefined;
|
|
@@ -115,7 +224,7 @@ export function normalizeOllamaBaseURL(baseURL) {
|
|
|
115
224
|
export function saveConfig(config) {
|
|
116
225
|
const dir = getConfigDir();
|
|
117
226
|
mkdirSync(dir, { recursive: true });
|
|
118
|
-
|
|
227
|
+
atomicWriteFileSync(path.join(dir, CONFIG_FILE_NAME), JSON.stringify(config, null, 2));
|
|
119
228
|
cachedConfig = null;
|
|
120
229
|
}
|
|
121
230
|
export function isConfigured() {
|
|
@@ -167,7 +276,7 @@ function loadModelsCache() {
|
|
|
167
276
|
}
|
|
168
277
|
function saveModelsCache(models) {
|
|
169
278
|
mkdirSync(getConfigDir(), { recursive: true });
|
|
170
|
-
|
|
279
|
+
atomicWriteFileSync(modelsCachePath(), JSON.stringify(models));
|
|
171
280
|
}
|
|
172
281
|
export async function fetchModels(baseURL, apiKey) {
|
|
173
282
|
try {
|
|
@@ -217,7 +326,9 @@ export async function runSetup() {
|
|
|
217
326
|
const choice = await ask(rl, "选择 (1/2/3/4)", "4");
|
|
218
327
|
if (choice === "1") {
|
|
219
328
|
const provider = await collectProvider(rl, undefined);
|
|
220
|
-
provider.name =
|
|
329
|
+
provider.name =
|
|
330
|
+
(await ask(rl, "Provider 名称", `provider-${providers.length + 1}`)).trim() ||
|
|
331
|
+
`provider-${providers.length + 1}`;
|
|
221
332
|
providers.push(provider);
|
|
222
333
|
config.activeProvider = provider.name;
|
|
223
334
|
config.providers = providers;
|
|
@@ -272,9 +383,7 @@ async function collectProvider(rl, existing) {
|
|
|
272
383
|
console.log(" 3. ollama (本地 Ollama)");
|
|
273
384
|
console.log();
|
|
274
385
|
const typeChoice = await ask(rl, "选择 Provider (1/2/3)", existing?.type === "ollama" ? "3" : existing?.type === "openai" ? "2" : "1");
|
|
275
|
-
const providerType = typeChoice === "3" ? "ollama"
|
|
276
|
-
: typeChoice === "2" ? "openai"
|
|
277
|
-
: "openai-compatible";
|
|
386
|
+
const providerType = typeChoice === "3" ? "ollama" : typeChoice === "2" ? "openai" : "openai-compatible";
|
|
278
387
|
let baseURL;
|
|
279
388
|
let apiKey;
|
|
280
389
|
if (providerType === "ollama") {
|
package/dist/confirm.js
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
+
import { getEffectiveConfig } from "./config.js";
|
|
3
|
+
import { normalizeQuestionOptions, normalizeQuestionPrompt } from "./question-format.js";
|
|
2
4
|
import readline from "readline";
|
|
3
5
|
let autoApprove = false;
|
|
4
6
|
export function setAutoApprove(value) {
|
|
5
7
|
autoApprove = value;
|
|
6
8
|
}
|
|
7
9
|
export function isAutoApprove() {
|
|
10
|
+
const ctx = confirmAls.getStore();
|
|
11
|
+
if (ctx)
|
|
12
|
+
return ctx.autoApprove;
|
|
8
13
|
if (autoApprove)
|
|
9
14
|
return true;
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
return getPermissionMode() === "allow-all";
|
|
16
|
+
}
|
|
17
|
+
let permissionOverride;
|
|
18
|
+
export function setPermissionOverride(mode) {
|
|
19
|
+
permissionOverride = mode;
|
|
20
|
+
}
|
|
21
|
+
export function getPermissionOverride() {
|
|
22
|
+
return permissionOverride;
|
|
23
|
+
}
|
|
24
|
+
export function getPermissionMode() {
|
|
25
|
+
if (permissionOverride)
|
|
26
|
+
return permissionOverride;
|
|
27
|
+
const config = getEffectiveConfig();
|
|
28
|
+
return config.permission === "allow-all" || config.permission === "accept-edits" || config.permission === "ask"
|
|
29
|
+
? config.permission
|
|
30
|
+
: "ask";
|
|
31
|
+
}
|
|
32
|
+
export function isEditAutoApprove() {
|
|
33
|
+
if (isAutoApprove())
|
|
34
|
+
return true;
|
|
35
|
+
return getPermissionMode() === "accept-edits";
|
|
36
|
+
}
|
|
37
|
+
const confirmAls = new AsyncLocalStorage();
|
|
38
|
+
export function runWithConfirmContext(ctx, fn) {
|
|
39
|
+
return confirmAls.run(ctx, fn);
|
|
12
40
|
}
|
|
13
41
|
/** Optional readline interface to pause/resume during confirmation prompts. */
|
|
14
42
|
let _rl = null;
|
|
@@ -27,17 +55,28 @@ export function setTuiQuestion(handler) {
|
|
|
27
55
|
}
|
|
28
56
|
/** Ask the user a free-form question. Returns the answer, or null if cancelled. */
|
|
29
57
|
export async function askQuestion(prompt, options) {
|
|
58
|
+
const safePrompt = normalizeQuestionPrompt(prompt);
|
|
59
|
+
const choices = normalizeQuestionOptions(options);
|
|
60
|
+
const safeOptions = choices.length > 0 ? choices : undefined;
|
|
61
|
+
const ctx = confirmAls.getStore();
|
|
62
|
+
if (ctx) {
|
|
63
|
+
if (ctx.autoApprove)
|
|
64
|
+
return null;
|
|
65
|
+
if (ctx.onQuestion)
|
|
66
|
+
return ctx.onQuestion(safePrompt, safeOptions);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
30
69
|
if (_tuiQuestion)
|
|
31
|
-
return _tuiQuestion(
|
|
70
|
+
return _tuiQuestion(safePrompt, safeOptions);
|
|
32
71
|
if (!process.stdin.isTTY) {
|
|
33
|
-
console.log(`\n\x1b[33m❓ ${
|
|
72
|
+
console.log(`\n\x1b[33m❓ ${safePrompt} (no interactive terminal — skipped)\x1b[0m`);
|
|
34
73
|
return null;
|
|
35
74
|
}
|
|
36
75
|
console.log();
|
|
37
|
-
console.log(`\x1b[33m❓ ${
|
|
38
|
-
if (
|
|
39
|
-
for (let i = 0; i <
|
|
40
|
-
console.log(`\x1b[90m ${i + 1}. ${
|
|
76
|
+
console.log(`\x1b[33m❓ ${safePrompt}\x1b[0m`);
|
|
77
|
+
if (safeOptions) {
|
|
78
|
+
for (let i = 0; i < safeOptions.length; i++) {
|
|
79
|
+
console.log(`\x1b[90m ${i + 1}. ${safeOptions[i]}\x1b[0m`);
|
|
41
80
|
}
|
|
42
81
|
}
|
|
43
82
|
return new Promise((resolve) => {
|
|
@@ -56,6 +95,14 @@ export async function askQuestion(prompt, options) {
|
|
|
56
95
|
* Display order: detail first, then [y/N] prompt at the bottom.
|
|
57
96
|
*/
|
|
58
97
|
export async function confirm(message) {
|
|
98
|
+
const ctx = confirmAls.getStore();
|
|
99
|
+
if (ctx) {
|
|
100
|
+
if (ctx.autoApprove)
|
|
101
|
+
return true;
|
|
102
|
+
if (ctx.onConfirm)
|
|
103
|
+
return ctx.onConfirm(message);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
59
106
|
if (isAutoApprove())
|
|
60
107
|
return true;
|
|
61
108
|
// If TUI mode is active, delegate to the TUI overlay
|
package/dist/context-window.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { readFileSync,
|
|
1
|
+
import { readFileSync, mkdirSync, existsSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { atomicWriteFileSync } from "./tools/atomic-file.js";
|
|
4
|
+
import { getConfigDir, getEffectiveConfig, getActiveProvider } from "./config.js";
|
|
4
5
|
/**
|
|
5
6
|
* Auto-detect context window size for the current model.
|
|
6
7
|
*
|
|
7
8
|
* Resolution order:
|
|
8
9
|
* 1. User config: provider.contextWindow (explicit override)
|
|
9
|
-
* 2. Provider-specific API (OpenRouter, vLLM, Ollama)
|
|
10
|
+
* 2. Provider-specific API (OpenRouter, vLLM, Ollama, OpenAI-compatible /models)
|
|
10
11
|
* 3. models.dev API lookup
|
|
11
|
-
* 4. Fallback:
|
|
12
|
+
* 4. Fallback: 512000 (memory only — never persisted as a detected value)
|
|
12
13
|
*/
|
|
13
|
-
const DEFAULT_CONTEXT_WINDOW =
|
|
14
|
-
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
14
|
+
export const DEFAULT_CONTEXT_WINDOW = 512000;
|
|
15
|
+
const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
16
|
+
const FALLBACK_MEMORY_TTL = 10 * 60 * 1000;
|
|
15
17
|
function cacheFile() {
|
|
16
18
|
return path.join(getConfigDir(), "context-window-cache.json");
|
|
17
19
|
}
|
|
@@ -31,26 +33,72 @@ function loadDiskCache() {
|
|
|
31
33
|
function saveCache(cache) {
|
|
32
34
|
const file = cacheFile();
|
|
33
35
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
-
|
|
36
|
+
atomicWriteFileSync(file, JSON.stringify(cache));
|
|
37
|
+
}
|
|
38
|
+
function cacheTtl(source) {
|
|
39
|
+
return source === "fallback" ? FALLBACK_MEMORY_TTL : CACHE_TTL;
|
|
35
40
|
}
|
|
36
41
|
function getCached(modelId) {
|
|
37
42
|
const memory = memoryCache.get(modelId);
|
|
38
|
-
if (memory && Date.now() - memory.timestamp <=
|
|
39
|
-
return memory.contextWindow;
|
|
40
|
-
const disk = loadDiskCache()[modelId];
|
|
41
|
-
if (disk && Date.now() - disk.timestamp <= CACHE_TTL) {
|
|
42
|
-
memoryCache.set(modelId, disk);
|
|
43
|
-
return disk.contextWindow;
|
|
43
|
+
if (memory && Date.now() - memory.timestamp <= cacheTtl(memory.source)) {
|
|
44
|
+
return { tokens: memory.contextWindow, source: memory.source };
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
const disk = loadDiskCache()[modelId];
|
|
47
|
+
if (!disk || disk.source !== "detected")
|
|
48
|
+
return null;
|
|
49
|
+
if (Date.now() - disk.timestamp > CACHE_TTL)
|
|
50
|
+
return null;
|
|
51
|
+
memoryCache.set(modelId, { ...disk, source: "detected" });
|
|
52
|
+
return { tokens: disk.contextWindow, source: "detected" };
|
|
46
53
|
}
|
|
47
|
-
function setCache(modelId, contextWindow) {
|
|
48
|
-
const entry = { contextWindow, timestamp: Date.now() };
|
|
54
|
+
function setCache(modelId, contextWindow, source) {
|
|
55
|
+
const entry = { contextWindow, timestamp: Date.now(), source };
|
|
49
56
|
memoryCache.set(modelId, entry);
|
|
57
|
+
if (source !== "detected")
|
|
58
|
+
return;
|
|
50
59
|
const cache = loadDiskCache();
|
|
51
60
|
cache[modelId] = entry;
|
|
52
61
|
saveCache(cache);
|
|
53
62
|
}
|
|
63
|
+
export function extractModelContextLength(model) {
|
|
64
|
+
if (!model || typeof model !== "object")
|
|
65
|
+
return null;
|
|
66
|
+
const rec = model;
|
|
67
|
+
const nested = [rec, rec.meta, rec.limits, rec.limit, rec.model_info];
|
|
68
|
+
const keys = [
|
|
69
|
+
"context_length",
|
|
70
|
+
"max_model_len",
|
|
71
|
+
"max_model_length",
|
|
72
|
+
"max_context_length",
|
|
73
|
+
"context_window",
|
|
74
|
+
"max_input_tokens",
|
|
75
|
+
"max_total_tokens",
|
|
76
|
+
"context",
|
|
77
|
+
"general.context_length",
|
|
78
|
+
];
|
|
79
|
+
for (const obj of nested) {
|
|
80
|
+
if (!obj || typeof obj !== "object")
|
|
81
|
+
continue;
|
|
82
|
+
const bag = obj;
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
const n = bag[key];
|
|
85
|
+
if (typeof n === "number" && Number.isFinite(n) && n >= 4096)
|
|
86
|
+
return Math.floor(n);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function matchListedModel(models, modelId) {
|
|
92
|
+
const exact = models.find((m) => m && typeof m === "object" && m.id === modelId);
|
|
93
|
+
if (exact)
|
|
94
|
+
return exact;
|
|
95
|
+
return models.find((m) => {
|
|
96
|
+
if (!m || typeof m !== "object")
|
|
97
|
+
return false;
|
|
98
|
+
const id = m.id;
|
|
99
|
+
return typeof id === "string" && (id.endsWith(`/${modelId}`) || modelId.endsWith(`/${id}`));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
54
102
|
/** Try OpenRouter: GET /api/v1/models returns context_length per model */
|
|
55
103
|
async function tryOpenRouter(baseURL, apiKey, modelId) {
|
|
56
104
|
if (!baseURL.includes("openrouter"))
|
|
@@ -63,8 +111,7 @@ async function tryOpenRouter(baseURL, apiKey, modelId) {
|
|
|
63
111
|
if (!response.ok)
|
|
64
112
|
return null;
|
|
65
113
|
const data = (await response.json());
|
|
66
|
-
|
|
67
|
-
return model?.context_length ?? null;
|
|
114
|
+
return extractModelContextLength(matchListedModel(data.data ?? [], modelId));
|
|
68
115
|
}
|
|
69
116
|
catch {
|
|
70
117
|
return null;
|
|
@@ -83,9 +130,27 @@ async function tryVllm(baseURL, apiKey, modelId) {
|
|
|
83
130
|
if (!response.ok)
|
|
84
131
|
return null;
|
|
85
132
|
const data = (await response.json());
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
133
|
+
return extractModelContextLength(matchListedModel(data.data ?? [], modelId));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Any OpenAI-compatible /models that advertises context length (1M custom endpoints included). */
|
|
140
|
+
async function tryProviderModels(baseURL, apiKey, modelId) {
|
|
141
|
+
if (!baseURL)
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
const url = `${baseURL.replace(/\/$/, "")}/models`;
|
|
145
|
+
const response = await fetch(url, {
|
|
146
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
147
|
+
signal: AbortSignal.timeout(8000),
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok)
|
|
150
|
+
return null;
|
|
151
|
+
const data = (await response.json());
|
|
152
|
+
const models = Array.isArray(data) ? data : (data.data ?? []);
|
|
153
|
+
return extractModelContextLength(matchListedModel(models, modelId));
|
|
89
154
|
}
|
|
90
155
|
catch {
|
|
91
156
|
return null;
|
|
@@ -108,9 +173,7 @@ async function tryOllama(baseURL, modelId) {
|
|
|
108
173
|
return null;
|
|
109
174
|
const data = (await response.json());
|
|
110
175
|
// Ollama returns model_info with context length
|
|
111
|
-
const ctxLength = data.model_info?.["general.context_length"] ??
|
|
112
|
-
data.model_info?.context_length ??
|
|
113
|
-
data.parameters?.num_ctx;
|
|
176
|
+
const ctxLength = data.model_info?.["general.context_length"] ?? data.model_info?.context_length ?? data.parameters?.num_ctx;
|
|
114
177
|
return typeof ctxLength === "number" ? ctxLength : null;
|
|
115
178
|
}
|
|
116
179
|
catch {
|
|
@@ -155,35 +218,32 @@ async function detectContextWindow(config, id) {
|
|
|
155
218
|
tryOpenRouter(baseURL, apiKey, id),
|
|
156
219
|
tryOllama(baseURL, id),
|
|
157
220
|
tryVllm(baseURL, apiKey, id),
|
|
221
|
+
tryProviderModels(baseURL, apiKey, id),
|
|
158
222
|
tryModelsDev(id),
|
|
159
223
|
]);
|
|
160
224
|
const found = results.find((v) => v !== null);
|
|
161
225
|
if (found) {
|
|
162
|
-
setCache(id, found);
|
|
163
|
-
return found;
|
|
226
|
+
setCache(id, found, "detected");
|
|
227
|
+
return { tokens: found, source: "detected" };
|
|
164
228
|
}
|
|
165
|
-
setCache(id, DEFAULT_CONTEXT_WINDOW);
|
|
166
|
-
return DEFAULT_CONTEXT_WINDOW;
|
|
229
|
+
setCache(id, DEFAULT_CONTEXT_WINDOW, "fallback");
|
|
230
|
+
return { tokens: DEFAULT_CONTEXT_WINDOW, source: "fallback" };
|
|
167
231
|
}
|
|
168
232
|
/**
|
|
169
|
-
* Get context window size
|
|
170
|
-
*
|
|
171
|
-
* Concurrent calls for the same model share a single probe.
|
|
233
|
+
* Get context window size and whether it was configured, detected, or guessed.
|
|
234
|
+
* Fallback 512k is never written to disk as a detected value.
|
|
172
235
|
*/
|
|
173
|
-
export async function
|
|
174
|
-
const config =
|
|
236
|
+
export async function getContextWindowInfo(modelId) {
|
|
237
|
+
const config = getEffectiveConfig();
|
|
175
238
|
const provider = getActiveProvider(config);
|
|
176
|
-
// 1. Explicit user config
|
|
177
239
|
if (provider?.contextWindow)
|
|
178
|
-
return provider.contextWindow;
|
|
240
|
+
return { tokens: provider.contextWindow, source: "config" };
|
|
179
241
|
const id = modelId ?? provider?.defaultModel;
|
|
180
242
|
if (!id)
|
|
181
|
-
return DEFAULT_CONTEXT_WINDOW;
|
|
182
|
-
// 2. Check cache
|
|
243
|
+
return { tokens: DEFAULT_CONTEXT_WINDOW, source: "fallback" };
|
|
183
244
|
const cached = getCached(id);
|
|
184
|
-
if (cached
|
|
245
|
+
if (cached)
|
|
185
246
|
return cached;
|
|
186
|
-
// 3. Reuse an in-flight probe for the same model
|
|
187
247
|
const pending = inFlight.get(id);
|
|
188
248
|
if (pending)
|
|
189
249
|
return pending;
|
|
@@ -196,3 +256,11 @@ export async function getContextWindow(modelId) {
|
|
|
196
256
|
inFlight.delete(id);
|
|
197
257
|
}
|
|
198
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Get context window size for the current model.
|
|
261
|
+
* Tries multiple sources, caches detected results in memory and on disk.
|
|
262
|
+
* Concurrent calls for the same model share a single probe.
|
|
263
|
+
*/
|
|
264
|
+
export async function getContextWindow(modelId) {
|
|
265
|
+
return (await getContextWindowInfo(modelId)).tokens;
|
|
266
|
+
}
|