@hifullmoon/aicommit 2.2.1 → 2.2.2

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.
@@ -10,12 +10,7 @@
10
10
  "models": {
11
11
  "default": {
12
12
  "modelId": "MiniMax-M3",
13
- "extraBody": {
14
- "thinking": {
15
- "type": "disabled"
16
- },
17
- "reasoning_split": true
18
- }
13
+ "reasoning": { "mode": "on", "effort": "medium" }
19
14
  }
20
15
  }
21
16
  },
@@ -25,7 +20,10 @@
25
20
  "apiKeyEnv": "DEEPSEEK_API_KEY",
26
21
  "defaultModel": "chat",
27
22
  "models": {
28
- "chat": { "modelId": "deepseek-v4-flash" }
23
+ "chat": {
24
+ "modelId": "deepseek-v4-flash",
25
+ "reasoning": { "mode": "on", "effort": "high" }
26
+ }
29
27
  }
30
28
  },
31
29
  "openrouter": {
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.2.2] - 2026-08-31
8
+
9
+ ### Added
10
+
11
+ - Added per-model reasoning mode and effort selection to interactive setup, with a balanced `medium` default and model-aware OpenAI option filtering.
12
+
7
13
  ## [2.2.1] - 2026-08-29
8
14
 
9
15
  ### Fixed
@@ -164,7 +170,8 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
164
170
  - Added file-level split planning and execution with Git-state concurrency checks.
165
171
  - Added provider presets and user/project configuration boundaries.
166
172
 
167
- [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.2.1...HEAD
173
+ [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.2.2...HEAD
174
+ [2.2.2]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.2
168
175
  [2.2.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.1
169
176
  [2.2.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.0
170
177
  [2.1.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.1.0
package/README.md CHANGED
@@ -46,6 +46,8 @@ aicommit setup
46
46
 
47
47
  It walks you through choosing built-in provider defaults (OpenAI, DeepSeek, OpenRouter, MiniMax, Kimi Code, and Ollama) or entering a custom OpenAI-compatible endpoint, then entering your API key and one or more models, choosing a default model and commit language, and optionally testing the connection. Configuration is written atomically to the user config (`~/.aicommit/config.json`); a malformed or old-format existing file is backed up before replacement. The legacy `~/.aicommit.config.json` path remains readable and `aicommit setup` migrates its settings to the canonical path when saving.
48
48
 
49
+ The model step configures reasoning independently for each profile. `auto` sends no explicit reasoning switch and leaves the behavior to the provider, `on` requests reasoning and then prompts for its effort, and `off` explicitly disables reasoning when the model supports that switch. Effort defaults to `medium`. For known OpenAI models, setup removes unsupported mode and effort choices, including `off` when reasoning cannot be disabled. When editing an existing model, the wizard preselects valid saved values, falling back to the global reasoning settings when applicable; an unsupported saved mode falls back to `auto`.
50
+
49
51
  To configure by hand, start from [.aicommit.config.example.json](.aicommit.config.example.json). User config is loaded first, then allow-listed generation preferences from the project config at `./.aicommit.config.json` are deep-merged over it. Project config may set `language`, `commitPolicy`, `stripFiles`, `temperature`, and lower diff/token/timeout or repository-context ceilings. A project-owned `prompt` is ignored unless the user config explicitly sets `allowProjectPrompt: true`. Connection/provider fields (including `apiKeyEnv`), reasoning request controls, unknown keys, and attempts to raise a ceiling are ignored with a warning. This prevents a cloned repository from redirecting an authenticated request or silently increasing its cost/data scope.
50
52
 
51
53
  To keep a key out of the JSON file, set `"apiKeyEnv": "OPENAI_API_KEY"` (and leave `apiKey` empty), or enter `env:OPENAI_API_KEY` in the setup wizard. Environment variables take priority over every other credential source and are recommended for CI and other stateless environments.
@@ -68,10 +70,7 @@ Each provider owns one or more named model profiles. Switch providers with `-p`
68
70
  "default": {
69
71
  "label": "MiniMax M3",
70
72
  "modelId": "MiniMax-M3",
71
- "extraBody": {
72
- "thinking": { "type": "disabled" },
73
- "reasoning_split": true
74
- }
73
+ "reasoning": { "mode": "on", "effort": "medium" }
75
74
  }
76
75
  }
77
76
  },
@@ -81,14 +80,85 @@ Each provider owns one or more named model profiles. Switch providers with `-p`
81
80
  "apiKeyEnv": "DEEPSEEK_API_KEY",
82
81
  "defaultModel": "chat",
83
82
  "models": {
84
- "chat": { "modelId": "deepseek-v4-flash" },
85
- "reasoner": { "modelId": "deepseek-v4-pro" }
83
+ "chat": {
84
+ "modelId": "deepseek-v4-flash",
85
+ "reasoning": { "mode": "on", "effort": "medium" }
86
+ },
87
+ "reasoner": {
88
+ "modelId": "deepseek-v4-pro",
89
+ "reasoning": { "mode": "on", "effort": "high" }
90
+ }
91
+ }
92
+ },
93
+ "openai": {
94
+ "providerType": "openai",
95
+ "apiUrl": "https://api.openai.com/v1/chat/completions",
96
+ "apiKeyEnv": "OPENAI_API_KEY",
97
+ "defaultModel": "fast",
98
+ "models": {
99
+ "fast": {
100
+ "modelId": "gpt-4o",
101
+ "reasoning": { "mode": "auto" }
102
+ },
103
+ "reasoner": {
104
+ "modelId": "gpt-5.6-sol",
105
+ "reasoning": { "mode": "on", "effort": "medium" }
106
+ }
107
+ }
108
+ },
109
+ "openrouter": {
110
+ "providerType": "openrouter",
111
+ "apiUrl": "https://openrouter.ai/api/v1/chat/completions",
112
+ "apiKeyEnv": "OPENROUTER_API_KEY",
113
+ "defaultModel": "auto",
114
+ "models": {
115
+ "auto": {
116
+ "modelId": "openrouter/auto",
117
+ "reasoning": { "mode": "auto" }
118
+ },
119
+ "quality": {
120
+ "modelId": "openai/gpt-5.6-terra",
121
+ "reasoning": { "mode": "on", "effort": "high" }
122
+ }
123
+ }
124
+ },
125
+ "ollama": {
126
+ "providerType": "ollama",
127
+ "apiUrl": "http://127.0.0.1:11434/api/chat",
128
+ "defaultModel": "qwen",
129
+ "models": {
130
+ "qwen": {
131
+ "modelId": "qwen3:8b",
132
+ "reasoning": { "mode": "on", "effort": "medium" }
133
+ },
134
+ "deepseek": {
135
+ "modelId": "deepseek-r1:8b",
136
+ "reasoning": { "mode": "on", "effort": "medium" }
137
+ }
86
138
  }
87
139
  }
88
140
  }
89
141
  }
90
142
  ```
91
143
 
144
+ After exporting the API-key variables used by the providers you configured, validate each profile before its first real commit:
145
+
146
+ ```bash
147
+ export MINIMAX_API_KEY='your-minimax-api-key'
148
+ export DEEPSEEK_API_KEY='your-deepseek-api-key'
149
+ export OPENAI_API_KEY='your-openai-api-key'
150
+ export OPENROUTER_API_KEY='your-openrouter-api-key'
151
+
152
+ aicommit doctor -p minimax -m default
153
+ aicommit doctor -p deepseek -m reasoner
154
+ aicommit doctor -p openai -m reasoner
155
+ aicommit doctor -p openrouter -m quality
156
+ aicommit doctor -p ollama -m qwen
157
+
158
+ aicommit -p minimax
159
+ aicommit -p deepseek -m reasoner
160
+ ```
161
+
92
162
  `schemaVersion`, `defaultProvider`, `providers`, and every provider's `providerType`, `apiUrl`, `defaultModel`, and non-empty `models` map are required. Without `-p`, AICommit selects `defaultProvider`; without `-m`, it selects that provider's `defaultModel`. Model profiles inherit global generation settings and provider connection settings, then may override `temperature`, `maxTokens`, `timeoutMs`, `reasoning`, and `extraBody`. Provider and model names are stable local aliases; `modelId` is the identifier sent to the API.
93
163
 
94
164
  This is the only supported user-config shape. Earlier flat or provider-level `modelId` configurations are rejected; run `aicommit setup` or migrate them explicitly.
@@ -123,44 +193,10 @@ This is the only supported user-config shape. Earlier flat or provider-level `mo
123
193
  | `stripFiles` | Extra files to stub out of the diff like lock files, matched by basename with `*`/`?` wildcards, e.g. `["*.min.js", "*.map", "*.snap"]` (default: `[]`; project-level entries are merged with user-level ones, not replaced) |
124
194
  | `regenerateWithDiff` | `true` re-sends the full diff on every regenerate for more varied rewrites; `false` (default) only asks the model to reword its previous message, which is far cheaper |
125
195
  | `extraBody` | Model-profile JSON fields merged into the request body, except `model`/`messages` (default: `{}`) |
126
- | `reasoning` | Global or model-profile reasoning controls: `mode`, `effort`, `maxTokens`, and `maxDisplayChars`; defaults to `mode: "on"` and streams reasoning automatically |
196
+ | `reasoning` | Global or model-profile reasoning controls: `mode` / `effort` (default: `on` / `medium`), `maxTokens`, and `maxDisplayChars` |
127
197
 
128
198
  Works with OpenAI, DeepSeek, [OpenRouter](https://openrouter.ai), MiniMax, [Kimi Code](https://www.kimi.com/code/docs/), Ollama (native `/api/chat` or OpenAI-compatible `/v1/chat/completions`), LiteLLM, and other compatible endpoints. HTTPS is required for remote endpoints; plaintext HTTP is accepted only for localhost/loopback.
129
199
 
130
- ### Kimi Code example
131
-
132
- Create an API key in the Kimi Code console, export it without storing the secret in JSON, and select the bundled preset in `aicommit setup`. For a manual configuration, use the OpenAI-compatible full endpoint:
133
-
134
- ```bash
135
- export KIMI_API_KEY='your-kimi-code-api-key'
136
- ```
137
-
138
- ```json
139
- {
140
- "schemaVersion": 1,
141
- "defaultProvider": "kimi-code",
142
- "providers": {
143
- "kimi-code": {
144
- "providerType": "custom",
145
- "apiUrl": "https://api.kimi.com/coding/v1/chat/completions",
146
- "apiKeyEnv": "KIMI_API_KEY",
147
- "defaultModel": "default",
148
- "models": {
149
- "default": { "modelId": "kimi-for-coding" }
150
- }
151
- }
152
- }
153
- }
154
- ```
155
-
156
- Verify the endpoint, key, and model before the first commit:
157
-
158
- ```bash
159
- aicommit doctor -p kimi-code
160
- ```
161
-
162
- `kimi-for-coding` is available to all Kimi Code membership tiers and follows the service's rolling model upgrades. Kimi Code membership keys use `api.kimi.com`; Kimi Platform pay-as-you-go keys use a different endpoint and are not interchangeable.
163
-
164
200
  See the bilingual [provider compatibility table](docs/provider-compatibility.md) for streaming, reasoning, token-budget, usage, and authentication boundaries.
165
201
 
166
202
  ### Repository policy and bounded context
package/README.zh-CN.md CHANGED
@@ -48,6 +48,8 @@ aicommit setup
48
48
 
49
49
  向导会引导你选择内置 Provider 默认值(OpenAI、DeepSeek、OpenRouter、MiniMax、Kimi Code 和 Ollama),或填写自定义 OpenAI 兼容端点;随后输入 API Key 和一个或多个模型、选择默认模型和提交信息语言,并可选测试连接。配置会原子写入用户配置文件 `~/.aicommit/config.json`;如果已有文件格式错误或属于旧格式,替换前会先备份。旧路径 `~/.aicommit.config.json` 仍可读取,运行 `aicommit setup` 并保存时会把其中设置迁移到规范路径。
50
50
 
51
+ 模型步骤会为每个配置单独设置推理。`auto` 不发送显式推理开关,交给 Provider 使用默认行为;`on` 请求推理并继续选择强度;`off` 在模型支持该开关时显式关闭推理。强度默认为 `medium`。对于已知的 OpenAI 模型,setup 会过滤不支持的模式和强度选项;无法关闭推理时也不会列出 `off`。编辑已有模型时,向导会回填其中仍有效的保存值;模型级未配置时,则在适用情况下回填全局推理设置,不再受支持的旧模式会回退到 `auto`。
52
+
51
53
  如需手动配置,请从 [.aicommit.config.example.json](.aicommit.config.example.json) 开始。AICommit 先加载用户配置,再将项目配置 `./.aicommit.config.json` 中白名单内的生成偏好深度合并到用户配置之上。项目配置可以设置 `language`、`commitPolicy`、`stripFiles`、`temperature`,也可以降低 diff、token、timeout 或仓库上下文上限。项目拥有的 `prompt` 默认会被忽略,除非用户配置明确设置 `allowProjectPrompt: true`。连接或 Provider 字段(包括 `apiKeyEnv`)、推理请求控制、未知字段,以及任何试图提高上限的配置,都会被忽略并给出警告。这样可以防止克隆的仓库重定向已鉴权请求,或在不知情的情况下扩大成本和数据范围。
52
54
 
53
55
  如需避免把密钥写入 JSON,请设置 `"apiKeyEnv": "OPENAI_API_KEY"`(并将 `apiKey` 留空),或在 setup 向导中输入 `env:OPENAI_API_KEY`。环境变量优先于所有其他凭据来源,推荐用于 CI 和其他无状态环境。
@@ -70,10 +72,7 @@ AICommit 也可以读取操作系统上已经配置的 Git credential helper。
70
72
  "default": {
71
73
  "label": "MiniMax M3",
72
74
  "modelId": "MiniMax-M3",
73
- "extraBody": {
74
- "thinking": { "type": "disabled" },
75
- "reasoning_split": true
76
- }
75
+ "reasoning": { "mode": "on", "effort": "medium" }
77
76
  }
78
77
  }
79
78
  },
@@ -83,14 +82,85 @@ AICommit 也可以读取操作系统上已经配置的 Git credential helper。
83
82
  "apiKeyEnv": "DEEPSEEK_API_KEY",
84
83
  "defaultModel": "chat",
85
84
  "models": {
86
- "chat": { "modelId": "deepseek-v4-flash" },
87
- "reasoner": { "modelId": "deepseek-v4-pro" }
85
+ "chat": {
86
+ "modelId": "deepseek-v4-flash",
87
+ "reasoning": { "mode": "on", "effort": "medium" }
88
+ },
89
+ "reasoner": {
90
+ "modelId": "deepseek-v4-pro",
91
+ "reasoning": { "mode": "on", "effort": "high" }
92
+ }
93
+ }
94
+ },
95
+ "openai": {
96
+ "providerType": "openai",
97
+ "apiUrl": "https://api.openai.com/v1/chat/completions",
98
+ "apiKeyEnv": "OPENAI_API_KEY",
99
+ "defaultModel": "fast",
100
+ "models": {
101
+ "fast": {
102
+ "modelId": "gpt-4o",
103
+ "reasoning": { "mode": "auto" }
104
+ },
105
+ "reasoner": {
106
+ "modelId": "gpt-5.6-sol",
107
+ "reasoning": { "mode": "on", "effort": "medium" }
108
+ }
109
+ }
110
+ },
111
+ "openrouter": {
112
+ "providerType": "openrouter",
113
+ "apiUrl": "https://openrouter.ai/api/v1/chat/completions",
114
+ "apiKeyEnv": "OPENROUTER_API_KEY",
115
+ "defaultModel": "auto",
116
+ "models": {
117
+ "auto": {
118
+ "modelId": "openrouter/auto",
119
+ "reasoning": { "mode": "auto" }
120
+ },
121
+ "quality": {
122
+ "modelId": "openai/gpt-5.6-terra",
123
+ "reasoning": { "mode": "on", "effort": "high" }
124
+ }
125
+ }
126
+ },
127
+ "ollama": {
128
+ "providerType": "ollama",
129
+ "apiUrl": "http://127.0.0.1:11434/api/chat",
130
+ "defaultModel": "qwen",
131
+ "models": {
132
+ "qwen": {
133
+ "modelId": "qwen3:8b",
134
+ "reasoning": { "mode": "on", "effort": "medium" }
135
+ },
136
+ "deepseek": {
137
+ "modelId": "deepseek-r1:8b",
138
+ "reasoning": { "mode": "on", "effort": "medium" }
139
+ }
88
140
  }
89
141
  }
90
142
  }
91
143
  }
92
144
  ```
93
145
 
146
+ 导出所配置 Provider 使用的 API Key 环境变量后,建议在第一次真实提交前逐个验证模型配置:
147
+
148
+ ```bash
149
+ export MINIMAX_API_KEY='your-minimax-api-key'
150
+ export DEEPSEEK_API_KEY='your-deepseek-api-key'
151
+ export OPENAI_API_KEY='your-openai-api-key'
152
+ export OPENROUTER_API_KEY='your-openrouter-api-key'
153
+
154
+ aicommit doctor -p minimax -m default
155
+ aicommit doctor -p deepseek -m reasoner
156
+ aicommit doctor -p openai -m reasoner
157
+ aicommit doctor -p openrouter -m quality
158
+ aicommit doctor -p ollama -m qwen
159
+
160
+ aicommit -p minimax
161
+ aicommit -p deepseek -m reasoner
162
+ ```
163
+
94
164
  `schemaVersion`、`defaultProvider`、`providers`,以及每个 Provider 的 `providerType`、`apiUrl`、`defaultModel` 和非空 `models` 都是必填项。未指定 `-p` 时选择 `defaultProvider`;未指定 `-m` 时选择该 Provider 的 `defaultModel`。模型配置会继承全局生成设置和 Provider 连接设置,并可覆盖 `temperature`、`maxTokens`、`timeoutMs`、`reasoning` 与 `extraBody`。Provider 名和模型名是稳定的本地别名,`modelId` 才是发送给 API 的模型标识。
95
165
 
96
166
  这是唯一支持的用户配置格式。旧版扁平配置或 Provider 级 `modelId` 会被直接拒绝;请运行 `aicommit setup` 或显式迁移。
@@ -125,44 +195,10 @@ AICommit 也可以读取操作系统上已经配置的 Git credential helper。
125
195
  | `stripFiles` | 额外替换为占位的文件,按 basename 使用 `*` / `?` 通配,如 `["*.min.js", "*.map", "*.snap"]`(默认:`[]`;项目项与用户项合并而非覆盖) |
126
196
  | `regenerateWithDiff` | `true` 表示每次重写都重发完整 diff,以获得更多变化;`false`(默认)只要求模型改写上一条消息,成本更低 |
127
197
  | `extraBody` | 模型配置中合并到请求体的 JSON 字段,但不允许覆盖 `model` / `messages`(默认:`{}`) |
128
- | `reasoning` | 全局或模型级推理控制:`mode`、`effort`、`maxTokens` 和 `maxDisplayChars`;默认为 `mode: "on"`,并自动流式展示推理 |
198
+ | `reasoning` | 全局或模型级推理控制:`mode` / `effort`(默认:`on` / `medium`)、`maxTokens` 和 `maxDisplayChars` |
129
199
 
130
200
  AICommit 支持 OpenAI、DeepSeek、[OpenRouter](https://openrouter.ai)、MiniMax、[Kimi Code](https://www.kimi.com/code/docs/)、Ollama(原生 `/api/chat` 或 OpenAI 兼容 `/v1/chat/completions`)、LiteLLM,以及其他兼容端点。远程端点必须使用 HTTPS;明文 HTTP 只允许 localhost / loopback。
131
201
 
132
- ### Kimi Code 示例
133
-
134
- 在 Kimi Code 控制台创建 API Key,通过环境变量导出,避免把密钥存入 JSON,然后在 `aicommit setup` 中选择内置预设。手动配置时请使用完整的 OpenAI 兼容端点:
135
-
136
- ```bash
137
- export KIMI_API_KEY='your-kimi-code-api-key'
138
- ```
139
-
140
- ```json
141
- {
142
- "schemaVersion": 1,
143
- "defaultProvider": "kimi-code",
144
- "providers": {
145
- "kimi-code": {
146
- "providerType": "custom",
147
- "apiUrl": "https://api.kimi.com/coding/v1/chat/completions",
148
- "apiKeyEnv": "KIMI_API_KEY",
149
- "defaultModel": "default",
150
- "models": {
151
- "default": { "modelId": "kimi-for-coding" }
152
- }
153
- }
154
- }
155
- }
156
- ```
157
-
158
- 首次提交前验证端点、Key 和模型:
159
-
160
- ```bash
161
- aicommit doctor -p kimi-code
162
- ```
163
-
164
- `kimi-for-coding` 对所有 Kimi Code 会员档位开放,并随服务滚动升级模型。Kimi Code 会员 Key 使用 `api.kimi.com`;Kimi 开放平台按量付费 Key 使用不同端点,两者不能混用。
165
-
166
202
  流式输出、推理、token 预算、usage 和鉴权边界,请参阅双语 [Provider 兼容表](docs/provider-compatibility.md)。
167
203
 
168
204
  ### 仓库策略与受限上下文
@@ -26,7 +26,7 @@ The release workflow uses npm Trusted Publishing without a long-lived `NPM_TOKEN
26
26
  ```bash
27
27
  workdir=$(mktemp -d)
28
28
  cd "$workdir"
29
- npm install --package-lock-only @hifullmoon/aicommit@2.2.1
29
+ npm install --package-lock-only @hifullmoon/aicommit@2.2.2
30
30
  npm audit signatures
31
31
  ```
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifullmoon/aicommit",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "Safe, local-first AI commit message generator for Git workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from 'node:module';
3
3
  import chalk from 'chalk';
4
4
 
5
5
  import { ERROR_CATEGORIES, fail } from './errors.js';
6
+ import { REASONING_EFFORTS } from './providers.js';
6
7
 
7
8
  const _require = createRequire(import.meta.url);
8
9
  const { version: VERSION } = _require('../package.json');
@@ -364,7 +365,7 @@ export function parseArgs(args = process.argv.slice(2)) {
364
365
  }
365
366
  }
366
367
 
367
- const reasoningLevels = ['off', 'low', 'medium', 'high', 'xhigh', 'max'];
368
+ const reasoningLevels = ['off', ...REASONING_EFFORTS];
368
369
  if (cliReasoning && !reasoningLevels.includes(cliReasoning)) {
369
370
  throw fail(
370
371
  ERROR_CATEGORIES.CONFIG,
package/src/config.js CHANGED
@@ -14,7 +14,12 @@ import {
14
14
  validateRepositoryContextConfig,
15
15
  } from './context.js';
16
16
  import { readTeamPolicy } from './team-policy.js';
17
- import { isProviderType, PROVIDER_TYPES } from './providers.js';
17
+ import {
18
+ DEFAULT_REASONING_EFFORT,
19
+ isProviderType,
20
+ PROVIDER_TYPES,
21
+ REASONING_EFFORTS,
22
+ } from './providers.js';
18
23
  import { projectConfigPath, resolveConfigLocations, userConfigLocations } from './config-paths.js';
19
24
 
20
25
  // Repository-owned config is untrusted input: a cloned repository must never
@@ -173,7 +178,7 @@ export const DEFAULT_CONFIG = {
173
178
  // switch.
174
179
  reasoning: {
175
180
  mode: 'on',
176
- effort: 'medium',
181
+ effort: DEFAULT_REASONING_EFFORT,
177
182
  maxTokens: 4096,
178
183
  maxDisplayChars: 12000,
179
184
  },
@@ -508,10 +513,8 @@ export function validateConfig(config) {
508
513
  if (!['auto', 'on', 'off'].includes(config.reasoning.mode)) {
509
514
  throw new Error('Invalid config "reasoning.mode": expected "auto", "on", or "off".');
510
515
  }
511
- if (!['low', 'medium', 'high', 'xhigh', 'max'].includes(config.reasoning.effort)) {
512
- throw new Error(
513
- 'Invalid config "reasoning.effort": expected low, medium, high, xhigh, or max.',
514
- );
516
+ if (!REASONING_EFFORTS.includes(config.reasoning.effort)) {
517
+ throw new Error(`Invalid config "reasoning.effort": expected ${REASONING_EFFORTS.join(', ')}.`);
515
518
  }
516
519
  for (const key of ['enabledBody', 'disabledBody']) {
517
520
  const value = config.reasoning[key];
package/src/providers.js CHANGED
@@ -6,6 +6,8 @@ export const PROVIDER_TYPES = Object.freeze([
6
6
  'ollama',
7
7
  'custom',
8
8
  ]);
9
+ export const REASONING_EFFORTS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
10
+ export const DEFAULT_REASONING_EFFORT = 'medium';
9
11
  const PROVIDER_TYPE_SET = new Set(PROVIDER_TYPES);
10
12
 
11
13
  export function isProviderType(value) {
@@ -62,6 +64,24 @@ function openAIReasoningEfforts(modelId) {
62
64
  return null;
63
65
  }
64
66
 
67
+ // Setup uses this to avoid offering an effort that a recognized official
68
+ // OpenAI model will reject. Other adapters either accept the common effort
69
+ // vocabulary, normalize it, or are model-dependent, so they retain the full
70
+ // list and let the user/provider make the final choice.
71
+ export function reasoningEffortsForModel(providerType, modelId) {
72
+ if ((providerType || '').toLowerCase() === 'openai') {
73
+ const supported = openAIReasoningEfforts(modelId);
74
+ if (supported) return supported.filter((effort) => effort !== 'none');
75
+ }
76
+ return [...REASONING_EFFORTS];
77
+ }
78
+
79
+ export function canDisableReasoningForModel(providerType, modelId) {
80
+ if ((providerType || '').toLowerCase() !== 'openai') return true;
81
+ const supported = openAIReasoningEfforts(modelId);
82
+ return !supported || supported.includes('none');
83
+ }
84
+
65
85
  function openAIReasoningEffort(modelId, enabled, effort) {
66
86
  const requested = enabled ? effort : 'none';
67
87
  const supported = openAIReasoningEfforts(modelId);
@@ -179,7 +199,7 @@ function applyReasoning(payload, provider, modelId, reasoning, nativeOllama) {
179
199
  if (mode === 'auto') return;
180
200
 
181
201
  const enabled = mode === 'on';
182
- const effort = reasoning?.effort || 'medium';
202
+ const effort = reasoning?.effort || DEFAULT_REASONING_EFFORT;
183
203
 
184
204
  if (provider === 'openai') {
185
205
  if (!isOpenAIReasoningModel(modelId)) return;
package/src/setup.js CHANGED
@@ -9,11 +9,56 @@ import password from '@inquirer/password';
9
9
  import confirm from '@inquirer/confirm';
10
10
 
11
11
  import { checkConnection } from './api.js';
12
- import { CONFIG_SCHEMA_VERSION, isSecureApiUrl, validateUserConfig } from './config.js';
12
+ import {
13
+ CONFIG_SCHEMA_VERSION,
14
+ DEFAULT_CONFIG,
15
+ isSecureApiUrl,
16
+ validateUserConfig,
17
+ } from './config.js';
13
18
  import { vimSelect } from './ui.js';
14
19
  import { fileExists, formatMs, indentError, maskApiKey } from './utils.js';
15
20
  import { loadProviderPresetManifest } from './provider-presets.js';
16
21
  import { resolveConfigLocations, userConfigLocations } from './config-paths.js';
22
+ import {
23
+ canDisableReasoningForModel,
24
+ getProviderAdapter,
25
+ reasoningEffortsForModel,
26
+ } from './providers.js';
27
+
28
+ const REASONING_MODES = new Set(['auto', 'on', 'off']);
29
+ const REASONING_MODE_CHOICES = Object.freeze([
30
+ {
31
+ name: 'Provider default (auto)',
32
+ value: 'auto',
33
+ description: 'Do not send an explicit reasoning switch',
34
+ },
35
+ {
36
+ name: 'Enabled (on)',
37
+ value: 'on',
38
+ description: 'Request reasoning and configure its effort',
39
+ },
40
+ {
41
+ name: 'Disabled (off)',
42
+ value: 'off',
43
+ description: 'Explicitly disable reasoning when the model supports that switch',
44
+ },
45
+ ]);
46
+
47
+ function suggestedReasoningMode({ currentModel, globalReasoning, apiUrl, providerType, modelId }) {
48
+ const configured = currentModel.reasoning?.mode ?? globalReasoning?.mode;
49
+ if (REASONING_MODES.has(configured)) return configured;
50
+ return getProviderAdapter({ apiUrl, providerType, modelId }).capabilities.reasoning === 'native'
51
+ ? 'on'
52
+ : 'auto';
53
+ }
54
+
55
+ function effortDescription(effort) {
56
+ if (effort === 'low') return 'Fastest and lowest-cost reasoning';
57
+ if (effort === 'medium') return 'Balanced reasoning (default)';
58
+ if (effort === 'high') return 'Deeper reasoning with more latency and tokens';
59
+ if (effort === 'xhigh') return 'Very deep reasoning for models that support it';
60
+ return 'Maximum reasoning for models that support it';
61
+ }
17
62
 
18
63
  // Merge the wizard's answers into the one supported Provider/Model schema.
19
64
  // Existing providers and unrelated global settings are preserved; the
@@ -175,6 +220,7 @@ export async function runSetup(dependencies = {}) {
175
220
 
176
221
  // ── 4. Model ────────────────────────────────────────────────────────
177
222
 
223
+ const providerType = presetAdapter || existingProvider.providerType || 'custom';
178
224
  const models = { ...presetModels, ...existingProvider.models };
179
225
  let suggestedModel = existingProvider.defaultModel || presetDefaultModel;
180
226
  while (true) {
@@ -192,7 +238,46 @@ export async function runSetup(dependencies = {}) {
192
238
  default: currentModel.modelId || undefined,
193
239
  validate: (v) => (v.trim() ? true : 'Model ID is required'),
194
240
  });
195
- models[modelName] = { ...currentModel, modelId: modelIdInput.trim() };
241
+ const modelId = modelIdInput.trim();
242
+ const reasoningModeChoices = canDisableReasoningForModel(providerType, modelId)
243
+ ? REASONING_MODE_CHOICES
244
+ : REASONING_MODE_CHOICES.filter((choice) => choice.value !== 'off');
245
+ const suggestedMode = suggestedReasoningMode({
246
+ currentModel,
247
+ globalReasoning: existing.reasoning,
248
+ apiUrl,
249
+ providerType,
250
+ modelId,
251
+ });
252
+ const reasoningMode = await selectPrompt({
253
+ message: `Reasoning mode for ${modelName}`,
254
+ default: reasoningModeChoices.some((choice) => choice.value === suggestedMode)
255
+ ? suggestedMode
256
+ : 'auto',
257
+ choices: reasoningModeChoices,
258
+ });
259
+ const reasoning = { ...currentModel.reasoning, mode: reasoningMode };
260
+ if (reasoningMode === 'on') {
261
+ const efforts = reasoningEffortsForModel(providerType, modelId);
262
+ const configuredEffort = currentModel.reasoning?.effort ?? existing.reasoning?.effort;
263
+ const defaultEffort = efforts.includes(configuredEffort)
264
+ ? configuredEffort
265
+ : efforts.includes(DEFAULT_CONFIG.reasoning.effort)
266
+ ? DEFAULT_CONFIG.reasoning.effort
267
+ : efforts[0];
268
+ reasoning.effort = await selectPrompt({
269
+ message: `Reasoning effort for ${modelName}`,
270
+ default: defaultEffort,
271
+ choices: efforts.map((effort) => ({
272
+ name: effort,
273
+ value: effort,
274
+ description: effortDescription(effort),
275
+ })),
276
+ });
277
+ } else {
278
+ delete reasoning.effort;
279
+ }
280
+ models[modelName] = { ...currentModel, modelId, reasoning };
196
281
  const addAnother = await confirmPrompt({
197
282
  message: 'Add another model?',
198
283
  default: false,
@@ -214,6 +299,11 @@ export async function runSetup(dependencies = {}) {
214
299
  })),
215
300
  });
216
301
  const selectedModel = models[defaultModel];
302
+ const selectedReasoning = {
303
+ ...DEFAULT_CONFIG.reasoning,
304
+ ...existing.reasoning,
305
+ ...selectedModel.reasoning,
306
+ };
217
307
 
218
308
  // ── 5. Commit message language ──────────────────────────────────────
219
309
 
@@ -230,7 +320,7 @@ export async function runSetup(dependencies = {}) {
230
320
  apiUrl,
231
321
  apiKey,
232
322
  apiKeyEnv,
233
- providerType: presetAdapter || existingProvider.providerType || 'custom',
323
+ providerType,
234
324
  defaultModel,
235
325
  models,
236
326
  ...(existingProvider.retry ? { retry: existingProvider.retry } : {}),
@@ -257,6 +347,7 @@ export async function runSetup(dependencies = {}) {
257
347
  const report = await connectionCheck({
258
348
  ...entry,
259
349
  ...selectedModel,
350
+ reasoning: selectedReasoning,
260
351
  apiKey: apiKeyEnv ? process.env[apiKeyEnv] : apiKey,
261
352
  maxTokens: 64,
262
353
  timeoutMs: 120000,
@@ -292,6 +383,16 @@ export async function runSetup(dependencies = {}) {
292
383
  console.log(
293
384
  ' ' + chalk.dim(` Provider: ${providerName}/${defaultModel} (${selectedModel.modelId})`),
294
385
  );
386
+ console.log(
387
+ ' ' +
388
+ chalk.dim(
389
+ ` Reasoning: ${
390
+ selectedReasoning.mode === 'on'
391
+ ? `${selectedReasoning.mode}/${selectedReasoning.effort}`
392
+ : selectedReasoning.mode
393
+ }`,
394
+ ),
395
+ );
295
396
  console.log(
296
397
  ' ' + chalk.dim(` API key: ${apiKeyEnv ? `env:${apiKeyEnv}` : maskApiKey(apiKey)}`),
297
398
  );