Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

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

Ouverte
#145 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
35/100
Type d'issue
Fonctionnalité
Clarté
Clairement spécifiée
Activité
Active
Stack technique
rust
Domaine
ai, backend

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

背景:--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(),
Langage dominant
Rust
Étoiles
4
Forks
0
Merge moyen
5 h 32 min
PR mergées (30 j)
7

Préparer son environnement

  • Fournit un Dockerfile ou un fichier Docker Compose
  • Aucun modèle de pull request
  • Aucun guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de jeffkit/recursive

Toutes les issues de jeffkit/recursive

Issues similaires

Plus d'issues Rust

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.