converse-mcp-server 3.3.0 → 3.5.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/.env.example CHANGED
@@ -80,9 +80,9 @@ OPENROUTER_API_KEY=your_openrouter_api_key_here
80
80
  # WARNING: Interactive policies may cause hangs in server/headless mode
81
81
  # CODEX_APPROVAL_POLICY=never
82
82
 
83
- # Default Codex model (default: gpt-5.6-sol)
84
- # Options: gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark
85
- # CODEX_MODEL=gpt-5.6-sol
83
+ # Default Codex backend model (default: gpt-6-astra). Per-request override: models: ["codex:<model>"]
84
+ # Options: gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.3-codex-spark
85
+ # CODEX_MODEL=gpt-6-astra
86
86
 
87
87
  # ============================================
88
88
  # Server Configuration
package/README.md CHANGED
@@ -267,7 +267,7 @@ Reasoning maps to `high` (enabled) or `none` (disabled) on Medium 3.5 and Small;
267
267
  - **deepseek-v4-pro** (default; aliases: `deepseek`, `deepseek-pro`): Flagship MoE model with thinking mode (1M context, 384K max output, text-only)
268
268
  - **deepseek-v4-flash** (alias: `deepseek-flash`): Faster, lower-cost V4 tier with thinking mode (1M context, 384K max output, text-only)
269
269
 
270
- Thinking mode maps `reasoning_effort` to `none` (off), `high` (enabled levels), or `max`.
270
+ Thinking mode maps `reasoning_effort` to `none` (off), `high` (enabled levels up to `high`), or `max` (`xhigh` and `max`).
271
271
 
272
272
  ### OpenRouter Models
273
273
 
@@ -281,7 +281,9 @@ Any other model works via its full `provider/model` slug or the `openrouter:` na
281
281
 
282
282
  ### Codex Models
283
283
 
284
- - **codex**: OpenAI Codex agentic coding assistant
284
+ - **codex**: OpenAI Codex agentic coding assistant (GPT-6 Astra by default)
285
+ - Pick another backend per request with `codex:<model>` (e.g. `codex:sol`, `codex:gpt-5.6-terra`) or globally with `CODEX_MODEL`; backends: `gpt-6-astra` (alias `astra`), `gpt-5.6-sol` (`sol`), `gpt-5.6-terra` (`terra`), `gpt-5.6-luna` (`luna`), `gpt-5.5`, `gpt-5.3-codex-spark` (`spark`)
286
+ - `reasoning_effort` maps onto the tiers the chosen backend accepts (GPT-6 Astra: `low` through `max`, no `none`)
285
287
  - Thread-based sessions with persistent context
286
288
  - Direct filesystem access from working directory
287
289
  - Typical response time: 6-20 seconds (longer for complex tasks)
@@ -357,6 +359,7 @@ CODEX_API_KEY=your_codex_api_key_here # Optional if ChatGPT login availabl
357
359
  CODEX_SANDBOX_MODE=read-only # read-only (default), workspace-write, danger-full-access
358
360
  CODEX_SKIP_GIT_CHECK=true # true (default), false
359
361
  CODEX_APPROVAL_POLICY=never # never (default), untrusted, on-failure, on-request
362
+ CODEX_MODEL=gpt-6-astra # gpt-6-astra (default), gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5
360
363
  ```
361
364
 
362
365
  ### Configuration Options
package/docs/API.md CHANGED
@@ -84,8 +84,8 @@ MCP_TRANSPORT=stdio npm start
84
84
  },
85
85
  "reasoning_effort": {
86
86
  "type": "string",
87
- "enum": ["none", "minimal", "low", "medium", "high", "max"],
88
- "description": "Reasoning depth for thinking models. 'none' (fastest, GPT-5.1+ only), 'minimal', 'low', 'medium' (balanced), 'high', 'max'. Default: 'medium'."
87
+ "enum": ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
88
+ "description": "Reasoning depth for thinking models, weakest to strongest. Passed through by name when the model accepts it, otherwise clamped to the nearest tier it does. Default: 'medium'."
89
89
  },
90
90
  "async": {
91
91
  "type": "boolean",
@@ -404,7 +404,7 @@ Provide models as plain name strings in the `models` array. Bare names and alias
404
404
  |-------|---------|---------|-------|
405
405
  | `grok-4.5` | `grok`, `grok-4.5-latest`, `grok-build-latest` | 500K | Flagship: image input, reasoning content, native web/X search via Agent Tools |
406
406
 
407
- `reasoning_effort` maps to Grok's `low`/`medium`/`high`; Grok 4.5 always reasons and cannot be disabled. Web search is attached automatically and the model decides whether to use it.
407
+ `reasoning_effort` clamps into Grok's `low`/`medium`/`high` (`xhigh` and `max` become `high`); Grok 4.5 always reasons and cannot be disabled. Web search is attached automatically and the model decides whether to use it.
408
408
 
409
409
  ### Anthropic Models (API-based)
410
410
 
@@ -417,7 +417,7 @@ Provide models as plain name strings in the `models` array. Bare names and alias
417
417
  | `claude-sonnet-4-6` | `sonnet`, `sonnet-4.6` | 200K (1M beta) | 64K | Best speed/intelligence balance, adaptive thinking |
418
418
  | `claude-haiku-4-5-20251001` | `haiku`, `haiku-4.5` | 200K | 64K | Fast and intelligent |
419
419
 
420
- Models with adaptive thinking control depth via `reasoning_effort`, which maps to Anthropic's `effort` parameter. System prompts are automatically cached for 1 hour; cache stats appear in response metadata as `cache_creation_input_tokens` / `cache_read_input_tokens`.
420
+ Models with adaptive thinking control depth via `reasoning_effort`, which is passed by name to Anthropic's `effort` parameter and clamped to what each model accepts: Fable 5, Opus 5, Opus 4.8, and Opus 4.7 take `low`–`max`; Opus 4.6 and Sonnet 4.6 lack `xhigh` (it becomes `max`); Opus 4.5 tops out at `high`. `none` and `minimal` become `low` everywhere. System prompts are automatically cached for 1 hour; cache stats appear in response metadata as `cache_creation_input_tokens` / `cache_read_input_tokens`.
421
421
 
422
422
  ### Mistral Models
423
423
 
@@ -436,7 +436,7 @@ Models with adaptive thinking control depth via `reasoning_effort`, which maps t
436
436
  | `deepseek-v4-pro` | `deepseek`, `deepseek-pro` | 1M | 384K | Flagship MoE, thinking mode, text-only |
437
437
  | `deepseek-v4-flash` | `deepseek-flash` | 1M | 384K | Faster, lower-cost V4 tier, text-only |
438
438
 
439
- `reasoning_effort`: `none` disables thinking; enabled levels use `high`; `max` uses `max`.
439
+ `reasoning_effort`: `none` disables thinking; enabled levels up to `high` use `high`; `xhigh` and `max` use `max`.
440
440
 
441
441
  ### OpenRouter Models
442
442
 
@@ -456,12 +456,13 @@ Any other model works via its full `provider/model` slug (e.g. `anthropic/claude
456
456
 
457
457
  **Codex** is an agentic coding assistant with direct filesystem access:
458
458
 
459
- - **Model**: `codex` (underlying model: GPT-5.6)
459
+ - **Model**: `codex` (underlying model: GPT-6 Astra by default)
460
+ - **Backend selection**: `codex:<model>` per request (e.g. `codex:astra`, `codex:sol`, `codex:gpt-5.6-terra`), or `CODEX_MODEL` globally; unknown names pass through to the CLI verbatim
460
461
  - **Thread-based sessions**: persistent conversation history via `continuation_id` in `chat` mode
461
462
  - **Direct file access**: reads files from the working directory (paths relative to `CLIENT_CWD`)
462
463
  - **Response times**: 6-20 seconds typical (complex tasks may take minutes)
463
464
  - **Authentication**: ChatGPT login OR `CODEX_API_KEY` (NOT `OPENAI_API_KEY`)
464
- - `reasoning_effort` and web search are not applicable — Codex manages its own execution
465
+ - `reasoning_effort` is clamped onto the tiers the chosen backend accepts (GPT-6 Astra: `low`–`max`, no `none`; GPT-5.6: `none`–`max`); web search is not applicable — Codex manages its own execution
465
466
 
466
467
  ### Claude Agent SDK (subscription)
467
468
 
package/docs/PROVIDERS.md CHANGED
@@ -101,9 +101,11 @@ This guide documents all supported AI providers in the Converse MCP Server and t
101
101
  - `CODEX_SANDBOX_MODE` - Filesystem access control (default: read-only)
102
102
  - `CODEX_SKIP_GIT_CHECK` - Skip Git repository validation (default: true)
103
103
  - `CODEX_APPROVAL_POLICY` - Command approval behavior (default: never)
104
- - `CODEX_MODEL` - Underlying model for Codex sessions (default: gpt-5.6-sol; e.g. gpt-5.6-terra, gpt-5.6-luna, gpt-5.5)
104
+ - `CODEX_MODEL` - Underlying model for Codex sessions (default: gpt-6-astra; e.g. gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5)
105
105
  - **Supported Models**:
106
- - `codex` - OpenAI Codex agentic coding assistant (GPT-5.6 by default)
106
+ - `codex` - OpenAI Codex agentic coding assistant (GPT-6 Astra by default)
107
+ - `codex:<model>` - Same, with an explicit backend: `codex:astra`, `codex:sol`, `codex:terra`, `codex:luna`, `codex:gpt-5.5`, `codex:spark`, or any slug the Codex CLI knows
108
+ - `reasoning_effort` is clamped onto what the backend accepts (GPT-6 Astra: `low`–`max`, no `none`)
107
109
  - Thread-based sessions with persistent context
108
110
  - Direct filesystem access from working directory
109
111
  - Typical response time: 6-20 seconds (longer for complex tasks)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converse-mcp-server",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "Converse MCP Server - Converse with other LLMs with chat and consensus tools",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -93,28 +93,28 @@
93
93
  ".env.example"
94
94
  ],
95
95
  "dependencies": {
96
- "@anthropic-ai/claude-agent-sdk": "^0.3.258",
96
+ "@anthropic-ai/claude-agent-sdk": "^0.3.261",
97
97
  "@anthropic-ai/sdk": "^0.123.0",
98
98
  "@github/copilot-sdk": "^1.0.11",
99
- "@google/genai": "^2.20.0",
99
+ "@google/genai": "^2.21.0",
100
100
  "@lydell/node-pty": "1.2.0-beta.15",
101
101
  "@mistralai/mistralai": "^2.6.4",
102
102
  "@modelcontextprotocol/sdk": "^1.30.0",
103
- "@openai/codex-sdk": "^0.152.1",
103
+ "@openai/codex-sdk": "^0.153.3",
104
104
  "cors": "^2.8.6",
105
105
  "dotenv": "^17.4.2",
106
106
  "express": "^5.2.1",
107
107
  "lru-cache": "^11.5.2",
108
108
  "nanoid": "^6.0.1",
109
- "openai": "^7.9.0",
109
+ "openai": "^7.10.0",
110
110
  "p-limit": "^7.3.2",
111
111
  "vite": "^8.2.2"
112
112
  },
113
113
  "devDependencies": {
114
- "@vitest/coverage-v8": "^4.1.11",
114
+ "@vitest/coverage-v8": "^5.0.0",
115
115
  "cross-env": "^10.1.0",
116
- "eslint": "^10.9.1",
116
+ "eslint": "^10.10.0",
117
117
  "rimraf": "^6.1.3",
118
- "vitest": "^4.1.11"
118
+ "vitest": "^5.0.0"
119
119
  }
120
120
  }
package/src/config.js CHANGED
@@ -280,9 +280,9 @@ const CONFIG_SCHEMA = {
280
280
  },
281
281
  CODEX_MODEL: {
282
282
  type: 'string',
283
- default: 'gpt-5.6-sol',
283
+ default: 'gpt-6-astra',
284
284
  description:
285
- 'Default Codex model (e.g., gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5)',
285
+ 'Default Codex backend model (e.g., gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5)',
286
286
  },
287
287
 
288
288
  // Copilot configuration
@@ -295,11 +295,12 @@ function generateConfigurationTips(tools) {
295
295
  if (effortSchema.enum) {
296
296
  for (const value of effortSchema.enum) {
297
297
  const descriptions = {
298
- none: 'No reasoning, fastest response (GPT-5.1+ only)',
298
+ none: 'Reasoning off, fastest response (where the model allows it)',
299
299
  minimal: 'Quick responses with minimal reasoning',
300
300
  low: 'Light analysis, simple problems',
301
301
  medium: 'Balanced reasoning (default)',
302
302
  high: 'Deep analysis, complex problems',
303
+ xhigh: 'Very deep analysis, one step below the ceiling',
303
304
  max: 'Maximum reasoning capability',
304
305
  };
305
306
  output += `- **${value}**: ${descriptions[value] || value}\n`;
@@ -10,6 +10,14 @@
10
10
 
11
11
  import { debugLog, debugError } from '../utils/console.js';
12
12
  import { ProviderError, ErrorCodes, StopReasons } from './interface.js';
13
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
14
+
15
+ // Values each model accepts for the `effort` parameter. Anthropic cannot
16
+ // switch reasoning off via effort, so 'none' and 'minimal' clamp up to 'low'.
17
+ // Not every model that supports 'max' supports 'xhigh'.
18
+ const EFFORT_TIERS_FULL = ['low', 'medium', 'high', 'xhigh', 'max'];
19
+ const EFFORT_TIERS_NO_XHIGH = ['low', 'medium', 'high', 'max'];
20
+ const EFFORT_TIERS_LEGACY = ['low', 'medium', 'high'];
13
21
 
14
22
  // Define supported Claude models with their capabilities
15
23
  const SUPPORTED_MODELS = {
@@ -26,6 +34,7 @@ const SUPPORTED_MODELS = {
26
34
  timeout: 1800000,
27
35
  supportsEffort: true,
28
36
  effortGA: true,
37
+ effortTiers: EFFORT_TIERS_FULL,
29
38
  supportsCompaction: true,
30
39
  description:
31
40
  'Claude Fable 5 - Most capable model for the most demanding reasoning and long-horizon agentic work',
@@ -51,6 +60,7 @@ const SUPPORTED_MODELS = {
51
60
  timeout: 1800000,
52
61
  supportsEffort: true,
53
62
  effortGA: true,
63
+ effortTiers: EFFORT_TIERS_FULL,
54
64
  supportsCompaction: true,
55
65
  description:
56
66
  'Claude Opus 5 - Most capable Opus for complex agentic coding and deep reasoning',
@@ -79,6 +89,7 @@ const SUPPORTED_MODELS = {
79
89
  timeout: 1800000,
80
90
  supportsEffort: true,
81
91
  effortGA: true,
92
+ effortTiers: EFFORT_TIERS_FULL,
82
93
  supports1MContext: true,
83
94
  supportsCompaction: true,
84
95
  description:
@@ -109,6 +120,7 @@ const SUPPORTED_MODELS = {
109
120
  timeout: 1800000,
110
121
  supportsEffort: true,
111
122
  effortGA: true,
123
+ effortTiers: EFFORT_TIERS_FULL,
112
124
  supports1MContext: true,
113
125
  supportsCompaction: true,
114
126
  description:
@@ -139,6 +151,7 @@ const SUPPORTED_MODELS = {
139
151
  timeout: 1800000,
140
152
  supportsEffort: true,
141
153
  effortGA: true,
154
+ effortTiers: EFFORT_TIERS_NO_XHIGH,
142
155
  supports1MContext: true,
143
156
  supportsCompaction: true,
144
157
  description:
@@ -167,6 +180,7 @@ const SUPPORTED_MODELS = {
167
180
  maxThinkingTokens: 64000,
168
181
  timeout: 900000,
169
182
  supportsEffort: true, // Opus 4.5 effort parameter (requires beta header)
183
+ effortTiers: EFFORT_TIERS_LEGACY,
170
184
  description:
171
185
  'Claude Opus 4.5 - Previous most intelligent model combining maximum capability with practical performance',
172
186
  aliases: [
@@ -223,6 +237,7 @@ const SUPPORTED_MODELS = {
223
237
  timeout: 900000,
224
238
  supportsEffort: true,
225
239
  effortGA: true, // Effort is generally available, no beta header required
240
+ effortTiers: EFFORT_TIERS_NO_XHIGH,
226
241
  supports1MContext: true, // Beta 1M context support
227
242
  supportsCompaction: true, // Beta server-side context compaction
228
243
  description:
@@ -311,22 +326,10 @@ const THINKING_BUDGETS = {
311
326
  low: 0.15, // 15% of max thinking tokens
312
327
  medium: 0.33, // 33% of max thinking tokens (default)
313
328
  high: 0.67, // 67% of max thinking tokens
329
+ xhigh: 0.85, // 85% of max thinking tokens
314
330
  max: 1.0, // 100% of max thinking tokens
315
331
  };
316
332
 
317
- /**
318
- * Effort parameter mapping for Opus 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6, and Opus 4.5
319
- * Maps reasoning_effort values to Anthropic's effort parameter values
320
- */
321
- const EFFORT_MAP = {
322
- none: 'low',
323
- minimal: 'low',
324
- low: 'medium',
325
- medium: 'high',
326
- high: 'xhigh',
327
- max: 'max',
328
- };
329
-
330
333
  /**
331
334
  * Custom error class for Anthropic provider errors
332
335
  */
@@ -716,16 +719,17 @@ export const anthropicProvider = {
716
719
 
717
720
  // Add effort parameter for models that support it (uses output_config)
718
721
  if (modelConfig.supportsEffort && reasoning_effort) {
719
- const effortValue = EFFORT_MAP[reasoning_effort];
720
- if (effortValue) {
721
- requestPayload.output_config = {
722
- ...requestPayload.output_config,
723
- effort: effortValue,
724
- };
725
- debugLog(
726
- `[Anthropic] Effort parameter set to "${effortValue}" for ${resolvedModel} (from reasoning_effort: ${reasoning_effort})`,
727
- );
728
- }
722
+ const effortValue = clampReasoningEffort(
723
+ reasoning_effort,
724
+ modelConfig.effortTiers,
725
+ );
726
+ requestPayload.output_config = {
727
+ ...requestPayload.output_config,
728
+ effort: effortValue,
729
+ };
730
+ debugLog(
731
+ `[Anthropic] Effort parameter set to "${effortValue}" for ${resolvedModel} (from reasoning_effort: ${reasoning_effort})`,
732
+ );
729
733
  }
730
734
 
731
735
  // If streaming is requested and model doesn't support it, fall back to non-streaming
@@ -16,36 +16,74 @@
16
16
  import { debugLog, debugError } from '../utils/console.js';
17
17
  import { ProviderError, ErrorCodes, StopReasons } from './interface.js';
18
18
  import { normalizeExtendedPath } from '../utils/pathUtils.js';
19
+ import {
20
+ EFFORT_LADDER,
21
+ clampReasoningEffort,
22
+ } from '../utils/reasoningEffort.js';
19
23
 
20
- // Supported Codex models with their configurations
24
+ /**
25
+ * Backend models Codex can run, keyed by the slug passed to the CLI as
26
+ * --model. The reasoning tiers are the ones each model's API accepts, verified
27
+ * against the API's own rejection messages (gpt-6-astra: "Supported values
28
+ * are: 'low', 'medium', 'high', 'xhigh', and 'max'"). The SDK's
29
+ * ModelReasoningEffort type is the union across models, so the backend is the
30
+ * authority and requests are clamped per model.
31
+ *
32
+ * Codex also exposes 'ultra' above 'max', but that tier turns on automatic
33
+ * sub-agent delegation — a change in how the run executes, not just how deep
34
+ * it reasons — so it is deliberately absent from every supportedEfforts list
35
+ * and nothing at the tool level can select it.
36
+ */
37
+ const CODEX_BACKEND_MODELS = {
38
+ 'gpt-6-astra': {
39
+ aliases: ['astra', 'gpt-6', 'gpt6', 'gpt6-astra'],
40
+ contextWindow: 272000,
41
+ supportedEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
42
+ },
43
+ 'gpt-5.6-sol': {
44
+ aliases: ['sol', 'gpt-5.6', 'gpt5.6', 'gpt-5.6-codex'],
45
+ contextWindow: 272000,
46
+ supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
47
+ },
48
+ 'gpt-5.6-terra': {
49
+ aliases: ['terra'],
50
+ contextWindow: 272000,
51
+ supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
52
+ },
53
+ 'gpt-5.6-luna': {
54
+ aliases: ['luna'],
55
+ contextWindow: 272000,
56
+ supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
57
+ },
58
+ 'gpt-5.5': {
59
+ aliases: ['gpt5.5'],
60
+ contextWindow: 272000,
61
+ supportedEfforts: ['low', 'medium', 'high', 'xhigh'],
62
+ },
63
+ 'gpt-5.3-codex-spark': {
64
+ aliases: ['spark', 'codex-spark'],
65
+ contextWindow: 128000,
66
+ supportedEfforts: ['low', 'medium', 'high', 'xhigh'],
67
+ },
68
+ };
69
+
70
+ const DEFAULT_BACKEND_MODEL = 'gpt-6-astra';
71
+
72
+ // The single user-facing model the router exposes. The backend model behind it
73
+ // comes from CODEX_MODEL, or from a `codex:<model>` spec.
21
74
  const SUPPORTED_MODELS = {
22
75
  codex: {
23
76
  modelName: 'codex',
24
- friendlyName: 'OpenAI Codex (GPT-5.6)',
25
- contextWindow: 400000,
77
+ friendlyName: 'OpenAI Codex (GPT-6 Astra)',
78
+ contextWindow: CODEX_BACKEND_MODELS[DEFAULT_BACKEND_MODEL].contextWindow,
26
79
  maxOutputTokens: 128000,
27
80
  supportsStreaming: true,
28
81
  supportsImages: true, // Codex SDK 0.118+ supports images via --image (local_image input)
29
82
  supportsWebSearch: false, // Codex accesses files directly, not web
30
- // Reasoning tiers this model's backend actually accepts. GPT-5.6 dropped
31
- // 'minimal' and added 'none', while the SDK's ModelReasoningEffort type
32
- // still advertises the pre-5.6 set — the backend is the authority, so the
33
- // accepted tiers are declared per model and requests are clamped onto them.
34
- supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh'],
35
83
  timeout: 1800000, // 30 minutes
36
84
  description:
37
- 'OpenAI Codex agentic coding assistant with local file access and tool execution (GPT-5.6)',
38
- aliases: [
39
- 'gpt-5-codex',
40
- 'gpt5-codex',
41
- 'gpt-5.2-codex',
42
- 'gpt-5.3-codex',
43
- 'gpt5.3-codex',
44
- 'gpt-5.5',
45
- 'gpt5.5',
46
- 'gpt-5.6-codex',
47
- 'gpt5.6-codex',
48
- ],
85
+ 'OpenAI Codex agentic coding assistant with local file access and tool execution (GPT-6 Astra by default; pick another backend with codex:<model> or CODEX_MODEL)',
86
+ aliases: [],
49
87
  },
50
88
  };
51
89
 
@@ -195,6 +233,57 @@ function extractPromptText(input) {
195
233
  .join('\n\n');
196
234
  }
197
235
 
236
+ /**
237
+ * Resolve a backend slug or alias (case-insensitive) to its catalog slug.
238
+ * @param {string} name
239
+ * @returns {string|null}
240
+ */
241
+ function findBackendSlug(name) {
242
+ const lower = String(name || '').trim().toLowerCase();
243
+ if (!lower) {
244
+ return null;
245
+ }
246
+ if (CODEX_BACKEND_MODELS[lower]) {
247
+ return lower;
248
+ }
249
+ return (
250
+ Object.keys(CODEX_BACKEND_MODELS).find((slug) =>
251
+ CODEX_BACKEND_MODELS[slug].aliases.includes(lower),
252
+ ) || null
253
+ );
254
+ }
255
+
256
+ /**
257
+ * Catalog entry for a backend model, or null when the slug is not catalogued.
258
+ * @param {string} name - Backend slug or alias
259
+ * @returns {{ slug: string, aliases: string[], contextWindow: number, supportedEfforts: string[] }|null}
260
+ */
261
+ export function getBackendModelConfig(name) {
262
+ const slug = findBackendSlug(name);
263
+ return slug ? { slug, ...CODEX_BACKEND_MODELS[slug] } : null;
264
+ }
265
+
266
+ /**
267
+ * Resolve the requested model spec to the backend slug passed to the CLI.
268
+ *
269
+ * `codex` uses CODEX_MODEL (default gpt-6-astra); `codex:<model>` names a
270
+ * backend directly, by slug or alias. Unknown names pass through verbatim so a
271
+ * newly released model works before it is catalogued here — the CLI rejects
272
+ * anything the backend does not know.
273
+ *
274
+ * @param {string} spec - Requested model, e.g. 'codex' or 'codex:sol'
275
+ * @param {Object} [config] - Loaded configuration
276
+ * @returns {string} Backend slug for the SDK's `model` option
277
+ */
278
+ export function resolveBackendModel(spec, config) {
279
+ const raw = String(spec || '').trim();
280
+ const requested = raw.toLowerCase().startsWith('codex:')
281
+ ? raw.slice('codex:'.length).trim()
282
+ : '';
283
+ const name = requested || config?.providers?.codexmodel || DEFAULT_BACKEND_MODEL;
284
+ return findBackendSlug(name) || name;
285
+ }
286
+
198
287
  /**
199
288
  * Get thread ID from continuation metadata.
200
289
  * Codex thread IDs are stored per call-plan in `providerThreads`, keyed by a
@@ -216,56 +305,6 @@ async function getThreadIdFromContinuation(
216
305
  }
217
306
  }
218
307
 
219
- /**
220
- * Every Codex reasoning tier, weakest to strongest. Used to clamp a requested
221
- * tier onto the set a given model actually accepts.
222
- */
223
- const EFFORT_LADDER = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'];
224
-
225
- /**
226
- * Tool-level reasoning_effort values translated to their Codex equivalent.
227
- * Tool enum: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'max'
228
- */
229
- const EFFORT_ALIASES = {
230
- none: 'none',
231
- minimal: 'minimal',
232
- low: 'low',
233
- medium: 'medium',
234
- high: 'high',
235
- max: 'xhigh',
236
- };
237
-
238
- /**
239
- * Map a tool-level reasoning_effort onto a tier the target model accepts.
240
- *
241
- * When the requested tier isn't in the model's supported set, the nearest
242
- * *stronger* tier wins: nudging 'minimal' up to 'low' keeps reasoning on,
243
- * where falling back to 'none' would silently switch it off.
244
- *
245
- * @param {string} effort - Tool-level reasoning_effort value
246
- * @param {string[]} [supported] - Tiers the model accepts
247
- * @returns {string} A tier from `supported`
248
- */
249
- export function mapReasoningEffort(effort, supported = EFFORT_LADDER) {
250
- const desired = EFFORT_ALIASES[effort] || 'medium';
251
- if (supported.includes(desired)) {
252
- return desired;
253
- }
254
-
255
- const rank = EFFORT_LADDER.indexOf(desired);
256
- const stronger = EFFORT_LADDER.slice(rank + 1).find((tier) =>
257
- supported.includes(tier),
258
- );
259
- if (stronger) {
260
- return stronger;
261
- }
262
-
263
- const weaker = EFFORT_LADDER.slice(0, rank)
264
- .reverse()
265
- .find((tier) => supported.includes(tier));
266
- return weaker || 'medium';
267
- }
268
-
269
308
  /**
270
309
  * Resolve a user-facing model name to its entry in SUPPORTED_MODELS.
271
310
  * @param {string} modelName
@@ -403,8 +442,9 @@ export const codexProvider = {
403
442
  const approvalPolicy = config.providers?.codexapprovalpolicy || 'never';
404
443
 
405
444
  // Create or resume thread
445
+ const backendModel = resolveBackendModel(model, config);
406
446
  const threadOptions = {
407
- model: config.providers?.codexmodel,
447
+ model: backendModel,
408
448
  workingDirectory,
409
449
  sandboxMode,
410
450
  skipGitRepoCheck,
@@ -413,13 +453,12 @@ export const codexProvider = {
413
453
 
414
454
  if (reasoning_effort) {
415
455
  const supportedEfforts =
416
- findModelConfig(model)?.supportedEfforts ||
417
- SUPPORTED_MODELS.codex.supportedEfforts;
418
- const mappedEffort = mapReasoningEffort(reasoning_effort, supportedEfforts);
456
+ getBackendModelConfig(backendModel)?.supportedEfforts || EFFORT_LADDER;
457
+ const mappedEffort = clampReasoningEffort(reasoning_effort, supportedEfforts);
419
458
  threadOptions.modelReasoningEffort = mappedEffort;
420
- if (mappedEffort !== EFFORT_ALIASES[reasoning_effort]) {
459
+ if (mappedEffort !== reasoning_effort) {
421
460
  debugLog(
422
- `[Codex] reasoning_effort "${reasoning_effort}" not supported by ${model} — using "${mappedEffort}"`,
461
+ `[Codex] reasoning_effort "${reasoning_effort}" not supported by ${backendModel} — using "${mappedEffort}"`,
423
462
  );
424
463
  }
425
464
  }
@@ -470,6 +509,7 @@ export const codexProvider = {
470
509
  metadata: {
471
510
  provider: 'codex',
472
511
  model,
512
+ backendModel,
473
513
  threadId: threadIdFromStream || thread.id,
474
514
  usage: usage
475
515
  ? {
@@ -17,6 +17,7 @@ import { delimiter, dirname, join } from 'node:path';
17
17
  import { fileURLToPath } from 'node:url';
18
18
  import { debugLog, debugError } from '../utils/console.js';
19
19
  import { ProviderError, ErrorCodes, StopReasons } from './interface.js';
20
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
20
21
 
21
22
  const SUPPORTED_MODELS = {
22
23
  copilot: {
@@ -517,20 +518,14 @@ function resolveSessionModel(requestModel, config) {
517
518
  * - session.error → { data: { errorType, message } }
518
519
  */
519
520
  /**
520
- * Map tool-level reasoning_effort values to Copilot SDK's ReasoningEffort.
521
- * Tool enum: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'max'
522
- * SDK enum: 'low' | 'medium' | 'high' | 'xhigh'
521
+ * Values the Copilot SDK's ReasoningEffort type accepts. The SDK tops out at
522
+ * 'xhigh' and cannot switch reasoning off, so 'max' clamps down and
523
+ * 'none'/'minimal' clamp up.
523
524
  */
525
+ const COPILOT_EFFORT_TIERS = ['low', 'medium', 'high', 'xhigh'];
526
+
524
527
  function mapReasoningEffort(effort) {
525
- const mapping = {
526
- none: 'low',
527
- minimal: 'low',
528
- low: 'low',
529
- medium: 'medium',
530
- high: 'high',
531
- max: 'xhigh',
532
- };
533
- return mapping[effort] || undefined;
528
+ return clampReasoningEffort(effort, COPILOT_EFFORT_TIERS);
534
529
  }
535
530
 
536
531
  /**
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { createOpenAICompatibleProvider } from './openai-compatible.js';
9
9
  import { debugLog } from '../utils/console.js';
10
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
10
11
 
11
12
  // Define supported DeepSeek models with their capabilities.
12
13
  // V4 unified the catalog: both tiers share a 1M context window and a 384K output
@@ -65,13 +66,19 @@ function validateApiKey(apiKey) {
65
66
  return apiKey.length >= 32;
66
67
  }
67
68
 
69
+ /**
70
+ * Values DeepSeek's `reasoning_effort` field accepts once thinking is on.
71
+ * There is no lower documented tier, so every enabled level below high clamps
72
+ * up to "high" (the default `medium` runs thinking-on rather than silently
73
+ * disabling it) and `xhigh` clamps up to "max".
74
+ */
75
+ const DEEPSEEK_EFFORT_TIERS = ['high', 'max'];
76
+
68
77
  /**
69
78
  * Map a Converse reasoning_effort level to DeepSeek's thinking-mode request
70
- * fields. DeepSeek exposes only two independent controls: a `thinking` toggle
71
- * ({type:"enabled"|"disabled"}) and a `reasoning_effort` field that accepts
72
- * ONLY "high" or "max". Every enabled level below max maps to "high" (there is
73
- * no lower documented tier), preserving enabled-reasoning intent so the default
74
- * `medium` runs thinking-on rather than silently disabling it.
79
+ * fields. DeepSeek exposes two independent controls: a `thinking` toggle
80
+ * ({type:"enabled"|"disabled"}) and the `reasoning_effort` tier. Only `none`
81
+ * flips the toggle off.
75
82
  */
76
83
  function applyReasoning(requestPayload, reasoningEffort) {
77
84
  if (reasoningEffort === 'none') {
@@ -81,7 +88,10 @@ function applyReasoning(requestPayload, reasoningEffort) {
81
88
  }
82
89
 
83
90
  requestPayload.thinking = { type: 'enabled' };
84
- requestPayload.reasoning_effort = reasoningEffort === 'max' ? 'max' : 'high';
91
+ requestPayload.reasoning_effort = clampReasoningEffort(
92
+ reasoningEffort,
93
+ DEEPSEEK_EFFORT_TIERS,
94
+ );
85
95
  }
86
96
 
87
97
  /**
@@ -31,6 +31,7 @@ import { join, delimiter } from 'node:path';
31
31
  import { randomUUID } from 'node:crypto';
32
32
  import { debugLog, debugError } from '../utils/console.js';
33
33
  import { ProviderError, ErrorCodes, StopReasons } from './interface.js';
34
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
34
35
 
35
36
  // Prompts at or below this length pass directly as the -p argv value (fast
36
37
  // path). Larger prompts are written to a file and -p carries a bootstrap
@@ -174,32 +175,25 @@ export function findAgyBinary() {
174
175
  return _cachedAgyPath;
175
176
  }
176
177
 
178
+ // agy variant tiers per model base. Flash offers Low/Medium/High; Pro has no
179
+ // Medium variant.
180
+ const FLASH_EFFORT_TIERS = ['low', 'medium', 'high'];
181
+ const PRO_EFFORT_TIERS = ['low', 'high'];
182
+
177
183
  /**
178
184
  * Map a reasoning_effort value to the agy parenthesized variant suffix.
179
- * Flash supports Low/Medium/High; Pro supports Low/High (no Medium).
185
+ * Unset effort selects High, agy's own default.
180
186
  * @param {string} base - agy model base ('Gemini 3.8 Flash' / 'Gemini 3.1 Pro')
181
187
  * @param {string} [reasoningEffort]
182
188
  * @returns {string} e.g. '(Low)', '(Medium)', '(High)'
183
189
  */
184
190
  function effortSuffix(base, reasoningEffort) {
185
- const isPro = /pro/i.test(base);
186
- const effort = (reasoningEffort || '').toLowerCase();
187
-
188
- switch (effort) {
189
- case 'none':
190
- case 'minimal':
191
- case 'low':
192
- return '(Low)';
193
- case 'medium':
194
- // Pro has no Medium variant — fall back to High
195
- return isPro ? '(High)' : '(Medium)';
196
- case 'high':
197
- case 'max':
198
- return '(High)';
199
- default:
200
- // unset → High
191
+ if (!reasoningEffort) {
201
192
  return '(High)';
202
193
  }
194
+ const tiers = /pro/i.test(base) ? PRO_EFFORT_TIERS : FLASH_EFFORT_TIERS;
195
+ const tier = clampReasoningEffort(reasoningEffort.toLowerCase(), tiers);
196
+ return `(${tier[0].toUpperCase()}${tier.slice(1)})`;
203
197
  }
204
198
 
205
199
  /**
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { GoogleGenAI } from '@google/genai';
9
9
  import { debugLog, debugError } from '../utils/console.js';
10
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
10
11
 
11
12
  // Define supported Gemini models with their capabilities
12
13
  const SUPPORTED_MODELS = {
@@ -161,9 +162,14 @@ const THINKING_BUDGETS = {
161
162
  low: 0.08, // 8% of max - light reasoning tasks
162
163
  medium: 0.33, // 33% of max - balanced reasoning (default)
163
164
  high: 0.67, // 67% of max - complex analysis
165
+ xhigh: 0.85, // 85% of max - near-full budget
164
166
  max: 1.0, // 100% of max - full thinking budget
165
167
  };
166
168
 
169
+ // thinkingLevel values for models without an explicit thinkingLevels list
170
+ // (Gemini 3.0 Pro exposes only low/high).
171
+ const BINARY_THINKING_LEVELS = ['low', 'high'];
172
+
167
173
  /**
168
174
  * Custom error class for Google provider errors
169
175
  */
@@ -527,37 +533,13 @@ export const googleProvider = {
527
533
  // Add thinking configuration for models that support it
528
534
  if (modelConfig.supportsThinking && reasoning_effort) {
529
535
  if (modelConfig.thinkingMode === 'level') {
530
- let thinkingLevel;
531
- if (modelConfig.thinkingLevels) {
532
- // Model supports specific levels (e.g., Gemini 3.1 Pro: minimal/low/medium/high)
533
- const levelMap = {
534
- none: 'minimal',
535
- minimal: 'minimal',
536
- low: 'low',
537
- medium: 'medium',
538
- high: 'high',
539
- max: 'high',
540
- };
541
- thinkingLevel = levelMap[reasoning_effort] || 'high';
542
- if (!modelConfig.thinkingLevels.includes(thinkingLevel)) {
543
- // Clamp to the nearest supported level: a request below the
544
- // model's floor (e.g. minimal on 3.8 Flash) takes the lowest
545
- // level, anything else the highest.
546
- const rank = ['minimal', 'low', 'medium', 'high'];
547
- const [lowest] = modelConfig.thinkingLevels;
548
- thinkingLevel =
549
- rank.indexOf(thinkingLevel) < rank.indexOf(lowest)
550
- ? lowest
551
- : modelConfig.thinkingLevels[
552
- modelConfig.thinkingLevels.length - 1
553
- ];
554
- }
555
- } else {
556
- // Binary levels only (Gemini 3.0 Pro: low/high)
557
- thinkingLevel = ['minimal', 'low'].includes(reasoning_effort)
558
- ? 'low'
559
- : 'high';
560
- }
536
+ // Gemini 3.x thinkingLevel names are tool-level tiers, so the request
537
+ // clamps straight onto the levels the model lists (none → minimal or
538
+ // the floor, xhigh/max the ceiling).
539
+ const thinkingLevel = clampReasoningEffort(
540
+ reasoning_effort,
541
+ modelConfig.thinkingLevels || BINARY_THINKING_LEVELS,
542
+ );
561
543
  generationConfig.thinkingConfig = { thinkingLevel };
562
544
  } else {
563
545
  // Gemini 2.5: Use thinking budget (token count)
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { debugLog, debugError } from '../utils/console.js';
9
9
  import { ProviderError, ErrorCodes, StopReasons } from './interface.js';
10
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
10
11
 
11
12
  // Define supported Mistral models with their capabilities.
12
13
  // Reasoning (`reasoning_effort`) is supported only on Medium 3.5 and Small 4;
@@ -61,19 +62,22 @@ const SUPPORTED_MODELS = {
61
62
  },
62
63
  };
63
64
 
65
+ /**
66
+ * Values Mistral documents for `reasoning_effort`. Every enabled level clamps
67
+ * to "high" so the default `medium` runs thinking-on; only `none` disables.
68
+ */
69
+ const MISTRAL_EFFORT_TIERS = ['none', 'high'];
70
+
64
71
  /**
65
72
  * Map a Converse reasoning_effort level to Mistral's documented request value.
66
- * Mistral documents only "high" and "none". Every enabled level maps to "high"
67
- * to preserve enabled-reasoning intent (Converse's default `medium` runs
68
- * thinking-on); only `none` disables. Returns null when reasoning must not be
69
- * forwarded — i.e. the model does not support it (Large 3) or is an unknown
70
- * pass-through ID (capability-gated).
73
+ * Returns null when reasoning must not be forwarded i.e. the model does not
74
+ * support it (Large 3) or is an unknown pass-through ID (capability-gated).
71
75
  */
72
76
  function resolveReasoningEffort(level, modelConfig) {
73
77
  if (!modelConfig?.supportsReasoning) {
74
78
  return null;
75
79
  }
76
- return level === 'none' ? 'none' : 'high';
80
+ return clampReasoningEffort(level, MISTRAL_EFFORT_TIERS);
77
81
  }
78
82
 
79
83
  /**
@@ -7,6 +7,21 @@
7
7
 
8
8
  import OpenAI from 'openai';
9
9
  import { debugLog, debugError } from '../utils/console.js';
10
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
11
+
12
+ // Values each family accepts for reasoning effort, per the model pages at
13
+ // developers.openai.com/api/docs/models. GPT-5.6 is the only family with
14
+ // 'max'; the GPT-5.4 tier stops at 'xhigh'; the original GPT-5 minis kept
15
+ // 'minimal' but never gained 'xhigh'; the o-series predates both ends of the
16
+ // ladder; GPT-5.4 Pro starts at 'medium'. Models without a list are passed
17
+ // the requested value unchanged, except uncatalogued GPT-5 Pro snapshots,
18
+ // which are only known to accept 'high'.
19
+ const GPT_56_EFFORT_TIERS = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
20
+ const GPT_54_EFFORT_TIERS = ['none', 'low', 'medium', 'high', 'xhigh'];
21
+ const GPT_54_PRO_EFFORT_TIERS = ['medium', 'high', 'xhigh'];
22
+ const GPT_5_EFFORT_TIERS = ['minimal', 'low', 'medium', 'high'];
23
+ const O_SERIES_EFFORT_TIERS = ['low', 'medium', 'high'];
24
+ const PRO_PASSTHROUGH_EFFORT_TIERS = ['high'];
10
25
 
11
26
  // Define supported models with their capabilities
12
27
  const SUPPORTED_MODELS = {
@@ -19,7 +34,7 @@ const SUPPORTED_MODELS = {
19
34
  supportsImages: true,
20
35
  supportsWebSearch: true,
21
36
  supportsResponsesAPI: true,
22
- supportsNoneReasoningEffort: true,
37
+ supportedEfforts: GPT_56_EFFORT_TIERS,
23
38
  timeout: 10800000, // 3 hours
24
39
  description:
25
40
  'Flagship GPT-5.6 model (1M context, 128K output) - Frontier reasoning, coding, agentic workflows. Most token-efficient flagship',
@@ -44,7 +59,7 @@ const SUPPORTED_MODELS = {
44
59
  supportsImages: true,
45
60
  supportsWebSearch: true,
46
61
  supportsResponsesAPI: true,
47
- supportsNoneReasoningEffort: true,
62
+ supportedEfforts: GPT_56_EFFORT_TIERS,
48
63
  timeout: 5400000, // 90 minutes
49
64
  description:
50
65
  'Lower-cost GPT-5.6 (400K context, 128K output) - Performance competitive with GPT-5.5 at half the flagship price',
@@ -59,7 +74,7 @@ const SUPPORTED_MODELS = {
59
74
  supportsImages: true,
60
75
  supportsWebSearch: true,
61
76
  supportsResponsesAPI: true,
62
- supportsNoneReasoningEffort: true,
77
+ supportedEfforts: GPT_56_EFFORT_TIERS,
63
78
  timeout: 1800000, // 30 minutes
64
79
  description:
65
80
  'Fastest, most affordable GPT-5.6 (400K context, 128K output) - High-volume, latency-sensitive workloads',
@@ -74,7 +89,7 @@ const SUPPORTED_MODELS = {
74
89
  supportsImages: true,
75
90
  supportsWebSearch: true,
76
91
  supportsResponsesAPI: true,
77
- supportsNoneReasoningEffort: true,
92
+ supportedEfforts: GPT_54_EFFORT_TIERS,
78
93
  timeout: 10800000, // 3 hours
79
94
  description:
80
95
  'Latest flagship model (1M context, 128K output) - Superior reasoning, coding, agentic workflows, computer use. Most token-efficient reasoning model',
@@ -92,6 +107,7 @@ const SUPPORTED_MODELS = {
92
107
  supportsImages: true,
93
108
  supportsWebSearch: true,
94
109
  supportsResponsesAPI: true,
110
+ supportedEfforts: GPT_5_EFFORT_TIERS,
95
111
  timeout: 5400000, // 90 minutes
96
112
  description:
97
113
  'Faster, cost-efficient GPT-5 (400K context, 128K output) - Well-defined tasks, precise prompts',
@@ -106,6 +122,7 @@ const SUPPORTED_MODELS = {
106
122
  supportsImages: true,
107
123
  supportsWebSearch: false, // GPT-5-nano doesn't support web search
108
124
  supportsResponsesAPI: true,
125
+ supportedEfforts: GPT_5_EFFORT_TIERS,
109
126
  timeout: 1800000, // 30 minutes
110
127
  description:
111
128
  'Fastest, most cost-efficient GPT-5 (400K context, 128K output) - Summarization, classification',
@@ -120,6 +137,7 @@ const SUPPORTED_MODELS = {
120
137
  supportsImages: true,
121
138
  supportsWebSearch: true,
122
139
  supportsResponsesAPI: true,
140
+ supportedEfforts: GPT_54_EFFORT_TIERS,
123
141
  timeout: 5400000, // 90 minutes
124
142
  description:
125
143
  'Fast, efficient GPT-5.4 (400K context, 128K output) - Coding, subagents, computer use, tool use. 2x faster than GPT-5 mini',
@@ -139,6 +157,7 @@ const SUPPORTED_MODELS = {
139
157
  supportsImages: true,
140
158
  supportsWebSearch: false,
141
159
  supportsResponsesAPI: true,
160
+ supportedEfforts: GPT_54_EFFORT_TIERS,
142
161
  timeout: 1800000, // 30 minutes
143
162
  description:
144
163
  'Smallest, cheapest GPT-5.4 (400K context, 128K output) - Classification, data extraction, ranking, coding subagents',
@@ -159,6 +178,7 @@ const SUPPORTED_MODELS = {
159
178
  supportsWebSearch: true,
160
179
  supportsResponsesAPI: true,
161
180
  supportsDeepResearch: false,
181
+ supportedEfforts: GPT_54_PRO_EFFORT_TIERS,
162
182
  timeout: 10800000, // 180 minutes
163
183
  description:
164
184
  'Maximum performance reasoning model (1M context, 272K output) - Most complex tasks, extended compute time (EXPENSIVE)',
@@ -179,6 +199,7 @@ const SUPPORTED_MODELS = {
179
199
  supportsImages: true,
180
200
  supportsWebSearch: true,
181
201
  supportsResponsesAPI: true,
202
+ supportedEfforts: O_SERIES_EFFORT_TIERS,
182
203
  timeout: 1800000, // 30 minutes
183
204
  description:
184
205
  'Strong reasoning (200K context) - Logical problems, code generation, systematic analysis',
@@ -193,6 +214,7 @@ const SUPPORTED_MODELS = {
193
214
  supportsImages: true,
194
215
  supportsWebSearch: true,
195
216
  supportsResponsesAPI: true,
217
+ supportedEfforts: O_SERIES_EFFORT_TIERS,
196
218
  timeout: 10800000, // 180 minutes
197
219
  description:
198
220
  'Professional-grade reasoning (200K context) - EXTREMELY EXPENSIVE: Only for the most complex problems',
@@ -207,6 +229,7 @@ const SUPPORTED_MODELS = {
207
229
  supportsImages: true,
208
230
  supportsWebSearch: true,
209
231
  supportsResponsesAPI: true,
232
+ supportedEfforts: O_SERIES_EFFORT_TIERS,
210
233
  timeout: 540000, // 9 minutes
211
234
  description:
212
235
  'Latest reasoning model (200K context) - Optimized for shorter contexts, rapid reasoning',
@@ -311,17 +334,32 @@ function resolveModelName(modelName) {
311
334
  }
312
335
 
313
336
  /**
314
- * Resolve the reasoning effort actually sent to the API for a given model.
315
- * GPT-5 Pro models only accept 'high'. GPT-5.6 models dropped 'minimal'
316
- * (supported efforts: none, low, medium, high, xhigh, max), so 'minimal'
317
- * maps to the closest supported value.
337
+ * Whether a request to this model should carry a reasoning effort at all.
338
+ * Catalogued models say so through their declared tiers; pass-through IDs
339
+ * fall back to the o-series / GPT-5 name heuristic.
318
340
  */
319
- function resolveReasoningEffort(resolvedModel, reasoningEffort) {
341
+ function acceptsReasoningEffort(resolvedModel, modelConfig) {
342
+ if (modelConfig.supportedEfforts) {
343
+ return true;
344
+ }
345
+ return resolvedModel.startsWith('o3') || resolvedModel.startsWith('gpt-5');
346
+ }
347
+
348
+ /**
349
+ * Resolve the reasoning effort actually sent to the API. Catalogued models
350
+ * clamp onto their declared tiers. Pass-through IDs are matched by family
351
+ * where the tiers are known (GPT-5 Pro snapshots, GPT-5.6 snapshots) and
352
+ * otherwise keep the requested value.
353
+ */
354
+ function resolveReasoningEffort(resolvedModel, modelConfig, reasoningEffort) {
355
+ if (modelConfig.supportedEfforts) {
356
+ return clampReasoningEffort(reasoningEffort, modelConfig.supportedEfforts);
357
+ }
320
358
  if (resolvedModel.endsWith('-pro') && resolvedModel.startsWith('gpt-5')) {
321
- return 'high';
359
+ return clampReasoningEffort(reasoningEffort, PRO_PASSTHROUGH_EFFORT_TIERS);
322
360
  }
323
- if (resolvedModel.startsWith('gpt-5.6') && reasoningEffort === 'minimal') {
324
- return 'low';
361
+ if (resolvedModel.startsWith('gpt-5.6')) {
362
+ return clampReasoningEffort(reasoningEffort, GPT_56_EFFORT_TIERS);
325
363
  }
326
364
  return reasoningEffort;
327
365
  }
@@ -511,13 +549,13 @@ export const openaiProvider = {
511
549
  requestPayload.tools = [{ type: 'web_search_preview' }];
512
550
  }
513
551
 
514
- // Add reasoning effort for thinking models (o3 series and GPT-5 family)
515
- if (
516
- (resolvedModel.startsWith('o3') || resolvedModel.startsWith('gpt-5')) &&
517
- reasoning_effort
518
- ) {
552
+ if (acceptsReasoningEffort(resolvedModel, modelConfig) && reasoning_effort) {
519
553
  requestPayload.reasoning = {
520
- effort: resolveReasoningEffort(resolvedModel, reasoning_effort),
554
+ effort: resolveReasoningEffort(
555
+ resolvedModel,
556
+ modelConfig,
557
+ reasoning_effort,
558
+ ),
521
559
  summary: 'auto', // Enable reasoning summaries
522
560
  };
523
561
  }
@@ -531,13 +569,10 @@ export const openaiProvider = {
531
569
  ...cleanOptions,
532
570
  };
533
571
 
534
- // Add reasoning effort for thinking models (o3 series and GPT-5 family)
535
- if (
536
- (resolvedModel.startsWith('o3') || resolvedModel.startsWith('gpt-5')) &&
537
- reasoning_effort
538
- ) {
572
+ if (acceptsReasoningEffort(resolvedModel, modelConfig) && reasoning_effort) {
539
573
  requestPayload.reasoning_effort = resolveReasoningEffort(
540
574
  resolvedModel,
575
+ modelConfig,
541
576
  reasoning_effort,
542
577
  );
543
578
  }
@@ -26,6 +26,7 @@ import {
26
26
  lookupOpenRouterModel,
27
27
  DiscoveryStatus,
28
28
  } from './openrouter-discovery.js';
29
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
29
30
 
30
31
  // Curated static catalog (verified live 2026-07-11). getSupportedModels() must
31
32
  // return exactly these 8 slugs even after a discovery call has run — dynamic
@@ -255,7 +256,7 @@ function extractReasoningText(details) {
255
256
  * 1. passthrough (openrouter/auto) → null (router decides)
256
257
  * 2. mandatory (kimi-k2.7-code) → { enabled: true } (cannot disable)
257
258
  * 3. effort-tiered (glm/deepseek) → clamp into supported_efforts
258
- * (max→xhigh, else→high; none→disabled)
259
+ * (nearest stronger tier; none→disabled)
259
260
  * 4. enable/disable-only (qwen/kimi) → { enabled: false } for none, else true
260
261
  * 5. unavailable metadata → null (omit conservatively)
261
262
  *
@@ -280,14 +281,9 @@ function buildOpenRouterReasoning(modelConfig, reasoningEffort) {
280
281
  reasoning.supported_efforts.length > 0
281
282
  ) {
282
283
  if (level === 'none') return { enabled: false };
283
- const wanted = level === 'max' ? 'xhigh' : 'high';
284
- const efforts = reasoning.supported_efforts;
285
- const effort = efforts.includes(wanted)
286
- ? wanted
287
- : efforts.includes('high')
288
- ? 'high'
289
- : efforts[0];
290
- return { effort };
284
+ return {
285
+ effort: clampReasoningEffort(level, reasoning.supported_efforts),
286
+ };
291
287
  }
292
288
 
293
289
  // Enable/disable-only.
@@ -14,6 +14,7 @@
14
14
 
15
15
  import OpenAI from 'openai';
16
16
  import { debugLog, debugError } from '../utils/console.js';
17
+ import { clampReasoningEffort } from '../utils/reasoningEffort.js';
17
18
 
18
19
  // Curated catalog: grok-4.5 only (verified live 2026-07-11 against
19
20
  // GET https://api.x.ai/v1/models/grok-4.5 — id, aliases, 500k context).
@@ -85,27 +86,15 @@ function resolveModelName(modelName) {
85
86
  }
86
87
 
87
88
  /**
88
- * Map Converse reasoning_effort to a value grok-4.5 accepts.
89
- *
90
- * grok-4.5 supports ONLY low/medium/high (default high) and cannot disable
91
- * reasoning there is no `none`/off value. Sending an unsupported value
92
- * (e.g. `none`/`minimal`/`max`) returns HTTP 400, so unsupported Converse
93
- * levels are clamped into {low, medium, high} rather than forwarded.
89
+ * Tiers grok-4.5 accepts (default high). It cannot disable reasoning — there
90
+ * is no `none`/off value — and sending an unsupported value (`none`,
91
+ * `minimal`, `xhigh`, `max`) returns HTTP 400, so requests are clamped into
92
+ * this set rather than forwarded.
94
93
  */
94
+ const GROK_EFFORT_TIERS = ['low', 'medium', 'high'];
95
+
95
96
  function resolveReasoningEffort(reasoningEffort) {
96
- switch (reasoningEffort) {
97
- case 'none':
98
- case 'minimal':
99
- case 'low':
100
- return 'low';
101
- case 'medium':
102
- return 'medium';
103
- case 'high':
104
- case 'max':
105
- return 'high';
106
- default:
107
- return 'high';
108
- }
97
+ return clampReasoningEffort(reasoningEffort, GROK_EFFORT_TIERS);
109
98
  }
110
99
 
111
100
  /**
package/src/tools/chat.js CHANGED
@@ -27,6 +27,7 @@ import { applyTokenLimit, getTokenLimit } from '../utils/tokenLimiter.js';
27
27
  import { validateAllPaths } from '../utils/fileValidator.js';
28
28
  import { SummarizationService } from '../services/summarizationService.js';
29
29
  import { exportConversation } from '../utils/conversationExporter.js';
30
+ import { EFFORT_LADDER } from '../utils/reasoningEffort.js';
30
31
  import {
31
32
  getDefaultModelForProvider,
32
33
  getProviderUnavailableMessage,
@@ -1187,9 +1188,9 @@ chatTool.inputSchema = {
1187
1188
  },
1188
1189
  reasoning_effort: {
1189
1190
  type: 'string',
1190
- enum: ['none', 'minimal', 'low', 'medium', 'high', 'max'],
1191
+ enum: EFFORT_LADDER,
1191
1192
  description:
1192
- 'Reasoning depth for thinking models. Examples: "none" (no reasoning, fastest - GPT-5.1+ only), "minimal", "low", "medium" (balanced), "high", "max". Default: "medium"',
1193
+ 'Reasoning depth for thinking models, weakest to strongest: "none" (reasoning off, where the model allows it), "minimal", "low", "medium" (balanced), "high", "xhigh", "max". Passed through by name when the model accepts it, otherwise clamped to the nearest tier it does. Default: "medium"',
1193
1194
  },
1194
1195
  async: {
1195
1196
  type: 'boolean',
@@ -138,9 +138,10 @@ function classifyModelSpec(spec, providers) {
138
138
  }
139
139
 
140
140
  // Other explicit namespaces pass the spec through unchanged; the target
141
- // provider strips its own prefix (preserves current copilot/claude/gemini-cli
141
+ // provider strips its own prefix (preserves current codex/copilot/claude/gemini-cli
142
142
  // behavior).
143
143
  if (
144
+ lower.startsWith('codex:') ||
144
145
  lower.startsWith('copilot:') ||
145
146
  lower.startsWith('claude:') ||
146
147
  lower.startsWith('gemini:')
@@ -308,8 +309,9 @@ export function mapModelToProvider(model, providers) {
308
309
  return 'openai';
309
310
  }
310
311
 
311
- // Check Codex (exact match only - don't route "gpt-5-codex" etc to Codex provider)
312
- if (modelLower === 'codex') {
312
+ // Check Codex: bare `codex` or the `codex:<backend>` namespace (e.g. codex:astra,
313
+ // codex:gpt-5.6-sol). Bare "gpt-*" names never route here — they are OpenAI API models.
314
+ if (modelLower === 'codex' || modelLower.startsWith('codex:')) {
313
315
  return 'codex';
314
316
  }
315
317
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Reasoning Effort Ladder
3
+ *
4
+ * The tool-level `reasoning_effort` vocabulary is the union of the tiers the
5
+ * providers expose, ordered weakest to strongest. Every provider declares the
6
+ * subset its model accepts and clamps the request onto it here, so the same
7
+ * word means the same tier everywhere and a provider never re-derives the
8
+ * ordering on its own.
9
+ */
10
+
11
+ /**
12
+ * Every tool-level reasoning_effort value, weakest first. Doubles as the
13
+ * `enum` of the chat tool's `reasoning_effort` parameter.
14
+ * @type {string[]}
15
+ */
16
+ export const EFFORT_LADDER = [
17
+ 'none',
18
+ 'minimal',
19
+ 'low',
20
+ 'medium',
21
+ 'high',
22
+ 'xhigh',
23
+ 'max',
24
+ ];
25
+
26
+ /**
27
+ * Effort applied when the caller leaves reasoning_effort unset or sends a
28
+ * value outside the ladder.
29
+ */
30
+ export const DEFAULT_EFFORT = 'medium';
31
+
32
+ /**
33
+ * Clamp a requested effort onto the tiers a model accepts.
34
+ *
35
+ * The requested tier wins when the model accepts it. Otherwise the nearest
36
+ * *stronger* accepted tier wins: nudging 'minimal' up to 'low' keeps reasoning
37
+ * on, where falling back to 'none' would silently switch it off. Only when
38
+ * nothing stronger exists does the nearest weaker tier apply (e.g. 'max' on a
39
+ * model that tops out at 'xhigh').
40
+ *
41
+ * Tiers in `supported` that are not on the ladder are ignored for ranking but
42
+ * the first of them is used as a last resort when nothing on the ladder
43
+ * matches, so a provider-declared list is never answered with a tier it
44
+ * doesn't contain. An empty list is invalid capability data and throws rather
45
+ * than inventing a tier the model may reject.
46
+ *
47
+ * @param {string|undefined} effort - Tool-level reasoning_effort value
48
+ * @param {string[]} supported - Tiers the model accepts (any order)
49
+ * @returns {string} A tier from `supported`
50
+ */
51
+ export function clampReasoningEffort(effort, supported) {
52
+ if (!Array.isArray(supported) || supported.length === 0) {
53
+ throw new TypeError(
54
+ 'clampReasoningEffort requires a non-empty list of supported tiers',
55
+ );
56
+ }
57
+
58
+ const desired = EFFORT_LADDER.includes(effort) ? effort : DEFAULT_EFFORT;
59
+ if (supported.includes(desired)) {
60
+ return desired;
61
+ }
62
+
63
+ const rank = EFFORT_LADDER.indexOf(desired);
64
+ const stronger = EFFORT_LADDER.slice(rank + 1).find((tier) =>
65
+ supported.includes(tier),
66
+ );
67
+ if (stronger) {
68
+ return stronger;
69
+ }
70
+
71
+ const weaker = EFFORT_LADDER.slice(0, rank)
72
+ .reverse()
73
+ .find((tier) => supported.includes(tier));
74
+ return weaker || supported[0];
75
+ }