Skip to content

feat: add Claude and Codex effort controls#943

Merged
viper151 merged 9 commits into
mainfrom
feat/claude-codex-effort
Jul 3, 2026
Merged

feat: add Claude and Codex effort controls#943
viper151 merged 9 commits into
mainfrom
feat/claude-codex-effort

Conversation

@viper151

@viper151 viper151 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds effort controls for both Claude and Codex and wires the selected value all the way from the chat composer to each provider SDK.

The change includes:

  • extending shared provider model metadata with optional effort definitions
  • populating Claude fallback models with supported effort values and defaults
  • reading Codex effort support from the dynamic model cache, with fallback definitions for known models
  • adding a compact effort selector to the chat composer for Claude and Codex only
  • persisting the selected effort per provider and reconciling invalid stored values when the model changes
  • sending effort in chat.send and applying it in the backend:
    • Claude via the SDK effort option
    • Codex via modelReasoningEffort
  • bumping the provider-model cache version so stale Codex caches without effort metadata are refreshed

Why

Users already know these controls by the provider-native effort names, so the UI should expose them directly instead of normalizing them into a custom abstraction.

This also fixes an important Codex failure mode: when the model cache lacked effort metadata, the app could silently drop the UI selection and Codex would fall back to the CLI config value instead.

User Impact

After this change:

  • Claude and Codex sessions can choose effort from the message composer
  • the control only appears when the current provider/model supports effort
  • the selected value is preserved per provider
  • Default means “do not force an override”; the provider keeps its own default behavior

Summary by CodeRabbit

  • New Features
    • Added a “reasoning effort” selector for Claude and Codex in the chat composer, with per-provider saving and model-aware option updates.
    • Extended POST /api/agent with an optional effort parameter for effort-capable providers.
  • Bug Fixes
    • Effort selections are validated against the active model’s allowed values; unsupported/invalid values automatically revert to defaults.
    • Improved model listing so only appropriate models are shown for selection.
  • Documentation
    • Updated API docs for the new effort parameter and supported providers.
  • Chores
    • Updated provider model metadata and bumped the cached model format to support effort options.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds reasoning-effort metadata for Claude and Codex providers, validates effort in server-side SDK query flows, and threads effort state through chat provider state, composer submission, and the composer dropdown UI. It also updates the agent API docs and request handling.

Changes

Provider Effort Selection

Layer / File(s) Summary
Effort contracts and capability flags
server/shared/types.ts, src/types/app.ts, server/modules/providers/services/provider-capabilities.service.ts, src/components/chat/constants/providerEffort.ts
ProviderModelOption gains an optional effort object, provider capabilities gain supportsEffort, and shared effort constants/helpers are added for chat state and UI.
Claude and Codex model metadata
server/modules/providers/list/claude/claude-models.provider.ts, server/modules/providers/list/codex/codex-models.provider.ts, server/modules/providers/services/provider-models.service.ts
Claude fallback models gain effort metadata and a lookup helper; Codex cached model fields and fallback options gain effort metadata, model eligibility filtering changes, and the provider models cache version increments.
Server-side effort resolution
server/claude-sdk.js, server/openai-codex.js
Claude SDK mapping loads provider effort models and validates requested effort; Codex query validates requested effort and passes modelReasoningEffort to the SDK, with session tracking and registration timing adjustments.
Chat effort state and composer wiring
src/components/chat/hooks/useChatProviderState.ts, src/components/chat/hooks/useChatComposerState.ts, src/components/chat/view/ChatInterface.tsx, src/components/chat/view/subcomponents/ChatComposer.tsx
Chat provider state stores and reconciles effort per provider, composer submission includes effort, and the chat UI renders an effort dropdown and persists selections.

Agent request wiring and cleanup

Layer / File(s) Summary
Request parameter propagation and docs
server/routes/agent.js, public/api-docs.html
The agent route parses and forwards the new effort request field, and the API docs describe the added parameter and supported providers.
Non-functional spacing change
src/components/chat/view/subcomponents/CommandResultModal.tsx
Whitespace between two useMemo blocks was adjusted.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ChatComposer
  participant useChatProviderState
  participant useChatComposerState
  participant ClaudeSDK
  participant CodexSDK

  User->>ChatComposer: select effort
  ChatComposer->>useChatProviderState: onSelectEffort(effort)
  useChatProviderState->>useChatProviderState: setStoredProviderEffort(provider, effort)
  User->>ChatComposer: submit message
  ChatComposer->>useChatComposerState: handleSubmit()
  useChatComposerState->>ClaudeSDK: sendMessage(options.effort)
  useChatComposerState->>CodexSDK: sendMessage(options.effort)
Loading

Possibly related PRs

Poem

A bunny hops with effort bright,
And picks the setting just right. 🐰
Claude and Codex join the race,
With tidy choices in their place.
One soft click, then off it goes,
Into the stream the message flows.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding effort controls for Claude and Codex.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/claude-codex-effort

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/components/chat/view/ChatInterface.tsx Fixed
Comment thread src/components/chat/view/ChatInterface.tsx Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/modules/providers/list/codex/codex-models.provider.ts (1)

94-116: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

effort.values can end up empty after filtering, but effort object is still assigned.

The outer guard (Array.isArray(...) && length > 0) only checks the raw supported_reasoning_levels array. If every entry lacks a valid effort string, the inner .filter(Boolean) produces an empty values array, yet effort is still set to { default, values: [] } instead of undefined. Downstream, ChatInterface.tsx's composerEffortOptions only falls back to FALLBACK_COMPOSER_EFFORT_OPTIONS when effort.values.length is falsy, but since this object exists with values: [], the check modelOption?.effort?.values?.length is 0 (falsy) so it does fall back — meaning the UI would offer fallback effort options that may not actually be valid for this specific model, and the backend's per-model effort validation would then reject them.

🐛 Proposed fix to avoid assigning empty effort
 const mapCodexModel = (model: CodexCachedModel): ProviderModelOption => ({
   value: model.slug as string,
   label: readOptionalString(model.display_name) ?? (model.slug as string),
   description: readOptionalString(model.description),
-  effort: Array.isArray(model.supported_reasoning_levels) && model.supported_reasoning_levels.length > 0
-    ? {
-        default: readOptionalString(model.default_reasoning_level) ?? undefined,
-        values: model.supported_reasoning_levels
-          .map((level) => {
-            const value = readOptionalString(level?.effort);
-            if (!value) {
-              return null;
-            }
-
-            return {
-              value,
-              description: readOptionalString(level?.description),
-            };
-          })
-          .filter((level): level is NonNullable<typeof level> => Boolean(level)),
-      }
-    : undefined,
+  effort: ((): ProviderModelOption['effort'] => {
+    const values = Array.isArray(model.supported_reasoning_levels)
+      ? model.supported_reasoning_levels
+          .map((level) => {
+            const value = readOptionalString(level?.effort);
+            return value ? { value, description: readOptionalString(level?.description) } : null;
+          })
+          .filter((level): level is NonNullable<typeof level> => Boolean(level))
+      : [];
+
+    return values.length > 0
+      ? { default: readOptionalString(model.default_reasoning_level) ?? undefined, values }
+      : undefined;
+  })(),
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/providers/list/codex/codex-models.provider.ts` around lines 94
- 116, The `mapCodexModel` helper is still assigning an `effort` object even
when `supported_reasoning_levels` maps down to no valid entries after filtering.
Update `mapCodexModel` in `codex-models.provider.ts` so it only sets `effort`
when the derived `values` array is non-empty, otherwise return `undefined` for
`effort`. Keep the existing `readOptionalString` mapping for `default` and each
level, but add the final empty-array guard around the constructed `effort`
object so `ProviderModelOption` never exposes an empty effort list.
🧹 Nitpick comments (4)
server/claude-sdk.js (1)

44-53: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Add optional chaining on OPTIONS lookup.

If modelsDefinition is a truthy object without an OPTIONS array (e.g. a malformed catalog returned by the provider models service), modelsDefinition.OPTIONS.find(...) throws. This is currently caught by the outer try/catch in queryClaudeSDK, so it degrades to a failed query rather than a crash, but a defensive ?. would avoid an unnecessary user-facing failure for malformed catalog data.

🛡️ Suggested defensive tweak
 function resolveClaudeEffort(model, effort, modelsDefinition = CLAUDE_FALLBACK_MODELS) {
-  const selectedModel = modelsDefinition.OPTIONS
+  const selectedModel = modelsDefinition?.OPTIONS
     .find((option) => option.value === model) || null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/claude-sdk.js` around lines 44 - 53, The model lookup in
resolveClaudeEffort assumes modelsDefinition.OPTIONS always exists, which can
throw for malformed catalog data. Update the OPTIONS access in
resolveClaudeEffort to use optional chaining before calling find so a missing or
invalid OPTIONS array safely falls back to null/undefined. Keep the effort
validation logic unchanged and make sure the fix is localized to
resolveClaudeEffort.
src/components/chat/view/ChatInterface.tsx (1)

19-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Effort fallback values duplicated across files.

FALLBACK_COMPOSER_EFFORT_OPTIONS repeats the same literal ['low', 'medium', 'high', 'xhigh', 'max'] / ['low', 'medium', 'high', 'xhigh'] lists as FALLBACK_EFFORT_VALUES in useChatProviderState.ts. Keeping two (likely three, counting backend fallback definitions) copies of the same effort tiers risks drift if a tier is ever added/removed. Consider extracting a single shared constant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chat/view/ChatInterface.tsx` around lines 19 - 22, The
fallback effort tier lists are duplicated in ChatInterface and the chat provider
state, so update FALLBACK_COMPOSER_EFFORT_OPTIONS to use a shared source of
truth instead of hardcoded literals. Extract the common effort tiers into one
shared constant/module and reference it from both ChatInterface and
useChatProviderState so the values stay aligned if tiers change.
src/components/chat/hooks/useChatProviderState.ts (1)

309-334: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant localStorage read duplicates currentEffort.

reconcileStoredEffort re-reads localStorage.getItem(storageKey) instead of just using the currentEffort parameter that was passed in. Since currentEffort (the React state) and the localStorage value are kept in sync by setStoredProviderEffort and by this same reconciliation effect, the two should always match — the separate storedEffort lookup appears to just duplicate currentEffort. Consider simplifying to operate on currentEffort alone.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chat/hooks/useChatProviderState.ts` around lines 309 - 334,
The reconcileStoredEffort callback is redundantly reading from localStorage even
though currentEffort already represents the same persisted state. Update
reconcileStoredEffort in useChatProviderState to operate on currentEffort alone,
removing the extra storageKey/localStorage.getItem path and keeping the
allowedValues checks and DEFAULT_EFFORT_VALUE fallback behavior intact.
src/components/chat/hooks/useChatComposerState.ts (1)

166-176: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Direct localStorage read for effort duplicates the reactive prop.

getLatestProviderEffort re-reads ${provider}-effort from safeLocalStorage instead of using claudeEffort/codexEffort directly, even though those props are already part of handleSubmit's dependency array and should be current at submit time (since setStoredProviderEffort updates both state and localStorage synchronously). This adds an extra access path that must stay consistent with the hook's own state, increasing the surface for drift without a clear functional benefit.

Also applies to: 748-753

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chat/hooks/useChatComposerState.ts` around lines 166 - 176,
`getLatestProviderEffort` is redundantly reading `${provider}-effort` from
`safeLocalStorage` instead of using the already reactive `claudeEffort` and
`codexEffort` values available in `handleSubmit`. Update the
`handleSubmit`/effort resolution path to rely on the hook state props directly,
keeping the existing fallback behavior for non-Claude/Codex providers, and
remove the extra localStorage access so the logic stays aligned with the
dependency-tracked state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/chat/hooks/useChatProviderState.ts`:
- Around line 301-307: The fallback logic in getAllowedEffortValues is too broad
because it uses FALLBACK_EFFORT_VALUES whenever option?.effort?.values is
missing, even for cataloged models that simply don’t support effort; change it
to return an empty list when getModelOption finds a model but it has no effort
config, and only use the provider fallback when the model itself is unknown.
Update reconcileStoredEffort behavior accordingly so unsupported models reset to
'default', and apply the same correction to composerEffortOptions in
ChatInterface.tsx.

In `@src/components/chat/view/ChatInterface.tsx`:
- Around line 245-260: The composer effort fallback in ChatInterface should only
be used when the current model is not found in providerModelCatalog, not when a
найден modelOption simply lacks effort support. Update composerEffortOptions so
it returns modelOption.effort.values when present, returns an empty list when
modelOption exists without effort, and keeps the provider fallback only for the
missing-catalog case; mirror the same behavior in getAllowedEffortValues in
useChatProviderState to keep UI and state consistent.

In `@src/components/chat/view/subcomponents/ChatComposer.tsx`:
- Around line 211-233: The Escape key handler in ChatComposer’s useEffect is
allowing the event to reach ChatInterface’s global capture listener, which can
abort an active session unintentionally. Update the local handleKeyDown logic
for the effort dropdown to stop propagation when Escape is pressed before
closing the menu, or otherwise gate the global abort path so it ignores Escape
while the dropdown is open. Keep the fix scoped to the ChatComposer dropdown
behavior and the related ChatInterface abort handling.

---

Outside diff comments:
In `@server/modules/providers/list/codex/codex-models.provider.ts`:
- Around line 94-116: The `mapCodexModel` helper is still assigning an `effort`
object even when `supported_reasoning_levels` maps down to no valid entries
after filtering. Update `mapCodexModel` in `codex-models.provider.ts` so it only
sets `effort` when the derived `values` array is non-empty, otherwise return
`undefined` for `effort`. Keep the existing `readOptionalString` mapping for
`default` and each level, but add the final empty-array guard around the
constructed `effort` object so `ProviderModelOption` never exposes an empty
effort list.

---

Nitpick comments:
In `@server/claude-sdk.js`:
- Around line 44-53: The model lookup in resolveClaudeEffort assumes
modelsDefinition.OPTIONS always exists, which can throw for malformed catalog
data. Update the OPTIONS access in resolveClaudeEffort to use optional chaining
before calling find so a missing or invalid OPTIONS array safely falls back to
null/undefined. Keep the effort validation logic unchanged and make sure the fix
is localized to resolveClaudeEffort.

In `@src/components/chat/hooks/useChatComposerState.ts`:
- Around line 166-176: `getLatestProviderEffort` is redundantly reading
`${provider}-effort` from `safeLocalStorage` instead of using the already
reactive `claudeEffort` and `codexEffort` values available in `handleSubmit`.
Update the `handleSubmit`/effort resolution path to rely on the hook state props
directly, keeping the existing fallback behavior for non-Claude/Codex providers,
and remove the extra localStorage access so the logic stays aligned with the
dependency-tracked state.

In `@src/components/chat/hooks/useChatProviderState.ts`:
- Around line 309-334: The reconcileStoredEffort callback is redundantly reading
from localStorage even though currentEffort already represents the same
persisted state. Update reconcileStoredEffort in useChatProviderState to operate
on currentEffort alone, removing the extra storageKey/localStorage.getItem path
and keeping the allowedValues checks and DEFAULT_EFFORT_VALUE fallback behavior
intact.

In `@src/components/chat/view/ChatInterface.tsx`:
- Around line 19-22: The fallback effort tier lists are duplicated in
ChatInterface and the chat provider state, so update
FALLBACK_COMPOSER_EFFORT_OPTIONS to use a shared source of truth instead of
hardcoded literals. Extract the common effort tiers into one shared
constant/module and reference it from both ChatInterface and
useChatProviderState so the values stay aligned if tiers change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d969bedc-daa9-4414-b99b-51627438edda

📥 Commits

Reviewing files that changed from the base of the PR and between 2ebe64f and d618abb.

📒 Files selected for processing (12)
  • server/claude-sdk.js
  • server/modules/providers/list/claude/claude-models.provider.ts
  • server/modules/providers/list/codex/codex-models.provider.ts
  • server/modules/providers/services/provider-models.service.ts
  • server/openai-codex.js
  • server/shared/types.ts
  • src/components/chat/hooks/useChatComposerState.ts
  • src/components/chat/hooks/useChatProviderState.ts
  • src/components/chat/view/ChatInterface.tsx
  • src/components/chat/view/subcomponents/ChatComposer.tsx
  • src/components/chat/view/subcomponents/CommandResultModal.tsx
  • src/types/app.ts
💤 Files with no reviewable changes (1)
  • src/components/chat/view/subcomponents/CommandResultModal.tsx

Comment thread src/components/chat/hooks/useChatProviderState.ts Outdated
Comment thread src/components/chat/view/ChatInterface.tsx Outdated
Comment thread src/components/chat/view/subcomponents/ChatComposer.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/chat/hooks/useChatProviderState.ts`:
- Around line 557-569: The current effort value in useChatProviderState is still
read directly from providerEfforts, which can expose a stale selection after a
model change before the reconciliation effect runs. Update currentProviderEffort
in useChatProviderState to derive the reconciled value with
reconcileStoredEffort(provider, providerModels[provider], ...) using the same
effort options source as currentProviderEffortOptions, and keep the existing
effect only for persisting/clearing invalid stored values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 53d99807-7551-437d-a21b-33c78a46f5e0

📥 Commits

Reviewing files that changed from the base of the PR and between d618abb and 0206a1f.

📒 Files selected for processing (8)
  • server/claude-sdk.js
  • server/modules/providers/list/codex/codex-models.provider.ts
  • server/modules/providers/services/provider-capabilities.service.ts
  • src/components/chat/constants/providerEffort.ts
  • src/components/chat/hooks/useChatComposerState.ts
  • src/components/chat/hooks/useChatProviderState.ts
  • src/components/chat/view/ChatInterface.tsx
  • src/components/chat/view/subcomponents/ChatComposer.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/chat/view/subcomponents/ChatComposer.tsx
  • src/components/chat/view/ChatInterface.tsx
  • server/modules/providers/list/codex/codex-models.provider.ts
  • server/claude-sdk.js

Comment thread src/components/chat/hooks/useChatProviderState.ts Outdated
@viper151 viper151 requested a review from blackmammoth July 1, 2026 03:37
Comment thread src/components/chat/view/subcomponents/ChatComposer.tsx Fixed
@viper151 viper151 merged commit d272922 into main Jul 3, 2026
5 checks passed
shokosanma pushed a commit to shokosanma/claudecodeui that referenced this pull request Jul 4, 2026
* feat: add Claude and Codex effort controls

* refactor: generic provider effort handling

* fix: reconcile provider effort after model changes

* fix: pass effort through external agent api

* feat: add effort support for opencode

* chore: update gpt fallback models

* fix: use portal for showing effort dropdown

---------

Co-authored-by: Haileyesus <118998054+blackmammoth@users.noreply.github.com>
agogo233 added a commit to agogo233/claudecodeui that referenced this pull request Jul 7, 2026
* upstream/main:
  feat(redesign): skills and MCP action controls in settings (siteboon#942)
  Update localized README docs
  chore(release): v1.36.0
  feat: add Claude and Codex effort controls (siteboon#943)
  fix desktop release asset upload
  chore(release): v1.35.1
  fix: remove obsolete semantic helper release jobs
  Feat/design improvements and minor bug fixes (siteboon#939)
  Clarify desktop companion in README
  Point desktop downloads to CloudCLI
agogo233 added a commit to agogo233/claudecodeui that referenced this pull request Jul 7, 2026
* upstream/main:
  feat(redesign): skills and MCP action controls in settings (siteboon#942)
  Update localized README docs
  chore(release): v1.36.0
  feat: add Claude and Codex effort controls (siteboon#943)
  fix desktop release asset upload
  chore(release): v1.35.1
  fix: remove obsolete semantic helper release jobs
  Feat/design improvements and minor bug fixes (siteboon#939)
  Clarify desktop companion in README
  Point desktop downloads to CloudCLI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants