Skip to content

feat(reasoning): support anthropic adaptive reasoning#1487

Merged
zerob13 merged 6 commits into
devfrom
feat/claude-adaptive-reasoning
Apr 18, 2026
Merged

feat(reasoning): support anthropic adaptive reasoning#1487
zerob13 merged 6 commits into
devfrom
feat/claude-adaptive-reasoning

Conversation

@yyhhyyyyyy
Copy link
Copy Markdown
Collaborator

@yyhhyyyyyy yyhhyyyyyy commented Apr 18, 2026

support anthropic adaptive reasoning

Summary by CodeRabbit

  • New Features

    • Added reasoning visibility control (omitted or summarized) across model settings, chat status bar, and new-thread defaults
    • Added "Max" reasoning effort level
    • Reasoning controls now adapt to provider capability (Anthropic-aware behavior)
  • Chores

    • Updated AI SDK provider dependencies and agent registry versions
    • Persisted reasoning visibility in session storage with migration support
    • Added translations for reasoning settings in multiple languages

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

Adds provider-aware reasoning visibility (UI, persistence, normalization), generalizes capability/route resolution for providers/models, and threads capability-aware Anthropic handling through presenters, runtime, provider mappings, shared types, SQLite schema, and tests.

Changes

Cohort / File(s) Summary
Deps & Registry
package.json, resources/acp-registry/registry.json
Bumped multiple @ai-sdk/* dependency ranges and updated agent registry versions and distribution references.
Routing & Shared Model Utils
src/shared/model.ts, src/shared/types/model-db.ts
Added route meta types, endpoint resolution helpers, Claude-family detection, provider->capability mapping (resolveProviderCapabilityProviderId), reasoning visibility enums/normalizers, and Anthropic-specific reasoning helpers.
Config Presenter Core
src/main/presenter/configPresenter/index.ts, src/main/presenter/configPresenter/modelConfig.ts, src/main/presenter/configPresenter/modelCapabilities.ts
Generalized capability/route resolution (getCapabilityProviderId/resolveCapabilityRoute), included reasoningVisibility in ModelConfig, and switched temperature/temperature-fallback logic to shared helpers.
Agent Runtime & Presenter Mapping
src/main/presenter/agentRuntimePresenter/index.ts
Persist/load/sanitize reasoningVisibility in persisted session generation settings and use capability-aware reasoning enablement.
LLM Provider Runtime & Options
src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts, src/main/presenter/llmProviderPresenter/aiSdk/providerOptionsMapper.ts
Threaded capabilityProviderId and supportsOfficialAnthropicReasoning into runtime and provider-options mapping; changed Anthropic adaptive option emission and temperature handling to be capability-driven.
Provider Routing / Registry
src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts, src/main/presenter/llmProviderPresenter/providerRegistry.ts
Reworked New-API endpoint selection to route-driven resolution, added configurable anthropicBaseUrl for ZenMux, and set capability/provider flags per route.
SQLite Schema & Persistence
src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts
Added reasoning_visibility column (schema v20), migration handling, updated row type and read/write logic for generation settings.
Renderer UI & Stores
src/renderer/src/components/chat/ChatStatusBar.vue, src/renderer/src/components/settings/ModelConfigDialog.vue, src/renderer/src/stores/ui/draft.ts, src/renderer/src/pages/NewThreadPage.vue
Added reasoning-visibility Select and gating; capability-aware defaults and normalization; draft store and new-thread reset now track reasoningVisibility.
Provider Definition
src/main/presenter/llmProviderPresenter/providerRegistry.ts
Added optional anthropicBaseUrl to provider definition and set it for zenmux.
I18n
src/renderer/src/i18n/*/settings.json
Added max reasoning-effort option and new reasoningVisibility localization entries across locales.
Types & Presenters d.ts
src/shared/types/agent-interface.d.ts, src/shared/types/presenters/*.d.ts, src/shared/types/model-db.ts
Exposed reasoningVisibility in session/model types and updated presenter type declarations; added new enums, helpers, and normalization functions.
Tests
test/main/**, test/renderer/**
Extensive test additions/updates covering capability routing, Anthropic adaptive reasoning mapping and gating, reasoning visibility normalization, temperature fallbacks, runtime propagation, DB migration, and UI behavior.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI as Renderer UI
  participant Presenter
  participant Runtime
  participant Provider
  participant DB as SQLite

  User->>UI: select model / set reasoningVisibility
  UI->>Presenter: updateGenerationSettings(reasoningVisibility)
  Presenter->>DB: persist generation settings (reasoning_visibility)
  UI->>Presenter: request run (includes capabilityProviderId hint)
  Presenter->>Runtime: buildRuntimeContext(capabilityProviderId, supportsOfficialAnthropicReasoning)
  Runtime->>Provider: buildProviderOptions (may include Anthropic adaptive fields)
  Provider->>Provider: resolve route & endpointType (capability mapping)
  Runtime->>Provider: send request (providerOptions, thinking config)
  Provider->>Runtime: response / stream events
  Runtime->>UI: emit stream events / traces
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Suggested reviewers

  • zerob13

"🐰
I hopped through routes, found visibility bright,
Anthropic whispers now shown just right,
Defaults persisted, controls in view,
Routes remapped, tests hopped too,
A tiny rabbit celebrates the new sight!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective of the changeset: adding support for Anthropic's adaptive reasoning feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/claude-adaptive-reasoning

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
src/renderer/src/components/chat/ChatStatusBar.vue (1)

1956-1979: ⚠️ Potential issue | 🟠 Major

Use the shared effective-reasoning check for Anthropic draft defaults.

The renderer gates defaults on modelConfig.reasoning === true, while the backend uses getReasoningEffectiveEnabledForProvider(...) with both reasoning and reasoningEffort. This can make draft sessions drop reasoningEffort/reasoningVisibility for models that are effectively enabled through the default effort/portrait path.

🐛 Proposed fix to align renderer defaults with backend sanitization
 import {
   ANTHROPIC_REASONING_VISIBILITY_VALUES,
   DEFAULT_REASONING_EFFORT_OPTIONS as FALLBACK_REASONING_EFFORT_OPTIONS,
+  getReasoningEffectiveEnabledForProvider,
   hasAnthropicReasoningToggle,
   isReasoningEffort,
   normalizeAnthropicReasoningVisibilityValue,
   isVerbosity,
   type AnthropicReasoningVisibility,
@@
   const anthropicReasoningToggle = hasAnthropicReasoningToggle(
     resolvedCapabilityProviderId,
     portrait
   )
-  const canDefaultReasoningEffort = !anthropicReasoningToggle || modelConfig.reasoning === true
+  const anthropicReasoningEnabled = anthropicReasoningToggle
+    ? getReasoningEffectiveEnabledForProvider(resolvedCapabilityProviderId, portrait, {
+        reasoning: modelConfig.reasoning,
+        reasoningEffort: modelConfig.reasoningEffort
+      })
+    : true
 
-  if (supportsReasoningEffort(portrait) && canDefaultReasoningEffort) {
+  if (
+    supportsReasoningEffort(portrait) &&
+    (!anthropicReasoningToggle || anthropicReasoningEnabled)
+  ) {
     const effort = normalizeReasoningEffort(
       portrait,
       modelConfig.reasoningEffort ?? portrait?.effort
@@
   const reasoningVisibility = normalizeReasoningVisibility(
     resolvedCapabilityProviderId,
     portrait,
     modelConfig.reasoningVisibility ?? portrait?.visibility
   )
-  if (modelConfig.reasoning === true && reasoningVisibility) {
+  if (anthropicReasoningToggle && anthropicReasoningEnabled && reasoningVisibility) {
     defaults.reasoningVisibility = reasoningVisibility
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/chat/ChatStatusBar.vue` around lines 1956 - 1979,
The renderer currently gates setting defaults.reasoningEffort and
defaults.reasoningVisibility on modelConfig.reasoning === true (via
canDefaultReasoningEffort), which diverges from backend logic that uses
getReasoningEffectiveEnabledForProvider(...) and considers the effective
reasoning state including default effort/portrait; update the renderer to use
the same effective check rather than only modelConfig.reasoning: replace the
canDefaultReasoningEffort logic (which uses anthropicReasoningToggle and
modelConfig.reasoning) with a call to the shared effective reasoning check (the
same predicate/utility used by getReasoningEffectiveEnabledForProvider or a new
exported helper), and then allow normalizeReasoningEffort(...) and
normalizeReasoningVisibility(...) to set defaults when that effective-enabled
check returns true (keeping supportsReasoningEffort, normalizeReasoningEffort,
normalizeReasoningVisibility, and anthropicReasoningToggle references intact).
🧹 Nitpick comments (4)
src/main/presenter/configPresenter/modelConfig.ts (1)

431-476: Include reasoningVisibility in fallback/merge branches for consistency.

reasoningVisibility is new but is not listed alongside the other reasoning-derived fields in either branch:

  • In the default-fallback object (Lines 432–446), reasoningEffort and verbosity are explicitly initialized to undefined; reasoningVisibility is missing, so finalConfig!.isUserDefined = false returns an object without that key at all.
  • In the stored-non-user merge (Lines 449–475), every other portrait-derived field (reasoning, thinkingBudget, forceInterleavedThinkingCompat, reasoningEffort, verbosity) is explicitly re-pinned from finalConfig to ensure portrait changes win over stale stored values. reasoningVisibility is only carried via the ...finalConfig spread, making the intent inconsistent and easy to break in future edits.
♻️ Suggested alignment
       reasoningEffort: undefined,
+      reasoningVisibility: undefined,
       verbosity: undefined,
         reasoningEffort: finalConfig.reasoningEffort,
+        reasoningVisibility: finalConfig.reasoningVisibility,
         verbosity: finalConfig.verbosity
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/configPresenter/modelConfig.ts` around lines 431 - 476,
The code omits reasoningVisibility from the default fallback and the explicit
merge overrides, causing inconsistent propagation; add reasoningVisibility:
undefined to the initial finalConfig fallback (near
DEFAULT_MODEL_CAPABILITY_FALLBACKS initialization) and add reasoningVisibility:
finalConfig.reasoningVisibility to the storedConfig/storedSource !== 'user'
merge block (alongside reasoning, thinkingBudget,
forceInterleavedThinkingCompat, reasoningEffort, verbosity) so portrait-derived
visibility is explicitly preserved.
src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts (2)

228-321: Minor: route argument could include type / supportedEndpointTypes for future-proofing.

Each resolveProviderCapabilityProviderId(this.provider.id, { endpointType }, modelId) call passes only endpointType. That's sufficient today because resolveNewApiEndpointTypeFromRoute short-circuits on a valid endpointType — but if the helper's precedence ever changes (e.g., giving type: ModelType.ImageGeneration higher priority), these sites would silently drift. Since you already have this.resolveNewApiEndpointType(modelId) above, consider passing a richer route shape (e.g., also include the stored model's supportedEndpointTypes/type) or hardcoding the capability provider per case ('anthropic', 'gemini', 'openai') given the switch already disambiguates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts` around
lines 228 - 321, The calls to
resolveProviderCapabilityProviderId(this.provider.id, { endpointType }, modelId)
only pass endpointType, which may allow future precedence changes in
resolveNewApiEndpointTypeFromRoute to pick the wrong capability; update each
call in the switch inside the strategy === 'new-api' branch (where
resolveNewApiEndpointType(modelId) is used and providerPatch is built for cases
'anthropic', 'gemini', 'openai-response', 'image-generation', and default) to
pass a richer route object (e.g., include type: ModelType.ImageGeneration or
supportedEndpointTypes from the model) or explicitly choose the capability
provider id per case so the resolved capability cannot drift if helper
precedence changes.

172-179: Consider validating anthropicBaseUrl at provider construction for earlier failure detection.

The getConfiguredAnthropicBaseUrl() method throws during resolveRouteDecision() on anthropic/* requests if this.definition.anthropicBaseUrl is missing. While zenmux's registry definition correctly sets anthropicBaseUrl: 'https://zenmux.ai/api/anthropic', validating at AiSdkProvider construction would catch any future misconfiguration earlier in the provider lifecycle rather than at first request time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts` around
lines 172 - 179, The provider currently defers validating
this.definition.anthropicBaseUrl until getConfiguredAnthropicBaseUrl() is
called, causing runtime failures in resolveRouteDecision(); instead validate and
normalize anthropicBaseUrl in the AiSdkProvider constructor: check
this.definition.anthropicBaseUrl exists and .trim() is non-empty, throw a clear
Error referencing this.provider.id if missing, and optionally store the trimmed
value on a new private field (e.g., this.configuredAnthropicBaseUrl) to replace
getConfiguredAnthropicBaseUrl() callers so misconfiguration is detected at
construction time.
src/renderer/src/components/settings/ModelConfigDialog.vue (1)

996-1011: Optional: redundant normalization branches.

The supportsReasoningVisibility block at lines 1004–1011 runs unconditionally and only overwrites config.value.reasoningVisibility when normalizeAnthropicReasoningVisibilityValue(...) returns a truthy value — i.e. it only no-ops or idempotently rewrites a valid value. For user-defined configs with an invalid visibility, the stale value is kept here and then corrected by the watcher at line 1320 once reasoning is enabled. Functionally correct, but the two-pass normalization is a bit hard to follow; consider collapsing into a single normalize-then-default step inside loadConfig.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/settings/ModelConfigDialog.vue` around lines 996
- 1011, The code runs normalization twice: first in loadConfig (setting
config.value.reasoningVisibility to
normalizeAnthropicReasoningVisibilityValue(...) ??
capabilityReasoningVisibilityDefault.value) and then again unconditionally in a
later supportsReasoningVisibility.value block that only overwrites when
normalization returns truthy; collapse these into one clear step by removing the
redundant second branch and ensure loadConfig performs
normalizeAnthropicReasoningVisibilityValue(config.value.reasoningVisibility)
once and falls back to capabilityReasoningVisibilityDefault.value when
normalization returns falsy; update the watcher (the reasoning-enabled watcher)
only if necessary so there’s a single canonical normalization path tied to
loadConfig and the supportsReasoningVisibility flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts`:
- Around line 154-158: The current runtime uses the provider-level fallback for
capabilityProviderId when calling buildProviderOptions and when checking
supportsTemperatureControlRuntime, which lets routed providers (e.g., "new-api"
Claude) bypass model-aware routing; update both call sites to resolve
capabilityProviderId using the presenter’s model-aware helper
getCapabilityProviderId(providerId, modelId) (pass context.provider.id and
context.provider.modelId) and fall back to context.provider.capabilityProviderId
or context.provider.id only afterwards, then pass that resolved id into
buildProviderOptions and into supportsTemperatureControlRuntime so routing
honors model-level mappings.

In `@src/renderer/src/i18n/da-DK/settings.json`:
- Around line 422-429: The option labels under reasoningVisibility still have
English prefixes; update the values for options.omitted and options.summarized
to use Danish prefixes (e.g., change "Omitted - Skjul ræsonneringsindhold" to a
Danish-prefixed string like "Udeladt - Skjul ræsonneringsindhold" and
"Summarized - Vis ræsonnering som resumé" to "Opsummeret - Vis ræsonnering som
resumé") so the user-facing text is fully localized while keeping the rest of
each phrase intact.

In `@src/shared/types/presenters/legacy.presenters.d.ts`:
- Line 168: The CONVERSATION_SETTINGS declaration in legacy.presenters.d.ts is
missing the new optional property reasoningVisibility, so update the legacy
conversation settings type(s) to include reasoningVisibility?:
ReasoningVisibility wherever CONVERSATION_SETTINGS (and the equivalent legacy
conversation settings type around the other noted location) is defined; ensure
both the type used by createConversation and updateConversationSettings accept
this optional field so legacy callers can pass the persisted session setting
through.

In `@test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts`:
- Around line 2086-2158: The test's getReasoningPortrait mock doesn't cover the
capability-provider path so production code that queries the portrait via the
mapped provider falls back to the generic portrait; update the mock for
configPresenter.getReasoningPortrait to return the disabled Anthropics portrait
when the capability provider is used (i.e. treat providerId === 'anthropic' and
modelId === 'claude-opus-4-7' the same as the current new-api branch), so the
sequence involving configPresenter.getCapabilityProviderId,
agent.initSession('s1', { providerId: 'new-api', modelId: 'claude-opus-4-7' })
and agent.updateGenerationSettings('s1', ...) exercises the Anthropic capability
route and validates the reasoning fields are dropped.

In `@test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts`:
- Around line 175-209: The test currently asserts absence of temperature on
tracePayloads[0]?.body but the runtime emits temperature at the top-level of the
trace payload; update the assertion to check tracePayloads[0] itself (e.g.,
expect(tracePayloads[0]).not.toHaveProperty('temperature')) so the test fails if
temperature is emitted at the top level; modify the assertion in
aiSdkRuntime.test (near the runAiSdkCoreStream invocation and the
emitRequestTrace mock) to validate the top-level payload rather than
payload.body.

In `@test/main/presenter/sqlitePresenter.test.ts`:
- Line 509: The long SQL select literal 'SELECT system_prompt, summary_text,
summary_cursor_order_seq, force_interleaved_thinking_compat,
reasoning_visibility FROM deepchat_sessions WHERE id = ?' exceeds 100 chars;
split the string into multiple single-quoted parts so no line is longer than 100
characters (e.g. break after column list and before the FROM/WHERE clause) and
concatenate them (keep single quotes and no semicolons) where this literal
appears in the test file so the final SQL is unchanged but wrapped to satisfy
the Oxfmt width rule.

In `@test/renderer/components/ChatStatusBar.test.ts`:
- Around line 361-363: The mock for getCapabilityProviderId must match the real
synchronous signature (providerId: string, modelId: string); update the mock in
ChatStatusBar.test to remove async and accept two parameters so it returns
options.capabilityProviderId ?? providerId synchronously (e.g.,
vi.fn().mockImplementation((providerId: string, modelId: string) =>
options.capabilityProviderId ?? providerId)), ensuring callers like
getReasoningPortrait and supportsReasoningCapability see the correct signature.

---

Outside diff comments:
In `@src/renderer/src/components/chat/ChatStatusBar.vue`:
- Around line 1956-1979: The renderer currently gates setting
defaults.reasoningEffort and defaults.reasoningVisibility on
modelConfig.reasoning === true (via canDefaultReasoningEffort), which diverges
from backend logic that uses getReasoningEffectiveEnabledForProvider(...) and
considers the effective reasoning state including default effort/portrait;
update the renderer to use the same effective check rather than only
modelConfig.reasoning: replace the canDefaultReasoningEffort logic (which uses
anthropicReasoningToggle and modelConfig.reasoning) with a call to the shared
effective reasoning check (the same predicate/utility used by
getReasoningEffectiveEnabledForProvider or a new exported helper), and then
allow normalizeReasoningEffort(...) and normalizeReasoningVisibility(...) to set
defaults when that effective-enabled check returns true (keeping
supportsReasoningEffort, normalizeReasoningEffort, normalizeReasoningVisibility,
and anthropicReasoningToggle references intact).

---

Nitpick comments:
In `@src/main/presenter/configPresenter/modelConfig.ts`:
- Around line 431-476: The code omits reasoningVisibility from the default
fallback and the explicit merge overrides, causing inconsistent propagation; add
reasoningVisibility: undefined to the initial finalConfig fallback (near
DEFAULT_MODEL_CAPABILITY_FALLBACKS initialization) and add reasoningVisibility:
finalConfig.reasoningVisibility to the storedConfig/storedSource !== 'user'
merge block (alongside reasoning, thinkingBudget,
forceInterleavedThinkingCompat, reasoningEffort, verbosity) so portrait-derived
visibility is explicitly preserved.

In `@src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts`:
- Around line 228-321: The calls to
resolveProviderCapabilityProviderId(this.provider.id, { endpointType }, modelId)
only pass endpointType, which may allow future precedence changes in
resolveNewApiEndpointTypeFromRoute to pick the wrong capability; update each
call in the switch inside the strategy === 'new-api' branch (where
resolveNewApiEndpointType(modelId) is used and providerPatch is built for cases
'anthropic', 'gemini', 'openai-response', 'image-generation', and default) to
pass a richer route object (e.g., include type: ModelType.ImageGeneration or
supportedEndpointTypes from the model) or explicitly choose the capability
provider id per case so the resolved capability cannot drift if helper
precedence changes.
- Around line 172-179: The provider currently defers validating
this.definition.anthropicBaseUrl until getConfiguredAnthropicBaseUrl() is
called, causing runtime failures in resolveRouteDecision(); instead validate and
normalize anthropicBaseUrl in the AiSdkProvider constructor: check
this.definition.anthropicBaseUrl exists and .trim() is non-empty, throw a clear
Error referencing this.provider.id if missing, and optionally store the trimmed
value on a new private field (e.g., this.configuredAnthropicBaseUrl) to replace
getConfiguredAnthropicBaseUrl() callers so misconfiguration is detected at
construction time.

In `@src/renderer/src/components/settings/ModelConfigDialog.vue`:
- Around line 996-1011: The code runs normalization twice: first in loadConfig
(setting config.value.reasoningVisibility to
normalizeAnthropicReasoningVisibilityValue(...) ??
capabilityReasoningVisibilityDefault.value) and then again unconditionally in a
later supportsReasoningVisibility.value block that only overwrites when
normalization returns truthy; collapse these into one clear step by removing the
redundant second branch and ensure loadConfig performs
normalizeAnthropicReasoningVisibilityValue(config.value.reasoningVisibility)
once and falls back to capabilityReasoningVisibilityDefault.value when
normalization returns falsy; update the watcher (the reasoning-enabled watcher)
only if necessary so there’s a single canonical normalization path tied to
loadConfig and the supportsReasoningVisibility flag.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 284f4a1d-9bde-4fde-a061-126caf8ba55f

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec8f2d and 0a07ed7.

📒 Files selected for processing (46)
  • package.json
  • resources/acp-registry/registry.json
  • resources/model-db/providers.json
  • src/main/presenter/agentRuntimePresenter/index.ts
  • src/main/presenter/configPresenter/index.ts
  • src/main/presenter/configPresenter/modelCapabilities.ts
  • src/main/presenter/configPresenter/modelConfig.ts
  • src/main/presenter/llmProviderPresenter/aiSdk/providerOptionsMapper.ts
  • src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts
  • src/main/presenter/llmProviderPresenter/providerRegistry.ts
  • src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts
  • src/main/presenter/sqlitePresenter/tables/deepchatSessions.ts
  • src/renderer/src/components/chat/ChatStatusBar.vue
  • src/renderer/src/components/settings/ModelConfigDialog.vue
  • src/renderer/src/i18n/da-DK/settings.json
  • src/renderer/src/i18n/en-US/settings.json
  • src/renderer/src/i18n/fa-IR/settings.json
  • src/renderer/src/i18n/fr-FR/settings.json
  • src/renderer/src/i18n/he-IL/settings.json
  • src/renderer/src/i18n/ja-JP/settings.json
  • src/renderer/src/i18n/ko-KR/settings.json
  • src/renderer/src/i18n/pt-BR/settings.json
  • src/renderer/src/i18n/ru-RU/settings.json
  • src/renderer/src/i18n/zh-CN/settings.json
  • src/renderer/src/i18n/zh-HK/settings.json
  • src/renderer/src/i18n/zh-TW/settings.json
  • src/renderer/src/pages/NewThreadPage.vue
  • src/renderer/src/stores/ui/draft.ts
  • src/shared/model.ts
  • src/shared/types/agent-interface.d.ts
  • src/shared/types/model-db.ts
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/shared/types/presenters/session.presenter.d.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts
  • test/main/presenter/configPresenter/modelCapabilities.test.ts
  • test/main/presenter/configPresenter/providerModelCapabilityMapping.test.ts
  • test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts
  • test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts
  • test/main/presenter/llmProviderPresenter/newApiProvider.test.ts
  • test/main/presenter/llmProviderPresenter/promptCacheStrategy.test.ts
  • test/main/presenter/modelConfig.test.ts
  • test/main/presenter/sqlitePresenter.test.ts
  • test/main/shared/model.test.ts
  • test/main/shared/modelDb.test.ts
  • test/renderer/components/ChatStatusBar.test.ts
  • test/renderer/components/ModelConfigDialog.test.ts

Comment thread src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts
Comment on lines +422 to +429
"reasoningVisibility": {
"label": "Synlighed for ræsonnering",
"description": "Styrer om ræsonneringsindhold skjules eller vises som et kort resumé, når ræsonnering er aktiveret",
"placeholder": "Vælg synlighed for ræsonnering",
"options": {
"omitted": "Omitted - Skjul ræsonneringsindhold",
"summarized": "Summarized - Vis ræsonnering som resumé"
}
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.

⚠️ Potential issue | 🟡 Minor

Localize the visible option prefixes.

Omitted and Summarized are user-facing English labels in the Danish locale; translate the prefixes to keep this control consistent with the rest of the page.

🌐 Proposed localization tweak
-          "omitted": "Omitted - Skjul ræsonneringsindhold",
-          "summarized": "Summarized - Vis ræsonnering som resumé"
+          "omitted": "Udeladt - Skjul ræsonneringsindhold",
+          "summarized": "Opsummeret - Vis ræsonnering som resumé"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"reasoningVisibility": {
"label": "Synlighed for ræsonnering",
"description": "Styrer om ræsonneringsindhold skjules eller vises som et kort resumé, når ræsonnering er aktiveret",
"placeholder": "Vælg synlighed for ræsonnering",
"options": {
"omitted": "Omitted - Skjul ræsonneringsindhold",
"summarized": "Summarized - Vis ræsonnering som resumé"
}
"reasoningVisibility": {
"label": "Synlighed for ræsonnering",
"description": "Styrer om ræsonneringsindhold skjules eller vises som et kort resumé, når ræsonnering er aktiveret",
"placeholder": "Vælg synlighed for ræsonnering",
"options": {
"omitted": "Udeladt - Skjul ræsonneringsindhold",
"summarized": "Opsummeret - Vis ræsonnering som resumé"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/i18n/da-DK/settings.json` around lines 422 - 429, The option
labels under reasoningVisibility still have English prefixes; update the values
for options.omitted and options.summarized to use Danish prefixes (e.g., change
"Omitted - Skjul ræsonneringsindhold" to a Danish-prefixed string like "Udeladt
- Skjul ræsonneringsindhold" and "Summarized - Vis ræsonnering som resumé" to
"Opsummeret - Vis ræsonnering som resumé") so the user-facing text is fully
localized while keeping the rest of each phrase intact.

forceInterleavedThinkingCompat?: boolean
// New parameters for GPT-5 series
reasoningEffort?: ReasoningEffort
reasoningVisibility?: ReasoningVisibility
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.

⚠️ Potential issue | 🟡 Minor

Add reasoningVisibility to legacy conversation settings too.

reasoningVisibility is now part of persisted session settings, but CONVERSATION_SETTINGS still omits it. Legacy createConversation / updateConversationSettings callers will not be able to pass the new setting through this declaration.

Suggested type alignment
   enabledMcpTools?: string[]
   thinkingBudget?: number
   reasoningEffort?: ReasoningEffort
+  reasoningVisibility?: ReasoningVisibility
   verbosity?: Verbosity
   selectedVariantsMap?: Record<string, string>

Also applies to: 1247-1250

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/types/presenters/legacy.presenters.d.ts` at line 168, The
CONVERSATION_SETTINGS declaration in legacy.presenters.d.ts is missing the new
optional property reasoningVisibility, so update the legacy conversation
settings type(s) to include reasoningVisibility?: ReasoningVisibility wherever
CONVERSATION_SETTINGS (and the equivalent legacy conversation settings type
around the other noted location) is defined; ensure both the type used by
createConversation and updateConversationSettings accept this optional field so
legacy callers can pass the persisted session setting through.

Comment on lines +175 to +209
const tracePayloads: Array<{ body?: Record<string, unknown> }> = []
const context = {
providerKind: 'openai-compatible',
provider: {
id: 'aihubmix',
apiType: 'openai-compatible'
},
configPresenter: {
supportsTemperatureControl: vi.fn().mockReturnValue(false)
},
defaultHeaders: {},
emitRequestTrace: vi.fn(async (_modelConfig, payload) => {
tracePayloads.push(payload)
})
} as any

const events = []
for await (const event of runAiSdkCoreStream(
context,
[],
modelId,
{
apiEndpoint: 'chat',
functionCall: false
} as any,
0.5,
2048,
[]
)) {
events.push(event)
}

const events = []
for await (const event of runAiSdkCoreStream(
context,
[],
'claude-opus-4-7-think',
{
apiEndpoint: 'chat',
functionCall: false
} as any,
0.5,
2048,
[]
)) {
events.push(event)
const request = mockStreamText.mock.calls[0]?.[0] as Record<string, unknown>
expect(request).not.toHaveProperty('temperature')
expect(tracePayloads[0]?.body).not.toHaveProperty('temperature')
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.

⚠️ Potential issue | 🟡 Minor

Assert the actual trace payload, not body.

The runtime emits temperature at the top level of the trace payload, so checking tracePayloads[0]?.body can pass even if the trace regresses and includes temperature.

Suggested test fix
-      const tracePayloads: Array<{ body?: Record<string, unknown> }> = []
+      const tracePayloads: Array<Record<string, unknown>> = []
@@
       const request = mockStreamText.mock.calls[0]?.[0] as Record<string, unknown>
+      expect(tracePayloads).toHaveLength(1)
       expect(request).not.toHaveProperty('temperature')
-      expect(tracePayloads[0]?.body).not.toHaveProperty('temperature')
+      expect(tracePayloads[0]).not.toHaveProperty('temperature')
       expect(events).toEqual([])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts` around lines
175 - 209, The test currently asserts absence of temperature on
tracePayloads[0]?.body but the runtime emits temperature at the top-level of the
trace payload; update the assertion to check tracePayloads[0] itself (e.g.,
expect(tracePayloads[0]).not.toHaveProperty('temperature')) so the test fails if
temperature is emitted at the top level; modify the assertion in
aiSdkRuntime.test (near the runAiSdkCoreStream invocation and the
emitRequestTrace mock) to validate the top-level payload rather than
payload.body.

const row = checkDb
.prepare(
'SELECT system_prompt, summary_text, summary_cursor_order_seq, force_interleaved_thinking_compat FROM deepchat_sessions WHERE id = ?'
'SELECT system_prompt, summary_text, summary_cursor_order_seq, force_interleaved_thinking_compat, reasoning_visibility FROM deepchat_sessions WHERE id = ?'
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.

⚠️ Potential issue | 🟡 Minor

Wrap the long SQL select string.

Line 509 exceeds the configured 100-character width.

Suggested formatting
-        'SELECT system_prompt, summary_text, summary_cursor_order_seq, force_interleaved_thinking_compat, reasoning_visibility FROM deepchat_sessions WHERE id = ?'
+        [
+          'SELECT system_prompt, summary_text, summary_cursor_order_seq,',
+          'force_interleaved_thinking_compat, reasoning_visibility',
+          'FROM deepchat_sessions WHERE id = ?'
+        ].join(' ')

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use single quotes, no semicolons, and maintain 100 character line width per Oxfmt configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/sqlitePresenter.test.ts` at line 509, The long SQL select
literal 'SELECT system_prompt, summary_text, summary_cursor_order_seq,
force_interleaved_thinking_compat, reasoning_visibility FROM deepchat_sessions
WHERE id = ?' exceeds 100 chars; split the string into multiple single-quoted
parts so no line is longer than 100 characters (e.g. break after column list and
before the FROM/WHERE clause) and concatenate them (keep single quotes and no
semicolons) where this literal appears in the test file so the final SQL is
unchanged but wrapped to satisfy the Oxfmt width rule.

Comment on lines +361 to +363
getCapabilityProviderId: vi
.fn()
.mockImplementation(async (providerId: string) => options.capabilityProviderId ?? providerId),
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the real signature and how ChatStatusBar.vue consumes it.
rg -nP --type=ts -C2 '\bgetCapabilityProviderId\s*\(' -g '!**/*.test.ts' -g '!**/*.spec.ts'
fd -i 'ChatStatusBar.vue' --exec rg -nP -C3 'getCapabilityProviderId|capabilityProviderId' {}

Repository: ThinkInAIXYZ/deepchat

Length of output: 7705


🏁 Script executed:

# Check if test exercises getCapabilityProviderId through other methods
sed -n '361,400p' test/renderer/components/ChatStatusBar.test.ts

# Look for actual calls to configPresenter methods in the test that might use getCapabilityProviderId
rg -n 'configPresenter\.(supportsReasoningCapability|getReasoningPortrait|getThinkingBudgetRange)' test/renderer/components/ChatStatusBar.test.ts | head -20

Repository: ThinkInAIXYZ/deepchat

Length of output: 1762


🏁 Script executed:

# Check real signatures of these methods in configPresenter
rg -n 'supportsReasoningCapability|getReasoningPortrait|getThinkingBudgetRange' src/main/presenter/configPresenter/index.ts -A1

# Check if the test actually calls these presenter methods
rg -n 'configPresenter\.(supportsReasoningCapability|getReasoningPortrait|getThinkingBudgetRange|getCapabilityProviderId)' test/renderer/components/ChatStatusBar.test.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 685


🏁 Script executed:

# Check what the test does with configPresenter - look for actual calls
rg -n 'configPresenter\.' test/renderer/components/ChatStatusBar.test.ts | head -30

# Also check the broader context of how the presenter is used
sed -n '300,400p' test/renderer/components/ChatStatusBar.test.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 4318


🏁 Script executed:

# Check if getCapabilityProviderId is ever called by the component or test
rg -n 'configPresenter\.getCapabilityProviderId' test/renderer/components/ChatStatusBar.test.ts
rg -n 'this\.getCapabilityProviderId' src/renderer/components/ChatStatusBar.vue

Repository: ThinkInAIXYZ/deepchat

Length of output: 146


🏁 Script executed:

# Find ChatStatusBar.vue
fd -i 'ChatStatusBar.vue'

# Check for calls to getCapabilityProviderId in test
rg -n 'getCapabilityProviderId' test/renderer/components/ChatStatusBar.test.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 151


🏁 Script executed:

# Check if ChatStatusBar.vue calls getCapabilityProviderId
rg -n 'getCapabilityProviderId' src/renderer/src/components/chat/ChatStatusBar.vue

# Also check the real implementation calls
rg -n 'getCapabilityProviderId' src/main/presenter/configPresenter/index.ts | wc -l

Repository: ThinkInAIXYZ/deepchat

Length of output: 67


🏁 Script executed:

# Get actual lines where getCapabilityProviderId appears in ChatStatusBar.vue
rg -n 'getCapabilityProviderId' src/renderer/src/components/chat/ChatStatusBar.vue -A2

# Confirm the mock is async while real is sync
sed -n '691p' src/main/presenter/configPresenter/index.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 137


🏁 Script executed:

# Check full context around getCapabilityProviderId in ChatStatusBar.vue
rg -C5 'getCapabilityProviderId' src/renderer/src/components/chat/ChatStatusBar.vue

# Also verify the test doesn't call configPresenter.getCapabilityProviderId directly
rg 'configPresenter\.getCapabilityProviderId\(' test/renderer/components/ChatStatusBar.test.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 47


🏁 Script executed:

# Let's check the first rg result more carefully - get actual context
rg -n 'getCapabilityProviderId' src/renderer/src/components/chat/ChatStatusBar.vue

# Also check if test uses mocked methods that internally depend on getCapabilityProviderId
rg -n 'configPresenter\.(supportsReasoningCapability|getReasoningPortrait|getThinkingBudgetRange)' test/renderer/components/ChatStatusBar.test.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 47


🏁 Script executed:

# Check if ChatStatusBar is actually using configPresenter at all in the test
rg -n 'configPresenter' src/renderer/src/components/chat/ChatStatusBar.vue | head -5

# Look at what methods the component actually calls on configPresenter
rg -o 'configPresenter\.\w+' src/renderer/src/components/chat/ChatStatusBar.vue | sort | uniq -c

Repository: ThinkInAIXYZ/deepchat

Length of output: 791


🏁 Script executed:

# Check what getReasoningPortrait does - see if it actually calls getCapabilityProviderId
sed -n '706,711p' src/main/presenter/configPresenter/index.ts

# Check what the test mocks for getReasoningPortrait
grep -A2 'getReasoningPortrait:' test/renderer/components/ChatStatusBar.test.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 542


🏁 Script executed:

# Final verification: is getCapabilityProviderId ever actually invoked in any context?
# Check if test uses any method that would call getCapabilityProviderId
grep -n 'getReasoningPortrait\|supportsReasoningCapability\|getThinkingBudgetRange\|getTemperatureCapability\|supportsTemperatureControl\|getReasoningEffortDefault\|supportsReasoningEffortCapability\|getVerbosityDefault\|supportsVerbosityCapability' test/renderer/components/ChatStatusBar.test.ts

# Also verify the mocked methods are actually called in tests
rg -n '\.toHaveBeenCalled' test/renderer/components/ChatStatusBar.test.ts | grep -E 'getReasoningPortrait|supportsReasoningCapability|getThinkingBudgetRange'

Repository: ThinkInAIXYZ/deepchat

Length of output: 812


🏁 Script executed:

# Check if any test assertions verify getCapabilityProviderId was called
grep -n 'getCapabilityProviderId' test/renderer/components/ChatStatusBar.test.ts

# Check if the mocked methods that depend on getCapabilityProviderId are actually used/asserted
rg -n 'expect.*getReasoningPortrait|expect.*supportsReasoningCapability|expect.*getThinkingBudgetRange' test/renderer/components/ChatStatusBar.test.ts | head -10

Repository: ThinkInAIXYZ/deepchat

Length of output: 100


Mock signature does not match the real method.

getCapabilityProviderId(providerId: string, modelId: string) in src/main/presenter/configPresenter/index.ts:691 is synchronous and takes 2 parameters, but the mock (lines 361-363) is async and accepts only 1 parameter. While the mock is not directly invoked in this test, it should match the real signature for consistency and to prevent bugs if the test is later modified to exercise the dependent methods (getReasoningPortrait, supportsReasoningCapability, etc. that call getCapabilityProviderId internally).

🔧 Suggested fix
-    getCapabilityProviderId: vi
-      .fn()
-      .mockImplementation(async (providerId: string) => options.capabilityProviderId ?? providerId),
+    getCapabilityProviderId: vi
+      .fn()
+      .mockImplementation(
+        (providerId: string, _modelId: string) => options.capabilityProviderId ?? providerId
+      ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getCapabilityProviderId: vi
.fn()
.mockImplementation(async (providerId: string) => options.capabilityProviderId ?? providerId),
getCapabilityProviderId: vi
.fn()
.mockImplementation(
(providerId: string, _modelId: string) => options.capabilityProviderId ?? providerId
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/renderer/components/ChatStatusBar.test.ts` around lines 361 - 363, The
mock for getCapabilityProviderId must match the real synchronous signature
(providerId: string, modelId: string); update the mock in ChatStatusBar.test to
remove async and accept two parameters so it returns
options.capabilityProviderId ?? providerId synchronously (e.g.,
vi.fn().mockImplementation((providerId: string, modelId: string) =>
options.capabilityProviderId ?? providerId)), ensuring callers like
getReasoningPortrait and supportsReasoningCapability see the correct signature.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
test/main/presenter/llmProviderPresenter/newApiProvider.test.ts (1)

251-256: Guard against an undefined anthropicBaseUrl silently passing.

toBe(definition?.anthropicBaseUrl) will pass trivially if both sides are undefined (e.g., registry regression drops anthropicBaseUrl from zenmux). Consider asserting the expected value upfront so the intent of the test is preserved even if the registry changes.

♻️ Suggested tightening
     const definition = resolveAiSdkProviderDefinition(zenmuxProvider)
+    expect(definition?.anthropicBaseUrl).toBeTruthy()
 
     expect(routeDecision.providerKind).toBe('anthropic')
     expect(routeDecision.supportsOfficialAnthropicReasoning).toBe(true)
     expect(runtimeProvider.apiType).toBe('anthropic')
     expect(runtimeProvider.baseUrl).toBe(definition?.anthropicBaseUrl)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/newApiProvider.test.ts` around lines
251 - 256, The test currently compares runtimeProvider.baseUrl to
definition?.anthropicBaseUrl which can both be undefined and silently pass;
before the existing expect runtimeProvider.baseUrl assertion (and using
resolveAiSdkProviderDefinition(zenmuxProvider)), add an explicit assertion that
definition?.anthropicBaseUrl is defined (e.g.,
expect(definition?.anthropicBaseUrl).toBeDefined()) or assert it equals the
expected literal URL, then assert runtimeProvider.baseUrl equals that defined
value so the test fails if the registry drops anthropicBaseUrl; reference
resolveAiSdkProviderDefinition, definition, runtimeProvider, and routeDecision
in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/src/components/chat/ChatStatusBar.vue`:
- Around line 1680-1686: The computed showReasoningVisibility currently hides
the control when localSettings.value?.reasoningVisibility is undefined; remove
that presence check so the control is shown whenever Anthropic
capabilities/options are available. Update the computed (referencing
showReasoningVisibility, isAcpAgent, localSettings, hasAnthropicReasoningToggle,
capabilityProviderId, capabilityReasoningPortrait) to gate only on
!isAcpAgent.value, Boolean(localSettings.value), and
hasAnthropicReasoningToggle(...), letting the Select's existing fallback handle
missing persisted reasoningVisibility values.

In `@src/shared/model.ts`:
- Around line 80-83: The function isClaudeOpus47FamilyModelId has a misleading
"Family" suffix but performs exact matches; rename the function to
isClaudeOpus47ModelId (and update all call sites) to reflect exact-match
semantics, or alternatively change its check inside normalizeUnprefixedModelId
to use startsWith('claude-opus-4-7') if you prefer family-style matching; update
the exported function name and any imports/usages accordingly to keep API
consistent with isClaudeFamilyModelId naming.

In `@test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts`:
- Around line 177-210: The test must exercise the capability-provider route:
change the context to use providerKind 'capability-provider' (instead of
'openai-compatible') and make configPresenter.supportsTemperatureControl return
true so the runtime takes the capability-provider branch when calling
runAiSdkCoreStream; keep the rest of the setup and then assert that the outgoing
request (mockStreamText.mock.calls[0][0]) and tracePayloads[0].body still do not
include a 'temperature' property. Use the symbols context.providerKind,
context.configPresenter.supportsTemperatureControl, and runAiSdkCoreStream to
locate and modify the test.
- Around line 254-304: The test currently uses portraitSpy =
vi.spyOn(modelCapabilities, 'getReasoningPortrait').mockReturnValue(...) which
masks whether getReasoningPortrait was invoked with the correct provider and
model; after calling runAiSdkGenerateText assert the spy received the expected
routing args (e.g. expect(portraitSpy).toHaveBeenCalledWith('anthropic',
'zenmux', 'anthropic/claude-opus-4-7') or the exact two/three-arg signature used
by getReasoningPortrait in your code) before restoring the spy so the test
verifies the lookup route as well as the returned portrait; locate
getReasoningPortrait, portraitSpy and runAiSdkGenerateText in the test to add
this assertion.

---

Nitpick comments:
In `@test/main/presenter/llmProviderPresenter/newApiProvider.test.ts`:
- Around line 251-256: The test currently compares runtimeProvider.baseUrl to
definition?.anthropicBaseUrl which can both be undefined and silently pass;
before the existing expect runtimeProvider.baseUrl assertion (and using
resolveAiSdkProviderDefinition(zenmuxProvider)), add an explicit assertion that
definition?.anthropicBaseUrl is defined (e.g.,
expect(definition?.anthropicBaseUrl).toBeDefined()) or assert it equals the
expected literal URL, then assert runtimeProvider.baseUrl equals that defined
value so the test fails if the registry drops anthropicBaseUrl; reference
resolveAiSdkProviderDefinition, definition, runtimeProvider, and routeDecision
in your changes.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: bea5f950-c397-4919-8ad0-162289e7b15b

📥 Commits

Reviewing files that changed from the base of the PR and between 0a07ed7 and 31ebb9f.

📒 Files selected for processing (14)
  • src/main/presenter/configPresenter/index.ts
  • src/main/presenter/configPresenter/modelConfig.ts
  • src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts
  • src/renderer/src/components/chat/ChatStatusBar.vue
  • src/renderer/src/components/settings/ModelConfigDialog.vue
  • src/shared/model.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts
  • test/main/presenter/configPresenter/providerModelCapabilityMapping.test.ts
  • test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts
  • test/main/presenter/llmProviderPresenter/newApiProvider.test.ts
  • test/main/presenter/modelConfig.test.ts
  • test/main/shared/model.test.ts
  • test/renderer/components/ChatStatusBar.test.ts
  • test/renderer/components/ModelConfigDialog.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • test/main/presenter/modelConfig.test.ts
  • src/main/presenter/configPresenter/modelConfig.ts
  • test/main/shared/model.test.ts
  • src/renderer/src/components/settings/ModelConfigDialog.vue
  • test/renderer/components/ChatStatusBar.test.ts

Comment thread src/renderer/src/components/chat/ChatStatusBar.vue
Comment thread src/shared/model.ts
Comment on lines +80 to +83
export function isClaudeOpus47FamilyModelId(modelId: string | undefined): boolean {
const normalizedModelId = normalizeUnprefixedModelId(modelId)
return normalizedModelId === 'claude-opus-4-7' || normalizedModelId === 'claude-opus-4-7-think'
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all call sites to confirm whether exact-match or family semantics is expected.
rg -nP --type=ts -C3 '\bisClaudeOpus47FamilyModelId\s*\('

Repository: ThinkInAIXYZ/deepchat

Length of output: 1873


🏁 Script executed:

#!/bin/bash

# Check for other model ID classifier functions to see naming/pattern conventions
rg -nP --type=ts 'export\s+function\s+is.*ModelId\s*\(' | head -20

Repository: ThinkInAIXYZ/deepchat

Length of output: 365


🏁 Script executed:

#!/bin/bash

# Search for any dated variant model IDs or version patterns in the codebase
rg -nP '(claude-opus-4-7|claude-[a-z0-9-]+)-\d{8}|claude-[a-z0-9-]+-latest|claude-[a-z0-9-]+-v\d+'

Repository: ThinkInAIXYZ/deepchat

Length of output: 15921


🏁 Script executed:

#!/bin/bash

# Check how normalizeUnprefixedModelId works to understand what gets stripped
rg -nP --type=ts -A5 'function normalizeUnprefixedModelId'

Repository: ThinkInAIXYZ/deepchat

Length of output: 464


🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -A3 'export function isClaudeFamilyModelId'

Repository: ThinkInAIXYZ/deepchat

Length of output: 291


Misleading function name — rename for consistency with isClaudeFamilyModelId.

The "family" suffix suggests broad matching like isClaudeFamilyModelId() (which uses .includes('claude')), but this function only matches exact IDs: claude-opus-4-7 and claude-opus-4-7-think. Rename to isClaudeOpus47ModelId to accurately reflect exact-match semantics, or switch to startsWith('claude-opus-4-7') if Anthropic ships dated variants (though none exist in the model-db currently).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/model.ts` around lines 80 - 83, The function
isClaudeOpus47FamilyModelId has a misleading "Family" suffix but performs exact
matches; rename the function to isClaudeOpus47ModelId (and update all call
sites) to reflect exact-match semantics, or alternatively change its check
inside normalizeUnprefixedModelId to use startsWith('claude-opus-4-7') if you
prefer family-style matching; update the exported function name and any
imports/usages accordingly to keep API consistent with isClaudeFamilyModelId
naming.

Comment on lines +177 to +210
const context = {
providerKind: 'openai-compatible',
provider: {
id: 'aihubmix',
apiType: 'openai-compatible'
},
configPresenter: {
supportsTemperatureControl: vi.fn().mockReturnValue(false)
},
defaultHeaders: {},
emitRequestTrace: vi.fn(async (_modelConfig, payload) => {
tracePayloads.push(payload)
})
} as any

const events = []
for await (const event of runAiSdkCoreStream(
context,
[],
modelId,
{
apiEndpoint: 'chat',
functionCall: false
} as any,
0.5,
2048,
[]
)) {
events.push(event)
}

const request = mockStreamText.mock.calls[0]?.[0] as Record<string, unknown>
expect(request).not.toHaveProperty('temperature')
expect(tracePayloads[0]?.body).not.toHaveProperty('temperature')
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.

⚠️ Potential issue | 🟡 Minor

Exercise the capability-provider route in this test.

Right now supportsTemperatureControl returns false for any provider, so this passes even if the runtime queries aihubmix instead of Anthropic capabilities.

Suggested test tightening
       const context = {
         providerKind: 'openai-compatible',
         provider: {
           id: 'aihubmix',
-          apiType: 'openai-compatible'
+          apiType: 'openai-compatible',
+          capabilityProviderId: 'anthropic'
         },
         configPresenter: {
           supportsTemperatureControl: vi.fn().mockReturnValue(false)
@@
 
       const request = mockStreamText.mock.calls[0]?.[0] as Record<string, unknown>
+      expect(context.configPresenter.supportsTemperatureControl).toHaveBeenCalledWith(
+        'anthropic',
+        modelId
+      )
       expect(request).not.toHaveProperty('temperature')
       expect(tracePayloads[0]?.body).not.toHaveProperty('temperature')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const context = {
providerKind: 'openai-compatible',
provider: {
id: 'aihubmix',
apiType: 'openai-compatible'
},
configPresenter: {
supportsTemperatureControl: vi.fn().mockReturnValue(false)
},
defaultHeaders: {},
emitRequestTrace: vi.fn(async (_modelConfig, payload) => {
tracePayloads.push(payload)
})
} as any
const events = []
for await (const event of runAiSdkCoreStream(
context,
[],
modelId,
{
apiEndpoint: 'chat',
functionCall: false
} as any,
0.5,
2048,
[]
)) {
events.push(event)
}
const request = mockStreamText.mock.calls[0]?.[0] as Record<string, unknown>
expect(request).not.toHaveProperty('temperature')
expect(tracePayloads[0]?.body).not.toHaveProperty('temperature')
const context = {
providerKind: 'openai-compatible',
provider: {
id: 'aihubmix',
apiType: 'openai-compatible',
capabilityProviderId: 'anthropic'
},
configPresenter: {
supportsTemperatureControl: vi.fn().mockReturnValue(false)
},
defaultHeaders: {},
emitRequestTrace: vi.fn(async (_modelConfig, payload) => {
tracePayloads.push(payload)
})
} as any
const events = []
for await (const event of runAiSdkCoreStream(
context,
[],
modelId,
{
apiEndpoint: 'chat',
functionCall: false
} as any,
0.5,
2048,
[]
)) {
events.push(event)
}
const request = mockStreamText.mock.calls[0]?.[0] as Record<string, unknown>
expect(context.configPresenter.supportsTemperatureControl).toHaveBeenCalledWith(
'anthropic',
modelId
)
expect(request).not.toHaveProperty('temperature')
expect(tracePayloads[0]?.body).not.toHaveProperty('temperature')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts` around lines
177 - 210, The test must exercise the capability-provider route: change the
context to use providerKind 'capability-provider' (instead of
'openai-compatible') and make configPresenter.supportsTemperatureControl return
true so the runtime takes the capability-provider branch when calling
runAiSdkCoreStream; keep the rest of the setup and then assert that the outgoing
request (mockStreamText.mock.calls[0][0]) and tracePayloads[0].body still do not
include a 'temperature' property. Use the symbols context.providerKind,
context.configPresenter.supportsTemperatureControl, and runAiSdkCoreStream to
locate and modify the test.

Comment thread test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/renderer/src/components/chat/ChatStatusBar.vue (1)

2001-2028: ⚠️ Potential issue | 🟠 Major

Guard capability writes against stale async responses.

syncGenerationSettings checks generationSyncToken only after fetchCapabilities returns, but fetchCapabilities has already mutated capability refs by then. A slow request from the previous model can overwrite the current model’s capabilityProviderId/portrait and render or persist the wrong reasoning controls.

🐛 Proposed fix
-const fetchCapabilities = async (providerId: string, modelId: string): Promise<void> => {
+const fetchCapabilities = async (
+  providerId: string,
+  modelId: string,
+  isStale: () => boolean
+): Promise<void> => {
   try {
     const modelConfig = await configPresenter.getModelConfig(modelId, providerId)
-    capabilityProviderId.value = resolveCapabilityProviderIdForSelection(
+    const nextCapabilityProviderId = resolveCapabilityProviderIdForSelection(
       providerId,
       modelId,
       modelConfig.endpointType
     )
     const portrait = (await configPresenter.getReasoningPortrait?.(providerId, modelId)) ?? null
     const temperatureSupport = await configPresenter.supportsTemperatureControl?.(
       providerId,
       modelId
     )
+    const nextTemperatureSupport =
+      typeof temperatureSupport === 'boolean'
+        ? temperatureSupport
+        : ((await configPresenter.getTemperatureCapability?.(providerId, modelId)) ?? null)
+
+    if (isStale()) {
+      return
+    }
 
+    capabilityProviderId.value = nextCapabilityProviderId
     capabilityReasoningPortrait.value = portrait
     capabilitySupportsReasoning.value =
       typeof portrait?.supported === 'boolean' ? portrait.supported : null
-    capabilitySupportsTemperature.value =
-      typeof temperatureSupport === 'boolean'
-        ? temperatureSupport
-        : ((await configPresenter.getTemperatureCapability?.(providerId, modelId)) ?? null)
+    capabilitySupportsTemperature.value = nextTemperatureSupport
   } catch (error) {
     console.warn('[ChatStatusBar] Failed to fetch model capabilities:', error)
+    if (isStale()) {
+      return
+    }
     capabilityProviderId.value = providerId
     capabilitySupportsReasoning.value = null
     capabilityReasoningPortrait.value = null
     capabilitySupportsTemperature.value = null
   }
 }
-  await fetchCapabilities(selection.providerId, selection.modelId)
+  await fetchCapabilities(
+    selection.providerId,
+    selection.modelId,
+    () => token !== generationSyncToken
+  )
   if (token !== generationSyncToken) {
     return
   }

Also applies to: 2151-2154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/chat/ChatStatusBar.vue` around lines 2001 - 2028,
fetchCapabilities mutates capability refs after awaiting async calls, allowing
stale responses to overwrite current model settings; capture the current
generationSyncToken (the same token used by syncGenerationSettings) at the start
of fetchCapabilities and verify it is unchanged before applying any mutations
(capabilityProviderId, capabilityReasoningPortrait, capabilitySupportsReasoning,
capabilitySupportsTemperature), aborting writes if the token has changed; apply
the same guard to the other capability-write block referenced around lines
2151-2154 so only responses matching the captured token update state.
🧹 Nitpick comments (2)
test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts (1)

156-192: Minor: assert result.providerOptions?.openai is absent for the zenmux case.

This test pins the Anthropic-shape output but doesn't verify that zenmux — which is a routed provider — doesn't also accidentally emit an openai options bucket alongside. A single extra expect(result.providerOptions?.openai).toBeUndefined() would make the test resilient to future mapper changes that inadvertently add a second namespace for routed providers.

Proposed tightening
     expect(result.providerOptions?.anthropic).toMatchObject({
       toolStreaming: true,
       sendReasoning: true,
       effort: 'xhigh',
       thinking: {
         type: 'adaptive',
         display: 'summarized'
       }
     })
+    expect(result.providerOptions?.openai).toBeUndefined()
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts`
around lines 156 - 192, Add an assertion to the 'maps zenmux anthropic routes to
official anthropic adaptive reasoning controls' test that ensures routed
providers don't accidentally emit an OpenAI options bucket: after calling
buildProviderOptions and storing in result, add
expect(result.providerOptions?.openai).toBeUndefined() to confirm only the
anthropic namespace is present; reference the test's result variable and
buildProviderOptions call to locate where to insert the new assertion.
src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts (1)

21-40: Narrow the configPresenter contract by making getCapabilityProviderId required.

The interface IConfigPresenter marks getCapabilityProviderId as optional (line 521 in src/shared/types/presenters/legacy.presenters.d.ts), but the actual ConfigPresenter implementation always provides it (line 699 in src/main/presenter/configPresenter/index.ts). The code in resolveCapabilityProviderId uses optional chaining defensively (?.), which can mask implementation issues. Since this method is now integral to capability routing across the codebase, declare it as required on IConfigPresenter so the TypeScript contract matches runtime expectations and call sites reflect the actual dependency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts` around lines 21 -
40, The IConfigPresenter interface currently marks getCapabilityProviderId as
optional but runtime and ConfigPresenter implementation always provide it, so
update the IConfigPresenter declaration to make getCapabilityProviderId required
(remove the optional flag) so the TypeScript contract matches runtime; then
update any call sites like resolveCapabilityProviderId that use optional
chaining on configPresenter.getCapabilityProviderId to call it directly,
ensuring references to AiSdkRuntimeContext.configPresenter reflect the tightened
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/src/components/chat/ChatStatusBar.vue`:
- Around line 443-445: The Select is binding localSettings.reasoningVisibility
directly, but persisted values like "hidden"/"visible" may not match the current
reasoningVisibilityOptions ("omitted"/"summarized"), so add a normalized
computed (or method) used in the model-value binding that maps legacy values
("hidden"->"omitted", "visible"->"summarized") and falls back to
reasoningVisibilityOptions[0]?.value; update the model-value to use that
computed (referencing localSettings.reasoningVisibility and
reasoningVisibilityOptions) so older sessions show a valid option instead of an
out-of-options selection.

---

Outside diff comments:
In `@src/renderer/src/components/chat/ChatStatusBar.vue`:
- Around line 2001-2028: fetchCapabilities mutates capability refs after
awaiting async calls, allowing stale responses to overwrite current model
settings; capture the current generationSyncToken (the same token used by
syncGenerationSettings) at the start of fetchCapabilities and verify it is
unchanged before applying any mutations (capabilityProviderId,
capabilityReasoningPortrait, capabilitySupportsReasoning,
capabilitySupportsTemperature), aborting writes if the token has changed; apply
the same guard to the other capability-write block referenced around lines
2151-2154 so only responses matching the captured token update state.

---

Nitpick comments:
In `@src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts`:
- Around line 21-40: The IConfigPresenter interface currently marks
getCapabilityProviderId as optional but runtime and ConfigPresenter
implementation always provide it, so update the IConfigPresenter declaration to
make getCapabilityProviderId required (remove the optional flag) so the
TypeScript contract matches runtime; then update any call sites like
resolveCapabilityProviderId that use optional chaining on
configPresenter.getCapabilityProviderId to call it directly, ensuring references
to AiSdkRuntimeContext.configPresenter reflect the tightened contract.

In `@test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts`:
- Around line 156-192: Add an assertion to the 'maps zenmux anthropic routes to
official anthropic adaptive reasoning controls' test that ensures routed
providers don't accidentally emit an OpenAI options bucket: after calling
buildProviderOptions and storing in result, add
expect(result.providerOptions?.openai).toBeUndefined() to confirm only the
anthropic namespace is present; reference the test's result variable and
buildProviderOptions call to locate where to insert the new assertion.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71f5fc96-4ac5-4ce0-b556-1a97a753a79b

📥 Commits

Reviewing files that changed from the base of the PR and between 31ebb9f and 65d0937.

📒 Files selected for processing (11)
  • resources/acp-registry/registry.json
  • resources/model-db/providers.json
  • src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts
  • src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts
  • src/renderer/src/components/chat/ChatStatusBar.vue
  • src/shared/model.ts
  • test/main/presenter/llmProviderPresenter/aiSdkProviderOptionsMapper.test.ts
  • test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts
  • test/main/presenter/llmProviderPresenter/newApiProvider.test.ts
  • test/main/shared/model.test.ts
  • test/renderer/components/ChatStatusBar.test.ts
✅ Files skipped from review due to trivial changes (2)
  • test/main/presenter/llmProviderPresenter/aiSdkRuntime.test.ts
  • test/main/shared/model.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/main/presenter/llmProviderPresenter/newApiProvider.test.ts
  • test/renderer/components/ChatStatusBar.test.ts
  • src/main/presenter/llmProviderPresenter/providers/aiSdkProvider.ts
  • resources/acp-registry/registry.json

Comment on lines +443 to +445
:model-value="
localSettings.reasoningVisibility ?? reasoningVisibilityOptions[0]?.value
"
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.

⚠️ Potential issue | 🟡 Minor

Normalize persisted visibility before binding it to the Select.

localSettings.reasoningVisibility can contain broader persisted values like hidden/visible, but this Select only renders Anthropic options (omitted, summarized). Bind the normalized value so older sessions don’t show an out-of-options selection.

🐛 Proposed fix
                         :model-value="
-                          localSettings.reasoningVisibility ?? reasoningVisibilityOptions[0]?.value
+                          normalizeReasoningVisibility(
+                            capabilityProviderId,
+                            capabilityReasoningPortrait,
+                            localSettings.reasoningVisibility
+                          ) ?? reasoningVisibilityOptions[0]?.value
                         "
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
:model-value="
localSettings.reasoningVisibility ?? reasoningVisibilityOptions[0]?.value
"
:model-value="
normalizeReasoningVisibility(
capabilityProviderId,
capabilityReasoningPortrait,
localSettings.reasoningVisibility
) ?? reasoningVisibilityOptions[0]?.value
"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/chat/ChatStatusBar.vue` around lines 443 - 445,
The Select is binding localSettings.reasoningVisibility directly, but persisted
values like "hidden"/"visible" may not match the current
reasoningVisibilityOptions ("omitted"/"summarized"), so add a normalized
computed (or method) used in the model-value binding that maps legacy values
("hidden"->"omitted", "visible"->"summarized") and falls back to
reasoningVisibilityOptions[0]?.value; update the model-value to use that
computed (referencing localSettings.reasoningVisibility and
reasoningVisibilityOptions) so older sessions show a valid option instead of an
out-of-options selection.

@zerob13 zerob13 merged commit e76e64a into dev Apr 18, 2026
3 checks passed
@zhangmo8 zhangmo8 deleted the feat/claude-adaptive-reasoning branch April 21, 2026 03:53
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