thincoder 0.12.32 → 0.12.33
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 -2
- package/bin/thincoder.mjs +0 -1
- package/package.json +1 -1
- package/src/agent-tools/consult.mjs +68 -16
- package/src/agent-tools/escalate.mjs +10 -7
- package/src/agent-tools/subagent.mjs +27 -10
- package/src/config.mjs +16 -42
- package/src/embedding.mjs +1 -1
- package/src/provider/core.mjs +1 -1
- package/src/tui/cmd-advisor.mjs +2 -9
- package/src/tui/pickers.mjs +2 -11
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ Running from source: replace `thincoder` above with `node bin/thincoder.mjs`.
|
|
|
96
96
|
|
|
97
97
|
Slash commands in the TUI: `/help`, `/model` (two-level picker: first select provider, then model; `/model <provider>:<name>` switches directly), `/submodel` (subagent models per type — picker over global + explore/plan/coder/eng-coder slots, or `/submodel <type> <provider:model>` directly), `/shell` (platform-aware picker of available shells — e.g. `/shell` → pick Git Bash/pwsh, or `/shell "C:\Program Files\Git\bin\bash.exe"`, `/shell reset`; fixes win11 cmd encoding/command issues), `/provider` (add/remove providers, set keys, custom endpoints), `/think` (thinking mode toggle and reasoning effort), `/config` (view config, `/config embedkey` for the embedding key, `/config set` for parameters), `/session` (list/switch archived sessions), `/reindex` (rebuild the index), `/extract` (extract knowledge from the current session), `/restore` (restore checkpoint), `/clear`, `/exit`. High-frequency commands support abbreviations: `/h` `/x` `/m` `/p` `/t` `/c` `/n`. Typing `/` shows live matching hints in the status bar. Model picker supports search/filter — type to narrow down results.
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
Configuration comes exclusively from `~/.thincoder/config.json` — no environment-variable configuration is supported.
|
|
100
100
|
|
|
101
101
|
> **Kimi note**: Kimi has **two separate platforms with non-interchangeable API keys** — Moonshot (`https://api.moonshot.cn/v1`, keys `sk-...`, platform.moonshot.cn) and **Kimi For Coding** (`https://api.kimi.com/coding/v1`, keys `sk-kimi-...`, platform.kimi.com, model ID `k3`). Use the `kimi` preset for Moonshot and `kimi-code` for Kimi For Coding — putting one platform's key on the other's endpoint fails with 401 (a hint is appended when the key/baseURL look mismatched).
|
|
102
102
|
|
|
@@ -125,7 +125,7 @@ Environment variables: `THINCODER_API_KEY` (or `DEEPSEEK_API_KEY` / `OPENAI_API_
|
|
|
125
125
|
"embedding": {
|
|
126
126
|
// optional: without it, retrieval is pure FTS
|
|
127
127
|
"baseURL": "https://api.siliconflow.cn/v1",
|
|
128
|
-
"apiKey": "sk-...",
|
|
128
|
+
"apiKey": "sk-...",
|
|
129
129
|
"model": "BAAI/bge-m3",
|
|
130
130
|
},
|
|
131
131
|
"agent": {
|
package/bin/thincoder.mjs
CHANGED
|
@@ -56,7 +56,6 @@ Usage:
|
|
|
56
56
|
thincoder -v, --version Print version
|
|
57
57
|
|
|
58
58
|
Config: ~/.thincoder/config.json (providers[] + activeProvider; manage via /provider, /model in TUI)
|
|
59
|
-
Env: THINCODER_API_KEY, THINCODER_BASE_URL, THINCODER_MODEL, THINCODER_ACTIVE_PROVIDER
|
|
60
59
|
`
|
|
61
60
|
|
|
62
61
|
/** Unified message when no API key is configured */
|
package/package.json
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* CONSULT_BASE overlay }); activity streams to the parent TUI via the relay
|
|
12
12
|
* prefix `consult#<id>/` (same channel subagent uses), not onSubagent/onToolPanel.
|
|
13
13
|
*/
|
|
14
|
-
import { createAgent, runAgent, readonlyToolNames } from "../agent.mjs"
|
|
14
|
+
import { createAgent, runAgent, readonlyToolNames, ContinueError } from "../agent.mjs"
|
|
15
15
|
import { resolveChildProvider } from "./subagent.mjs"
|
|
16
|
+
import { specForModel } from "../config.mjs"
|
|
16
17
|
|
|
17
18
|
function consultLabel(m) {
|
|
18
19
|
return `${m.provider}:${m.model}`
|
|
@@ -90,21 +91,38 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
90
91
|
const agent = ctx.agent
|
|
91
92
|
const timeoutMs = agent?.config?.agent?.consultTimeoutMs ?? 600_000
|
|
92
93
|
let timedOut = false
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const armWatchdog = () => {
|
|
95
|
+
const t = setTimeout(() => {
|
|
96
|
+
timedOut = true
|
|
97
|
+
try { ctrl.abort() } catch { /* already settled */ }
|
|
98
|
+
}, timeoutMs)
|
|
99
|
+
t.unref?.()
|
|
100
|
+
return t
|
|
101
|
+
}
|
|
102
|
+
let watchdog = armWatchdog()
|
|
97
103
|
const label = consultLabel(m)
|
|
98
104
|
try {
|
|
99
105
|
// Provider resolution: consultModels entries are { provider, model, effort? } — resolve
|
|
100
106
|
// via the subagent's provider resolver ("provider:model" handles cross-provider picks).
|
|
101
107
|
const provider = resolveChildProvider(agent, `${m.provider}:${m.model}`)
|
|
102
|
-
if (!provider?.apiKey?.trim()
|
|
108
|
+
if (!provider?.apiKey?.trim()) {
|
|
103
109
|
// resolveChildProvider may still lack a key; fail loudly like the plugin precheck
|
|
104
110
|
// (settleChild turns this message into a clear failed reply instead of a raw 401)
|
|
105
|
-
throw new Error(`consult model ${label} has no API key — check providers[${m.provider}].apiKey
|
|
111
|
+
throw new Error(`consult model ${label} has no API key — check providers[${m.provider}].apiKey in config.json`)
|
|
112
|
+
}
|
|
113
|
+
// Clamp the pool's effort to the model's reasoningEffortEnum — an out-of-enum
|
|
114
|
+
// value makes provider/core throw on EVERY chat call (candidate dies on takeoff).
|
|
115
|
+
// Symmetric with escalate.mjs; 2026-08-16 a real consult died on qwen3.8-max
|
|
116
|
+
// effort "high" (enum is xhigh/medium/low). Out-of-enum: DROP the effort entirely
|
|
117
|
+
// (the provider preset default may ALSO be out-of-enum for this override model).
|
|
118
|
+
if (m.effort) {
|
|
119
|
+
const enumList = specForModel(m.model).reasoningEffortEnum
|
|
120
|
+
if (enumList && !enumList.includes(m.effort)) {
|
|
121
|
+
delete provider.reasoningEffort
|
|
122
|
+
} else {
|
|
123
|
+
provider.reasoningEffort = m.effort
|
|
124
|
+
}
|
|
106
125
|
}
|
|
107
|
-
if (m.effort) provider.reasoningEffort = m.effort
|
|
108
126
|
|
|
109
127
|
// Read-only consultant: filter the parent tool set down to readonly tools + main_history.
|
|
110
128
|
const allowed = readonlyToolNames(agent.tools ?? [])
|
|
@@ -135,16 +153,50 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
135
153
|
onToolCall: ctx.callbacks?.onToolCall ? (name, args) => ctx.callbacks.onToolCall(`${relayPrefix}${name}`, args) : null,
|
|
136
154
|
}
|
|
137
155
|
|
|
156
|
+
// Turn-cap continue loop (TURN-CAP-CONTINUE.md): hitting the cap asks the user via
|
|
157
|
+
// the SAME y/n panel the main agent uses (ctx.onPermissionRequest "continue") —
|
|
158
|
+
// unlimited continues, each with a fresh turn budget AND a re-armed wall-clock
|
|
159
|
+
// watchdog (a continue is a fresh budget, the clock restarts too). Parallel
|
|
160
|
+
// consultants serialize their prompts through a session-level queue. Declined /
|
|
161
|
+
// headless → failed reply (partial diagnosis).
|
|
138
162
|
const runner = ctx.runAgent ?? runAgent
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
for (let resume = false; ; resume = true) {
|
|
164
|
+
try {
|
|
165
|
+
const result = await runner(child, "# Problem\n" + problem, childCallbacks, {
|
|
166
|
+
depth: 1,
|
|
167
|
+
maxTurns: agent?.config?.agent?.consultTurns ?? 40,
|
|
168
|
+
signal: ctrl.signal,
|
|
169
|
+
resume,
|
|
170
|
+
})
|
|
171
|
+
settleChild(session, id, label, true, String(result ?? ""))
|
|
172
|
+
return
|
|
173
|
+
} catch (e) {
|
|
174
|
+
if (e instanceof ContinueError) {
|
|
175
|
+
let go = false
|
|
176
|
+
if (ctx.onPermissionRequest) {
|
|
177
|
+
const ask = () => ctx.onPermissionRequest("continue", { turns: e.turn, agent: label })
|
|
178
|
+
session.continueQueue = (session.continueQueue ?? Promise.resolve()).then(ask, ask)
|
|
179
|
+
go = await session.continueQueue
|
|
180
|
+
}
|
|
181
|
+
if (go) {
|
|
182
|
+
clearTimeout(watchdog)
|
|
183
|
+
timedOut = false // fresh budget → fresh clock
|
|
184
|
+
watchdog = armWatchdog()
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
settleChild(session, id, label, false, `turn cap reached (${e.turn} turns) — stopped, diagnosis may be partial`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
const note = timedOut ? `consultation timed out after ${Math.round(timeoutMs / 60000)}min (agent.consultTimeoutMs)` : e?.message ?? String(e)
|
|
191
|
+
settleChild(session, id, label, false, note)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
}
|
|
145
195
|
} catch (e) {
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
// Errors BEFORE the runner (provider resolution, createAgent) or a throwing
|
|
197
|
+
// continue-prompt settle as failed replies — the runner's own errors are already
|
|
198
|
+
// handled inside the loop above.
|
|
199
|
+
settleChild(session, id, label, false, e?.message ?? String(e))
|
|
148
200
|
} finally {
|
|
149
201
|
clearTimeout(watchdog)
|
|
150
202
|
}
|
|
@@ -70,16 +70,20 @@ export const escalateTool = {
|
|
|
70
70
|
} catch (e) {
|
|
71
71
|
return `Error: ${e.message}`
|
|
72
72
|
}
|
|
73
|
-
if (!provider?.apiKey?.trim()
|
|
74
|
-
return `Error: provider "${pick.provider}" has no API key — set it in config.json
|
|
73
|
+
if (!provider?.apiKey?.trim()) {
|
|
74
|
+
return `Error: provider "${pick.provider}" has no API key — set it in config.json before flying it in`
|
|
75
75
|
}
|
|
76
76
|
let effortNote = ""
|
|
77
77
|
if (pick.effort) {
|
|
78
78
|
// Clamp the pool's effort to the model's reasoningEffortEnum — an out-of-enum
|
|
79
79
|
// value makes provider/core.mjs throw on EVERY chat call (candidate dies on takeoff).
|
|
80
|
+
// Out-of-enum: DROP the effort entirely (the provider preset default may ALSO be
|
|
81
|
+
// out-of-enum for this override model — e.g. qwenplan preset default "high" is
|
|
82
|
+
// invalid for qwen3.8-max, enum xhigh/medium/low).
|
|
80
83
|
const enumList = specForModel(pick.model).reasoningEffortEnum
|
|
81
84
|
if (enumList && !enumList.includes(pick.effort)) {
|
|
82
|
-
effortNote = ` (effort "${pick.effort}" unsupported by ${pick.model},
|
|
85
|
+
effortNote = ` (effort "${pick.effort}" unsupported by ${pick.model}, dropped)`
|
|
86
|
+
delete provider.reasoningEffort
|
|
83
87
|
} else {
|
|
84
88
|
provider.reasoningEffort = pick.effort
|
|
85
89
|
}
|
|
@@ -131,9 +135,8 @@ export const escalateTool = {
|
|
|
131
135
|
// resumed run passes resume:true, so runAgent does NOT re-inject the task text
|
|
132
136
|
// (setup.mjs skips input on resume) and keeps the child's history + mutation
|
|
133
137
|
// bookkeeping, with a fresh maxTurns budget per run. No permission handler
|
|
134
|
-
// (headless) or a declined prompt falls through to the partial-work return
|
|
135
|
-
//
|
|
136
|
-
const MAX_RESUMES = 2
|
|
138
|
+
// (headless) or a declined prompt falls through to the partial-work return.
|
|
139
|
+
// Continues are UNLIMITED — the user can decline at any prompt.
|
|
137
140
|
for (let resumes = 0; ; resumes++) {
|
|
138
141
|
try {
|
|
139
142
|
const report = await runner(child, task, {
|
|
@@ -152,7 +155,7 @@ export const escalateTool = {
|
|
|
152
155
|
const msg = e?.message ?? String(e)
|
|
153
156
|
if (ctx.signal?.aborted || e?.name === "AbortError") throw e
|
|
154
157
|
if (e instanceof ContinueError) {
|
|
155
|
-
if (
|
|
158
|
+
if (ctx.onPermissionRequest) {
|
|
156
159
|
const go = await ctx.onPermissionRequest("continue", { turns: e.turn, agent: tag })
|
|
157
160
|
if (go) continue // fresh maxTurns budget; task NOT re-injected (resume:true)
|
|
158
161
|
}
|
|
@@ -32,19 +32,12 @@ export function effectiveSubagentModel(parent, role, modelArg) {
|
|
|
32
32
|
* "provider" → the named provider's configured model
|
|
33
33
|
* "model" → same provider as the parent, different model
|
|
34
34
|
* null → parent's provider unchanged.
|
|
35
|
-
* API keys
|
|
35
|
+
* API keys come from config.json only (env vars are not a key source).
|
|
36
36
|
*/
|
|
37
37
|
export function resolveChildProvider(parent, modelArg) {
|
|
38
38
|
if (!modelArg) return { ...parent.provider }
|
|
39
39
|
const providers = parent.config?.providersList ?? []
|
|
40
|
-
const withKey = (p) => {
|
|
41
|
-
if (p.apiKey?.trim()) return { ...p, apiKey: p.apiKey.trim() }
|
|
42
|
-
if (process.env.THINCODER_API_KEY) return { ...p, apiKey: process.env.THINCODER_API_KEY }
|
|
43
|
-
const envMap = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }
|
|
44
|
-
const keyVar = envMap[p.name]
|
|
45
|
-
if (keyVar && process.env[keyVar]) return { ...p, apiKey: process.env[keyVar] }
|
|
46
|
-
return { ...p }
|
|
47
|
-
}
|
|
40
|
+
const withKey = (p) => (p.apiKey?.trim() ? { ...p, apiKey: p.apiKey.trim() } : { ...p })
|
|
48
41
|
if (modelArg.includes(":")) {
|
|
49
42
|
const [pname, mname] = modelArg.split(":")
|
|
50
43
|
const p = providers.find((x) => x.name === pname)
|
|
@@ -181,7 +174,31 @@ export const subagentTool = {
|
|
|
181
174
|
: null,
|
|
182
175
|
}
|
|
183
176
|
const childRunOpts = buildChildRunOpts(ctx)
|
|
184
|
-
let report =
|
|
177
|
+
let report = ""
|
|
178
|
+
// Turn-cap continue loop (TURN-CAP-CONTINUE.md): hitting the cap asks the user via
|
|
179
|
+
// the SAME y/n panel the main agent uses (ctx.onPermissionRequest "continue") —
|
|
180
|
+
// unlimited continues, resume:true keeps the child's history + mutation bookkeeping,
|
|
181
|
+
// fresh budget each run. Prompts queue through parent._permQueue (same as write
|
|
182
|
+
// approval) so parallel children never pop two panels at once. Declined / headless
|
|
183
|
+
// → partial-work return. Non-ContinueError errors still propagate (dispatch.mjs
|
|
184
|
+
// turns them into Error tool results — unchanged behavior).
|
|
185
|
+
for (let resume = false; ; resume = true) {
|
|
186
|
+
try {
|
|
187
|
+
report = await runAgent(child, input, childOpts, { ...childRunOpts, resume })
|
|
188
|
+
break
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (!(e instanceof ContinueError)) throw e
|
|
191
|
+
let go = false
|
|
192
|
+
if (ctx.onPermissionRequest) {
|
|
193
|
+
const ask = () => ctx.onPermissionRequest("continue", { turns: e.turn, agent: `${role ?? "sub"}#${subId}` })
|
|
194
|
+
parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
|
|
195
|
+
go = await parent._permQueue
|
|
196
|
+
}
|
|
197
|
+
if (go) continue
|
|
198
|
+
if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
|
|
199
|
+
return `Subagent (${role}) stopped: turn cap reached (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${report || ""}`
|
|
200
|
+
}
|
|
201
|
+
}
|
|
185
202
|
|
|
186
203
|
// Report too short = incomplete handoff: send back for expansion once (inspired by kimi-code's summaryPolicy: min 200 chars, retry 1 time).
|
|
187
204
|
// The child agent's history is still intact; the continuation instruction is appended as new input so it can see its own earlier work.
|
package/src/config.mjs
CHANGED
|
@@ -21,6 +21,8 @@ export const PROVIDER_PRESETS = {
|
|
|
21
21
|
"glm-code": { baseURL: "https://open.bigmodel.cn/api/coding/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 128000, desc: "Zhipu GLM Coding Plan (coding endpoint — same key as GLM; server-forced thinking)" },
|
|
22
22
|
qwen: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen3.7-max", reasoningEffort: "high", maxTokens: 131072, desc: "Qwen / Alibaba" },
|
|
23
23
|
qwenplan: { baseURL: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", model: "qwen3.7-max", reasoningEffort: "high", maxTokens: 131072, desc: "Qwen Token Plan (百炼套餐)" },
|
|
24
|
+
mimo: { baseURL: "https://api.xiaomimimo.com/v1", model: "mimo-v2.5-pro", thinking: { type: "enabled" }, maxTokens: 131072, desc: "MiMo (Xiaomi)" },
|
|
25
|
+
mimoplan: { baseURL: "https://token-plan-cn.xiaomimimo.com/v1", model: "mimo-v2.5-pro", thinking: { type: "enabled" }, maxTokens: 131072, desc: "MiMo Token Plan (小米套餐 — tp- keys; 与按量付费 sk- 密钥不通用)" },
|
|
24
26
|
minimax: { baseURL: "https://api.minimaxi.com/v1", model: "MiniMax-M3", thinking: { type: "adaptive" }, maxTokens: 128000, chatPath: "/text/chatcompletion_v2", desc: "MiniMax" },
|
|
25
27
|
openai: { baseURL: "https://api.openai.com/v1", model: "gpt-4o", desc: "OpenAI" },
|
|
26
28
|
claude: { baseURL: "https://api.anthropic.com/v1", model: "claude-sonnet-4", format: "anthropic", maxTokens: 8192, desc: "Claude (Anthropic)" },
|
|
@@ -34,12 +36,7 @@ export const PROVIDER_PRESETS = {
|
|
|
34
36
|
groq: { baseURL: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile", maxTokens: 32768, desc: "Groq" },
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
// Default provider matches deepseek preset (strip the desc display field)
|
|
38
|
-
const { desc: _, ...deepseekPreset } = PROVIDER_PRESETS.deepseek
|
|
39
|
-
|
|
40
39
|
const DEFAULTS = {
|
|
41
|
-
providers: [{ name: "deepseek", ...deepseekPreset }],
|
|
42
|
-
activeProvider: "deepseek",
|
|
43
40
|
activeModel: null, // optional: override provider.model (set via /model picker or /model provider:model)
|
|
44
41
|
agent: {
|
|
45
42
|
maxTurns: 100,
|
|
@@ -116,12 +113,17 @@ const MODEL_SPECS = [
|
|
|
116
113
|
["qwen3.8-max-preview", { context: 1_000_000, maxOutput: 128_000, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "high"], tempRange: [0, 2] }],
|
|
117
114
|
// qwen3.7-max rejects image parts outright (DashScope 400 "Unexpected item type in content") — text-only
|
|
118
115
|
["qwen3.7-max", { context: 1_000_000, maxOutput: 128_000, thinking: true, partialMode: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "high"], tempRange: [0, 2] }],
|
|
119
|
-
["qwen3.8-max", { context: 1_000_000, maxOutput: 128_000, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "
|
|
116
|
+
["qwen3.8-max", { context: 1_000_000, maxOutput: 128_000, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
|
|
120
117
|
["qwen-max", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
|
|
121
118
|
["qwen-plus", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
|
|
122
119
|
["qwen", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
|
|
123
120
|
// MiniMax series
|
|
124
121
|
["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
|
|
122
|
+
// MiMo series (Xiaomi — OpenAI-compatible https://api.xiaomimimo.com/v1;
|
|
123
|
+
// deep thinking via thinking.type, default ON; multi-turn tool calls MUST echo
|
|
124
|
+
// reasoning_content back exactly like DeepSeek V4, else 400 on follow-ups)
|
|
125
|
+
["mimo-v2.5-pro", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
|
|
126
|
+
["mimo-v2.5", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
|
|
125
127
|
["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
|
|
126
128
|
["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, cacheMode: "auto", noUsageStream: true }],
|
|
127
129
|
// Grok series (xAI — OpenAI-compatible)
|
|
@@ -198,10 +200,8 @@ export function normalizeProxy(proxy) {
|
|
|
198
200
|
|
|
199
201
|
/**
|
|
200
202
|
* Load configuration.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* THINCODER_ACTIVE_MODEL overrides the active model (wins over THINCODER_MODEL — see loadConfig)
|
|
204
|
-
* Provider-specific key fallbacks (when providers[] lacks a key): DEEPSEEK_API_KEY / OPENAI_API_KEY
|
|
203
|
+
* No env-var overrides — config.json is the single source of truth
|
|
204
|
+
* (API keys, baseURL, model, activeProvider all come from the file).
|
|
205
205
|
*/
|
|
206
206
|
/** Keep only { header: "string value" } pairs from a provider's headers field — anything
|
|
207
207
|
* else (null, arrays, nested objects) is dropped so it can never reach a fetch call.
|
|
@@ -230,10 +230,10 @@ export function loadConfig() {
|
|
|
230
230
|
const merged = {
|
|
231
231
|
...DEFAULTS,
|
|
232
232
|
...config,
|
|
233
|
-
providers: Array.isArray(config.providers)
|
|
233
|
+
providers: Array.isArray(config.providers)
|
|
234
234
|
? config.providers.map((p) => sanitizeProviderHeaders({ ...p }))
|
|
235
|
-
:
|
|
236
|
-
activeProvider: config.activeProvider ??
|
|
235
|
+
: [],
|
|
236
|
+
activeProvider: config.activeProvider ?? "",
|
|
237
237
|
agent: { ...DEFAULTS.agent, ...config.agent },
|
|
238
238
|
memory: { ...DEFAULTS.memory, ...config.memory },
|
|
239
239
|
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
@@ -275,41 +275,15 @@ export function loadConfig() {
|
|
|
275
275
|
// 保证 agent.config.proxy 永远是规范形态或 undefined
|
|
276
276
|
merged.proxy = normalizeProxy(merged.proxy)
|
|
277
277
|
|
|
278
|
-
// Env var overrides activeProvider
|
|
279
|
-
if (process.env.THINCODER_ACTIVE_PROVIDER) {
|
|
280
|
-
merged.activeProvider = process.env.THINCODER_ACTIVE_PROVIDER
|
|
281
|
-
}
|
|
282
|
-
|
|
283
278
|
// Get the currently active provider
|
|
284
279
|
const active = findProvider(merged.providers, merged.activeProvider)
|
|
285
280
|
|
|
286
281
|
// Build runtime provider object (for agent.provider usage)
|
|
287
282
|
const runtimeProvider = { ...active }
|
|
288
283
|
|
|
289
|
-
//
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
if (process.env.THINCODER_MODEL) runtimeProvider.model = process.env.THINCODER_MODEL
|
|
293
|
-
|
|
294
|
-
// activeModel overrides provider's default model (env > config)
|
|
295
|
-
const activeModel = process.env.THINCODER_ACTIVE_MODEL || merged.activeModel
|
|
296
|
-
if (activeModel) runtimeProvider.model = activeModel
|
|
297
|
-
merged.activeModel = activeModel || null // normalize for agent.activeModel
|
|
298
|
-
|
|
299
|
-
// apiKey also falls back to env vars (when providers doesn't include a key)
|
|
300
|
-
// Provider-specific env vars only apply to the matching provider name, preventing keys from leaking to wrong endpoints
|
|
301
|
-
// NOTE: only deepseek/openai have provider-specific fallbacks by design — the other presets
|
|
302
|
-
// intentionally rely on THINCODER_API_KEY or keys stored in config.json (no silent env pickup).
|
|
303
|
-
if (!runtimeProvider.apiKey?.trim()) {
|
|
304
|
-
const envMap = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }
|
|
305
|
-
const keyVar = envMap[merged.activeProvider]
|
|
306
|
-
if (keyVar && process.env[keyVar]) runtimeProvider.apiKey = process.env[keyVar]
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// embedding apiKey
|
|
310
|
-
if (!merged.embedding.apiKey) {
|
|
311
|
-
merged.embedding.apiKey = process.env.SILICONFLOW_API_KEY || process.env.THINCODER_EMBEDDING_API_KEY
|
|
312
|
-
}
|
|
284
|
+
// activeModel overrides provider's default model (config only)
|
|
285
|
+
if (merged.activeModel) runtimeProvider.model = merged.activeModel
|
|
286
|
+
merged.activeModel = merged.activeModel || null // normalize for agent.activeModel
|
|
313
287
|
|
|
314
288
|
// Compaction threshold follows the model
|
|
315
289
|
const explicitThreshold = config.agent?.compactThreshold
|
package/src/embedding.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const BATCH_SIZE = 32 // max texts per request (within SiliconFlow limits)
|
|
|
12
12
|
/** Create an embedder. config: { baseURL, apiKey, model } */
|
|
13
13
|
export function createEmbedder(config) {
|
|
14
14
|
if (!config?.baseURL) throw new Error("embedding config: baseURL is required — configure embedding.baseURL in ~/.thincoder/config.json")
|
|
15
|
-
if (!config?.apiKey) throw new Error("embedding config: apiKey is required —
|
|
15
|
+
if (!config?.apiKey) throw new Error("embedding config: apiKey is required — configure embedding.apiKey in ~/.thincoder/config.json")
|
|
16
16
|
if (!config?.model) throw new Error("embedding config: model is required — configure embedding.model in ~/.thincoder/config.json")
|
|
17
17
|
return {
|
|
18
18
|
baseURL: config.baseURL.replace(/\/+$/, ""),
|
package/src/provider/core.mjs
CHANGED
|
@@ -19,7 +19,7 @@ const FETCH_TIMEOUT_MS = 600_000
|
|
|
19
19
|
/** Create a validated provider config object from raw config */
|
|
20
20
|
export function createProvider(config) {
|
|
21
21
|
if (!config?.baseURL) throw new Error("provider config: baseURL is required — configure providers in ~/.thincoder/config.json")
|
|
22
|
-
if (!config?.apiKey) throw new Error("provider config: apiKey is required —
|
|
22
|
+
if (!config?.apiKey) throw new Error("provider config: apiKey is required — configure it in ~/.thincoder/config.json")
|
|
23
23
|
if (!config?.model) throw new Error("provider config: model is required — configure in ~/.thincoder/config.json")
|
|
24
24
|
return {
|
|
25
25
|
baseURL: config.baseURL.replace(/\/+$/, ""),
|
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -174,11 +174,7 @@ async function fetchAdvisorModels(agent) {
|
|
|
174
174
|
const result = new Map()
|
|
175
175
|
await Promise.all((agent.providers || []).map(async (p) => {
|
|
176
176
|
try {
|
|
177
|
-
const
|
|
178
|
-
let apiKey = p.apiKey
|
|
179
|
-
if (!apiKey && envKey && process.env[envKey]) apiKey = process.env[envKey]
|
|
180
|
-
if (!apiKey) apiKey = process.env.THINCODER_API_KEY
|
|
181
|
-
const models = await listModels({ baseURL: p.baseURL, apiKey: apiKey ?? "" }, { signal: AbortSignal.timeout(10000) })
|
|
177
|
+
const models = await listModels({ baseURL: p.baseURL, apiKey: p.apiKey ?? "" }, { signal: AbortSignal.timeout(10000) })
|
|
182
178
|
result.set(p.name, { models, error: null })
|
|
183
179
|
} catch (err) {
|
|
184
180
|
result.set(p.name, { models: [], error: err.message.slice(0, 40) })
|
|
@@ -193,10 +189,7 @@ function buildModelEntries(agent, cfg, cache) {
|
|
|
193
189
|
|
|
194
190
|
for (const p of agent.providers || []) {
|
|
195
191
|
const cached = cache.get(p.name)
|
|
196
|
-
const hasKey = !!
|
|
197
|
-
|| (p.name === "deepseek" && process.env.DEEPSEEK_API_KEY)
|
|
198
|
-
|| (p.name === "openai" && process.env.OPENAI_API_KEY)
|
|
199
|
-
|| process.env.THINCODER_API_KEY)
|
|
192
|
+
const hasKey = !!p.apiKey
|
|
200
193
|
const noteParts = [p.baseURL]
|
|
201
194
|
if (!hasKey) noteParts.push("(no key)")
|
|
202
195
|
if (agent.activeProvider === p.name) noteParts.push("← active")
|
package/src/tui/pickers.mjs
CHANGED
|
@@ -121,13 +121,9 @@ export function createPickers(ctx) {
|
|
|
121
121
|
return e.action === "switch" ? `switch:${e.provider}:${e.model}` : `action:${e.action}`
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/** Get API key for a provider (
|
|
124
|
+
/** Get API key for a provider (config.json only — env vars are not a key source) */
|
|
125
125
|
function getApiKey(providerName, providerConfig) {
|
|
126
|
-
|
|
127
|
-
let apiKey = providerConfig.apiKey
|
|
128
|
-
if (!apiKey && envKey && process.env[envKey]) apiKey = process.env[envKey]
|
|
129
|
-
if (!apiKey) apiKey = process.env.THINCODER_API_KEY
|
|
130
|
-
return apiKey
|
|
126
|
+
return providerConfig.apiKey
|
|
131
127
|
}
|
|
132
128
|
|
|
133
129
|
/** Level 1: Show provider list. Selecting a provider opens Level 2 (model list). */
|
|
@@ -296,11 +292,6 @@ export function createPickers(ctx) {
|
|
|
296
292
|
// If selecting the provider's default model, clear activeModel; otherwise set it
|
|
297
293
|
agent.activeModel = item.model !== providerDefault ? item.model : null
|
|
298
294
|
agent.provider = { ...target }
|
|
299
|
-
if (!agent.provider.apiKey) {
|
|
300
|
-
const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[item.provider]
|
|
301
|
-
if (envKey && process.env[envKey]) agent.provider.apiKey = process.env[envKey]
|
|
302
|
-
}
|
|
303
|
-
if (!agent.provider.apiKey) agent.provider.apiKey = process.env.THINCODER_API_KEY
|
|
304
295
|
if (agent.config?.agent?.compactThresholdAuto) {
|
|
305
296
|
const { resolveCompactThreshold } = await import("../config.mjs")
|
|
306
297
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, item.model).value
|