Skip to content

🤖 feat: update default model to Claude Opus 4.7#3180

Merged
ammario merged 7 commits into
mainfrom
opus-4-7-model-update
Apr 16, 2026
Merged

🤖 feat: update default model to Claude Opus 4.7#3180
ammario merged 7 commits into
mainfrom
opus-4-7-model-update

Conversation

@ammar-agent

Copy link
Copy Markdown
Collaborator

Summary

Updates the default Opus model from claude-opus-4-6 to claude-opus-4-7, adds support for the new native xhigh effort level, and enables display: "summarized" for Opus 4.7's adaptive thinking to receive summarized thinking content.

Background

Anthropic released Claude Opus 4.7 on April 16, 2026. Key changes relevant to Mux:

  • New xhigh effort level: Opus 4.7 introduces a native xhigh ("extra high") effort level in the API between high and max, giving finer control over reasoning vs latency. On Opus 4.6, our xhigh ThinkingLevel mapped to "max" effort (they were synonyms). On Opus 4.7, xhigh and max are distinct API-level effort values.
  • Summarized thinking: Claude 4 models return summarized thinking by default, but Opus 4.7 requires explicitly setting display: "summarized" on the thinking configuration to receive thinking content in responses. Without this, thinking blocks would be empty.
  • Same pricing as Opus 4.6: $5/M input, $25/M output. Native 1M context window, 128K max output.

Implementation

Model registry & pricing

  • knownModels.ts: OPUS entry → claude-opus-4-7 with tokenizer override (upstream tokenizer not yet available)
  • models-extra.ts: New pricing entry for claude-opus-4-7 (same pricing as 4.6)
  • models.ts: Added native 1M context pattern for Opus 4.7

Thinking level support (all 6 levels)

  • policy.ts: Opus 4.7 gets ["off", "low", "medium", "high", "xhigh", "max"] — all 6 levels, since xhigh and max are distinct effort levels on this model
  • thinking.ts: AnthropicEffortLevel now includes "xhigh". getAnthropicEffort() is model-aware: returns native "xhigh" for Opus 4.7, "max" for Opus 4.6 (backward compatible)
  • Display labels: Opus 4.7 shows "XHIGH" for the xhigh level (like OpenAI), distinguishing it from "MAX"

Provider options

  • providerOptions.ts: Opus 4.7 uses adaptive thinking with display: "summarized" to receive thinking content. Added AnthropicProviderOptionsExtended type to accommodate the SDK not yet typing "xhigh" effort or "display" on adaptive thinking.

Workflows & docs

  • Updated CI workflows (auto-cleanup, nightly-terminal-bench) to use opus-4-7
  • Updated docs examples and synced generated files

Risks

Low risk — this is a straightforward model version bump. The main subtlety is the display: "summarized" requirement for Opus 4.7 thinking content and the new xhigh effort level. Both are additive and backward-compatible (Opus 4.6 paths are unchanged). The AnthropicProviderOptionsExtended type will need cleanup once the SDK adds native xhigh effort support.


Generated with mux • Model: anthropic:claude-opus-4-6 • Thinking: xhigh • Cost: $1.69

- Update OPUS model from claude-opus-4-6 to claude-opus-4-7
- Add native xhigh effort level support (between high and max)
- Add display: summarized for Opus 4.7 adaptive thinking
- Add all 6 thinking levels for Opus 4.7 (off/low/med/high/xhigh/max)
- Add model-aware effort mapping (xhigh -> native xhigh on 4.7, max on 4.6)
- Add pricing entry and native 1M context support for Opus 4.7
- Update workflows, docs, and generated files

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfc68e29dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/nightly-terminal-bench.yml Outdated
@mintlify

mintlify Bot commented Apr 16, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Mux 🟢 Ready View Preview Apr 16, 2026, 4:12 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

The @ai-sdk/anthropic Zod schema rejects 'xhigh' as an effort value and strips
unknown keys on the thinking object, so we can't emit Opus 4.7's native xhigh
effort or the required display: 'summarized' through providerOptions. Handle
both in the Anthropic fetch wrapper:

- Always wrap Anthropic fetch (direct + gateway) so Opus 4.7 wire transforms
  run even when disableBetaFeatures skips cache_control injection
- Inject thinking.display='summarized' for any adaptive thinking request
  targeting claude-opus-4-7 or newer
- Emit x-mux-anthropic-effort header from buildRequestHeaders when user
  selected xhigh ThinkingLevel on Opus 4.7; fetch wrapper strips the header
  and rewrites output_config.effort to 'xhigh' before sending to Anthropic
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de07765ead

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/utils/ai/providerOptions.ts
Comment thread src/common/types/thinking.ts Outdated
- Update three test fixtures from claude-opus-4-6 to claude-opus-4-7
  (cli run default help, useCreationWorkspace pendingStreamModel,
  useModelsFromSettings Copilot dot-form ID, slash command E2E)
- Deduplicate THINKING_LABELS_LIST in CLI help (xhigh/max both map to MAX
  as default display label; was producing duplicated 'MAX, MAX')
- Match any claude-opus variant in nightly-terminal-bench workflow so
  dispatched 4.6 runs keep xhigh thinking (addresses Codex review)
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

… support future Opus versions

- Gate MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER emission on routePassesHeaders
  so non-passthrough gateways (OpenRouter, Bedrock) don't receive this
  Mux-internal header and forward it externally
- Extend anthropicSupportsNativeXhigh regex to match Opus 4.7+ including
  future versions (4.8..4-99, Opus 5+) so xhigh doesn't silently collapse
  to max on upcoming models
- Use the helper consistently in policy.ts and providerOptions.ts
- Update providerModelFactory display injection regex to match same range
- Add tests: hypothetical Opus 4.8, openrouter non-passthrough gateway,
  policy coverage for future Opus versions
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e06f8aa3fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/types/thinking.ts Outdated
The AI SDK gateway wraps thinking/effort inside providerOptions.anthropic.*
instead of the direct Anthropic wire format, and identifies the model via
the ai-model-id header. Our fetch wrapper now patches both shapes:

- Direct Anthropic:   top-level thinking / output_config.effort
- AI SDK gateway:     providerOptions.anthropic.{thinking,effort}

Also added debug logs so users can verify display injection in action.

Adds test coverage for both wire shapes (5 direct + 2 gateway).
The mux-gateway path uses a different body shape than direct Anthropic:
  { prompt, providerOptions: { anthropic: { thinking, effort } } }

with the model exposed via the ai-model-id header rather than `model`
in the body. The fetch wrapper previously only touched the direct Anthropic
body, so thinking.display was never injected and effort was never rewritten
when routing through mux-gateway. This likely explains missing thinking
traces when using Opus 4.7 via the gateway.

Changes:
- Detect Opus 4.7+ via either `body.model` OR `ai-model-id` header
- Inject thinking.display=summarized into providerOptions.anthropic.thinking
  for gateway requests in addition to the direct path
- Rewrite providerOptions.anthropic.effort to xhigh for gateway requests
  when the override header is present
- Export wrapFetchWithAnthropicCacheControl + add unit tests covering both
  body shapes (direct + gateway) for display injection, effort override,
  header stripping, and backwards-compat with Opus 4.6
- Add debug log when display is injected to aid diagnosing thinking-trace
  issues in live sessions
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

getThinkingDisplayLabel used a hardcoded 'opus-4-7' substring check, which
would silently collapse xhigh/max to MAX for future Opus versions (4.8+, 5+)
even though anthropicSupportsNativeXhigh already recognizes them. Use the
shared helper so UI labels stay consistent with policy and header logic.
@ammar-agent

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ammario ammario merged commit 3f7cb2b into main Apr 16, 2026
22 checks passed
@ammario ammario deleted the opus-4-7-model-update branch April 16, 2026 17:06
ammario pushed a commit that referenced this pull request May 28, 2026
## Summary

Adds first-class support for [Claude Opus
4.8](https://www.anthropic.com/news/claude-opus-4-8) (released May 28,
2026) and promotes it to Mux's default `opus`/`OPUS` model in place of
Opus 4.7.

## Background

Anthropic shipped Opus 4.8 today as a same-shape successor to Opus 4.7:

- **Identical pricing**: $5 / $25 per million input/output tokens (cache
write $6.25, cache read $0.50).
- **Identical envelope**: native 1M context window, 128K max output.
- **Same effort/thinking surface**: `effort` defaults to `high`; `xhigh`
and `max` remain distinct API effort values.
- **API ID**: `claude-opus-4-8` (dateless, pinned snapshot — same
convention as 4.6/4.7).

Mux already routes Opus 4.7+ through the right wire-format transforms
via regex (`anthropicSupportsNativeXhigh()` matches
`claude-opus-4-(?:[7-9]|\d{2,})`), so the xhigh effort header rewrite
and `thinking.display: "summarized"` injection apply to 4.8
automatically. This PR is therefore mostly a model-id bump plus new
pricing metadata.

## Implementation

### Model registry & pricing

- `knownModels.ts`: `OPUS` → `claude-opus-4-8` (alias `opus`, default
model). Tokenizer override stays on `anthropic/claude-opus-4.5` since
the upstream tokenizer hasn't published a newer encoding and
tokenization is unchanged across the 4.x line.
- `models-extra.ts`: added `claude-opus-4-8` capability/pricing entry
mirroring 4.7 (1M input, 128K output, full reasoning/vision/PDF/tool
support).
- `models.ts`: added `claude-opus-4-8` to `ANTHROPIC_NATIVE_1M_PATTERNS`
so the context window is treated as native (not beta-gated).

### Defaults & seed data

- `providerService.ts`: first-time mux-gateway seeding now lists
`anthropic/claude-opus-4-8` (was 4-7).
- CI workflows: `auto-cleanup`, `auto-cleanup-fixup`,
`nightly-terminal-bench`, and `terminal-bench` all reference the new
default Opus ID.

### Tests & docs

- Updated assertions that pinned the Opus 4.7 default
(`cli/run.test.ts`, `providerService.test.ts`, `slashCommands.spec.ts`
Playwright spec).
- Regenerated `docs/config/models.mdx` and
`docs/guides/github-actions.mdx` via `bun scripts/gen_docs.ts`.
- Regenerated `builtInSkillContent.generated.ts`.

The existing `policy.test.ts` and `providerOptions.test.ts` already
contained "hypothetical Opus 4.8" guard tests that exercise the
regex-based detectors — they pass unchanged and now exercise real
production behaviour.

## Validation

- `make typecheck`
- `make static-check`
- `bun test` on the touched suites: `knownModels`, `models`,
`modelDisplay`, `policy`, `providerOptions`, `providerService`,
`cli/run`, `StreamingMessageAggregator`.

## Risks

Low. The wire-format risk (`xhigh` effort + `summarized` thinking
display) was paid down in #3180 by detecting Opus 4.7+ via regex rather
than literal model IDs, so 4.8 inherits those code paths without any
branch changes. Pricing is identical to 4.7. The main user-visible
effect is that anyone on the `opus` alias automatically routes to the
new model — which is the intended behaviour.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking:
`xhigh`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh -->
kaannsaydamm pushed a commit to kaannsaydamm/steward that referenced this pull request Jul 14, 2026
…oder#3718)

## Summary

Changing the thinking/reasoning slider during an active turn now applies
to the **next model step** (the next provider request after the current
tool execution), instead of only taking effect on the next user message.
Idle-time changes behave exactly as before.

## Background

Each assistant turn spans multiple provider requests separated by tool
executions, and long agentic turns can run for many minutes. Previously
the thinking level was frozen at send time, so lowering effort on an
obviously-mechanical stretch (or raising it when the agent got stuck)
required interrupting the stream. Provider APIs are stateless per
request — nothing prevents per-step reasoning changes; Mux just never
rebuilt provider options mid-turn.

As a prerequisite, the Anthropic xhigh wire hack (header-marker +
fetch-wrapper body rewrite) is removed: the installed
`@ai-sdk/anthropic` passes `effort: "xhigh"` and adaptive
`thinking.display` through provider options directly, so mid-turn
rebuilds produce correct wire output without a parallel hack path.

## Implementation

- **Per-turn override holder**
(`src/node/services/thinkingOverride.ts`): a small mutable `{ pending,
applied, onApplied }` object created by `AgentSession` once a turn is
durably accepted, shared by reference through AIService into
StreamManager. Created before any async startup work, which closes the
race where a slider change lands while the turn is still PREPARING
(applies to its first request).
- **New ORPC route** `workspace.setActiveTurnThinkingLevel`: writes
`pending` (last-write-wins) on the live session; returns `accepted:
false` when idle/unknown. The frontend `ThinkingContext` fires it
best-effort after the existing persistence path, so workspace-setting
semantics are unchanged.
- **Consumption in `prepareStep`** (StreamManager): the pending level is
consumed exactly once per change; provider options are rebuilt via an
AIService-owned closure that repeats the send-time pipeline (min-level
floor → policy clamp → effective-level resolution → provider options →
`providers.jsonc` extras). The live `request.providerOptions` object is
mutated **in place** because the AI SDK deep-merges per-step options and
cannot remove stale nested keys (e.g. Anthropic `thinking` on a → off
transition); the rebuilt options are also returned from `prepareStep` as
defense in depth.
- **Model fallback** rebinds the same holder with a closure bound to the
fallback model; same-stream retries preserve holder and mutated options.
- Out of scope (persisted setting still applies next turn): xAI
`grok-4-1-fast` off↔non-off (requires a model-instance swap), model
changes, OpenAI Pro `reasoningMode`, ACP sessions.

## Validation

Dogfooded against the live Anthropic API (direct route) in a dev-server
sandbox, with per-step wire evidence from `devtools.jsonl`
(`rawRequest.thinking` / `.output_config`, steps identified by
message-count progression within one turn):

- **low → high mid-turn** (Sonnet 4.5): `budget_tokens` 4000 → 20000 at
the next step.
- **high → off mid-turn**: `thinking` key deleted from subsequent
requests, zero API errors (in-place deletion path).
- **off → medium mid-turn**: `thinking` added mid-turn.
- **max → xhigh mid-turn** (Sonnet 5, adaptive): `output_config.effort`
"max" → "xhigh".
- **Post-turn slider change**: route returns `accepted: false`; only the
persisted setting updates.
- **Phase 0 gate**: steady-state native xhigh sends
`output_config.effort` + `thinking: {type: "adaptive", display:
"summarized"}` directly — no effort header, no fetch-wrapper rewrite.
- Persisted assistant `metadata.thinkingLevel` reflects the final
applied level in every scenario; UI verified at 375px width.

## Risks

- **StreamManager `prepareStep`** is on the hot path of every stream.
The override branch is a cheap early-out when no holder/pending exists;
regression risk concentrates in multi-step turns with a mid-turn change
(mitigated by consume-once tests, fallback/retry tests, and live
dogfooding above).
- **In-place `providerOptions` mutation** relies on the SDK reading the
object captured at `streamText()` time. If a future SDK snapshot-copies
options, the `prepareStep` return value covers it (asserted by test).
- **Anthropic xhigh hack removal** changes the wire path for existing
xhigh users of Opus 4.6/Sonnet 4.6 (non-native `max` fallback) and
native models; both shapes are covered by provider-option tests and were
dogfooded live.

---

<details>
<summary>📋 Implementation Plan</summary>

# Mid-turn thinking-level changes: apply at the next model step

## Goal

When the user changes the thinking/reasoning slider while a turn is
streaming, the new level should take effect at the **next model step**
of the active stream (a "step" = one provider request; steps are
separated by tool executions). Today the level is captured once at send
time and is fixed for the whole turn.

Chosen semantics (per user): **auto-apply at next step** — no separate
"Apply now" button, no interrupt/continue. Changing the slider mid-turn
both (a) persists the workspace setting as today and (b) requests a
next-step override on the active stream.

## Verified current behavior (evidence)

- Send path captures `thinkingLevel` in `SendMessageOptions`
(`src/browser/hooks/useSendMessageOptions.ts`); backend clamps once via
`enforceThinkingPolicy` in `AgentSession`
(`src/node/services/agentSession.ts:3616-3640`, floor = `config.json`
`minThinkingLevelByModel` → `resolveMinimumThinkingLevel`).
- `AIService.streamMessage` builds `providerOptions` once
(`buildProviderOptions`, `src/node/services/aiService.ts:2393-2405`) and
merges `providers.jsonc` extras (`:2422-2449`), then calls
`StreamManager.startStream` with the fixed level (`:2800-2832`).
- `streamText()` is invoked once per stream
(`StreamManager.createStreamResult`,
`src/node/services/streamManager.ts:1617-1660`) with `providerOptions:
request.providerOptions`. Its `prepareStep` hook already returns
per-step overrides (messages, `activeTools`) and follows the
shared-mutable-state precedent of `toolSearchState`.
- AI SDK 7 (`ai@7.0.19`): `prepareStep` may return per-step
`providerOptions`, which are **deep-merged** (`mergeObjects`) with the
request-level options each step
(`node_modules/ai/src/generate-text/stream-text.ts:1861-1864`). Omitted
keys survive; `undefined` cannot delete a key. `PrepareStepResult` has
**no `headers` field**.
- The SDK reads the outer `providerOptions` reference on every step
(`mergeObjects(providerOptions, …)`), so in-place mutation of
`streamInfo.request.providerOptions` is visible to subsequent steps.
- Slider chain: `ThinkingSlider` → `useThinkingLevel` →
`ThinkingContext.setThinkingLevel`
(`src/browser/contexts/ThinkingContext.tsx:219-238`) →
`persistAgentAiSettings` → `api.workspace.updateAgentAISettings`.
Nothing reaches the active stream.
- Precedent for renderer→in-flight-stream mutation:
`api.workspace.interruptStream` → `WorkspaceService.interruptStream`
(`workspaceService.ts:8308-8381`) → `getOrCreateSession(workspaceId)` →
`AgentSession.interruptStream` → `AIService.stopStream` →
`StreamManager.stopStream`.
- Metadata: `streamInfo.thinkingLevel` feeds partials
(`streamManager.ts:2223-2225`), stream-end metadata (`:3059-3061`) and
the persisted assistant message; the model-fallback path already mutates
it mid-stream (`:2425-2428`).
- **Anthropic xhigh wire hack is obsolete** (verified against installed
`@ai-sdk/anthropic@4.0.11` source):
- The hack's own comments state it existed because the SDK Zod schema
"doesn't accept xhigh yet" (`src/common/types/thinking.ts:188-190`,
`providerOptions.ts:641-648`); introduced with Opus 4.7 (coder#3180),
extended for Sonnet 5 (coder#3664). Mux sends `effort: "max"` through the SDK
plus `x-mux-anthropic-effort: xhigh`, and the Anthropic fetch wrapper
rewrites `output_config.effort` on the wire
(`providerModelFactory.ts:342-401`).
- SDK 4.0.11 now accepts it: `effort:
z.enum(['low','medium','high','xhigh','max'])`
(`anthropic-language-model-options.ts:209`). Explicit provider options
bypass the SDK's `supportsXhighEffort` model gating — that gating only
applies to the SDK's top-level `reasoning` CallOption mapping, which Mux
does not use; explicit `anthropicOptions.effort` flows verbatim into
`output_config.effort` (`anthropic-language-model.ts:398-465`). This
also covers Mythos (absent from the SDK capability table) exactly like
today's wrapper rewrite.
- Same for `thinking.display`: the SDK schema now has `display:
z.enum(['omitted','summarized'])` on adaptive thinking and forwards it
(`anthropic-language-model-options.ts:95-102`,
`anthropic-language-model.ts:433-457`), so the wrapper's `display ??=
"summarized"` injection is also expressible directly in provider
options.
- Route safety: the anthropic options branch runs iff `formatProvider
=== "anthropic"`, which is the **same condition** as
`routePassesHeaders` (`resolveProviderOptionsNamespaceKey(origin,
routeProvider) === origin`). So every route that emits anthropic
`effort` today also gets the header + wrapper rewrite — direct and
mux-gateway requests already carry `"xhigh"` (and `display`) on the wire
in production. Sending them directly via provider options is
**wire-field equivalent** (same Anthropic body fields; the mux-internal
header disappears, but the wrapper stripped it before upstream anyway).
Non-passthrough gateways (OpenRouter/copilot) use different
provider-options branches and are unaffected.
- All Anthropic request builders go through `buildProviderOptions` (main
stream `aiService.ts:2393`, fallback `:2710`, advisor
`tools/advisor.ts:159`), so no other path depends on the wrapper
rewrites.

## Design overview

Core primitive: a **session-owned, per-turn live holder** (one object
per turn, shared by reference down the whole pipeline — same pattern as
`toolSearchState`):

```ts
interface ActiveTurnThinkingOverride {
  /** Raw level requested mid-turn; consumed at the next prepareStep (incl. step 1). */
  pending?: ThinkingLevel;
  /** Effective level after the most recent successful application. */
  applied?: ThinkingLevel;
  /** Sink wired by StreamManager to the owning streamInfo (metadata). */
  onApplied?: (level: ThinkingLevel) => void;
}
```

```text
ThinkingContext.setThinkingLevel (mid-turn)
  ├─ persistAgentAiSettings(...)                     (unchanged; future turns)
  └─ api.workspace.setActiveTurnThinkingLevel        (new, fire-and-forget)
       → router → WorkspaceService → sessions.get(id)?.setActiveTurnThinkingLevel(level)
            └─ session.activeTurnThinkingOverride != null (turn active: PREPARING or STREAMING)
                 ? holder.pending = level → { accepted: true }
                 : { accepted: false }    (idle: persisted settings already cover next turn)

AgentSession turn lifecycle
  ├─ turn durably accepted, options finalized (sendMessage / resumeStream): create holder
  │    BEFORE the user-message emit / onAccepted / any await, then
  │    pass explicitly into streamWithHistory → StreamMessageOptions
  │    → AIService → buildStreamRequestConfig → request.thinkingOverrideState (same object)
  └─ turn end (setTurnPhase→IDLE, interrupt, dispose): identity-guarded clear
       (if (this.activeTurnThinkingOverride === holder) … = null) — pending expires

StreamManager prepareStep (runs before EVERY step, including step 1 —
so a change during PREPARING/startup applies to the turn's first provider request)
  ├─ pending? → rebuildForLevel(pending)   // closure supplied by AIService
  │     ├─ policy clamp + resolveEffectiveThinkingLevel for the STREAM's model
  │     ├─ not applicable / no-op → clear pending, keep current level
  │     └─ applicable →
  │           • in-place replace streamInfo.request.providerOptions content
  │           • state.applied = effectiveLevel; state.onApplied(effectiveLevel)
  │             (onApplied sets streamInfo.thinkingLevel → partials/final metadata)
  │           • return { providerOptions: rebuilt } from prepareStep (defense in depth)
  └─ no pending → existing behavior
```

Why both in-place mutation **and** a per-step return: the SDK deep-merge
cannot delete keys (e.g. Anthropic `thinking` object when moving to
`off`), so mutating the live request object is the authoritative
mechanism; returning the rebuilt options from `prepareStep` is
redundant-but-harmless insurance if the SDK ever changes how it
snapshots options.

Why a session-owned holder instead of a StreamManager setter: a slider
change during AIService startup (model creation, MCP setup — seconds)
would miss a not-yet-registered stream. With the holder created at turn
start and threaded down by reference, writes land in the same object
`prepareStep` reads, closing the PREPARING window with zero extra
consumption points; and because `prepareStep` also runs before step 1,
no special pre-stream handling is needed.

## Scope rules (what can/cannot switch mid-turn)

Applied inside the rebuild closure; when a transition is not applicable,
the pending override is dropped for this turn (the new level still
applies to the next turn via persisted settings, exactly as today):

1. **Clamp first**: target level is clamped with
`enforceThinkingPolicy(streamModel, target, minThinkingLevel,
providersConfig)`. If clamped target == currently effective level →
no-op.
2. **xAI `grok-4-1-fast`**: `off` ↔ non-`off` selects a different model
instance (`providerModelFactory.ts:2014-2020`) → skipped.
(Non-off↔non-off is a no-op on xAI anyway — no effort field in provider
options.)
3. Everything else applies at the next step: OpenAI `reasoningEffort`,
**all Anthropic levels including into/out of native `xhigh`** (after
Phase 0 removes the header hack — effort becomes pure provider options),
Anthropic off↔on via in-place replacement, Google `thinkingConfig`,
OpenRouter `reasoning`.

After Phase 0, `buildRequestHeaders` is thinking-level-independent (the
only remaining conditional header is the 1M-context beta, keyed on
model+config), so `prepareStep`'s inability to change headers no longer
constrains any level transition.

Out of scope (v1): OpenAI Pro `reasoningMode` toggle mid-turn; model
changes mid-turn; already-queued messages (they keep the options
captured at queue time); an explicit "Apply now" interrupt+continue
action; ACP sessions; emitting a dedicated chat event for the change
(metadata carries the final level; can be added later if requested).

Known cosmetic limitation: the already-emitted `stream-start` event
keeps the initial level, so `WorkspaceStore.currentThinkingLevel` shows
the starting level until stream end. The persisted assistant message
metadata reflects the **last applied** level.

---

## Implementation

### Phase 0 — Remove the Anthropic xhigh wire hack (~ −50 LoC)

Prerequisite refactor that makes Anthropic effort a pure
provider-options concern (wire-field-equivalent output, verified above),
unblocking mid-turn xhigh transitions and deleting two wrapper rewrites.

The refactor must stay **model-aware**: today the wire-level `"xhigh"`
and `display` are applied only when
`anthropicSupportsNativeXhigh(model)` matches (header gating + wrapper
detection). The provider-options replacement mirrors exactly that gating
so non-native adaptive models (Opus 4.6 / Sonnet 4.6: `effort: "max"`,
**no** `display`) keep identical request fields:

1. `src/common/types/thinking.ts`:
- `AnthropicEffortLevel` gains `"xhigh"`. `getAnthropicEffort` becomes
model-aware: `getAnthropicEffort(level, capabilityModel)` returns
`"xhigh"` only when `level === "xhigh" &&
anthropicSupportsNativeXhigh(capabilityModel)`, else the current mapping
(xhigh→`"max"`). The `ANTHROPIC_EFFORT` table itself keeps `xhigh:
"max"` as the non-native fallback.
- Replace the stale "SDK doesn't accept xhigh yet" comment with the new
rationale (explicit provider options pass through verbatim as of
`@ai-sdk/anthropic@4.0.11`; add an upgrade-coupling note: the SDK's
`supportsXhighEffort` gating applies only to top-level `reasoning`
mapping — re-verify on major SDK upgrades).
2. `src/common/utils/ai/providerOptions.ts`:
- Anthropic adaptive branch: pass `capabilityModel` to
`getAnthropicEffort`, and set `thinking: { type: "adaptive", display:
"summarized" }` **only when
`anthropicSupportsNativeXhigh(capabilityModel)`** (matching the
wrapper's `targetsNativeXhighModel` condition; other adaptive models
keep `{ type: "adaptive" }` with no `display`, as today). Drop the stale
comment.
- `buildRequestHeaders`: delete the xhigh header branch and the
now-unused `thinkingLevel` parameter (update both call sites,
`aiService.ts:2413-2420` and `:2726`); delete the
`MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER` export.
3. `src/node/services/providerModelFactory.ts` (Anthropic fetch
wrapper): remove the effort-override header handling and the `display
??=` injection block (incl. the `targetsNativeXhighModel` detection that
exists solely for them). Cache-control injection stays untouched.
4. Update existing tests: `providerOptions.test.ts` (effort
mapping/xhigh header assertions), `providerModelFactory.test.ts`
(wrapper rewrite tests → assert pass-through/no rewrite). Add assertions
for both sides of the gate:
- native-xhigh model at xhigh → `effort: "xhigh"` + `thinking.display:
"summarized"`;
- non-native adaptive model (e.g. Opus 4.6) at its ladder top → `effort`
unchanged from today (`"max"`/clamped) and **no** `display` field.
5. Gate: targeted tests + one manual devtools.jsonl check that a
steady-state xhigh request is **wire-field equivalent** to today: same
Anthropic body fields (`output_config.effort`, `thinking.display`), with
the mux-internal header gone (it was stripped before reaching Anthropic
anyway, so upstream requests are unchanged).

Keep this phase a separate commit so it can be reverted independently of
the mid-turn feature.

### Phase 1 — StreamManager: consume the holder in prepareStep (~65 LoC)

`src/node/services/streamManager.ts`

1. Type: `ActiveTurnThinkingOverride` lives in a small **node-local**
shared module (e.g. `src/node/services/thinkingOverride.ts`) — it
carries a callback and is node runtime state, not IPC/shared data, so it
does not belong in `src/common/types/`.
2. Extend `StreamRequestConfig` with:
- `thinkingOverrideState?: ActiveTurnThinkingOverride` — **the session's
holder, by reference; never created inside StreamManager** (a
locally-created object would be invisible to the session's setter).
- `rebuildProviderOptionsForThinkingLevel?: (level: ThinkingLevel) => {
effectiveLevel: ThinkingLevel; providerOptions: Record<string, unknown>
} | null`
(closure built by AIService; `null` ⇒ not applicable / no-op per scope
rules)
3. Thread both through `buildStreamRequestConfig` →
`createStreamAtomically` → `startStream` params (same plumbing as
`onChunk` / `toolSearchState`).
4. **Stable providerOptions object**: in `buildStreamRequestConfig`,
when a rebuild closure is present, normalize `providerOptions` to a
guaranteed mutable object (`const finalProviderOptions = providerOptions
?? {}`) before it is captured by `streamText()`. Without this, an
initially-`undefined` options value would make later in-place mutation
unobservable (the SDK closes over the value passed at creation).
Unit-test this identity behavior.
5. In `createStreamResult`'s `prepareStep` (before the existing `if
(rewritten === stepMessages && activeTools === undefined) return
undefined;` early-return), add:
   ```ts
const thinkingOverride = applyPendingThinkingOverride(request); //
helper below
   ```
and include `...(thinkingOverride ? { providerOptions: thinkingOverride
} : {})` in the returned object (adjust the early-return condition
accordingly). Because `prepareStep` runs before **every** step including
the first, a change made during AIService startup is applied to the
turn's first provider request.
6. New private helper `applyPendingThinkingOverride(request)`:
- reads `request.thinkingOverrideState?.pending`; returns `undefined` if
unset;
- always clears `pending` (consume-once; a failed/no-op application must
not retry every step);
- calls `request.rebuildProviderOptionsForThinkingLevel(pending)`; on
`null` → `log.debug` (include workspace, from/to level, skip reason) +
return `undefined`;
- **in-place** replaces the contents of `request.providerOptions`
(delete own keys, `Object.assign` the rebuilt object — object identity
preserved) so the SDK's per-step deep-merge and all retry paths observe
it;
- sets `state.applied = effectiveLevel`, invokes
`state.onApplied?.(effectiveLevel)`, returns the rebuilt options object.
7. Wire the metadata sink in `createStreamAtomically`, immediately after
`streamInfo` is constructed (and **before** any code path can trigger a
step):
   ```ts
   if (request.thinkingOverrideState) {
request.thinkingOverrideState.onApplied = (level) => {
streamInfo.thinkingLevel = level; };
if (request.thinkingOverrideState.applied) streamInfo.thinkingLevel =
request.thinkingOverrideState.applied;
   }
   ```
(The catch-up sync covers re-attachment of a holder that already applied
a level.) Partial writes (`:2223`), stream-end metadata (`:3059`), and
the final assistant message then pick up the new level with no polling
in the process loop. Note: `createStreamResult` is called before
`streamInfo` exists (`:1698-1727`); the SDK does not invoke
`prepareStep` synchronously during `streamText()` construction, but the
catch-up sync makes ordering safe regardless.
8. No StreamManager setter method is needed — the session writes to the
shared holder directly (see Phase 3).
9. Retry/fallback interplay:
- **Empty-output retry & previousResponseId retry**: both reuse/spread
`streamInfo.request`, so the mutated `providerOptions`, holder
reference, closure, and `onApplied` sink carry forward automatically
(`streamManager.ts:2484-2521`, `:3540-3589`). No changes needed beyond
confirming in tests.
- **Model fallback** (`:2331-2445`): the fallback prepare context
(second argument of `ModelFallbackOptions.prepare`, currently `{
continuation?: … }`) gains `thinkingLevelOverride?: ThinkingLevel`,
populated from `request.thinkingOverrideState?.pending ??
request.thinkingOverrideState?.applied` (then clear `pending` — it is
folded into the fallback's baseline). `PreparedModelFallback`
(`prepared.data`) gains the fallback-model-bound
`rebuildProviderOptionsForThinkingLevel`. `nextRequest` is built with
the **same holder object** (so the session setter keeps working) and the
new closure — both attached in `buildStreamRequestConfig(...)`
**before** `createStreamResult(nextRequest, …)` is called, in case the
SDK eagerly prepares the first fallback step; `onApplied` still points
at the same `streamInfo`, so no re-wiring is required.

### Phase 2 — AIService: rebuild closure (+ fallback) (~70 LoC)

`src/node/services/aiService.ts` (inside `streamMessage`, after
`mergedProviderOptions` is computed at ~`:2449`)

1. Build the closure, capturing everything already in scope. It must
reproduce the **exact initial effective-level pipeline** (AgentSession
floor clamp → AIService `resolveEffectiveThinkingLevel`, `:1097-1101`),
not just `enforceThinkingPolicy`:
   ```ts
const rebuildProviderOptionsForThinkingLevel = (level: ThinkingLevel) =>
{
const clamped = enforceThinkingPolicy(modelString, level,
minThinkingLevel, providersConfig);
const effective = resolveEffectiveThinkingLevel(modelString, clamped,
this.providerService.getConfig());
if (effective === currentEffectiveLevelRef.current) return null; //
no-op
if (isXaiGrokFastVariantSwap(modelString,
currentEffectiveLevelRef.current, effective)) return null;
const rebuilt = buildProviderOptions(modelString, effective,
providerRequestMessages,
(id) => this.streamManager.isResponseIdLost(id),
effectiveMuxProviderOptions,
workspaceId, truncationMode, this.providerService.getConfig(),
routeProvider,
       promptCacheScope, reasoningMode);
const merged = mergeModelParameterExtras(rebuilt); // shared helper, see
note below
     currentEffectiveLevelRef.current = effective;
     return { effectiveLevel: effective, providerOptions: merged };
   };
   ```
   Notes:
- `minThinkingLevel`: `AgentSession` already computes the floor at
`:3616-3640`; pass it down via a new optional
`StreamMessageOptions.minThinkingLevel` field instead of re-resolving in
AIService (single source of truth). Fallback when absent (internal
callers): `resolveMinimumThinkingLevel(modelString, undefined,
providersConfig)`.
- `currentEffectiveLevelRef` is a tiny `{ current: ThinkingLevel }`
initialized to `effectiveThinkingLevel` so consecutive overrides diff
against the live level, not the send-time one.
- Factor the extras-merge at `:2431-2449` into a local
`mergeModelParameterExtras(options)` used by both the initial build and
the closure (DRY, no behavior change).
- The grok skip predicate is a small pure helper colocated in
`src/common/utils/thinking/policy.ts` (literal `grok-4-1-fast` check
mirroring `providerModelFactory.ts:2016`). No Anthropic predicate is
needed after Phase 0.
2. New optional `StreamMessageOptions` fields: `minThinkingLevel?:
ThinkingLevel` and `activeTurnThinkingOverride?:
ActiveTurnThinkingOverride` (the session holder). Pass the holder and
`rebuildProviderOptionsForThinkingLevel` into `startStream` (new params,
adjacent to `onChunk`). When no holder is supplied (internal callers:
compaction, sub-agent paths that don't need it yet), the feature is
inert for that stream.
3. Fallback prepare (`:2575-2765`): accept the new
`thinkingLevelOverride` context field from StreamManager; compute
`nextThinkingLevel = resolveEffectiveThinkingLevel(nextModelString,
enforceThinkingPolicy(nextModelString, thinkingLevelOverride ??
effectiveThinkingLevel, minThinkingLevelForNextModel, …), …)`, and
include a **new closure bound to the fallback model** (with its own
`currentEffectiveLevelRef` initialized to `nextThinkingLevel`) in the
returned `prepared.data` so mid-turn changes keep working after a
fallback hop.

### Phase 3 — ORPC route + session holder lifecycle (~55 LoC)

1. `src/common/orpc/schemas/api.ts` — add to the `workspace` namespace
(near `updateAgentAISettings`), using the repo-standard `Result` wrapper
like sibling mutating routes:
   ```ts
   setActiveTurnThinkingLevel: {
input: z.object({ workspaceId: z.string(), thinkingLevel:
ThinkingLevelSchema }),
output: ResultSchema(z.object({ accepted: z.boolean() }), z.string()),
   },
   ```
`accepted: false` (success) = nothing active to override — persisted
settings still cover the next turn. `accepted: true` = the level will be
used for the current turn's next model step **if one occurs**; it
expires silently otherwise. Errors are reserved for invalid
input/disposed session.
2. `src/node/orpc/router.ts` — handler mirroring
`updateAgentAISettings`'s shape, calling
`context.workspaceService.setActiveTurnThinkingLevel(input.workspaceId,
input.thinkingLevel)`.
3. `src/node/services/workspaceService.ts`:
   ```ts
setActiveTurnThinkingLevel(workspaceId: string, level: ThinkingLevel):
Result<{ accepted: boolean }, string> {
     const session = this.sessions.get(workspaceId.trim());
if (!session) return Ok({ accepted: false }); // no session ⇒ nothing
running
     return Ok(session.setActiveTurnThinkingLevel(level));
   }
   ```
(Deliberately `sessions.get`, not `getOrCreateSession` — creating a
session to tell it "change the turn you don't have" is pointless.)
4. `src/node/services/agentSession.ts` — owns the holder lifecycle:
- New private field `activeTurnThinkingOverride:
ActiveTurnThinkingOverride | null = null`.
- **Create after the user-message append/persistence succeeds and
`optionsForStream` is finalized — before emitting the user-message chat
event, before `internal.onAccepted`, and before any other
await/callback** that could let the renderer's slider route arrive. NOT
inside `streamWithHistory` (which runs only after runtime warmup).
Concretely: in `sendMessage`, just ahead of the `emitChatEvent({
…userMessage })` block (`:2820-2828`, well before
`setTurnPhase(TurnPhase.PREPARING)`; covers normal, edit, and
queued-dispatch turns), and at `resumeStream`'s equivalent point after
its options are finalized. Assign `this.activeTurnThinkingOverride =
holder` and pass `holder` **explicitly as a parameter** into
`streamWithHistory`, which forwards it (plus `minThinkingLevel`) via the
new `StreamMessageOptions` fields into `aiService.streamMessage`.
- **Pre-stream failure cleanup**: any path that returns after holder
creation without reaching a stream (`onAccepted` throws, startup abort
via `preparedTurnAbortController`, `streamWithHistory` error) must
identity-clear the holder (`if (this.activeTurnThinkingOverride ===
holder) this.activeTurnThinkingOverride = null`) — the existing
`startPreparedStream` finally-block that resets PREPARING→IDLE is the
natural place.
   - **Setter**:
     ```ts
setActiveTurnThinkingLevel(level: ThinkingLevel): { accepted: boolean }
{
       this.assertNotDisposed("setActiveTurnThinkingLevel");
       const holder = this.activeTurnThinkingOverride;
if (!holder) return { accepted: false }; // idle: persisted settings
cover the next turn
       holder.pending = level;                   // last write wins
       return { accepted: true };
     }
     ```
The holder exists exactly while a turn is active (PREPARING through
STREAMING), so no `isBusy()` heuristics are needed — and messages
sitting in the queue are untouched (they dispatch later with their own
captured options and a fresh holder).
- **Clear with an identity guard** wherever the turn ends — `if
(this.activeTurnThinkingOverride === holder)
this.activeTurnThinkingOverride = null;` — in the turn's
completion/abort paths (`setTurnPhase`→`IDLE` bookkeeping,
`interruptStream`, `dispose()`). The identity guard ensures a stale
aborted turn's cleanup (e.g. an edit preempting a PREPARING turn) cannot
clear the replacement turn's holder.
- Note: because `prepareStep` runs before step 1, a change landing
during PREPARING/startup is applied to the first provider request via
the normal pending path — no separate consumption point in
`streamWithHistory` is needed.

### Phase 4 — Frontend wiring (~12 LoC)

`src/browser/contexts/ThinkingContext.tsx`, inside `setThinkingLevel`
(`:219-238`), after `persistAgentAiSettings(...)`:

```ts
if (props.workspaceId) {
  api?.workspace
    .setActiveTurnThinkingLevel({ workspaceId: props.workspaceId, thinkingLevel: level })
    .catch(() => {/* best-effort: no active stream / transient IPC failure */});
}
```

- No renderer gating on "is streaming" — the backend no-ops cheaply when
idle, and gating via `useWorkspaceState(workspaceId).canInterrupt` would
break Storybook/test mounts of `ThinkingProvider` that have no
registered store workspace.
- Keybind path (`INCREASE_THINKING`/`DECREASE_THINKING`) already funnels
through `setThinkingLevel`, so it inherits the behavior.
- One-shot `/model+level` sends and `setReasoningMode` are untouched.

### Phase 5 — Tests (~150 LoC test code; not counted in product LoC)

1. `src/node/services/streamManager.test.ts` (follow the tool-search
suite pattern: `spyOn(aiSdk, "streamText")`, capture `prepareStep` from
`mock.calls[0][0]`, invoke directly):
- pending override → `prepareStep` returns rebuilt `providerOptions`,
`request.providerOptions` is replaced **in place** (same object
identity, old provider-namespace keys gone), `pending` cleared,
`applied` recorded, `onApplied` invoked (streamInfo.thinkingLevel
updated).
- initially-`undefined` providerOptions: request config normalizes to a
stable object so `streamText` receives the same reference that later
mutation targets (identity assertion).
- rebuild returns `null` → no `providerOptions` in the step result,
`pending` cleared (no retry storm), request options untouched.
- accepted-but-no-next-step: pending set after the final step → stream
ends normally, final metadata keeps the last **applied** (or original)
level, no leak into a later stream.
- empty-output retry / previousResponseId retry: after application,
re-created stream still carries mutated options (assert on
`streamInfo.request.providerOptions`).
- model fallback: `prepare` receives `thinkingLevelOverride` equal to
the pending/applied level; `nextRequest` reuses the **same holder
object** and gets the fallback-bound closure attached before
`createStreamResult` runs.
2. `src/node/services/aiService.test.ts`: closure behavior — clamps to
floor then applies `resolveEffectiveThinkingLevel`, no-op when effective
== current, skips xAI grok-4-1-fast off↔on, applies Anthropic
transitions into/out of `xhigh` as plain rebuilds, produces merged
extras identical in shape to the initial build for a plain level change.
3. `src/node/services/agentSession.*.test.ts` /
`workspaceService.test.ts`:
- `setActiveTurnThinkingLevel` → `{ accepted: false }` when idle (no
holder) and for unknown workspace; `{ accepted: true }` while a turn is
active; last-write-wins on consecutive calls (holder.pending reflects
the last level).
- holder lifecycle: created immediately after durable acceptance/options
finalization (before emit/onAccepted/any await), identity-cleared on
pre-stream failure / turn end / interrupt / dispose (a post-turn setter
call returns `accepted: false`); an edit-preempted turn's cleanup does
not clear the replacement turn's holder.
- PREPARING window: turn durably accepted, **before**
`setTurnPhase(PREPARING)` / `streamWithHistory` / stream registration →
slider route writes `pending` → the turn's first provider request
consumes it (assert via captured `prepareStep`); pre-stream failure
after holder creation identity-clears the holder.
- queued messages are NOT retroactively changed: queue a follow-up
mid-turn, change the level, let the queue dispatch → the follow-up turn
streams with its captured options (fresh holder, no inherited pending).
4. `src/browser/contexts/ThinkingContext.test.tsx`: `setThinkingLevel`
fires `api.workspace.setActiveTurnThinkingLevel` with the workspaceId
and level; NOT fired when the provider has no `workspaceId`
(project/global scope).

No tautological tests: every assertion above exercises branching (queued
vs not, applied vs skipped, merge-deletion semantics), not copy.

### Phase 6 — Validation gates & dogfooding

Local gates (run between phases, not only at the end):

- After Phase 0: `bun test src/common/utils/ai/providerOptions.test.ts
src/node/services/providerModelFactory.test.ts` + manual xhigh
wire-bytes regression check (step 0 below).
- After Phase 1–2: `bun test src/node/services/streamManager.test.ts
src/node/services/aiService.test.ts` (targeted; QuickJS-heavy suites
excluded as usual).
- After Phase 3–4: `MUX_ESLINT_CONCURRENCY=1 make static-check` + `bun
test src/browser/contexts/ThinkingContext.test.tsx`.

Dogfooding (evidence required; use `dev-server-sandbox` skill +
`agent-browser`):

0. **Phase 0 regression check**: on an Anthropic native-xhigh model at
steady xhigh (no mid-turn change), capture one request in
`devtools.jsonl` and diff `output_config.effort` + `thinking.display`
against a pre-change capture — must match (`"xhigh"` / `"summarized"`),
and the `x-mux-anthropic-effort` header must be absent from
`update.requestHeaders`. Also capture one Opus/Sonnet 4.6 request at its
top level — `effort` unchanged and no `display` field.
1. Start a sandboxed dev server (`dev-server-sandbox` skill); ensure a
**direct** provider route (disable sandbox gateway block or set
`routePriority: ["direct"]`) so `devtools.jsonl` shows wire-level
requests.
2. Enable API Debug Logs. In a test workspace, send a prompt guaranteed
to run several tool steps (e.g. "read these 3 files then summarize")
with thinking = low.
3. While the agent is mid-tool, move the slider to high (screenshot the
slider + streaming state via `agent-browser screenshot`).
4. After the turn: inspect `~/.mux/sessions/<ws>/devtools.jsonl`
`step-update.update.rawRequest` entries — step N (pre-change) must show
the low reasoning config, step N+1 the high config (e.g. OpenAI
`reasoning.effort`, Anthropic
`thinking.budget_tokens`/`output_config.effort`). Capture the two JSON
excerpts.
5. Verify the persisted assistant message metadata (`chat.jsonl`) has
`thinkingLevel: "high"` (last applied).
6. Repeat once on an Anthropic model for the off→high
in-place-replacement case (key deletion path), and once for high→xhigh
on a native-xhigh model (`output_config.effort` flips to `"xhigh"`
mid-turn — the transition Phase 0 unblocked).
7. Mobile check (repo rule): capture a ~375px-wide viewport screenshot
of the chat footer with the slider mid-turn — no layout change is
expected, but narrow-viewport verification is mandatory for UI-adjacent
changes.
8. Record a screen capture of the slider change mid-turn (ffmpeg x11grab
per repo notes; compress with libvpx to fit the 10MB attach limit). If
recording is environmentally blocked, document the exact blocker and
rely on the step-by-step screenshots + devtools excerpts.
9. Attach all screenshots/video via `attach_file` and include the two
rawRequest JSON excerpts in the summary.

## Risks & mitigations

| Risk | Mitigation |
| --- | --- |
| SDK deep-merge can't delete keys (off-transitions) | In-place mutation
of the live `request.providerOptions` object is authoritative; per-step
return is only defense-in-depth. Unit test asserts old namespace keys
are gone. |
| SDK later snapshots/clones providerOptions at `streamText()` time |
Per-step returned `providerOptions` still overrides conflicting leaves;
add a code comment flagging the coupling for SDK upgrades (like the MCP
OAuth note). |
| Prompt-cache churn (Anthropic breakpoints / OpenAI implicit caching
are level-independent; only reasoning params change) | No cache keys are
touched by the rebuild (same `promptCacheKey`/scope captured). Verified:
`buildProviderOptions` derives caching from workspace/scope, not level.
|
| `prepareProviderRequestMessages` used the send-time level for
Anthropic reasoning-part replay | Mid-turn steps use the SDK's
accumulated step messages, not re-prepared history; divergence only
affects reasoning-part filtering of *old* history within the same turn.
Accept + document as v1 limitation in a code comment. |
| Fallback/retry paths dropping the override | Explicit carry-through
(Phase 1.9 / Phase 2.3) + tests. |
| Model-swap (grok off↔on) transitions silently doing the wrong thing |
Explicit skip predicate + debug log; level then applies next turn
exactly as today. |
| Phase 0: SDK upgrade re-introduces effort gating for explicit provider
options | Coupling comment at `ANTHROPIC_EFFORT` (like the MCP OAuth
note); Phase 0 test asserts `effort: "xhigh"` survives to the request;
wire-field regression check in dogfooding. |
| Phase 0: mux-gateway rejects direct `xhigh`/`display` in provider
options | Already receiving these exact values today via the wrapper
rewrite of the gateway body shape — proven in production; regression
check covers it. |
| Phase 0: over-broad mapping sends `xhigh`/`display` to non-native
models (Opus/Sonnet 4.6) | Model-aware gating on
`anthropicSupportsNativeXhigh(capabilityModel)` in both
`getAnthropicEffort` and the `display` field, mirroring today's
header/wrapper conditions; both-sides-of-the-gate unit tests + 4.6
dogfooding capture. |
| Slider change lands during PREPARING / AIService startup (stream not
yet registered) and is lost | Session-owned per-turn holder created at
turn start and threaded down by reference; `prepareStep` runs before
step 1, so the first provider request already honors it (Phase 3.4). |
| Initially-`undefined` providerOptions makes in-place mutation
invisible to the SDK | Normalize to a stable object in
`buildStreamRequestConfig` (Phase 1.4) + identity unit test. |
| Override bleeding into queued follow-up turns | Holder is per-turn and
cleared at turn end; queued messages dispatch with their own captured
options + fresh holder. Regression test in Phase 5.3. |

## Net LoC estimate (product code)

| Phase | Est. net LoC |
| --- | --- |
| 0 Remove xhigh wire hack | ~ −50 |
| 1 StreamManager | ~65 |
| 2 AIService | ~70 |
| 3 ORPC/session holder lifecycle | ~55 |
| 4 Frontend | ~12 |
| **Total** | **~150 net** (upper bound ~200 with type plumbing
overhead) |

## Acceptance criteria

1. Changing the thinking slider during an active multi-step turn changes
the reasoning parameters of the next provider request within the same
turn (verified in `devtools.jsonl`).
2. Changing the slider when idle behaves exactly as today (persisted
settings only; route returns `accepted: false`).
3. A change during the PREPARING window (turn accepted, stream not yet
started) applies to that turn's first model request.
4. Clamping: a mid-turn request below the model floor / above the
ceiling applies the clamped effective level; a no-op change leaves the
request untouched.
5. Anthropic transitions into/out of native `xhigh` apply mid-turn;
steady-state requests are wire-field equivalent to pre-refactor
(native-xhigh: `output_config.effort: "xhigh"` + `thinking.display:
"summarized"`; non-native adaptive: unchanged `effort`, no `display`; no
`x-mux-anthropic-effort` header anywhere).
6. The one excluded transition (xAI grok-4-1-fast off↔on) does not alter
the in-flight stream and still applies on the next turn.
7. Final assistant message metadata records the last applied level;
interrupted/error partials also carry it.
8. Empty-output retry, previousResponseId retry, and model fallback
preserve the applied override.
9. All targeted tests + `make static-check` pass.

</details>

---

_Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking:
`xhigh` • Cost: `$150.32`_

<!-- mux-attribution: model=anthropic:claude-fable-5 thinking=xhigh
costs=150.32 -->
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