Skip to content

Make Claude Sonnet 5 adaptive thinking visible#100

Merged
ualtinok merged 2 commits into
cortexkit:mainfrom
iceteaSA:fix/sonnet5-thinking
Jul 1, 2026
Merged

Make Claude Sonnet 5 adaptive thinking visible#100
ualtinok merged 2 commits into
cortexkit:mainfrom
iceteaSA:fix/sonnet5-thinking

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Problem

Claude Sonnet 5 (claude-sonnet-5) enables adaptive thinking by default, but its thinking.display defaults to "omitted" — so the thinking field returns empty and thinking is invisible to the user. This is a silent change from earlier models where display defaulted to "summarized".

Fix

Normalize the request body for claude-sonnet-5 so adaptive thinking is visible:

  • If thinking is not explicitly disabled → set thinking: {type: "adaptive", display: "summarized"}.
  • If the caller explicitly sent {type: "disabled"} → preserve the disable, canonicalized to a bare {type: "disabled"} (the display field is invalid alongside type: "disabled" and can 400).
  • A manual {type: "enabled", budget_tokens} would 400 on Sonnet 5, so it is overwritten with the adaptive form.

This mirrors the existing Fable/Mythos 5 handling, with the Sonnet-5-specific difference that {type: "disabled"} is accepted (Fable/Mythos reject it).

claude-sonnet-5 is in the native model catalog, so no model registration is added.

Changes

  • packages/core/src/models.tsisClaudeSonnet5Model, CLAUDE_SONNET_5_MODEL_ID, CLAUDE_SONNET_5_ADAPTIVE_THINKING.
  • packages/opencode/src/transform.tsnormalizeSonnet5Request, called alongside the Fable/Mythos normalizer.
  • packages/pi/src/convert.ts — Sonnet 5 folded into the adaptive-thinking branch (Pi's typed options cannot express disable, so it always emits adaptive + summarized and maps reasoning to effort).

Tests

  • isClaudeSonnet5Model precision: matches claude-sonnet-5 and dated snapshots; rejects claude-sonnet-4-6, Fable/Mythos ids, and claude-sonnet-5x.
  • Transform: no-thinking → adaptive+summarized; manual enabled+budget → overwritten (budget dropped); explicit disabled → preserved and stripped to bare {type:"disabled"}; non-Sonnet-5 untouched.
  • Pi: produces display: "summarized" for claude-sonnet-5; reasoning→effort mapping intact.

All suites green (opencode 744, pi 47, e2e 2), typecheck and lint clean.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Make adaptive thinking visible for claude-sonnet-5. Requests now default thinking to {type: "adaptive", display: "summarized"} unless explicitly disabled, preventing empty thinking responses.

  • Bug Fixes
    • Normalize Sonnet 5 in packages/opencode/src/transform.ts: preserve {type:"disabled"} (strip display), override {type:"enabled", budget_tokens} to adaptive summarized, and set adaptive summarized when missing; non-Sonnet-5 untouched.
    • Add Sonnet 5 detection/constants in packages/core; update packages/pi to emit adaptive summarized and map reasoningoutput_config.effort (Pi cannot disable).
    • Refactor packages/pi to use each model family's constant (CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING vs CLAUDE_SONNET_5_ADAPTIVE_THINKING) so configs stay independent.

Written for commit 893c7c9. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR makes Claude Sonnet 5's adaptive thinking visible by injecting {type:"adaptive",display:"summarized"} into requests that would otherwise silently return an empty thinking field (Sonnet 5 defaults display to "omitted"). The change mirrors existing Fable/Mythos 5 handling in both the raw-body transform path and the Pi typed-options path.

  • packages/core/src/models.ts: Adds CLAUDE_SONNET_5_MODEL_ID, the CLAUDE_SONNET_5_ADAPTIVE_THINKING constant (currently aliased to CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING), and isClaudeSonnet5Model() with dash-prefix matching for dated snapshots.
  • packages/opencode/src/transform.ts: Introduces normalizeSonnet5Request() which—unlike the Fable/Mythos normalizer—preserves an explicit {type:"disabled"} (stripping invalid display), overwrites {type:"enabled",budget_tokens} (which would 400 on Sonnet 5), and defaults everything else to adaptive+summarized.
  • packages/pi/src/convert.ts: Folds Sonnet 5 into the adaptive-summarized branch and maps reasoning to output_config.effort; Pi's typed surface cannot express disable, so no disable case is needed here.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to Sonnet 5 model IDs and all non-Sonnet-5 requests are left completely untouched.

The normalizer correctly identifies the three distinct thinking states (absent/enabled → adaptive+summarized, explicit disabled → canonicalized bare disabled) with no overlap with the Fable/Mythos path. Tests cover all branches including the dash-prefix false-positive guard. Both code paths (raw transform and Pi typed interface) are updated consistently.

No files require special attention.

Important Files Changed

Filename Overview
packages/core/src/models.ts Adds CLAUDE_SONNET_5_MODEL_ID, CLAUDE_SONNET_5_ADAPTIVE_THINKING (aliased to Fable/Mythos constant), and isClaudeSonnet5Model() with correct dash-prefix snapshot matching. No logic errors; test coverage is thorough.
packages/opencode/src/transform.ts normalizeSonnet5Request correctly handles the three cases (no thinking → adaptive+summarized, type:enabled → overwrite, type:disabled → canonicalize). Perf telemetry captures both replacedExisting and display mode. No overlap with normalizeFableMythosRequest since model IDs don't intersect.
packages/pi/src/convert.ts Sonnet 5 folded cleanly into the adaptive-summarized + output_config.effort branch. The inability to express disable via Pi's typed surface is acknowledged in the comment. Uses CLAUDE_SONNET_5_ADAPTIVE_THINKING for the Sonnet 5 branch while Fable/Mythos retains its own constant.
packages/core/src/tests/models.test.ts New test file; covers bare ID, dated snapshot, negative cases (claude-sonnet-4-6, Fable/Mythos, no-dash suffix collision, non-string inputs). All edge cases for isClaudeSonnet5Model are exercised.
packages/opencode/src/tests/transform.test.ts Five new tests cover: no-thinking → adaptive+summarized; enabled+budget → overwrite with budget dropped; explicit disabled → preserved; disabled+display → stripped to bare disabled; non-Sonnet5 model → untouched.
packages/pi/src/tests/convert.test.ts Three new tests confirm adaptive+summarized output, output_config.effort mapping with reasoning, and display:'summarized' assertion for Sonnet 5 via the Pi path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming request body] --> B{isClaudeSonnet5Model?}
    B -- No --> C[normalizeSonnet5Request returns null]
    B -- Yes --> D{thinking present AND type is disabled?}
    D -- Yes --> E[Canonicalize to bare disabled object, strip display field]
    D -- No --> F[Set thinking to adaptive plus summarized, overwrite any enabled plus budget value]
    E --> G[Log sonnet5ThinkingDisplay as disabled]
    F --> H[Log sonnet5ThinkingDisplay as summarized]
    G --> I[Continue pipeline]
    H --> I

    subgraph Pi path
        P1[buildAnthropicRequest] --> P2{isClaudeSonnet5Model?}
        P2 -- Yes --> P3[body.thinking = adaptive plus summarized]
        P3 --> P4{options.reasoning set?}
        P4 -- Yes --> P5[body.output_config = effort]
        P4 -- No --> P6[No output_config]
        P2 -- No --> P7[Fable/Mythos or budget-based path]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Incoming request body] --> B{isClaudeSonnet5Model?}
    B -- No --> C[normalizeSonnet5Request returns null]
    B -- Yes --> D{thinking present AND type is disabled?}
    D -- Yes --> E[Canonicalize to bare disabled object, strip display field]
    D -- No --> F[Set thinking to adaptive plus summarized, overwrite any enabled plus budget value]
    E --> G[Log sonnet5ThinkingDisplay as disabled]
    F --> H[Log sonnet5ThinkingDisplay as summarized]
    G --> I[Continue pipeline]
    H --> I

    subgraph Pi path
        P1[buildAnthropicRequest] --> P2{isClaudeSonnet5Model?}
        P2 -- Yes --> P3[body.thinking = adaptive plus summarized]
        P3 --> P4{options.reasoning set?}
        P4 -- Yes --> P5[body.output_config = effort]
        P4 -- No --> P6[No output_config]
        P2 -- No --> P7[Fable/Mythos or budget-based path]
    end
Loading

Comments Outside Diff (1)

  1. packages/pi/src/convert.ts, line 1-15 (link)

    P2 The CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING import was dropped and replaced with CLAUDE_SONNET_5_ADAPTIVE_THINKING for both model families. Since CLAUDE_SONNET_5_ADAPTIVE_THINKING is currently just an alias (= CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING), they are identical today — but the Fable/Mythos spread now silently depends on the Sonnet 5 constant. If a future commit adjusts CLAUDE_SONNET_5_ADAPTIVE_THINKING to carry a Sonnet-5-specific value, Fable/Mythos behavior in this file changes without any signal at the call site. Keeping the original import alive makes each family independently evolvable.

Reviews (2): Last reviewed commit: "refactor(pi): use each model family's ow..." | Re-trigger Greptile

Sonnet 5 enables adaptive thinking by default but defaults thinking.display
to "omitted", so the thinking field returns empty and users see no thinking.
Inject {type:"adaptive",display:"summarized"} for claude-sonnet-5 unless the
caller explicitly disabled thinking.

Unlike Fable/Mythos (which reject a disable), Sonnet 5 accepts
{type:"disabled"}, so an intentional opt-out is preserved (canonicalized to a
bare {type:"disabled"} since display is invalid with disabled). A manual
{type:"enabled",budget_tokens} would 400 on Sonnet 5, so it is overwritten
with the adaptive form.
Comment thread packages/pi/src/convert.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 6 files

Confidence score: 4/5

  • In packages/pi/src/convert.ts, the shared use of CLAUDE_SONNET_5_ADAPTIVE_THINKING across isFableOrMythos5 and isSonnet5 can route Fable/Mythos behavior through Sonnet-specific config, which risks subtle model-selection mismatches in production; split these branches so each model family references its own constant before merging.
Architecture diagram
sequenceDiagram
    participant Client as API Client
    participant Gateway as Request Gateway
    participant Rewriter as rewriteRequestBody()
    participant Normalizer as Model Normalizer
    participant Pi as Pi Request Builder
    participant API as Anthropic API

    Note over Client,API: Claude Sonnet 5 Adaptive Thinking Flow

    Client->>Gateway: POST /v1/messages (model: claude-sonnet-5)

    Gateway->>Rewriter: rewriteRequestBody()
    Rewriter->>Normalizer: normalizeSonnet5Request()

    alt No thinking field in request
        Normalizer->>Normalizer: NEW: Inject {type:"adaptive", display:"summarized"}
        Normalizer-->>Rewriter: {display: "summarized"}
    else thinking.type === "disabled"
        Normalizer->>Normalizer: NEW: Canonicalize to bare {type:"disabled"}
        Normalizer-->>Rewriter: {display: "disabled"}
    else thinking.type === "enabled" (with budget_tokens)
        Normalizer->>Normalizer: NEW: Overwrite with adaptive + summarized
        Normalizer-->>Rewriter: {display: "summarized"}
    end

    alt Non-Sonnet-5 model
        Normalizer-->>Rewriter: return null (no change)
    end

    Rewriter->>Rewriter: Log perf metrics (sonnet5ThinkingDisplay)
    Rewriter-->>Gateway: Modified request body
    Gateway->>API: Forward request

    Note over Pi,API: Alternative Pi SDK Flow

    Client->>Pi: buildAnthropicRequest(model: claude-sonnet-5)
    Pi->>Pi: Check isClaudeSonnet5Model()
    Pi->>Pi: NEW: Inject {type:"adaptive", display:"summarized"}
    alt reasoning option provided
        Pi->>Pi: Map reasoning to output_config.effort
    end
    Pi-->>Client: Modified request body
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/pi/src/convert.ts
Split the Pi convert branch so Fable/Mythos uses
CLAUDE_FABLE_MYTHOS_5_SUMMARIZED_THINKING and Sonnet 5 uses
CLAUDE_SONNET_5_ADAPTIVE_THINKING, instead of sharing the Sonnet 5
constant for both. The constants are aliased today, but using each
family's own keeps their thinking configs independently evolvable.
@ualtinok ualtinok merged commit 34ea484 into cortexkit:main Jul 1, 2026
5 checks passed
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