feat(llm): 接通 thinking 强度(reasoning_effort / thinking_budget)——Goal 350 本地已验证实现 + patch
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
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(-)
实现要点
Config.reasoning_effort: Option<String>(新字段,紧邻thinking_budget):加入 Debug 脱敏输出 + 全部 13 处Config { .. }字面量(编译器强制,都是None)。- 两个 provider 各加两个字段(
reasoning_effort/thinking_budget)+with_reasoning_effort()/with_thinking_budget()builder,与既有with_max_tokens同风格。 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。
--effort一个 flag 驱动两个 knob:low → budget 0 + effort "low",high → 16000 + "high",normal → 两者都不设。- opt-in 原则:不设任何 knob 时请求体与改动前逐字节一致(有
*_by_default测试钉住),因为部分 OpenAI 兼容网关会拒绝未知字段。 - HTTP 路径真正接线:两个请求体加
reasoning_effort;handlers.rs新增provider_with_overrides(),注入到唯一的 runtime 构造点build_session_runtime();无 knob 时走state.provider.clone()快路径(不 mutate 共享的Arc<dyn ChatProvider>)。 - 未触碰
run_core.rs::run_inner、ChatProvidertrait、响应侧解析;无新依赖;非测试代码无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 处偏差(落地时请注意)
- clippy
too_many_arguments:anthropic 的build_request到 8 参(比 openai 多一个system),-D warnings会红。我加了#[allow(clippy::too_many_arguments)]+ 注释——仓库里已有 10 处同样 allow(含openai.rs自己)。备选方案是把两个 knob 收进一个ThinkingKnobs结构体,那样更干净但改动面更大,我选了与 goal 签名一致的写法。 - 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)。 - HTTP 侧无法复用那个 helper:它在
recursive-clicrate 里,src/http属于库 crate。我在handlers.rs内镜像了同样provider_typematch,并用state.config.api_key(不引入新的 key 解析路径);api_key为None时退回共享 provider。 --effort拆成两个纯函数:保留原effort_thinking_budget()(原有测试不动),新增effort_reasoning_effort(),两者合并在一个测试里断言(遵守.dev/AGENTS.md的「env/CLI 解析测试必须是一个测试」)。
建议的后续(我没有做)
- 补
anthropic-beta: extended-thinking-2025-01-15header(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 ファイルあり
- プルリクエストのテンプレートなし
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
jeffkit/recursive のほかの issue
-
fix(pipeline): preflight kill-stale 在 v2 console/worker 路径把并发兄弟 run 当孤儿杀——同仓并发 run 互杀成链(今日 4 例实证)オープン
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
jeffkit/recursive#148 · コメント 2 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 38/100
jeffkit/recursive#147 · コメント 14 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 25/100
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 20/100
jeffkit/recursive#134 · コメント 6 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 30/100
jeffkit/recursive#132 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
jeffkit/recursive の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 75/100
element-hq/lk-jwt-service#248 ·
メンテナーはふだん 1 日以内に返信
-
agent:triaged bug bughunt pm:pipenv priority:p1
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
SocketDev/socket-patch#1219 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
pact-foundation/pact-cli#154 ·
メンテナーはふだん 3 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
antithesishq/bombadil#361 ·
メンテナーはふだん 1 日以内に返信