@cloudcli-ai/cloudcli 1.35.1 → 1.36.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.
Files changed (37) hide show
  1. package/dist/api-docs.html +8 -2
  2. package/dist/assets/{index-D1IdCTT6.js → index-S6cIy7hj.js} +260 -260
  3. package/dist/assets/index-uLUE6Qt1.css +32 -0
  4. package/dist/index.html +2 -2
  5. package/dist-server/server/claude-sdk.js +25 -26
  6. package/dist-server/server/claude-sdk.js.map +1 -1
  7. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +70 -1
  8. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  9. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +52 -11
  10. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  11. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +138 -1
  12. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  13. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +5 -0
  14. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  15. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  16. package/dist-server/server/modules/providers/tests/opencode-models.test.js +58 -1
  17. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  18. package/dist-server/server/openai-codex.js +9 -8
  19. package/dist-server/server/openai-codex.js.map +1 -1
  20. package/dist-server/server/opencode-cli.js +20 -2
  21. package/dist-server/server/opencode-cli.js.map +1 -1
  22. package/dist-server/server/routes/agent.js +14 -3
  23. package/dist-server/server/routes/agent.js.map +1 -1
  24. package/package.json +1 -1
  25. package/public/api-docs.html +8 -2
  26. package/server/claude-sdk.js +32 -26
  27. package/server/modules/providers/list/claude/claude-models.provider.ts +73 -1
  28. package/server/modules/providers/list/codex/codex-models.provider.ts +59 -11
  29. package/server/modules/providers/list/opencode/opencode-models.provider.ts +180 -1
  30. package/server/modules/providers/services/provider-capabilities.service.ts +7 -0
  31. package/server/modules/providers/services/provider-models.service.ts +1 -1
  32. package/server/modules/providers/tests/opencode-models.test.ts +62 -0
  33. package/server/openai-codex.js +9 -7
  34. package/server/opencode-cli.js +21 -2
  35. package/server/routes/agent.js +14 -3
  36. package/server/shared/types.ts +7 -0
  37. package/dist/assets/index-BvX30NW5.css +0 -32
@@ -489,7 +489,7 @@
489
489
  <span class="endpoint-path"><span class="api-url">http://localhost:3001</span>/api/agent</span>
490
490
  </div>
491
491
 
492
- <p>Trigger an AI agent (Claude, Cursor, or Codex) to work on a project.</p>
492
+ <p>Trigger an AI agent (Claude, Cursor, Codex, Gemini, or OpenCode) to work on a project.</p>
493
493
 
494
494
  <h4>Request Body Parameters</h4>
495
495
  <table>
@@ -524,7 +524,7 @@
524
524
  <td><code>provider</code></td>
525
525
  <td>string</td>
526
526
  <td><span class="badge badge-optional">Optional</span></td>
527
- <td><code>claude</code>, <code>cursor</code>, or <code>codex</code> (default: <code>claude</code>)</td>
527
+ <td><code>claude</code>, <code>cursor</code>, <code>codex</code>, <code>gemini</code>, or <code>opencode</code> (default: <code>claude</code>)</td>
528
528
  </tr>
529
529
  <tr>
530
530
  <td><code>stream</code></td>
@@ -540,6 +540,12 @@
540
540
  Model identifier for the AI provider (loading from constants...)
541
541
  </td>
542
542
  </tr>
543
+ <tr>
544
+ <td><code>effort</code></td>
545
+ <td>string</td>
546
+ <td><span class="badge badge-optional">Optional</span></td>
547
+ <td>Reasoning effort for Claude, Codex, and OpenCode models that expose effort metadata. Use <code>default</code> or omit it to let the provider decide.</td>
548
+ </tr>
543
549
  <tr>
544
550
  <td><code>cleanup</code></td>
545
551
  <td>boolean</td>
@@ -12,11 +12,13 @@
12
12
  * - WebSocket message streaming
13
13
  */
14
14
 
15
- import { query } from '@anthropic-ai/claude-agent-sdk';
16
15
  import crypto from 'crypto';
17
16
  import { promises as fs } from 'fs';
18
- import path from 'path';
19
17
  import os from 'os';
18
+ import path from 'path';
19
+
20
+ import { query } from '@anthropic-ai/claude-agent-sdk';
21
+
20
22
  import { CLAUDE_FALLBACK_MODELS } from './modules/providers/list/claude/claude-models.provider.js';
21
23
  import { providerModelsService } from './modules/providers/services/provider-models.service.js';
22
24
  import { resolveClaudeCodeExecutablePath } from './shared/claude-cli-path.js';
@@ -41,6 +43,15 @@ const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEO
41
43
 
42
44
  const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion', 'ExitPlanMode']);
43
45
 
46
+ function resolveClaudeEffort(model, effort, modelsDefinition = CLAUDE_FALLBACK_MODELS) {
47
+ const selectedModel = modelsDefinition?.OPTIONS?.find((option) => option.value === model) || null;
48
+ const allowedEfforts = selectedModel?.effort?.values
49
+ ?.map((value) => value.value) || [];
50
+ return typeof effort === 'string' && effort !== 'default' && allowedEfforts.includes(effort)
51
+ ? effort
52
+ : undefined;
53
+ }
54
+
44
55
  function createRequestId() {
45
56
  if (typeof crypto.randomUUID === 'function') {
46
57
  return crypto.randomUUID();
@@ -145,13 +156,8 @@ function matchesToolPermission(entry, toolName, input) {
145
156
  return false;
146
157
  }
147
158
 
148
- /**
149
- * Maps CLI options to SDK-compatible options format
150
- * @param {Object} options - CLI options
151
- * @returns {Object} SDK-compatible options
152
- */
153
159
  function mapCliOptionsToSDK(options = {}) {
154
- const { sessionId, cwd, toolsSettings, permissionMode } = options;
160
+ const { sessionId, cwd, toolsSettings, permissionMode, effort } = options;
155
161
 
156
162
  const sdkOptions = {};
157
163
 
@@ -163,32 +169,26 @@ function mapCliOptionsToSDK(options = {}) {
163
169
  // which does not reliably follow npm's shell wrappers like cross-spawn does.
164
170
  sdkOptions.pathToClaudeCodeExecutable = resolveClaudeCodeExecutablePath(process.env.CLAUDE_CLI_PATH);
165
171
 
166
- // Map working directory
167
172
  if (cwd) {
168
173
  sdkOptions.cwd = cwd;
169
174
  }
170
175
 
171
- // Map permission mode
172
176
  if (permissionMode && permissionMode !== 'default') {
173
177
  sdkOptions.permissionMode = permissionMode;
174
178
  }
175
179
 
176
- // Map tool settings
177
180
  const settings = toolsSettings || {
178
181
  allowedTools: [],
179
182
  disallowedTools: [],
180
183
  skipPermissions: false
181
184
  };
182
185
 
183
- // Handle tool permissions
184
186
  if (settings.skipPermissions && permissionMode !== 'plan') {
185
- // When skipping permissions, use bypassPermissions mode
186
187
  sdkOptions.permissionMode = 'bypassPermissions';
187
188
  }
188
189
 
189
190
  let allowedTools = [...(settings.allowedTools || [])];
190
191
 
191
- // Add plan mode default tools
192
192
  if (permissionMode === 'plan') {
193
193
  const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch'];
194
194
  for (const tool of planModeTools) {
@@ -207,22 +207,24 @@ function mapCliOptionsToSDK(options = {}) {
207
207
 
208
208
  sdkOptions.disallowedTools = settings.disallowedTools || [];
209
209
 
210
- // Map model (default to sonnet)
211
- // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m], fable
212
210
  sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
213
- // Model logged at query start below
214
211
 
215
- // Map system prompt configuration
212
+ const resolvedEffort = resolveClaudeEffort(
213
+ sdkOptions.model,
214
+ effort,
215
+ options.effortModels || CLAUDE_FALLBACK_MODELS,
216
+ );
217
+ if (resolvedEffort) {
218
+ sdkOptions.effort = resolvedEffort;
219
+ }
220
+
216
221
  sdkOptions.systemPrompt = {
217
222
  type: 'preset',
218
- preset: 'claude_code' // Required to use CLAUDE.md
223
+ preset: 'claude_code'
219
224
  };
220
225
 
221
- // Map setting sources for CLAUDE.md loading
222
- // This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
223
226
  sdkOptions.settingSources = ['project', 'user', 'local'];
224
227
 
225
- // Map resume session
226
228
  if (sessionId) {
227
229
  sdkOptions.resume = sessionId;
228
230
  }
@@ -533,20 +535,24 @@ async function queryClaudeSDK(command, options = {}, ws) {
533
535
  sessionId,
534
536
  options.model,
535
537
  );
538
+ let effortModels = CLAUDE_FALLBACK_MODELS;
539
+ try {
540
+ effortModels = (await providerModelsService.getProviderModels('claude')).models;
541
+ } catch (error) {
542
+ console.warn('[Claude SDK] Unable to load provider models for effort validation:', error);
543
+ }
536
544
 
537
- // Map CLI options to SDK format
538
545
  const sdkOptions = mapCliOptionsToSDK({
539
546
  ...options,
540
547
  model: resolvedModel || options.model,
548
+ effortModels,
541
549
  });
542
550
 
543
- // Load MCP configuration
544
551
  const mcpServers = await loadMcpConfig(options.cwd);
545
552
  if (mcpServers) {
546
553
  sdkOptions.mcpServers = mcpServers;
547
554
  }
548
555
 
549
- // Handle images - save to temp files and modify prompt
550
556
  const imageResult = await handleImages(command, options.images, options.cwd);
551
557
  const finalCommand = imageResult.modifiedCommand;
552
558
  tempImagePaths = imageResult.tempImagePaths;
@@ -650,7 +656,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
650
656
  return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
651
657
  };
652
658
 
653
- // Set stream-close timeout for interactive tools (Query constructor reads it synchronously). Claude Agent SDK has a default of 5s and this overrides it
659
+ // Query constructor reads this synchronously.
654
660
  const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
655
661
  process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
656
662
 
@@ -5,6 +5,7 @@ import type { IProviderModels } from '@/shared/interfaces.js';
5
5
  import type {
6
6
  ProviderChangeActiveModelInput,
7
7
  ProviderCurrentActiveModel,
8
+ ProviderModelOption,
8
9
  ProviderModelsDefinition,
9
10
  ProviderSessionActiveModelChange,
10
11
  } from '@/shared/types.js';
@@ -18,27 +19,89 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
18
19
  {
19
20
  value: 'default',
20
21
  label: 'Default (recommended)',
21
- description: 'Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok',
22
+ description: 'Use the Claude Code default model (currently Sonnet 4.6)',
23
+ effort: {
24
+ default: 'high',
25
+ values: [
26
+ { value: 'low' },
27
+ { value: 'medium' },
28
+ { value: 'high' },
29
+ { value: 'max' },
30
+ ],
31
+ },
22
32
  },
23
33
  {
24
34
  value: 'fable',
25
35
  label: 'Fable',
26
36
  description: 'Fable 5 · Most capable for your hardest and longest-running tasks · Uses your limits ~2× faster than Opus',
37
+ effort: {
38
+ default: 'high',
39
+ values: [
40
+ { value: 'low' },
41
+ { value: 'medium' },
42
+ { value: 'high' },
43
+ { value: 'xhigh' },
44
+ { value: 'max' },
45
+ ],
46
+ },
27
47
  },
28
48
  {
29
49
  value: "sonnet",
30
50
  label: "Sonnet",
31
51
  description: "Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok",
52
+ effort: {
53
+ default: 'high',
54
+ values: [
55
+ { value: 'low' },
56
+ { value: 'medium' },
57
+ { value: 'high' },
58
+ { value: 'max' },
59
+ ],
60
+ },
32
61
  },
33
62
  {
34
63
  value: 'sonnet[1m]',
35
64
  label: 'Sonnet (1M context)',
36
65
  description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok',
66
+ effort: {
67
+ default: 'high',
68
+ values: [
69
+ { value: 'low' },
70
+ { value: 'medium' },
71
+ { value: 'high' },
72
+ { value: 'max' },
73
+ ],
74
+ },
75
+ },
76
+ {
77
+ value: 'opus',
78
+ label: 'Opus',
79
+ description: 'Opus 4.8 · Best for everyday, complex tasks · ~2× usage vs Sonnet',
80
+ effort: {
81
+ default: 'high',
82
+ values: [
83
+ { value: 'low' },
84
+ { value: 'medium' },
85
+ { value: 'high' },
86
+ { value: 'xhigh' },
87
+ { value: 'max' },
88
+ ],
89
+ },
37
90
  },
38
91
  {
39
92
  value: 'opus[1m]',
40
93
  label: 'Opus 4.8 (1M context)',
41
94
  description: 'Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok',
95
+ effort: {
96
+ default: 'high',
97
+ values: [
98
+ { value: 'low' },
99
+ { value: 'medium' },
100
+ { value: 'high' },
101
+ { value: 'xhigh' },
102
+ { value: 'max' },
103
+ ],
104
+ },
42
105
  },
43
106
  {
44
107
  value: 'haiku',
@@ -48,6 +111,15 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
48
111
  ],
49
112
  DEFAULT: 'default',
50
113
  };
114
+
115
+ export const findClaudeModelOption = (model: string | undefined | null): ProviderModelOption | null => {
116
+ const normalizedModel = typeof model === 'string' ? model.trim() : '';
117
+ if (!normalizedModel) {
118
+ return null;
119
+ }
120
+
121
+ return CLAUDE_FALLBACK_MODELS.OPTIONS.find((option) => option.value === normalizedModel) ?? null;
122
+ };
51
123
  type ClaudeInitEvent = {
52
124
  sessionId?: string;
53
125
  session_id?: string;
@@ -21,11 +21,30 @@ import {
21
21
 
22
22
  export const CODEX_FALLBACK_MODELS: ProviderModelsDefinition = {
23
23
  OPTIONS: [
24
- { value: 'gpt-5.5', label: 'gpt-5.5' },
25
- { value: 'gpt-5.4', label: 'gpt-5.4' },
26
- { value: 'gpt-5.4-mini', label: 'gpt-5.4-mini' },
27
- { value: 'gpt-5.3-codex', label: 'gpt-5.3-codex' },
28
- { value: 'gpt-5.2', label: 'gpt-5.2' },
24
+ {
25
+ value: 'gpt-5.5',
26
+ label: 'gpt-5.5',
27
+ effort: {
28
+ default: 'medium',
29
+ values: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }, { value: 'xhigh' }],
30
+ },
31
+ },
32
+ {
33
+ value: 'gpt-5.4',
34
+ label: 'gpt-5.4',
35
+ effort: {
36
+ default: 'medium',
37
+ values: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }, { value: 'xhigh' }],
38
+ },
39
+ },
40
+ {
41
+ value: 'gpt-5.4-mini',
42
+ label: 'gpt-5.4-mini',
43
+ effort: {
44
+ default: 'medium',
45
+ values: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }, { value: 'xhigh' }],
46
+ },
47
+ },
29
48
  ],
30
49
  DEFAULT: 'gpt-5.4',
31
50
  };
@@ -37,6 +56,11 @@ type CodexCachedModel = {
37
56
  priority?: number;
38
57
  visibility?: string;
39
58
  supported_in_api?: boolean;
59
+ default_reasoning_level?: string;
60
+ supported_reasoning_levels?: Array<{
61
+ effort?: string;
62
+ description?: string;
63
+ }>;
40
64
  };
41
65
 
42
66
  const CODEX_MODELS_CACHE_PATH = path.join(os.homedir(), '.codex', 'models_cache.json');
@@ -51,15 +75,39 @@ const readCodexPriority = (value: unknown): number => (
51
75
  typeof value === 'number' && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER
52
76
  );
53
77
 
54
- const mapCodexModel = (model: CodexCachedModel): ProviderModelOption => ({
55
- value: model.slug as string,
56
- label: readOptionalString(model.display_name) ?? (model.slug as string),
57
- description: readOptionalString(model.description),
58
- });
78
+ const mapCodexModel = (model: CodexCachedModel): ProviderModelOption => {
79
+ const effortValues = Array.isArray(model.supported_reasoning_levels)
80
+ ? model.supported_reasoning_levels
81
+ .map((level) => {
82
+ const value = readOptionalString(level?.effort);
83
+ if (!value) {
84
+ return null;
85
+ }
86
+
87
+ return {
88
+ value,
89
+ description: readOptionalString(level?.description),
90
+ };
91
+ })
92
+ .filter((level): level is NonNullable<typeof level> => Boolean(level))
93
+ : [];
94
+
95
+ return {
96
+ value: model.slug as string,
97
+ label: readOptionalString(model.display_name) ?? (model.slug as string),
98
+ description: readOptionalString(model.description),
99
+ effort: effortValues.length > 0
100
+ ? {
101
+ default: readOptionalString(model.default_reasoning_level) ?? undefined,
102
+ values: effortValues,
103
+ }
104
+ : undefined,
105
+ };
106
+ };
59
107
 
60
108
  const buildCodexModelsDefinition = (models: CodexCachedModel[]): ProviderModelsDefinition => {
61
109
  const sortedModels = [...models]
62
- .filter((model) => model.visibility !== 'hidden' && model.supported_in_api !== false)
110
+ .filter((model) => model.visibility === 'list' && model.supported_in_api !== false)
63
111
  .sort((left, right) => readCodexPriority(left.priority) - readCodexPriority(right.priority));
64
112
 
65
113
  const options: ProviderModelOption[] = [];
@@ -74,6 +74,13 @@ const VERSION_TOKEN = /^[a-z]\d+$/i;
74
74
  const NUMERIC_TOKEN = /^\d+(?:\.\d+)*$/;
75
75
  const SHORT_ACRONYM_TOKEN = /^[a-z]{2,3}$/;
76
76
 
77
+ type OpenCodeVerboseModel = {
78
+ id?: string;
79
+ name?: string;
80
+ providerID?: string;
81
+ variants?: Record<string, unknown>;
82
+ };
83
+
77
84
  export const parseOpenCodeModelsStdout = (stdout: string): string[] => {
78
85
  const ids: string[] = [];
79
86
 
@@ -91,6 +98,83 @@ export const parseOpenCodeModelsStdout = (stdout: string): string[] => {
91
98
  return [...new Set(ids)];
92
99
  };
93
100
 
101
+ const countJsonBraceDelta = (value: string): number => {
102
+ let delta = 0;
103
+ let inString = false;
104
+ let escaped = false;
105
+
106
+ for (const character of value) {
107
+ if (escaped) {
108
+ escaped = false;
109
+ continue;
110
+ }
111
+
112
+ if (character === '\\') {
113
+ escaped = inString;
114
+ continue;
115
+ }
116
+
117
+ if (character === '"') {
118
+ inString = !inString;
119
+ continue;
120
+ }
121
+
122
+ if (inString) {
123
+ continue;
124
+ }
125
+
126
+ if (character === '{') {
127
+ delta += 1;
128
+ } else if (character === '}') {
129
+ delta -= 1;
130
+ }
131
+ }
132
+
133
+ return delta;
134
+ };
135
+
136
+ const isOpenCodeVerboseModel = (value: unknown): value is OpenCodeVerboseModel => {
137
+ const record = readObjectRecord(value);
138
+ return Boolean(record && readOptionalString(record.id));
139
+ };
140
+
141
+ export const parseOpenCodeVerboseModelsStdout = (stdout: string): OpenCodeVerboseModel[] => {
142
+ const models: OpenCodeVerboseModel[] = [];
143
+ let buffer: string[] = [];
144
+ let depth = 0;
145
+
146
+ for (const rawLine of stdout.split(/\r?\n/)) {
147
+ const line = rawLine.trim();
148
+ if (buffer.length === 0) {
149
+ if (line === '{') {
150
+ buffer = [rawLine];
151
+ depth = 1;
152
+ }
153
+ continue;
154
+ }
155
+
156
+ buffer.push(rawLine);
157
+ depth += countJsonBraceDelta(rawLine);
158
+
159
+ if (depth !== 0) {
160
+ continue;
161
+ }
162
+
163
+ try {
164
+ const parsed = JSON.parse(buffer.join('\n'));
165
+ if (isOpenCodeVerboseModel(parsed)) {
166
+ models.push(parsed);
167
+ }
168
+ } catch {
169
+ // Ignore malformed verbose blocks and fall back to the plain id parser.
170
+ }
171
+
172
+ buffer = [];
173
+ }
174
+
175
+ return models;
176
+ };
177
+
94
178
  const formatDateToken = (token: string): string => (
95
179
  `${token.slice(0, 4)}-${token.slice(4, 6)}-${token.slice(6, 8)}`
96
180
  );
@@ -155,6 +239,20 @@ const readOpenCodeModelParts = (id: string): { upstreamProvider: string; slug: s
155
239
  };
156
240
  };
157
241
 
242
+ const readOpenCodeVerboseModelId = (model: OpenCodeVerboseModel): string | null => {
243
+ const id = readOptionalString(model.id);
244
+ if (!id) {
245
+ return null;
246
+ }
247
+
248
+ if (id.includes('/')) {
249
+ return id;
250
+ }
251
+
252
+ const upstreamProvider = readOptionalString(model.providerID);
253
+ return upstreamProvider ? `${upstreamProvider}/${id}` : id;
254
+ };
255
+
158
256
  const labelForOpenCodeModelId = (id: string): string => {
159
257
  const fallbackLabel = OPENCODE_FALLBACK_MODELS.OPTIONS.find((option) => option.value === id)?.label;
160
258
  if (fallbackLabel) {
@@ -170,6 +268,52 @@ const descriptionForOpenCodeModelId = (id: string): string => {
170
268
  return upstreamProvider ? `${upstreamProvider} - ${id}` : id;
171
269
  };
172
270
 
271
+ const readOpenCodeVariantEffort = (key: string, value: unknown): string | null => {
272
+ const variant = readObjectRecord(value);
273
+ return readOptionalString(variant?.reasoningEffort)
274
+ ?? readOptionalString(variant?.effort)
275
+ ?? key;
276
+ };
277
+
278
+ const readOpenCodeEffortValues = (
279
+ variants: OpenCodeVerboseModel['variants'],
280
+ ): NonNullable<ProviderModelOption['effort']>['values'] => {
281
+ const effortValues: NonNullable<ProviderModelOption['effort']>['values'] = [];
282
+ const seenValues = new Set<string>();
283
+
284
+ for (const [key, value] of Object.entries(variants ?? {})) {
285
+ const effort = readOpenCodeVariantEffort(key, value);
286
+ if (!effort || seenValues.has(effort)) {
287
+ continue;
288
+ }
289
+
290
+ seenValues.add(effort);
291
+ effortValues.push({ value: effort });
292
+ }
293
+
294
+ return effortValues;
295
+ };
296
+
297
+ const mapOpenCodeVerboseModel = (model: OpenCodeVerboseModel): ProviderModelOption | null => {
298
+ const value = readOpenCodeVerboseModelId(model);
299
+ if (!value) {
300
+ return null;
301
+ }
302
+
303
+ const effortValues = readOpenCodeEffortValues(model.variants);
304
+
305
+ return {
306
+ value,
307
+ label: readOptionalString(model.name) ?? labelForOpenCodeModelId(value),
308
+ description: descriptionForOpenCodeModelId(value),
309
+ effort: effortValues.length > 0
310
+ ? {
311
+ values: effortValues,
312
+ }
313
+ : undefined,
314
+ };
315
+ };
316
+
173
317
  export const buildOpenCodeDefinitionFromIds = (ids: string[]): ProviderModelsDefinition => {
174
318
  const options: ProviderModelOption[] = ids.map((value) => ({
175
319
  value,
@@ -187,6 +331,36 @@ export const buildOpenCodeDefinitionFromIds = (ids: string[]): ProviderModelsDef
187
331
  };
188
332
  };
189
333
 
334
+ export const buildOpenCodeDefinitionFromVerboseModels = (
335
+ models: OpenCodeVerboseModel[],
336
+ ): ProviderModelsDefinition => {
337
+ const options: ProviderModelOption[] = [];
338
+ const seenValues = new Set<string>();
339
+
340
+ for (const model of models) {
341
+ const mappedModel = mapOpenCodeVerboseModel(model);
342
+ if (!mappedModel || seenValues.has(mappedModel.value)) {
343
+ continue;
344
+ }
345
+
346
+ seenValues.add(mappedModel.value);
347
+ options.push(mappedModel);
348
+ }
349
+
350
+ if (options.length === 0) {
351
+ return OPENCODE_FALLBACK_MODELS;
352
+ }
353
+
354
+ const defaultValue = options.find((option) => option.value === OPENCODE_FALLBACK_MODELS.DEFAULT)?.value
355
+ ?? options[0]?.value
356
+ ?? OPENCODE_FALLBACK_MODELS.DEFAULT;
357
+
358
+ return {
359
+ OPTIONS: options,
360
+ DEFAULT: defaultValue,
361
+ };
362
+ };
363
+
190
364
  const parseOpenCodeSessionModelValue = (rawModel: unknown): string | null => {
191
365
  if (typeof rawModel === 'string') {
192
366
  const trimmed = rawModel.trim();
@@ -214,7 +388,7 @@ const parseOpenCodeSessionModelValue = (rawModel: unknown): string | null => {
214
388
  };
215
389
 
216
390
  const runOpenCodeModelsCommand = (): Promise<string> => new Promise((resolve, reject) => {
217
- const openCodeProcess = spawnFunction('opencode', ['models'], {
391
+ const openCodeProcess = spawnFunction('opencode', ['models', '--verbose'], {
218
392
  cwd: process.cwd(),
219
393
  env: { ...process.env },
220
394
  });
@@ -273,6 +447,11 @@ export class OpenCodeProviderModels implements IProviderModels {
273
447
  async getSupportedModels(): Promise<ProviderModelsDefinition> {
274
448
  try {
275
449
  const stdout = await runOpenCodeModelsCommand();
450
+ const verboseModels = parseOpenCodeVerboseModelsStdout(stdout);
451
+ if (verboseModels.length > 0) {
452
+ return buildOpenCodeDefinitionFromVerboseModels(verboseModels);
453
+ }
454
+
276
455
  const ids = parseOpenCodeModelsStdout(stdout);
277
456
  if (ids.length === 0) {
278
457
  return OPENCODE_FALLBACK_MODELS;
@@ -21,6 +21,8 @@ type ProviderCapabilities = {
21
21
  supportsPermissionRequests: boolean;
22
22
  /** Whether the token-usage endpoint has data for this provider. */
23
23
  supportsTokenUsage: boolean;
24
+ /** Whether the provider runtime can accept model-level reasoning effort. */
25
+ supportsEffort: boolean;
24
26
  };
25
27
 
26
28
  /**
@@ -38,6 +40,7 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
38
40
  supportsAbort: true,
39
41
  supportsPermissionRequests: true,
40
42
  supportsTokenUsage: true,
43
+ supportsEffort: true,
41
44
  },
42
45
  cursor: {
43
46
  provider: 'cursor',
@@ -47,6 +50,7 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
47
50
  supportsAbort: true,
48
51
  supportsPermissionRequests: false,
49
52
  supportsTokenUsage: false,
53
+ supportsEffort: false,
50
54
  },
51
55
  codex: {
52
56
  provider: 'codex',
@@ -56,6 +60,7 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
56
60
  supportsAbort: true,
57
61
  supportsPermissionRequests: false,
58
62
  supportsTokenUsage: true,
63
+ supportsEffort: true,
59
64
  },
60
65
  gemini: {
61
66
  provider: 'gemini',
@@ -65,6 +70,7 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
65
70
  supportsAbort: true,
66
71
  supportsPermissionRequests: false,
67
72
  supportsTokenUsage: true,
73
+ supportsEffort: false,
68
74
  },
69
75
  opencode: {
70
76
  provider: 'opencode',
@@ -74,6 +80,7 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
74
80
  supportsAbort: true,
75
81
  supportsPermissionRequests: false,
76
82
  supportsTokenUsage: true,
83
+ supportsEffort: true,
77
84
  },
78
85
  };
79
86
 
@@ -16,7 +16,7 @@ import type {
16
16
  import { readProviderSessionActiveModelChange } from '@/shared/utils.js';
17
17
 
18
18
  export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
19
- const PROVIDER_MODELS_CACHE_VERSION = 1;
19
+ const PROVIDER_MODELS_CACHE_VERSION = 2;
20
20
  const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude', 'gemini']);
21
21
 
22
22
  type ProviderModelsServiceDependencies = {