Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions src/crates/ai-adapters/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,119 @@ mod tests {
assert_eq!(request_body["output_config"]["effort"], "medium");
}

#[test]
fn build_anthropic_request_body_keeps_manual_thinking_for_pre_adaptive_models() {
let client = AIClient::new(AIConfig {
name: "anthropic".to_string(),
base_url: "https://api.anthropic.com".to_string(),
request_url: "https://api.anthropic.com/v1/messages".to_string(),
api_key: "test-key".to_string(),
model: "claude-sonnet-4-5".to_string(),
format: "anthropic".to_string(),
context_window: 200000,
max_tokens: Some(8192),
temperature: None,
top_p: None,
reasoning_mode: ReasoningMode::Enabled,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
reasoning_effort: Some("high".to_string()),
thinking_budget_tokens: None,
custom_request_body: None,
custom_request_body_mode: None,
});

let request_body = anthropic::request::build_request_body(
&client,
&client.config.request_url,
None,
vec![json!({ "role": "user", "content": [{ "type": "text", "text": "hello" }] })],
None,
None,
);

assert_eq!(request_body["thinking"]["type"], "enabled");
assert_eq!(request_body["thinking"]["budget_tokens"], 6144);
assert!(request_body.get("output_config").is_none());
}

#[test]
fn build_anthropic_request_body_uses_adaptive_for_opus_4_7_and_newer() {
let client = AIClient::new(AIConfig {
name: "anthropic".to_string(),
base_url: "https://api.anthropic.com".to_string(),
request_url: "https://api.anthropic.com/v1/messages".to_string(),
api_key: "test-key".to_string(),
model: "claude-opus-4-8".to_string(),
format: "anthropic".to_string(),
context_window: 200000,
max_tokens: Some(8192),
temperature: None,
top_p: None,
reasoning_mode: ReasoningMode::Enabled,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
reasoning_effort: Some("high".to_string()),
thinking_budget_tokens: Some(2048),
custom_request_body: None,
custom_request_body_mode: None,
});

let request_body = anthropic::request::build_request_body(
&client,
&client.config.request_url,
None,
vec![json!({ "role": "user", "content": [{ "type": "text", "text": "hello" }] })],
None,
None,
);

assert_eq!(request_body["thinking"]["type"], "adaptive");
assert!(request_body["thinking"].get("budget_tokens").is_none());
assert_eq!(request_body["output_config"]["effort"], "high");
}

#[test]
fn build_anthropic_request_body_omits_disabled_for_mythos() {
let client = AIClient::new(AIConfig {
name: "anthropic".to_string(),
base_url: "https://api.anthropic.com".to_string(),
request_url: "https://api.anthropic.com/v1/messages".to_string(),
api_key: "test-key".to_string(),
model: "claude-mythos-preview".to_string(),
format: "anthropic".to_string(),
context_window: 200000,
max_tokens: Some(8192),
temperature: None,
top_p: None,
reasoning_mode: ReasoningMode::Disabled,
inline_think_in_text: false,
custom_headers: None,
custom_headers_mode: None,
skip_ssl_verify: false,
reasoning_effort: None,
thinking_budget_tokens: None,
custom_request_body: None,
custom_request_body_mode: None,
});

let request_body = anthropic::request::build_request_body(
&client,
&client.config.request_url,
None,
vec![json!({ "role": "user", "content": [{ "type": "text", "text": "hello" }] })],
None,
None,
);

assert!(request_body.get("thinking").is_none());
assert!(request_body.get("output_config").is_none());
}

#[test]
fn build_anthropic_request_body_adds_deepseek_reasoning_effort() {
let client = AIClient::new(AIConfig {
Expand Down
72 changes: 61 additions & 11 deletions src/crates/ai-adapters/src/providers/anthropic/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ use anyhow::Result;
use log::{debug, warn};
use reqwest::RequestBuilder;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AnthropicThinkingCapability {
ManualOnly,
AdaptivePreferred,
AdaptiveOnly,
AdaptiveDefaultNoDisabled,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ClaudeModelVersion {
major: u32,
minor: u32,
}

pub(crate) fn apply_headers(
client: &AIClient,
builder: RequestBuilder,
Expand All @@ -37,13 +51,40 @@ pub(crate) fn apply_headers(
})
}

fn anthropic_supports_adaptive_reasoning(model_name: &str) -> bool {
matches!(
model_name,
name if name.starts_with("claude-opus-4-6")
|| name.starts_with("claude-sonnet-4-6")
|| name.starts_with("claude-mythos")
)
fn parse_claude_model_version(model_name: &str, family: &str) -> Option<ClaudeModelVersion> {
let prefix = format!("claude-{family}-");
let rest = model_name.strip_prefix(&prefix)?;
let mut parts = rest.split('-');
let major = parts.next()?.parse().ok()?;
let minor = parts.next().and_then(|part| part.parse().ok()).unwrap_or(0);
Some(ClaudeModelVersion { major, minor })
}

fn anthropic_thinking_capability(model_name: &str) -> AnthropicThinkingCapability {
if model_name.starts_with("claude-mythos") {
return AnthropicThinkingCapability::AdaptiveDefaultNoDisabled;
}

if let Some(version) = parse_claude_model_version(model_name, "opus") {
if version.major == 4 && version.minor >= 7 {
return AnthropicThinkingCapability::AdaptiveOnly;
}
if version.major > 4 || (version.major == 4 && version.minor >= 6) {
return AnthropicThinkingCapability::AdaptivePreferred;
}
}

if let Some(version) = parse_claude_model_version(model_name, "sonnet") {
if version.major > 4 || (version.major == 4 && version.minor >= 6) {
return AnthropicThinkingCapability::AdaptivePreferred;
}
}

AnthropicThinkingCapability::ManualOnly
}

fn anthropic_supports_adaptive_reasoning(capability: AnthropicThinkingCapability) -> bool {
!matches!(capability, AnthropicThinkingCapability::ManualOnly)
}

fn default_anthropic_budget_tokens(max_tokens: Option<u32>) -> Option<u32> {
Expand Down Expand Up @@ -77,6 +118,7 @@ fn apply_reasoning_fields(
) {
let is_deepseek_reasoning_target =
is_deepseek_url(url) || is_deepseek_reasoning_effort_model(model_name);
let capability = anthropic_thinking_capability(model_name);

match mode {
ReasoningMode::Default => {
Expand All @@ -90,10 +132,18 @@ fn apply_reasoning_fields(
}
}
ReasoningMode::Disabled => {
request_body["thinking"] = serde_json::json!({ "type": "disabled" });
if capability == AnthropicThinkingCapability::AdaptiveDefaultNoDisabled {
warn!(
target: "ai::anthropic_stream_request",
"Model {} does not support thinking.type=disabled; omitting the field and relying on provider defaults",
model_name
);
} else {
request_body["thinking"] = serde_json::json!({ "type": "disabled" });
}
}
ReasoningMode::Enabled => {
if anthropic_supports_adaptive_reasoning(model_name) {
if anthropic_supports_adaptive_reasoning(capability) {
apply_anthropic_adaptive_reasoning(request_body, reasoning_effort);
return;
}
Expand All @@ -115,7 +165,7 @@ fn apply_reasoning_fields(
}
}
ReasoningMode::Adaptive => {
if anthropic_supports_adaptive_reasoning(model_name) {
if anthropic_supports_adaptive_reasoning(capability) {
apply_anthropic_adaptive_reasoning(request_body, reasoning_effort);
} else {
warn!(
Expand All @@ -137,7 +187,7 @@ fn apply_reasoning_fields(
}

if mode != ReasoningMode::Adaptive
&& !anthropic_supports_adaptive_reasoning(model_name)
&& !anthropic_supports_adaptive_reasoning(capability)
&& reasoning_effort.is_some_and(|value| !value.trim().is_empty())
{
warn!(
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/en-US/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@
"sendingToBtw": "Side session: {{title}}",
"modeDescriptions": {
"agentic": "Full-featured AI assistant with access to all tools for comprehensive software development tasks",
"Multitask": "Multitask mode: decompose work into orthogonal branches or a DAG and proactively use subagents in parallel when it helps",
"Multitask": "Multitask mode: decompose work into orthogonal branches and proactively use subagents in parallel when it helps",
"Claw": "Personal assistant mode for dedicated assistant workspaces and everyday task support",
"Plan": "Plan first, execute later — clarify requirements and create an implementation plan before coding",
"debug": "Evidence-driven systematic debugging: form hypotheses, gather runtime evidence, and fix with confidence",
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/en-US/scenes/agents.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@
"GenerateDoc": "Documentation agent: automatically generate code documentation and explanations",
"Init": "Init agent: help set up project structure and initial configuration",
"ReviewFixer": "Review fixer agent: automatically fix code issues based on review results",
"Multitask": "Multitask mode: decompose work into orthogonal branches or a DAG and use subagents in parallel when beneficial",
"Multitask": "Multitask mode: decompose work into orthogonal branches and use subagents in parallel when beneficial",
"Plan": "Plan mode: create detailed task plans and execution steps",
"Debug": "Debug mode: systematically diagnose and fix errors in code",
"Claw": "Claw mode: extract and integrate information from external sources",
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/zh-CN/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@
"sendingToBtw": "侧问会话:{{title}}",
"modeDescriptions": {
"agentic": "AI 主导执行,自动规划和完成编码任务,拥有完整的工具访问能力",
"Multitask": "多任务模式:将工作拆成正交分支或 DAG,并在合适时主动并行调度子 Agent 推进",
"Multitask": "多任务模式:将工作拆成正交分支,并在合适时主动并行调度子 Agent 推进",
"Claw": "个人助理模式:面向个人工作区和日常事务,使用独立的助理上下文",
"Plan": "先规划后执行,先明确需求并制定实施计划,再进行编码",
"debug": "证据驱动的系统化调试:提出假设、收集运行时证据、精准定位并修复问题",
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/zh-CN/scenes/agents.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@
"GenerateDoc": "文档生成智能体:自动生成代码文档和说明",
"Init": "初始化智能体:帮助设置项目结构和初始配置",
"ReviewFixer": "审查修复智能体:根据审查结果自动修复代码问题",
"Multitask": "多任务模式:将工作拆为正交分支或 DAG,并在合适时并行使用子智能体推进",
"Multitask": "多任务模式:将工作拆为正交分支,并在合适时并行使用子智能体推进",
"Plan": "规划模式:制定详细的任务计划和执行步骤",
"Debug": "调试模式:系统性地诊断和修复代码中的错误",
"Claw": "抓取模式:从外部源提取和整合信息",
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/zh-TW/flow-chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@
"sendingToBtw": "側問會話:{{title}}",
"modeDescriptions": {
"agentic": "AI 主導執行,自動規劃和完成編碼任務,擁有完整的工具訪問能力",
"Multitask": "多工模式:將工作拆成正交分支或 DAG,並在合適時主動並行調度子 Agent 推進",
"Multitask": "多工模式:將工作拆成正交分支,並在合適時主動並行調度子 Agent 推進",
"Claw": "個人助理模式:面向個人工作區和日常事務,使用獨立的助理上下文",
"Plan": "先規劃後執行,先明確需求並制定實施計劃,再進行編碼",
"debug": "證據驅動的系統化調試:提出假設、收集運行時證據、精準定位並修復問題",
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/locales/zh-TW/scenes/agents.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@
"GenerateDoc": "文件生成智慧體:自動生成程式碼文件和說明",
"Init": "初始化智慧體:幫助設定專案結構和初始配置",
"ReviewFixer": "審查修復智慧體:根據審查結果自動修復程式碼問題",
"Multitask": "多工模式:將工作拆為正交分支或 DAG,並在合適時並行使用子智慧體推進",
"Multitask": "多工模式:將工作拆為正交分支,並在合適時並行使用子智慧體推進",
"Plan": "規劃模式:制定詳細的任務計劃和執行步驟",
"Debug": "除錯模式:系統性地診斷和修復程式碼中的錯誤",
"Claw": "抓取模式:從外部來源提取和整合資訊",
Expand Down
Loading