Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

feat(llm): 接通 thinking 强度(reasoning_effort / thinking_budget)——Goal 350 本地已验证实现 + patch

オープン
#145 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
35/100
issue の種類
機能追加
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
rust
領域
ai, backend

調査の方向性

Start with the cited .dev/goals/350-reasoning-effort-thinking-budget.md, then review the linked patch and the named entry points: crates/recursive-cli/src/cli/builder.rs, crates/recursive-cli/src/main.rs, crates/recursive-tui/src/runtime_builder.rs, and src/http/handlers.rs. Run the listed format, clippy, and workspace test commands; verify the provider request-body tests and HTTP/CLI wiring. Done means the native knobs reach the appropriate provider while defaults remain unchanged and all quality gates pass.

索引モデルが issue の本文から書いたものです。

説明

背景:--effort 目前是静默 no-op

Config.thinking_budget 被 --effort 写入(crates/recursive-cli/src/main.rs),HTTP 的两个请求体也声明了 thinking_budget,但没有任何代码读回它:src/llm/ 零引用,两个 build_request 都不发 thinking 参数,src/http/handlers.rs 从不读请求体里的字段。所以 --effort low|high 现在改不动任何东西——只是主栈(DeepSeek 的 Anthropic 兼容端点)默认就吐 thinking,掩盖了请求侧从来没接上。

这正是 .dev/goals/350-reasoning-effort-thinking-budget.md 描述的状态。我在本地 worktree(.worktrees/reasoning-effort,分支 probe/reasoning-effort)按该 goal 实现了完整方案、跑通三闸门并做了真实抓包验证,没有提交任何代码,patch 在下面,供你们决定是否落地。

我做了什么(patch 全文)

https://gist.github.com/okguitar/aef4733f2a510680cc3acf14f6b37d32

16 files changed, 503 insertions(+), 10 deletions(-)

实现要点

  1. Config.reasoning_effort: Option<String>(新字段,紧邻 thinking_budget):加入 Debug 脱敏输出 + 全部 13 处 Config { .. } 字面量(编译器强制,都是 None)。
  2. 两个 provider 各加两个字段(reasoning_effort / thinking_budget)+ with_reasoning_effort() / with_thinking_budget() builder,与既有 with_max_tokens 同风格。
  3. build_request 只发各自生态的原生 knob,对方那个显式 let _ = ... 忽略并注释说明——不做 budget↔effort 换算(OpenAI 是枚举级别,Anthropic 是 token 数,单位不同)。
    • openai.rs:if let Some(effort) = reasoning_effort { req["reasoning_effort"] = ... }
    • anthropic.rs:Some(0) → {"type":"disabled"}(其余字段一律不动);Some(n) → {"type":"enabled","budget_tokens":n},并在序列化层强制 API 约束:temperature = 1.0、max_tokens ≥ n + 4096。
  4. --effort 一个 flag 驱动两个 knob:low → budget 0 + effort "low",high → 16000 + "high",normal → 两者都不设。
  5. opt-in 原则:不设任何 knob 时请求体与改动前逐字节一致(有 *_by_default 测试钉住),因为部分 OpenAI 兼容网关会拒绝未知字段。
  6. HTTP 路径真正接线:两个请求体加 reasoning_effort;handlers.rs 新增 provider_with_overrides(),注入到唯一的 runtime 构造点 build_session_runtime();无 knob 时走 state.provider.clone() 快路径(不 mutate 共享的 Arc<dyn ChatProvider>)。
  7. 未触碰 run_core.rs::run_inner、ChatProvider trait、响应侧解析;无新依赖;非测试代码无 unwrap()。

验证证据

质量门(全部绿)

cargo fmt --all -- --check                                  # exit 0
cargo clippy --all-targets --all-features -- -D warnings    # exit 0
cargo test --workspace                                      # exit 0(821 lib + 各集成套件全绿)

新增测试(11 个新 + 1 个扩展重命名):build_request_emits_thinking_enabled_with_budget、build_request_thinking_enabled_guarantees_max_tokens_headroom、build_request_emits_thinking_disabled_for_zero、build_request_omits_thinking_by_default、build_request_ignores_reasoning_effort_on_anthropic、build_request_includes_reasoning_effort_when_set、build_request_omits_reasoning_effort_by_default、build_request_ignores_thinking_budget_on_openai、with_thinking_budget_builder_plumbs_into_request、with_reasoning_effort_builder_plumbs_into_request、effort_flag_sets_both_config_knobs、provider_overrides_are_opt_in_and_rebuild。

端到端抓包(本地起 mock server 抓 CLI 实际发出的请求体,非单元测试):

场景 抓到的请求体
--provider anthropic --effort high {"temperature": 1.0, "max_tokens": 65536, "thinking": {"type": "enabled", "budget_tokens": 16000}}
--provider anthropic --effort low {"temperature": 0.2, "max_tokens": 65536, "thinking": {"type": "disabled"}}
--provider anthropic(无 flag) {"temperature": 0.2, "max_tokens": 65536}(无 thinking)
--provider openai --effort high {"temperature": 0.2, "reasoning_effort": "high"}
--provider openai --effort normal {"temperature": 0.2, "max_tokens": 65536}(无 reasoning_effort)

HTTP API 抓包(真起 recursive http,POST /run):

请求体 上游实际收到
anthropic + {"thinking_budget": 7000} {"temperature": 1.0, "thinking": {"type":"enabled","budget_tokens":7000}}
anthropic + {"reasoning_effort": "high"} 无 thinking、无 effort(对方 knob 正确忽略)
anthropic + 无 knob 无 thinking(默认不变)
openai + {"reasoning_effort": "high"} {"reasoning_effort": "high"}
openai + {"thinking_budget": 7000} 无 thinking(正确忽略)

与 goal 文档的 4 处偏差(落地时请注意)

  1. clippy too_many_arguments:anthropic 的 build_request 到 8 参(比 openai 多一个 system),-D warnings 会红。我加了 #[allow(clippy::too_many_arguments)] + 注释——仓库里已有 10 处同样 allow(含 openai.rs 自己)。备选方案是把两个 knob 收进一个 ThinkingKnobs 结构体,那样更干净但改动面更大,我选了与 goal 签名一致的写法。
  2. goal 里点名的 main.rs:703-752 / 1917-1926 已经不存在:那两处已重构为 cli/builder.rs::build_llm_provider() 单一 helper,CLI/ACP/HTTP 共用。所以实际只需 2 处接线(CLI helper + crates/recursive-tui/src/runtime_builder.rs::build_provider)。
  3. HTTP 侧无法复用那个 helper:它在 recursive-cli crate 里,src/http 属于库 crate。我在 handlers.rs 内镜像了同样 provider_type match,并用 state.config.api_key(不引入新的 key 解析路径);api_key 为 None 时退回共享 provider。
  4. --effort 拆成两个纯函数:保留原 effort_thinking_budget()(原有测试不动),新增 effort_reasoning_effort(),两者合并在一个测试里断言(遵守 .dev/AGENTS.md 的「env/CLI 解析测试必须是一个测试」)。

建议的后续(我没有做)

  • 补 anthropic-beta: extended-thinking-2025-01-15 header(DeepSeek 网关不需要,官方 Anthropic 可能需要),以及按需做 model allowlist。
  • config.toml / env 层面还没有 knob(目前只有 CLI --effort 和 HTTP 请求体);SDK 的 thinking_budget 之前是「透传但不生效」,这次接通后真的会生效,建议同步更新文档。
  • TUI 仍无 effort 指示器 / /effort(docs/tui-fake-cc-gap.md 里标 🔴 的那两行)。
  • Anthropic 走 enabled 时会把 temperature 覆盖成 1.0(API 硬约束),与用户显式设置的 temperature 冲突时的取舍值得写进文档。
  • 另:crates/recursive-cli/src/cli/control.rs 的 set_max_thinking_tokens 仍返回 "thinking budget not enforced by this runtime",这次接通后可以同步改掉。

如果方向 OK,我可以按你们的 review 意见调整后开 PR;也可以直接采纳 gist 里的 patch。


patch 全文(16 files, +503/-10)——与上方 gist 内容一致
diff --git a/crates/recursive-cli/src/cli/builder.rs b/crates/recursive-cli/src/cli/builder.rs
index 7c1e795e..3ae6dcec 100644
--- a/crates/recursive-cli/src/cli/builder.rs
+++ b/crates/recursive-cli/src/cli/builder.rs
@@ -519,6 +519,8 @@ pub(crate) fn build_llm_provider(
             let mut anthropic = AnthropicProvider::new(&config.api_base, api_key, &config.model)?
                 .with_temperature(config.temperature)
                 .with_max_tokens(config.max_tokens)
+                .with_thinking_budget(config.thinking_budget)
+                .with_reasoning_effort(config.reasoning_effort.clone())
                 .with_retry_policy(anthropic_retry);
             if let Some(rounds) = max_search_rounds {
                 anthropic = anthropic.with_max_search_rounds(rounds);
@@ -529,6 +531,8 @@ pub(crate) fn build_llm_provider(
             let mut openai = OpenAiProvider::new(&config.api_base, api_key, &config.model)?
                 .with_temperature(config.temperature)
                 .with_max_tokens(config.max_tokens)
+                .with_reasoning_effort(config.reasoning_effort.clone())
+                .with_thinking_budget(config.thinking_budget)
                 .with_retry_policy(retry);
             if let Some(rounds) = max_search_rounds {
                 openai = openai.with_max_search_rounds(rounds);
@@ -717,6 +721,7 @@ mod tests {
             headless: true,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
diff --git a/crates/recursive-cli/src/cli/resume.rs b/crates/recursive-cli/src/cli/resume.rs
index aa24ede8..9a0197d4 100644
--- a/crates/recursive-cli/src/cli/resume.rs
+++ b/crates/recursive-cli/src/cli/resume.rs
@@ -894,6 +894,7 @@ mod tests {
             headless: true,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
diff --git a/crates/recursive-cli/src/main.rs b/crates/recursive-cli/src/main.rs
index 9ff5f686..f646a50a 100644
--- a/crates/recursive-cli/src/main.rs
+++ b/crates/recursive-cli/src/main.rs
@@ -123,8 +123,8 @@ struct Cli {
     #[arg(short = 'n', long = "name")]
     name: Option<String>,
 
-    /// Reasoning effort level: low (no extended thinking), normal (default), high (max budget).
-    /// Currently effective for Anthropic models that support extended thinking.
+    /// Reasoning effort level: low (minimal/disabled thinking), normal (default), high (max).
+    /// Sets `reasoning_effort` (OpenAI reasoning models) and the thinking budget (Anthropic).
     #[arg(long = "effort", value_parser = ["low", "normal", "high"])]
     effort: Option<String>,
 
@@ -598,9 +598,14 @@ async fn main() -> anyhow::Result<()> {
     if matches!(cli.permission_mode.as_deref(), Some("auto")) {
         config.headless = true;
     }
-    // --effort: map to thinking_budget (low=0 disables, normal=default, high=max).
+    // --effort: map to BOTH native thinking knobs (Goal-350). Anthropic gets a
+    // token budget (low=0 disables, normal=default, high=max); OpenAI gets
+    // `reasoning_effort` (low/high pass through, normal=default). Each provider
+    // consumes only its own knob — there is deliberately no conversion between
+    // the two units.
     if let Some(effort) = &cli.effort {
         config.thinking_budget = effort_thinking_budget(effort);
+        config.reasoning_effort = effort_reasoning_effort(effort);
     }
     // --name: optional display name for the session.
     if let Some(name) = cli.name {
@@ -1808,6 +1813,18 @@ fn effort_thinking_budget(effort: &str) -> Option<u32> {
     }
 }
 
+/// `--effort` → OpenAI `reasoning_effort` mapping (Goal-350). `low` and `high`
+/// pass through unchanged; anything else (including `normal`) keeps the
+/// provider/model default by leaving the field off the wire. The Anthropic
+/// budget is produced by [`effort_thinking_budget`] — the two are independent
+/// because OpenAI and Anthropic expose different knobs.
+fn effort_reasoning_effort(effort: &str) -> Option<String> {
+    match effort {
+        "low" | "high" => Some(effort.to_string()),
+        _ => None, // "normal" → leave as default
+    }
+}
+
 /// Whether the effective output streams tokens: an explicit `--stream`, a JSON
 /// output mode that implies token streaming, or `--output-format stream-json`.
 fn resolve_effective_stream(
@@ -3025,6 +3042,7 @@ mod tests {
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
@@ -3390,12 +3408,19 @@ mod tests {
         assert_eq!(mask_key(Some("abcdefghijkl")), "abcd...ijkl");
     }
 
+    /// Goal-350: one test owns the whole `--effort` → config mapping (both
+    /// knobs), per the "env/CLI-parse tests must be ONE test" rule.
     #[test]
-    fn effort_thinking_budget_maps_low_high_and_default() {
+    fn effort_flag_sets_both_config_knobs() {
         assert_eq!(effort_thinking_budget("low"), Some(0));
         assert_eq!(effort_thinking_budget("high"), Some(16000));
         assert_eq!(effort_thinking_budget("normal"), None);
         assert_eq!(effort_thinking_budget(""), None);
+
+        assert_eq!(effort_reasoning_effort("low").as_deref(), Some("low"));
+        assert_eq!(effort_reasoning_effort("high").as_deref(), Some("high"));
+        assert_eq!(effort_reasoning_effort("normal"), None);
+        assert_eq!(effort_reasoning_effort(""), None);
     }
 
     #[test]
diff --git a/crates/recursive-tui/src/runtime_builder.rs b/crates/recursive-tui/src/runtime_builder.rs
index c3e236a1..9680edf9 100644
--- a/crates/recursive-tui/src/runtime_builder.rs
+++ b/crates/recursive-tui/src/runtime_builder.rs
@@ -44,12 +44,16 @@ fn build_provider(
             recursive::llm::AnthropicProvider::new(&config.api_base, api_key, &config.model)?
                 .with_temperature(config.temperature)
                 .with_max_tokens(config.max_tokens)
+                .with_thinking_budget(config.thinking_budget)
+                .with_reasoning_effort(config.reasoning_effort.clone())
                 .with_retry_policy(retry),
         ),
         _ => Arc::new(
             recursive::llm::OpenAiProvider::new(&config.api_base, api_key, &config.model)?
                 .with_temperature(config.temperature)
                 .with_max_tokens(config.max_tokens)
+                .with_reasoning_effort(config.reasoning_effort.clone())
+                .with_thinking_budget(config.thinking_budget)
                 .with_retry_policy(retry)
                 .with_max_search_rounds(config.max_search_rounds),
         ),
@@ -814,6 +818,7 @@ type = "openai"
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
diff --git a/src/config.rs b/src/config.rs
index 3d703053..92fac661 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -61,6 +61,12 @@ pub struct Config {
     /// Extended thinking budget for models that support it (e.g. Anthropic claude-3-7).
     /// `None` = model default; `Some(0)` = disable thinking; `Some(n)` = budget_tokens.
     pub thinking_budget: Option<u32>,
+    /// Reasoning effort for OpenAI-compatible reasoning models (o1/o3/GPT-5
+    /// family and compatible gateways): `"low"` | `"medium"` | `"high"`.
+    /// `None` = leave the request untouched (provider/model default).
+    /// Orthogonal to [`Config::thinking_budget`] (the Anthropic knob) — each
+    /// provider consumes only its native knob.
+    pub reasoning_effort: Option<String>,
     /// Optional display name for the session, shown in the /resume picker.
     pub session_name: Option<String>,
     /// Maximum total API spend in USD for this run. Checked after each turn.
@@ -153,6 +159,7 @@ impl std::fmt::Debug for Config {
             .field("headless", &self.headless)
             .field("memory_summary_limit", &self.memory_summary_limit)
             .field("thinking_budget", &self.thinking_budget)
+            .field("reasoning_effort", &self.reasoning_effort)
             .field("session_name", &self.session_name)
             .field("max_budget_usd", &self.max_budget_usd)
             .field("extra_dirs", &self.extra_dirs)
@@ -635,6 +642,7 @@ impl Config {
             headless,
             memory_summary_limit,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: file_extra_dirs,
@@ -1427,6 +1435,7 @@ max_tokens = 20000
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
@@ -2132,6 +2141,7 @@ preset = "totally-bogus-id"
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
@@ -2279,6 +2289,7 @@ api_key = "sk-from-file"
                 headless: false,
                 memory_summary_limit: 5,
                 thinking_budget: None,
+                reasoning_effort: None,
                 session_name: None,
                 max_budget_usd: None,
                 extra_dirs: Vec::new(),
@@ -2329,6 +2340,7 @@ api_key = "sk-from-file"
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
@@ -2417,6 +2429,7 @@ api_key = "sk-from-file"
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
diff --git a/src/http/cold_load.rs b/src/http/cold_load.rs
index b1bf8aff..c80cc7d4 100644
--- a/src/http/cold_load.rs
+++ b/src/http/cold_load.rs
@@ -354,6 +354,7 @@ mod tests {
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
diff --git a/src/http/handlers.rs b/src/http/handlers.rs
index b7fb8a6f..d77abcdc 100644
--- a/src/http/handlers.rs
+++ b/src/http/handlers.rs
@@ -16,6 +16,7 @@ use tokio::sync::broadcast;
 use tokio_stream::{wrappers::BroadcastStream, wrappers::IntervalStream, StreamExt};
 
 use crate::event::{AgentEvent, ChannelSink, NullSink};
+use crate::llm::{AnthropicProvider, ChatProvider, OpenAiProvider, RetryPolicy};
 use crate::message::Role;
 use crate::permissions::{LayeredPermissionsConfig, PermissionMode};
 use crate::runtime::AgentRuntimeBuilder;
@@ -93,10 +94,11 @@ fn build_session_runtime(
     system_prompt: String,
     prompt_segments: crate::system_prompt::PromptSegments,
     max_steps: usize,
+    provider: Arc<dyn ChatProvider>,
 ) -> AgentRuntimeBuilder {
     crate::runtime::apply_context_management(
         AgentRuntimeBuilder::new()
-            .llm(state.provider.clone())
+            .llm(provider)
             .tools(tool_registry)
             .system_prompt(system_prompt)
             .prompt_segments(prompt_segments)
@@ -114,6 +116,56 @@ fn build_session_runtime(
     )
 }
 
+/// Goal-350: rebuild the provider from `state.config` with per-request thinking
+/// knobs. Falls back to the shared `state.provider` when no knob is set, so the
+/// default path stays byte-identical and shares one provider instance.
+///
+/// Mirrors the `provider_type` match in
+/// `crates/recursive-cli/src/cli/builder.rs::build_llm_provider` — that helper
+/// lives in the CLI crate, which this library crate cannot call.
+fn provider_with_overrides(
+    state: &AppState,
+    thinking_budget: Option<u32>,
+    reasoning_effort: Option<&str>,
+) -> Result<Arc<dyn ChatProvider>, ApiError> {
+    if thinking_budget.is_none() && reasoning_effort.is_none() {
+        return Ok(state.provider.clone());
+    }
+    let cfg = &state.config;
+    // Reuse the key the server started with; there is deliberately no separate
+    // key-resolution path here.
+    let Some(api_key) = cfg.api_key.clone() else {
+        return Ok(state.provider.clone());
+    };
+    let retry = RetryPolicy {
+        max_retries: cfg.retry_max,
+        initial_backoff: Duration::from_secs(cfg.retry_initial_backoff_secs),
+        max_backoff: Duration::from_secs(cfg.retry_max_backoff_secs),
+    };
+    let effort = reasoning_effort.map(str::to_string);
+    let provider: Arc<dyn ChatProvider> = match cfg.provider_type.as_str() {
+        "anthropic" => Arc::new(
+            AnthropicProvider::new(&cfg.api_base, api_key, &cfg.model)
+                .map_err(|e| ApiError::internal(format!("failed to build provider: {e}")))?
+                .with_temperature(cfg.temperature)
+                .with_max_tokens(cfg.max_tokens)
+                .with_thinking_budget(thinking_budget)
+                .with_reasoning_effort(effort)
+                .with_retry_policy(retry),
+        ),
+        _ => Arc::new(
+            OpenAiProvider::new(&cfg.api_base, api_key, &cfg.model)
+                .map_err(|e| ApiError::internal(format!("failed to build provider: {e}")))?
+                .with_temperature(cfg.temperature)
+                .with_max_tokens(cfg.max_tokens)
+                .with_reasoning_effort(effort)
+                .with_thinking_budget(thinking_budget)
+                .with_retry_policy(retry),
+        ),
+    };
+    Ok(provider)
+}
+
 /// Update metrics after a successful agent run.
 fn record_run_success(metrics: &super::Metrics, steps: usize, usage: &crate::llm::TokenUsage) {
     metrics.agent_runs_total.fetch_add(1, Ordering::Relaxed);
@@ -247,6 +299,11 @@ pub(super) async fn run_agent(
         system_prompt,
         prompt_segments,
         max_steps,
+        provider_with_overrides(
+            &state,
+            body.thinking_budget,
+            body.reasoning_effort.as_deref(),
+        )?,
     )
     .build()
     .map_err(|e| ApiError::internal(format!("failed to build runtime: {e}")))?;
@@ -382,6 +439,11 @@ pub(super) async fn create_session(
         system_prompt,
         prompt_segments,
         max_steps,
+        provider_with_overrides(
+            &state,
+            body.thinking_budget,
+            body.reasoning_effort.as_deref(),
+        )?,
     )
     .build()
     .map_err(|e| ApiError::internal(format!("failed to build session runtime: {e}")))?;
@@ -742,6 +804,7 @@ pub(super) async fn fork_session(
         system_prompt,
         prompt_segments,
         state.config.max_steps,
+        state.provider.clone(),
     )
     .build()
     .map_err(|_| ApiError::internal("failed to build forked session runtime"))?;
@@ -1858,6 +1921,7 @@ pub(super) async fn agui_run(
         system_prompt,
         prompt_segments,
         state.config.max_steps,
+        state.provider.clone(),
     );
 
     // Seed the transcript if we're resuming.
@@ -2553,6 +2617,7 @@ mod tests {
             "sys".to_string(),
             crate::system_prompt::PromptSegments::default(),
             16,
+            state.provider.clone(),
         );
         let compactor = builder.compactor_for_test().expect("compactor installed");
         assert_eq!(compactor.threshold_chars, 7777);
@@ -2897,6 +2962,28 @@ mod tests {
         (state, runtime_arc)
     }
 
+    /// Goal-350: no knobs keep the shared provider (fast path); knobs rebuild a
+    /// provider from `state.config` so per-request thinking intensity can reach
+    /// the wire. The rebuilt provider must be a *different* instance — mutating
+    /// the shared `Arc<dyn ChatProvider>` is not an option.
+    #[tokio::test]
+    async fn provider_overrides_are_opt_in_and_rebuild() {
+        let (state, _runtime) = test_app_state_with_session("knob-session").await;
+
+        let shared = provider_with_overrides(&state, None, None).expect("no-knob path");
+        assert!(
+            Arc::ptr_eq(&shared, &state.provider),
+            "no knobs must return the shared provider"
+        );
+
+        let overridden =
+            provider_with_overrides(&state, Some(8000), Some("high")).expect("override path");
+        assert!(
+            !Arc::ptr_eq(&overridden, &state.provider),
+            "knobs must build a fresh provider"
+        );
+    }
+
     #[tokio::test]
     async fn get_session_status_idle_vs_plan_pending() {
         let sid = "test-plan-status";
diff --git a/src/http/mod.rs b/src/http/mod.rs
index 891c2e4d..209bf840 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -191,6 +191,9 @@ pub struct CreateSessionRequest {
     /// Extended-thinking token budget for models that support it (e.g.
     /// Anthropic claude-3-7). `0` disables thinking.
     pub thinking_budget: Option<u32>,
+    /// Goal-350: OpenAI `reasoning_effort` for reasoning models
+    /// (`"low"` / `"medium"` / `"high"`). `None` leaves the request untouched.
+    pub reasoning_effort: Option<String>,
     /// Permission mode: `"default"`, `"auto"`, `"strict"`, or `"bypass"`.
     pub permission_mode: Option<String>,
     /// Maximum total API spend in USD for this session. Agent stops after any
@@ -457,6 +460,9 @@ pub struct RunRequest {
     /// Extended-thinking token budget for models that support it (e.g.
     /// Anthropic claude-3-7). `0` disables thinking.
     pub thinking_budget: Option<u32>,
+    /// Goal-350: OpenAI `reasoning_effort` for reasoning models
+    /// (`"low"` / `"medium"` / `"high"`). `None` leaves the request untouched.
+    pub reasoning_effort: Option<String>,
     /// Permission mode: `"default"`, `"auto"`, `"strict"`, or `"bypass"`.
     pub permission_mode: Option<String>,
     /// Maximum total API spend in USD for this run.
@@ -1037,6 +1043,7 @@ pub fn build_openapi_spec() -> serde_json::Value {
                         "system_prompt": { "type": "string", "nullable": true },
                         "append_system_prompt": { "type": "string", "nullable": true },
                         "thinking_budget": { "type": "integer", "nullable": true },
+                        "reasoning_effort": { "type": "string", "nullable": true },
                         "permission_mode": { "type": "string", "enum": ["default", "auto", "strict", "bypass"], "nullable": true },
                         "max_budget_usd": { "type": "number", "nullable": true }
                     },
@@ -1076,6 +1083,7 @@ pub fn build_openapi_spec() -> serde_json::Value {
                         "session_name": { "type": "string", "nullable": true },
                         "max_steps": { "type": "integer", "nullable": true },
                         "thinking_budget": { "type": "integer", "nullable": true },
+                        "reasoning_effort": { "type": "string", "nullable": true },
                         "permission_mode": { "type": "string", "enum": ["default", "auto", "strict", "bypass"], "nullable": true },
                         "max_budget_usd": { "type": "number", "nullable": true }
                     }
@@ -1516,6 +1524,7 @@ mod goal_396_persistence_tests {
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
diff --git a/src/llm/anthropic.rs b/src/llm/anthropic.rs
index 30c8ec29..5936bd39 100644
--- a/src/llm/anthropic.rs
+++ b/src/llm/anthropic.rs
@@ -31,6 +31,14 @@ pub struct AnthropicProvider {
     client: Client,
     temperature: f64,
     max_tokens: u32,
+    /// Goal-350: Anthropic extended-thinking budget. `Some(0)` disables
+    /// thinking; `Some(n)` enables it with a token budget; `None` leaves the
+    /// request untouched (model/gateway default).
+    thinking_budget: Option<u32>,
+    /// Goal-350: OpenAI `reasoning_effort`. Stored for API symmetry with
+    /// `OpenAiProvider`; Anthropic has no effort level, so it is intentionally
+    /// never emitted here (no effort→budget conversion).
+    reasoning_effort: Option<String>,
     retry: RetryPolicy,
     /// Cap on `ToolSearchTool` round-trips. Set via `with_max_search_rounds()`
     /// for API parity with `OpenAiProvider`. Not yet consumed because Anthropic
@@ -68,6 +76,8 @@ impl AnthropicProvider {
             // Claude model's limit and is overridden via `with_max_tokens`
             // from `Config::from_env` (RECURSIVE_MAX_TOKENS) anyway.
             max_tokens: crate::llm::DEFAULT_MAX_TOKENS,
+            thinking_budget: None,
+            reasoning_effort: None,
             retry: RetryPolicy::default(),
             max_search_rounds: 3,
         })
@@ -91,6 +101,22 @@ impl AnthropicProvider {
         self
     }
 
+    /// Goal-350: set the Anthropic extended-thinking budget. `Some(0)`
+    /// disables thinking, `Some(n)` enables it with an `n`-token budget,
+    /// `None` leaves the request untouched.
+    pub fn with_thinking_budget(mut self, budget: Option<u32>) -> Self {
+        self.thinking_budget = budget;
+        self
+    }
+
+    /// Goal-350: accepted for signature symmetry with `OpenAiProvider`.
+    /// Anthropic has no `reasoning_effort` field, so this value is never
+    /// emitted.
+    pub fn with_reasoning_effort(mut self, effort: Option<String>) -> Self {
+        self.reasoning_effort = effort;
+        self
+    }
+
     pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
         self.retry = policy;
         self
@@ -193,6 +219,8 @@ impl ChatProvider for AnthropicProvider {
             system.as_deref(),
             &messages,
             tools,
+            self.reasoning_effort.as_deref(),
+            self.thinking_budget,
         );
         let url = format!("{}/v1/messages", self.base_url);
         let text = self.post_with_retry(&url, &body).await?;
@@ -234,6 +262,8 @@ impl AnthropicProvider {
             system.as_deref(),
             &messages,
             tools,
+            self.reasoning_effort.as_deref(),
+            self.thinking_budget,
         );
         body["stream"] = Value::Bool(true);
 
@@ -769,6 +799,12 @@ fn filter_leading_assistant(messages: &[Message]) -> Vec<Message> {
     result
 }
 
+// Goal-350 pushes this serializer to 8 args (it already carries `system` on top
+// of the shared shape). Bundling the two thinking knobs into a struct would
+// avoid the lint, but the plain signature keeps it symmetric with
+// `openai::build_request`; the repo already allows this lint at other
+// serializers (incl. `openai.rs`).
+#[allow(clippy::too_many_arguments)]
 fn build_request(
     model: &str,
     temperature: f64,
@@ -776,6 +812,8 @@ fn build_request(
     system: Option<&str>,
     messages: &[Message],
     tools: &[ToolSpec],
+    reasoning_effort: Option<&str>,
+    thinking_budget: Option<u32>,
 ) -> Value {
     let mut req = serde_json::json!({
         "model": model,
@@ -804,6 +842,39 @@ fn build_request(
         req["tools"] = Value::Array(tools_json);
     }
 
+    // Goal-350: Anthropic extended thinking. `Some(0)` explicitly disables it;
+    // `Some(n)` enables it with a token budget. Enabled thinking FORCES
+    // temperature=1 (the API rejects anything else) and requires `max_tokens`
+    // to leave output headroom above the budget — enforce both here so callers
+    // cannot trip the constraint. Opt-in only: `None` keeps the request
+    // byte-identical to before, which is what the DeepSeek Anthropic-compatible
+    // gateway (default-thinking) relies on.
+    // No `anthropic-beta: extended-thinking-2025-01-15` header yet — the
+    // DeepSeek gateway serves `thinking` without it; add it in a follow-up for
+    // official-Anthropic users if needed.
+    // `reasoning_effort` is the OpenAI knob — deliberately ignored (no
+    // effort→budget conversion; each provider keeps its native unit).
+    let _ = reasoning_effort;
+    if let Some(budget) = thinking_budget {
+        if budget == 0 {
+            req["thinking"] = serde_json::json!({ "type": "disabled" });
+        } else {
+            req["thinking"] = serde_json::json!({
+                "type": "enabled",
+                "budget_tokens": budget,
+            });
+            req["temperature"] = Value::from(1.0);
+            let min_max = budget.saturating_add(4096);
+            if req["max_tokens"]
+                .as_u64()
+                .map(|m| m < min_max as u64)
+                .unwrap_or(false)
+            {
+                req["max_tokens"] = Value::from(min_max);
+            }
+        }
+    }
+
     req
 }
 
@@ -1274,6 +1345,8 @@ mod tests {
             Some("You are helpful."),
             &[Message::user("Hi".to_string())],
             &[],
+            None,
+            None,
         );
         assert_eq!(req["system"], "You are helpful.");
         assert_eq!(req["model"], "claude-3");
@@ -1288,6 +1361,8 @@ mod tests {
             None,
             &[Message::user("Hi".to_string())],
             &[],
+            None,
+            None,
         );
         assert!(req.get("system").is_none());
     }
@@ -1306,10 +1381,145 @@ mod tests {
             None,
             &[Message::user("Hi".to_string())],
             &tools,
+            None,
+            None,
         );
         assert!(req.get("tools").is_some());
     }
 
+    /// Goal-350: Anthropic's native thinking knob reaches the wire.
+    #[test]
+    fn build_request_emits_thinking_enabled_with_budget() {
+        let req = build_request(
+            "claude-3-7-sonnet",
+            0.2,
+            32_000,
+            None,
+            &[Message::user("hi")],
+            &[],
+            None,
+            Some(8000),
+        );
+        assert_eq!(req["thinking"]["type"], "enabled");
+        assert_eq!(req["thinking"]["budget_tokens"], 8000);
+        assert_eq!(
+            req["temperature"], 1.0,
+            "enabled thinking forces temperature=1"
+        );
+    }
+
+    /// Goal-350: enabled thinking needs `max_tokens` headroom above the budget,
+    /// but must not shrink an already-generous cap.
+    #[test]
+    fn build_request_thinking_enabled_guarantees_max_tokens_headroom() {
+        let small = build_request(
+            "claude-3",
+            0.2,
+            4096,
+            None,
+            &[Message::user("hi")],
+            &[],
+            None,
+            Some(8000),
+        );
+        assert!(
+            small["max_tokens"].as_u64().unwrap_or(0) >= 8000 + 4096,
+            "max_tokens must leave output headroom above the budget, got {}",
+            small["max_tokens"]
+        );
+        let big = build_request(
+            "claude-3",
+            0.2,
+            64_000,
+            None,
+            &[Message::user("hi")],
+            &[],
+            None,
+            Some(8000),
+        );
+        assert_eq!(big["max_tokens"], 64_000, "a large cap stays untouched");
+    }
+
+    /// Goal-350: `Some(0)` disables thinking explicitly, without touching
+    /// temperature or max_tokens.
+    #[test]
+    fn build_request_emits_thinking_disabled_for_zero() {
+        let req = build_request(
+            "claude-3",
+            0.2,
+            4096,
+            None,
+            &[Message::user("hi")],
+            &[],
+            None,
+            Some(0),
+        );
+        assert_eq!(req["thinking"]["type"], "disabled");
+        assert_eq!(
+            req["temperature"], 0.2,
+            "disabling must not force temperature"
+        );
+        assert_eq!(req["max_tokens"], 4096);
+    }
+
+    /// Goal-350: default behaviour is byte-identical — opt-in only.
+    #[test]
+    fn build_request_omits_thinking_by_default() {
+        let req = build_request(
+            "claude-3",
+            0.2,
+            4096,
+            None,
+            &[Message::user("hi")],
+            &[],
+            None,
+            None,
+        );
+        assert!(req.get("thinking").is_none());
+        assert_eq!(req["temperature"], 0.2);
+    }
+
+    /// Goal-350: the OpenAI effort knob is a deliberate no-op on Anthropic.
+    #[test]
+    fn build_request_ignores_reasoning_effort_on_anthropic() {
+        let req = build_request(
+            "claude-3",
+            0.2,
+            4096,
+            None,
+            &[Message::user("hi")],
+            &[],
+            Some("high"),
+            None,
+        );
+        assert!(req.get("reasoning_effort").is_none());
+        assert!(req.get("thinking").is_none(), "no effort→budget conversion");
+    }
+
+    /// Goal-350: the builder setter stores the knob and it is what the
+    /// production call path hands to `build_request`.
+    #[test]
+    fn with_thinking_budget_builder_plumbs_into_request() {
+        let provider = AnthropicProvider::new("http://127.0.0.1:1", "sk-noop", "claude-3")
+            .unwrap()
+            .with_thinking_budget(Some(6000))
+            .with_reasoning_effort(Some("high".to_string()));
+        assert_eq!(provider.thinking_budget, Some(6000));
+        assert_eq!(provider.reasoning_effort.as_deref(), Some("high"));
+        let req = build_request(
+            &provider.model,
+            provider.temperature,
+            provider.max_tokens,
+            None,
+            &[Message::user("hi")],
+            &[],
+            provider.reasoning_effort.as_deref(),
+            provider.thinking_budget,
+        );
+        assert_eq!(req["thinking"]["budget_tokens"], 6000);
+        assert!(req.get("reasoning_effort").is_none());
+    }
+
     #[tokio::test]
     async fn test_a_mock_server_returns_canned_response() {
         // Spawn a mock server that returns a valid Anthropic response
@@ -1977,7 +2187,16 @@ data: {\"type\":\"message_stop\"}
                 parameters: json!({"type": "object"}),
             },
         ];
-        let body = build_request("claude-3", 0.2, 4096, None, &[Message::user("hi")], &specs);
+        let body = build_request(
+            "claude-3",
+            0.2,
+            4096,
+            None,
+            &[Message::user("hi")],
+            &specs,
+            None,
+            None,
+        );
         let tools = body["tools"].as_array().expect("tools should be array");
         assert_eq!(tools.len(), 2);
         let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
@@ -2072,7 +2291,16 @@ data: {\"type\":\"message_stop\"}
                 "anyOf": [{"required": ["text"]}]
             }),
         }];
-        let body = build_request("claude-3", 0.2, 4096, None, &[Message::user("hi")], &specs);
+        let body = build_request(
+            "claude-3",
+            0.2,
+            4096,
+            None,
+            &[Message::user("hi")],
+            &specs,
+            None,
+            None,
+        );
         let tool = &body["tools"][0];
         assert!(tool["input_schema"].get("anyOf").is_none());
         assert!(tool["input_schema"]["properties"]["text"].is_object());
diff --git a/src/llm/openai.rs b/src/llm/openai.rs
index 53e73e73..8e466fe0 100644
--- a/src/llm/openai.rs
+++ b/src/llm/openai.rs
@@ -47,6 +47,14 @@ pub struct OpenAiProvider {
     client: Client,
     temperature: f64,
     max_tokens: u32,
+    /// Goal-350: OpenAI `reasoning_effort` (`"low"`/`"medium"`/`"high"`).
+    /// `None` = never send the field (some compatible gateways reject unknown
+    /// fields, so this stays strictly opt-in).
+    reasoning_effort: Option<String>,
+    /// Goal-350: Anthropic extended-thinking budget. Stored for API symmetry
+    /// with `AnthropicProvider`; OpenAI has no token-budget knob, so it is
+    /// intentionally never emitted here (no budget→effort conversion).
+    thinking_budget: Option<u32>,
     retry: RetryPolicy,
     stream_tx: Option<StreamSender>,
     /// Algorithm used to resolve a `ToolSearchTool` query into a list of
@@ -85,6 +93,8 @@ impl OpenAiProvider {
             // (flash == pro), and 16384 truncated reasoning-heavy turns
             // mid-generation (`provider_stop:length` with empty content).
             max_tokens: crate::llm::DEFAULT_MAX_TOKENS,
+            reasoning_effort: None,
+            thinking_budget: None,
             retry: RetryPolicy::default(),
             stream_tx: None,
             search_engine: Arc::new(KeywordSearchEngine::new()),
@@ -129,6 +139,20 @@ impl OpenAiProvider {
         self
     }
 
+    /// Goal-350: set the OpenAI `reasoning_effort` knob (`"low"`/`"medium"`/
+    /// `"high"`). `None` leaves the request untouched.
+    pub fn with_reasoning_effort(mut self, effort: Option<String>) -> Self {
+        self.reasoning_effort = effort;
+        self
+    }
+
+    /// Goal-350: accepted for signature symmetry with `AnthropicProvider`.
+    /// OpenAI has no thinking-token budget, so this value is never emitted.
+    pub fn with_thinking_budget(mut self, budget: Option<u32>) -> Self {
+        self.thinking_budget = budget;
+        self
+    }
+
     pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
         self.retry = policy;
         self
@@ -227,6 +251,8 @@ impl ChatProvider for OpenAiProvider {
             self.max_tokens,
             messages,
             tools,
+            self.reasoning_effort.as_deref(),
+            self.thinking_budget,
         );
         let url = format!("{}/chat/completions", self.base_url);
         let text = self.post_json_with_retry(&url, &body, "complete").await?;
@@ -247,6 +273,8 @@ impl ChatProvider for OpenAiProvider {
             self.max_tokens,
             &req.messages,
             &[],
+            self.reasoning_effort.as_deref(),
+            self.thinking_budget,
         );
         body["response_format"] = serde_json::json!({
             "type": "json_schema",
@@ -546,6 +574,8 @@ impl OpenAiProvider {
             self.max_tokens,
             messages,
             tools,
+            self.reasoning_effort.as_deref(),
+            self.thinking_budget,
         );
         body["stream"] = Value::Bool(true);
         // Ask providers that gate usage emission behind a flag (MiniMax,
@@ -988,6 +1018,8 @@ fn build_request(
     max_tokens: u32,
     messages: &[Message],
     tools: &[ToolSpec],
+    reasoning_effort: Option<&str>,
+    thinking_budget: Option<u32>,
 ) -> Value {
     let mut req = serde_json::json!({
         "model": model,
@@ -1023,6 +1055,18 @@ fn build_request(
     if model_wants_reasoning_split(model) {
         req["reasoning_split"] = Value::Bool(true);
     }
+    // Goal-350: OpenAI reasoning models (o1/o3/GPT-5 family) honour
+    // `reasoning_effort`. Opt-in only — some OpenAI-compatible gateways reject
+    // unknown fields, so the field is emitted only when the caller explicitly
+    // asked for it (`--effort` / the HTTP API); the default request body stays
+    // byte-identical to before.
+    // `thinking_budget` is the Anthropic knob: OpenAI has no token-budget
+    // equivalent, so it is deliberately ignored here (no budget→effort
+    // conversion — the two ecosystems keep their native units).
+    let _ = thinking_budget;
+    if let Some(effort) = reasoning_effort {
+        req["reasoning_effort"] = Value::String(effort.to_string());
+    }
     req
 }
 
@@ -1722,7 +1766,7 @@ mod tests {
 
     #[test]
     fn builds_request_without_tools_omits_field() {
-        let req = build_request("m", 0.2, 16384, &[Message::user("hi")], &[]);
+        let req = build_request("m", 0.2, 16384, &[Message::user("hi")], &[], None, None);
         assert!(req.get("tools").is_none());
         assert_eq!(req["messages"][0]["role"], "user");
         assert_eq!(req["max_tokens"], 16384);
@@ -1734,10 +1778,79 @@ mod tests {
 
     #[test]
     fn builds_request_includes_max_tokens() {
-        let req = build_request("m", 0.2, 1024, &[Message::user("hi")], &[]);
+        let req = build_request("m", 0.2, 1024, &[Message::user("hi")], &[], None, None);
         assert_eq!(req["max_tokens"], 1024);
     }
 
+    /// Goal-350: the OpenAI-native thinking knob reaches the wire.
+    #[test]
+    fn build_request_includes_reasoning_effort_when_set() {
+        let req = build_request(
+            "o3",
+            0.2,
+            1024,
+            &[Message::user("hi")],
+            &[],
+            Some("high"),
+            None,
+        );
+        assert_eq!(req["reasoning_effort"], "high");
+    }
+
+    /// Goal-350: default behaviour is byte-identical — the field is opt-in.
+    #[test]
+    fn build_request_omits_reasoning_effort_by_default() {
+        let req = build_request("o3", 0.2, 1024, &[Message::user("hi")], &[], None, None);
+        assert!(
+            req.get("reasoning_effort").is_none(),
+            "reasoning_effort must be absent unless explicitly requested"
+        );
+    }
+
+    /// Goal-350: the Anthropic token budget is a deliberate no-op on OpenAI —
+    /// no `thinking` block and no budget→effort conversion.
+    #[test]
+    fn build_request_ignores_thinking_budget_on_openai() {
+        let req = build_request(
+            "o3",
+            0.2,
+            1024,
+            &[Message::user("hi")],
+            &[],
+            None,
+            Some(12345),
+        );
+        assert!(
+            req.get("thinking").is_none(),
+            "OpenAI has no thinking block"
+        );
+        assert!(req.get("budget_tokens").is_none());
+        assert!(req.get("reasoning_effort").is_none(), "no budget→effort");
+    }
+
+    /// Goal-350: the builder setter stores the knob and it is what
+    /// `build_request` receives from the production call path.
+    #[test]
+    fn with_reasoning_effort_builder_plumbs_into_request() {
+        let provider = OpenAiProvider::new("http://127.0.0.1:1", "sk-noop", "o3")
+            .unwrap()
+            .with_reasoning_effort(Some("low".to_string()))
+            .with_thinking_budget(Some(9000));
+        assert_eq!(provider.reasoning_effort.as_deref(), Some("low"));
+        assert_eq!(provider.thinking_budget, Some(9000));
+        let req = build_request(
+            &provider.model,
+            provider.temperature,
+            provider.max_tokens,
+            &[Message::user("hi")],
+            &[],
+            provider.reasoning_effort.as_deref(),
+            provider.thinking_budget,
+        );
+        assert_eq!(req["reasoning_effort"], "low");
+        assert!(req.get("thinking").is_none());
+    }
+
     #[test]
     fn builds_request_minimax_sets_reasoning_split() {
         for model in [
@@ -1746,7 +1859,7 @@ mod tests {
             "minimax-m3",
             "ab-MiniMax-test",
         ] {
-            let req = build_request(model, 0.2, 1024, &[Message::user("hi")], &[]);
+            let req = build_request(model, 0.2, 1024, &[Message::user("hi")], &[], None, None);
             assert_eq!(
                 req.get("reasoning_split"),
                 Some(&Value::Bool(true)),
diff --git a/src/multi.rs b/src/multi.rs
index 9d41e873..a97a0938 100644
--- a/src/multi.rs
+++ b/src/multi.rs
@@ -709,6 +709,7 @@ mod tests {
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
diff --git a/tests/agent_team_integration.rs b/tests/agent_team_integration.rs
index 81458c93..d4fbe7fa 100644
--- a/tests/agent_team_integration.rs
+++ b/tests/agent_team_integration.rs
@@ -61,6 +61,7 @@ fn test_config() -> Config {
         headless: false,
         memory_summary_limit: 5,
         thinking_budget: None,
+        reasoning_effort: None,
         session_name: None,
         max_budget_usd: None,
         extra_dirs: Vec::new(),
diff --git a/tests/agui_e2e.rs b/tests/agui_e2e.rs
index 2f47b59e..c9bee224 100644
--- a/tests/agui_e2e.rs
+++ b/tests/agui_e2e.rs
@@ -88,6 +88,7 @@ fn mock_config(workspace: PathBuf) -> Config {
         headless: false,
         memory_summary_limit: 5,
         thinking_budget: None,
+        reasoning_effort: None,
         session_name: None,
         max_budget_usd: None,
         extra_dirs: Vec::new(),
diff --git a/tests/http_common/mod.rs b/tests/http_common/mod.rs
index 8b934364..2c022441 100644
--- a/tests/http_common/mod.rs
+++ b/tests/http_common/mod.rs
@@ -161,6 +161,7 @@ pub fn mock_config() -> Config {
         headless: false,
         memory_summary_limit: 5,
         thinking_budget: None,
+        reasoning_effort: None,
         session_name: None,
         max_budget_usd: None,
         extra_dirs: Vec::new(),
diff --git a/tests/issue407-cancel-surfaces.rs b/tests/issue407-cancel-surfaces.rs
index 85958382..36fe163e 100644
--- a/tests/issue407-cancel-surfaces.rs
+++ b/tests/issue407-cancel-surfaces.rs
@@ -140,6 +140,7 @@ fn test_config(workspace: PathBuf) -> Config {
         headless: false,
         memory_summary_limit: 5,
         thinking_budget: None,
+        reasoning_effort: None,
         session_name: None,
         max_budget_usd: None,
         extra_dirs: Vec::new(),
diff --git a/tests/v050_integration.rs b/tests/v050_integration.rs
index ae4af855..bed3c499 100644
--- a/tests/v050_integration.rs
+++ b/tests/v050_integration.rs
@@ -53,6 +53,7 @@ mod v050_integration {
             headless: false,
             memory_summary_limit: 5,
             thinking_budget: None,
+            reasoning_effort: None,
             session_name: None,
             max_budget_usd: None,
             extra_dirs: Vec::new(),
主要言語
Rust
スター
4
フォーク
0
平均マージ
5時間 32分
マージ済み PR(30日)
7

環境構築

  • Dockerfile または Docker Compose ファイルあり
  • プルリクエストのテンプレートなし
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

jeffkit/recursive のほかの issue

jeffkit/recursive の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。