Skip to content

feat(one-reason): support configuring model reasoning effort - #11

Merged
yaonyan merged 2 commits into
mainfrom
feat/one-reason-reasoning-effort
Sep 8, 2026
Merged

yaonyan merged 2 commits into
mainfrom
feat/one-reason-reasoning-effort

Conversation

@yaonyan

@yaonyan yaonyan commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

What

Adds support for configuring a model's reasoning/thinking effort through environment variables and reason.json, without code changes:

  • ONE_REASON_REASONING_EFFORT (fallbacks: ONE_REASONING_EFFORT, then REASONING_EFFORT in reason.json)
  • Valid values: off, low, medium, high (unset = provider default)
  • reason auth can now persist REASONING_EFFORT, and --help/README/.env.example document it

Why

Most OpenAI-compatible reasoning APIs expose reasoning_effort (plus vendor-specific fields), but the package previously had no way to set it. The fields are not universal, so the mapping is provider-aware and deliberately best-effort:

Effort OpenAI-compatible OpenAI (native) Anthropic
low / medium / high reasoning_effort: low|medium|high reasoningEffort: low|medium|high Claude 4.6+ / Claude 5: adaptive thinking + effort; older Claude models: enabled + fixed budgetTokens (2048/8192/16384)
off reasoning_effort: "none" reasoningEffort: "minimal" thinking: { type: "disabled" }

Anthropic handling is split by model generation because the two thinking modes are not interchangeable: type: "enabled" + budgetTokens is rejected on Claude Opus 4.7+ / Claude 5, while Claude 4.6 and earlier have no effort parameter, so each effort level maps to a fixed thinking budget. off also requires @ai-sdk/anthropic >= 3.0.93 — earlier versions silently dropped thinking: { type: "disabled" } from the request, so thinking-stopped-by-default models like Sonnet 5 would keep thinking.

Caveats documented in the README: some providers ignore reasoning_effort (e.g., Zhipu GLM gates thinking via its own thinking.type), and always-reasoning models (e.g., MiniMax M2) may reject or ignore the parameter. Use a non-thinking model when thinking must be guaranteed off.

Verification

  • New request-body regression tests stub fetch and assert what the AI SDK actually sends for anthropic, openai-compatible, and openai, covering off/low/medium/high, adaptive vs legacy Anthropic models, and gateway-prefixed model ids. The off Anthropic test fails against the previously locked @ai-sdk/anthropic@3.0.68.
  • Local OpenAI-compatible captures: unset → no field; low/medium/high → forwarded verbatim; off → "none"; generic ONE_REASONING_EFFORT fallback works; uppercase values normalized; invalid values fail fast with a clear error.
  • Config precedence confirmed: reason.json REASONING_EFFORT: "high" sends reasoning_effort: "high"; setting ONE_REASON_REASONING_EFFORT=off overrides it to "none".
  • Live against the internal OpenAI-compatible gateway with kimi-k2.6: off ≈ 2s, low/high ≈ 21–25s, all returning schema-valid JSON.
  • Vendor probes showed GLM silently keeps thinking under reasoning_effort=none, while Kimi K2.6 honors it; native OpenAI mapping verified against the pinned AI SDK schemas.
  • pnpm --recursive typecheck, vitest run, oxfmt --check, and the one-reason build all pass.

Add REASONING_EFFORT (off/low/medium/high) to the env/config surface so
callers can control model thinking without editing code. Resolved values
are forwarded per provider: OpenAI-compatible gets the standard
reasoning_effort body field (off -> none), native OpenAI gets
reasoningEffort (off -> minimal), and Anthropic disables extended
thinking or sets the reasoning effort parameter. ACP is explicitly
unsupported. The CLI auth flow can persist REASONING_EFFORT and help/doc
text documents the best-effort semantics and provider caveats.
Resolve both review findings in the Anthropic branch of the reasoning-effort
mapping:

- low/medium/high now enable thinking instead of only forwarding effort.
  Claude 4.6+ and Claude 5 models get adaptive thinking plus the effort
  parameter, while older Claude models get the legacy enabled + budgetTokens
  form (2048/8192/16384 tokens per level). Effort never leaks into legacy
  requests.
- off now reaches the API: bump @ai-sdk/anthropic to ^3.0.116 (>= 3.0.93) so
  thinking: { type: "disabled" } is serialized instead of being dropped.

Add request-body regression tests that stub fetch and assert what the AI SDK
actually sends for anthropic, openai-compatible, and openai, and document the
per-generation Anthropic behavior in the README, .env.example, and --help.
@yaonyan

yaonyan commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both P2 findings from the review.

1. high/medium/low now actually enable Anthropic thinking

  • Claude 4.6+ / Claude 5 models (Sonnet 4.6/5, Opus 4.6/4.7/4.8/5, Fable 5): anthropic: { thinking: { type: "adaptive" }, effort } → wire body thinking: { type: "adaptive" } + output_config: { effort: ... }.
  • Claude 4.6 and earlier (4/4.1/4.5, Haiku 4.5, 3.x): anthropic: { thinking: { type: "enabled", budgetTokens } } (2048/8192/16384 for low/medium/high). Effort is an adaptive-only parameter, so it is never sent to legacy models.
  • Selection is by model generation, not a blanket adaptive mode.

2. off now reaches the wire

  • Bumped @ai-sdk/anthropic to ^3.0.116 (>= 3.0.93); earlier versions only serialized thinking for enabled/adaptive and dropped disabled.
  • Captured request body for off is now thinking: { type: "disabled" } for both adaptive and legacy models.

Regression tests

  • Added packages/one-reason/tests/reasoning-effort.test.ts (vitest) that stubs fetch and asserts the actual HTTP body sent by the pinned AI SDK providers (anthropic, openai-compatible, openai) across off/low/medium/high, adaptive vs legacy Anthropic models, and gateway-prefixed ids. The off Anthropic case fails against @ai-sdk/anthropic@3.0.68.
  • pnpm --recursive typecheck, vitest run, oxfmt --check, and the one-reason build all pass.

@yaonyan
yaonyan merged commit 1c8360a into main Sep 8, 2026
1 check 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.

1 participant