thincoder 0.8.2 → 0.8.4
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 +2 -1
- package/bin/thincoder.mjs +25 -25
- package/package.json +1 -1
- package/src/agent/dispatch.mjs +17 -12
- package/src/agent/helpers.mjs +22 -11
- package/src/agent/setup.mjs +32 -10
- package/src/agent-tools/goal.mjs +12 -11
- package/src/agent-tools/plan.mjs +5 -6
- package/src/agent-tools/recent-changes.mjs +3 -3
- package/src/agent-tools/skill.mjs +6 -6
- package/src/agent-tools/subagent.mjs +19 -18
- package/src/agent-tools/task.mjs +7 -26
- package/src/agent-tools/verify.mjs +21 -19
- package/src/agent-tools.mjs +3 -3
- package/src/agent.mjs +82 -92
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +9 -9
- package/src/cli/memory-command.mjs +1 -1
- package/src/cli/permission.mjs +3 -3
- package/src/cli/setup-wizard.mjs +15 -15
- package/src/config.mjs +56 -53
- package/src/context.mjs +59 -61
- package/src/distill.mjs +35 -35
- package/src/embedding.mjs +17 -17
- package/src/git/checkpoint.mjs +211 -38
- package/src/git/gitmem.mjs +21 -20
- package/src/markdown.mjs +13 -13
- package/src/mcp/helpers.mjs +6 -1
- package/src/mcp/transport-http.mjs +2 -1
- package/src/mcp/transport-stdio.mjs +3 -2
- package/src/mcp/transport-ws.mjs +3 -2
- package/src/mcp.mjs +5 -2
- package/src/memory/code-index.mjs +16 -14
- package/src/memory/code-sync.mjs +36 -26
- package/src/memory/core.mjs +42 -35
- package/src/memory/docs.mjs +24 -15
- package/src/memory/schema.mjs +28 -27
- package/src/memory.mjs +2 -2
- package/src/prompts/coder.md +0 -6
- package/src/prompts/discipline.md +9 -0
- package/src/prompts/main.md +24 -24
- package/src/prompts/system.md +21 -16
- package/src/provider/core.mjs +11 -6
- package/src/provider/index.mjs +2 -2
- package/src/provider/rate.mjs +11 -11
- package/src/session.mjs +73 -42
- package/src/skills.mjs +17 -17
- package/src/tools/checkpoint.md +6 -2
- package/src/tools/file.mjs +20 -14
- package/src/tools/git.mjs +83 -12
- package/src/tools/index.mjs +1 -1
- package/src/tools/ls.md +1 -1
- package/src/tools/patch.mjs +20 -18
- package/src/tools/repomap-parse.mjs +17 -17
- package/src/tools/repomap.mjs +29 -29
- package/src/tools/shared.mjs +55 -28
- package/src/tools/system.mjs +163 -118
- package/src/tools/web.mjs +6 -6
- package/src/tui/agent-turn.mjs +82 -26
- package/src/tui/ansi.mjs +5 -3
- package/src/tui/clipboard.mjs +2 -2
- package/src/tui/cmd-auto.mjs +3 -12
- package/src/tui/cmd-clear.mjs +1 -1
- package/src/tui/cmd-config.mjs +108 -23
- package/src/tui/cmd-exit.mjs +2 -2
- package/src/tui/cmd-extract.mjs +11 -3
- package/src/tui/cmd-goal.mjs +3 -12
- package/src/tui/cmd-help.mjs +2 -2
- package/src/tui/cmd-init.mjs +3 -3
- package/src/tui/cmd-mcp.mjs +38 -25
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +33 -13
- package/src/tui/cmd-plan.mjs +3 -12
- package/src/tui/cmd-reindex.mjs +2 -2
- package/src/tui/cmd-restore.mjs +1 -1
- package/src/tui/cmd-session.mjs +1 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +4 -11
- package/src/tui/config-helpers.mjs +6 -6
- package/src/tui/distill-cmd.mjs +4 -3
- package/src/tui/index.mjs +131 -72
- package/src/tui/interaction.mjs +13 -12
- package/src/tui/key-handler.mjs +34 -26
- package/src/tui/layout.mjs +22 -17
- package/src/tui/pickers.mjs +19 -19
- package/src/tui/render-frame.mjs +37 -11
- package/src/tui/render.mjs +25 -24
- package/src/tui/slash-commands.mjs +22 -22
- package/src/tui/startup.mjs +15 -14
- package/src/tui/wizard.mjs +11 -11
- package/src/tui.mjs +2 -2
- package/bin/thincoder.js +0 -4
- package/src/tools/bash.mjs +0 -144
- package/src/tools/glob.mjs +0 -51
- package/src/tools/grep.mjs +0 -100
- package/src/tools/ls.mjs +0 -36
- package/src/tools.mjs +0 -2
- package/src/tui-render.mjs +0 -4
package/src/cli/make-agent.mjs
CHANGED
|
@@ -6,26 +6,26 @@ import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } fro
|
|
|
6
6
|
import { repoOutlineTool } from "../tools/repomap.mjs"
|
|
7
7
|
import { builtinTools } from "../tools/index.mjs"
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
export async function
|
|
9
|
+
/** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
|
|
10
|
+
export async function assembleAgent() {
|
|
11
11
|
const config = loadConfig()
|
|
12
12
|
const provider = config.provider
|
|
13
13
|
const providers = config.providersList
|
|
14
14
|
const memory = createMemory({ dbPath: config.memory.dbPath })
|
|
15
|
-
//
|
|
15
|
+
// Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
|
|
16
16
|
if (config.embedding?.apiKey) {
|
|
17
17
|
const { createEmbedder } = await import("../embedding.mjs")
|
|
18
18
|
memory.embedder = createEmbedder(config.embedding)
|
|
19
19
|
}
|
|
20
20
|
const cwd = process.cwd()
|
|
21
|
-
// code/doc
|
|
21
|
+
// code/doc indices isolated by origin (project root dir): search only scoped to this project
|
|
22
22
|
memory.codeOrigin = cwd
|
|
23
|
-
// Project
|
|
23
|
+
// Project layer: sync .thincoder/memory/ dir to index on startup (sync if present, skip otherwise)
|
|
24
24
|
if (config.memory.projectDir) {
|
|
25
25
|
memory.projectOrigin = join(cwd, config.memory.projectDir)
|
|
26
26
|
await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
|
|
27
27
|
}
|
|
28
|
-
// Team
|
|
28
|
+
// Team layer (optional): auto-clone on first use; startup only indexes local dir, remote pull via explicit thincoder sync
|
|
29
29
|
const team = teamConfig(config)
|
|
30
30
|
if (team) {
|
|
31
31
|
const { ensureClone } = await import("../git/gitmem.mjs")
|
|
@@ -34,7 +34,7 @@ export async function makeAgent() {
|
|
|
34
34
|
}
|
|
35
35
|
const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd)]
|
|
36
36
|
|
|
37
|
-
// MCP servers
|
|
37
|
+
// MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
|
|
38
38
|
const mcpServers = config.mcp?.servers ?? []
|
|
39
39
|
let mcpTools = []
|
|
40
40
|
const mcpWarnings = []
|
|
@@ -67,7 +67,7 @@ export async function makeAgent() {
|
|
|
67
67
|
return agent
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
70
|
+
/** Read team config and fill in default dir; return null if not configured */
|
|
71
71
|
export function teamConfig(config) {
|
|
72
72
|
const team = config.memory?.team
|
|
73
73
|
if (!team?.repo) return null
|
|
@@ -75,7 +75,7 @@ export function teamConfig(config) {
|
|
|
75
75
|
return { name, repo: team.repo, dir: team.dir ?? join(configDir, "teams", name) }
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/** Entry author: git config user.name, fallback "unknown" */
|
|
79
79
|
export function gitAuthor() {
|
|
80
80
|
try {
|
|
81
81
|
return execSync("git config user.name", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "unknown"
|
package/src/cli/permission.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createInterface } from "node:readline"
|
|
2
2
|
|
|
3
|
-
/** CLI
|
|
3
|
+
/** CLI tool arg summary (truncated long JSON) */
|
|
4
4
|
export function summarize(toolArgs) {
|
|
5
5
|
const s = JSON.stringify(toolArgs)
|
|
6
6
|
return s.length > 120 ? s.slice(0, 120) + "..." : s
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/** Permission request key info (per-tool customized), aligned with TUI formatPermission. name may include sub-agent prefix ("coder/bash") — extract basename for matching */
|
|
10
10
|
export function formatPermission(name, args) {
|
|
11
11
|
const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(共 ${s.length} 字符)` : s)
|
|
12
12
|
const base = name.includes("/") ? name.split("/").pop() : name
|
|
@@ -23,7 +23,7 @@ export function formatPermission(name, args) {
|
|
|
23
23
|
return cap(summarize(args), 300)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/** Permission confirmation: interactive y/n on TTY; non-interactive defaults to deny (safety-first) */
|
|
27
27
|
export async function askPermission(name, toolArgs) {
|
|
28
28
|
if (!process.stdin.isTTY) {
|
|
29
29
|
console.error(`\n[deny] ${name} (non-interactive, side-effect tools require a TTY)`)
|
package/src/cli/setup-wizard.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { existsSync, readFileSync } from "node:fs"
|
|
|
2
2
|
import { createInterface } from "node:readline"
|
|
3
3
|
import { configPath, saveConfig, PROVIDER_PRESETS } from "../config.mjs"
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/** First-time setup (TTY chat / distill): ask a few questions to configure a provider, save to disk, return runtime provider. Cancel returns null. */
|
|
6
6
|
export async function setupWizard() {
|
|
7
|
-
//
|
|
7
|
+
// Buffered asker: rl.question loses lines when input is piped/fast-pasted (line arrives before question is registered)
|
|
8
8
|
const rl = createInterface({ input: process.stdin, terminal: false })
|
|
9
9
|
const buffered = []
|
|
10
10
|
let waiter = null
|
|
@@ -25,17 +25,17 @@ export async function setupWizard() {
|
|
|
25
25
|
})
|
|
26
26
|
try {
|
|
27
27
|
const presets = Object.entries(PROVIDER_PRESETS)
|
|
28
|
-
console.error("
|
|
28
|
+
console.error("First time using ThinCoder — let's configure a model provider:")
|
|
29
29
|
presets.forEach(([n, p], i) => console.error(` ${i + 1}. ${n.padEnd(10)} ${p.desc}`))
|
|
30
|
-
console.error(` ${presets.length + 1}.
|
|
31
|
-
const choice = Number((await ask(
|
|
30
|
+
console.error(` ${presets.length + 1}. Custom endpoint`)
|
|
31
|
+
const choice = Number((await ask(`Pick [1-${presets.length + 1}]: `)).trim())
|
|
32
32
|
let name, baseURL, model
|
|
33
33
|
if (choice === presets.length + 1) {
|
|
34
|
-
name = (await ask("
|
|
35
|
-
baseURL = (await ask("baseURL
|
|
36
|
-
model = (await ask("
|
|
34
|
+
name = (await ask("Name (e.g. my-openai): ")).trim()
|
|
35
|
+
baseURL = (await ask("baseURL (e.g. https://api.openai.com/v1): ")).trim().replace(/\/+$/, "")
|
|
36
|
+
model = (await ask("Model (e.g. gpt-4o): ")).trim()
|
|
37
37
|
if (!name || !/^https?:\/\//.test(baseURL) || !model) {
|
|
38
|
-
console.error("
|
|
38
|
+
console.error("Incomplete input or invalid baseURL — cancelled")
|
|
39
39
|
return null
|
|
40
40
|
}
|
|
41
41
|
} else if (choice >= 1 && choice <= presets.length) {
|
|
@@ -43,15 +43,15 @@ export async function setupWizard() {
|
|
|
43
43
|
baseURL = presets[choice - 1][1].baseURL
|
|
44
44
|
model = presets[choice - 1][1].model
|
|
45
45
|
} else {
|
|
46
|
-
console.error("
|
|
46
|
+
console.error("Invalid choice — cancelled")
|
|
47
47
|
return null
|
|
48
48
|
}
|
|
49
|
-
const apiKey = (await ask(
|
|
49
|
+
const apiKey = (await ask(`API key for ${name}: `)).trim()
|
|
50
50
|
if (!apiKey) {
|
|
51
|
-
console.error("key
|
|
51
|
+
console.error("API key cannot be empty — cancelled")
|
|
52
52
|
return null
|
|
53
53
|
}
|
|
54
|
-
const embedKey = (await ask("
|
|
54
|
+
const embedKey = (await ask("Optional: embedding API key (SiliconFlow, for vector search; press Enter to skip): ")).trim()
|
|
55
55
|
const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
|
|
56
56
|
const providers = raw.providers?.length ? raw.providers : []
|
|
57
57
|
const existing = providers.find((p) => p.name === name)
|
|
@@ -61,8 +61,8 @@ export async function setupWizard() {
|
|
|
61
61
|
raw.activeProvider = name
|
|
62
62
|
if (embedKey) raw.embedding = { ...(raw.embedding ?? {}), apiKey: embedKey }
|
|
63
63
|
saveConfig(raw)
|
|
64
|
-
console.error(
|
|
65
|
-
console.error(embedKey ? "
|
|
64
|
+
console.error(`Configured: ${name} / ${model} (saved to ${configPath})`)
|
|
65
|
+
console.error(embedKey ? "Vector search enabled\n" : "(No embedding key configured: memory search will use text-only FTS. Add embedding.apiKey to config.json to enable vector search later.)\n")
|
|
66
66
|
return { name, baseURL, model, apiKey }
|
|
67
67
|
} finally {
|
|
68
68
|
rl.close()
|
package/src/config.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* config.mjs —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* API key
|
|
2
|
+
* config.mjs — configuration loading and saving
|
|
3
|
+
* Multi-provider structure: providers[] + activeProvider
|
|
4
|
+
* Config file: ~/.thincoder/config.json
|
|
5
|
+
* API key can fall back to environment variables (when not configured in providers).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
@@ -12,7 +12,7 @@ import { join } from "node:path"
|
|
|
12
12
|
export const configDir = join(homedir(), ".thincoder")
|
|
13
13
|
export const configPath = join(configDir, "config.json")
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** Built-in provider presets: shared by /provider add <preset> and first-run wizard */
|
|
16
16
|
export const PROVIDER_PRESETS = {
|
|
17
17
|
deepseek: { baseURL: "https://api.deepseek.com/v1", model: "deepseek-v4-pro", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 393216, desc: "DeepSeek" },
|
|
18
18
|
kimi: { baseURL: "https://api.moonshot.cn/v1", model: "kimi-k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi / Moonshot" },
|
|
@@ -21,15 +21,17 @@ export const PROVIDER_PRESETS = {
|
|
|
21
21
|
minimax: { baseURL: "https://api.minimax.chat/v1", chatPath: "/text/chatcompletion_v2", model: "MiniMax-M3", maxTokens: 131072, desc: "MiniMax" },
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
const { desc:
|
|
24
|
+
// Default provider matches deepseek preset (strip the desc display field)
|
|
25
|
+
const { desc: _, ...deepseekPreset } = PROVIDER_PRESETS.deepseek
|
|
26
26
|
|
|
27
27
|
const DEFAULTS = {
|
|
28
28
|
providers: [{ name: "deepseek", ...deepseekPreset }],
|
|
29
29
|
activeProvider: "deepseek",
|
|
30
30
|
agent: {
|
|
31
31
|
maxTurns: 100,
|
|
32
|
+
subagentTurns: 100,
|
|
32
33
|
compactThreshold: 100000,
|
|
34
|
+
verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
|
|
33
35
|
},
|
|
34
36
|
memory: {
|
|
35
37
|
dbPath: join(configDir, "memory.db"),
|
|
@@ -46,60 +48,60 @@ const DEFAULTS = {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
+
* Known model capability spec table (prefix match, longer first).
|
|
52
|
+
* Used for compaction threshold derivation, continuation protocol selection, and capability-aware optimization.
|
|
51
53
|
*
|
|
52
|
-
* context:
|
|
53
|
-
* maxOutput:
|
|
54
|
-
* thinking:
|
|
55
|
-
* partialMode: Kimi/Qwen Partial Mode
|
|
56
|
-
* prefixMode: DeepSeek Prefix Completion
|
|
57
|
-
* multimodal:
|
|
58
|
-
* cacheMode:
|
|
59
|
-
* thinkApi:
|
|
60
|
-
* reasoningEcho: reasoning_content
|
|
61
|
-
* reasoningEffortEnum: reasoning_effort
|
|
62
|
-
* tempRange: temperature
|
|
54
|
+
* context: context window (tokens)
|
|
55
|
+
* maxOutput: max output tokens (defaults to context)
|
|
56
|
+
* thinking: whether thinking/reasoning mode is supported
|
|
57
|
+
* partialMode: Kimi/Qwen Partial Mode truncation continuation (assistant message with partial:true)
|
|
58
|
+
* prefixMode: DeepSeek Prefix Completion truncation continuation (uses /beta endpoint, with prefix:true)
|
|
59
|
+
* multimodal: whether multimodal (image/vision input supported)
|
|
60
|
+
* cacheMode: context caching mode: "auto"=automatic / "prompt"=needs explicit / "none"=unsupported
|
|
61
|
+
* thinkApi: thinking API type: "type"=thinking.type field / "effort"=reasoning_effort field
|
|
62
|
+
* reasoningEcho: reasoning_content cross-turn echo strategy: "required"=must echo (error if missing) / "optional"=echo optional (default: don't echo)
|
|
63
|
+
* reasoningEffortEnum: valid reasoning_effort enum values (if undeclared, no validation — passed through as-is)
|
|
64
|
+
* tempRange: valid temperature range [min, max] (if undeclared, no clamping)
|
|
63
65
|
*/
|
|
64
66
|
const MODEL_SPECS = [
|
|
65
|
-
// DeepSeek V4
|
|
67
|
+
// DeepSeek V4 series
|
|
66
68
|
["deepseek-v4-pro", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
|
|
67
69
|
["deepseek-v4-flash", { context: 256_000, maxOutput: 384_000, thinking: false, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
|
|
68
70
|
["deepseek-reasoner", { context: 256_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
|
|
69
71
|
["deepseek-chat", { context: 256_000, maxOutput: 384_000, thinking: false, prefixMode: true, cacheMode: "prompt", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["high", "max"], tempRange: [0, 2] }],
|
|
70
|
-
// Kimi
|
|
72
|
+
// Kimi series
|
|
71
73
|
["kimi-k3", { context: 1_000_000, maxOutput: 128_000, thinking: true, partialMode: true, multimodal: true, cacheMode: "prompt", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
|
|
72
74
|
["kimi-k2", { context: 256_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none" }],
|
|
73
75
|
["moonshot", { context: 128_000, maxOutput: 32_000, thinking: false, cacheMode: "none" }],
|
|
74
|
-
// GLM
|
|
76
|
+
// GLM series
|
|
75
77
|
["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1] }],
|
|
76
78
|
["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1] }],
|
|
77
79
|
["glm-4", { context: 128_000, maxOutput: 32_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1] }],
|
|
78
|
-
// GPT
|
|
80
|
+
// GPT series
|
|
79
81
|
["gpt-4.1", { context: 1_000_000, maxOutput: 128_000, thinking: false, cacheMode: "prompt" }],
|
|
80
82
|
["gpt-4o", { context: 128_000, maxOutput: 16_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
|
|
81
|
-
// Qwen
|
|
83
|
+
// Qwen series
|
|
82
84
|
["qwen3.8-max-preview", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
|
|
83
85
|
["qwen3.7-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
|
|
84
86
|
["qwen3.8-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
|
|
85
87
|
["qwen-max", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
|
|
86
88
|
["qwen-plus", { context: 1_000_000, maxOutput: 32_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
|
|
87
89
|
["qwen", { context: 1_000_000, maxOutput: 128_000, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
|
|
88
|
-
// MiniMax
|
|
90
|
+
// MiniMax series
|
|
89
91
|
["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", tempRange: [0, 2] }],
|
|
90
92
|
["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", tempRange: [0, 2] }],
|
|
91
93
|
["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, cacheMode: "auto" }],
|
|
92
94
|
]
|
|
93
95
|
const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000, cacheMode: "none" }
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
96
|
+
// Window utilization cap: 0.8 (DeepSeek internally uses full window; compaction itself costs an LLM call, premature compaction is pure waste.
|
|
97
|
+
// Reserve 20% headroom for post-compaction tail growth and output tokens)
|
|
98
|
+
// But for 1M-window models, 0.8 = 800K tokens — waiting until history grows that large would blow the TPM budget,
|
|
99
|
+
// and the compaction request itself might 429. Add caps: no more than 8× maxOutput (128K×8≈1M → still large but reasonable),
|
|
100
|
+
// no more than 300K (reasonable working ceiling for large-window models; beyond that cache hit rates drop)
|
|
99
101
|
const COMPACT_RATIO = 0.8
|
|
100
102
|
const COMPACT_CAP_TOKENS = 300_000
|
|
101
103
|
|
|
102
|
-
/**
|
|
104
|
+
/** Look up spec by model name prefix (case-insensitive), conservative default for unknown models */
|
|
103
105
|
export function specForModel(model) {
|
|
104
106
|
const m = (model ?? "").toLowerCase()
|
|
105
107
|
for (const [prefix, spec] of [...MODEL_SPECS].sort((a,b) => b[0].length - a[0].length)) {
|
|
@@ -108,24 +110,25 @@ export function specForModel(model) {
|
|
|
108
110
|
return DEFAULT_SPEC
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
/** Return the context window size for a given model name */
|
|
111
114
|
export function contextWindowForModel(model) {
|
|
112
115
|
return specForModel(model).context
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
/**
|
|
118
|
+
/** Derive compaction threshold; explicit is the value explicitly set in config file (takes priority), otherwise auto-computed from model */
|
|
116
119
|
export function resolveCompactThreshold(explicit, model) {
|
|
117
120
|
if (explicit != null) return { value: explicit, auto: false }
|
|
118
121
|
const spec = specForModel(model)
|
|
119
122
|
const ratioBased = Math.floor(spec.context * COMPACT_RATIO)
|
|
120
|
-
//
|
|
123
|
+
// Large-window models (1M) produce too-large ratio-based values; cap them — better to compact early than let history grow until it blows the TPM budget
|
|
121
124
|
const value = Math.min(ratioBased, COMPACT_CAP_TOKENS)
|
|
122
125
|
return { value, auto: true }
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
/**
|
|
126
|
-
*
|
|
127
|
-
* name
|
|
128
|
-
* name
|
|
129
|
+
* Find provider by name in providers[].
|
|
130
|
+
* Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
|
|
131
|
+
* Returns the first provider when name is empty.
|
|
129
132
|
*/
|
|
130
133
|
export function findProvider(providers, name) {
|
|
131
134
|
if (name) {
|
|
@@ -138,9 +141,9 @@ export function findProvider(providers, name) {
|
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* THINCODER_API_KEY / THINCODER_BASE_URL / THINCODER_MODEL
|
|
144
|
+
* Load configuration.
|
|
145
|
+
* Env var priority: THINCODER_ACTIVE_PROVIDER > config file activeProvider
|
|
146
|
+
* THINCODER_API_KEY / THINCODER_BASE_URL / THINCODER_MODEL override the current active provider's corresponding fields
|
|
144
147
|
*/
|
|
145
148
|
export function loadConfig() {
|
|
146
149
|
let config = {}
|
|
@@ -162,29 +165,29 @@ export function loadConfig() {
|
|
|
162
165
|
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
// baseURL
|
|
168
|
+
// Normalize baseURL trailing slash (prevents //chat/completions)
|
|
166
169
|
for (const p of merged.providers) {
|
|
167
170
|
if (p.baseURL) p.baseURL = p.baseURL.replace(/\/+$/, "")
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
//
|
|
173
|
+
// Env var overrides activeProvider
|
|
171
174
|
if (process.env.THINCODER_ACTIVE_PROVIDER) {
|
|
172
175
|
merged.activeProvider = process.env.THINCODER_ACTIVE_PROVIDER
|
|
173
176
|
}
|
|
174
177
|
|
|
175
|
-
//
|
|
178
|
+
// Get the currently active provider
|
|
176
179
|
const active = findProvider(merged.providers, merged.activeProvider)
|
|
177
180
|
|
|
178
|
-
//
|
|
181
|
+
// Build runtime provider object (for agent.provider usage)
|
|
179
182
|
const runtimeProvider = { ...active }
|
|
180
183
|
|
|
181
|
-
//
|
|
184
|
+
// Env vars override current active provider's fields
|
|
182
185
|
if (process.env.THINCODER_API_KEY) runtimeProvider.apiKey = process.env.THINCODER_API_KEY
|
|
183
186
|
if (process.env.THINCODER_BASE_URL) runtimeProvider.baseURL = process.env.THINCODER_BASE_URL
|
|
184
187
|
if (process.env.THINCODER_MODEL) runtimeProvider.model = process.env.THINCODER_MODEL
|
|
185
188
|
|
|
186
|
-
// apiKey
|
|
187
|
-
//
|
|
189
|
+
// apiKey also falls back to env vars (when providers doesn't include a key)
|
|
190
|
+
// Provider-specific env vars only apply to the matching provider name, preventing keys from leaking to wrong endpoints
|
|
188
191
|
if (!runtimeProvider.apiKey?.trim()) {
|
|
189
192
|
const envMap = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }
|
|
190
193
|
const keyVar = envMap[merged.activeProvider]
|
|
@@ -196,13 +199,13 @@ export function loadConfig() {
|
|
|
196
199
|
merged.embedding.apiKey = process.env.SILICONFLOW_API_KEY || process.env.THINCODER_EMBEDDING_API_KEY
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
//
|
|
202
|
+
// Compaction threshold follows the model
|
|
200
203
|
const explicitThreshold = config.agent?.compactThreshold
|
|
201
204
|
const { value, auto } = resolveCompactThreshold(explicitThreshold, runtimeProvider.model)
|
|
202
205
|
merged.agent.compactThreshold = value
|
|
203
206
|
merged.agent.compactThresholdAuto = auto
|
|
204
207
|
|
|
205
|
-
//
|
|
208
|
+
// Write back to merged for convenient access by upper layers
|
|
206
209
|
merged.provider = runtimeProvider
|
|
207
210
|
merged.providersList = merged.providers
|
|
208
211
|
|
|
@@ -210,12 +213,12 @@ export function loadConfig() {
|
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
/**
|
|
213
|
-
*
|
|
214
|
-
* providers[i].apiKey
|
|
216
|
+
* Save configuration. Preserves providers list structure and activeProvider pointer.
|
|
217
|
+
* providers[i].apiKey is only written when explicitly passed in (does not overwrite env-var-fallback keys).
|
|
215
218
|
*/
|
|
216
219
|
export function saveConfig(config) {
|
|
217
220
|
mkdirSync(configDir, { recursive: true })
|
|
218
|
-
// 0600
|
|
221
|
+
// 0600: config.json contains API keys, must not be world-readable (POSIX; chmod is best-effort on Windows)
|
|
219
222
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
|
|
220
|
-
try { chmodSync(configPath, 0o600) } catch { /* Windows
|
|
223
|
+
try { chmodSync(configPath, 0o600) } catch { /* may fail on Windows, ignore */ }
|
|
221
224
|
}
|