diff --git a/src/providers/anthropic.rs b/src/providers/anthropic.rs index 03f30fc06..7c6d646bb 100644 --- a/src/providers/anthropic.rs +++ b/src/providers/anthropic.rs @@ -200,12 +200,41 @@ impl AnthropicProvider { if Self::is_setup_token(credential) { request .header("Authorization", format!("Bearer {credential}")) - .header("anthropic-beta", "oauth-2025-04-20") + .header( + "anthropic-beta", + "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14", + ) + .header("anthropic-dangerous-direct-browser-access", "true") } else { request.header("x-api-key", credential) } } + /// For OAuth tokens, Anthropic requires the system prompt to start with the + /// Claude Code identity prefix. This prepends it to any existing system prompt. + fn apply_oauth_system_prompt(system: Option) -> Option { + let prefix = SystemBlock { + block_type: "text".to_string(), + text: "You are Claude Code, Anthropic's official CLI for Claude.".to_string(), + cache_control: Some(CacheControl::ephemeral()), + }; + match system { + Some(SystemPrompt::Blocks(mut blocks)) => { + blocks.insert(0, prefix); + Some(SystemPrompt::Blocks(blocks)) + } + Some(SystemPrompt::String(s)) => Some(SystemPrompt::Blocks(vec![ + prefix, + SystemBlock { + block_type: "text".to_string(), + text: s, + cache_control: Some(CacheControl::ephemeral()), + }, + ])), + None => Some(SystemPrompt::Blocks(vec![prefix])), + } + } + /// Cache system prompts larger than ~1024 tokens (3KB of text) fn should_cache_system(text: &str) -> bool { text.len() > 3072 @@ -537,15 +566,26 @@ impl Provider for AnthropicProvider { ) })?; - let request = ChatRequest { + let system = system_prompt.map(|s| SystemPrompt::String(s.to_string())); + let system = if Self::is_setup_token(credential) { + Self::apply_oauth_system_prompt(system) + } else { + system + }; + + let request = NativeChatRequest { model: model.to_string(), max_tokens: 4096, - system: system_prompt.map(ToString::to_string), - messages: vec![Message { + system, + messages: vec![NativeMessage { role: "user".to_string(), - content: message.to_string(), + content: vec![NativeContentOut::Text { + text: message.to_string(), + cache_control: None, + }], }], temperature, + tools: None, }; let mut request = self @@ -563,8 +603,11 @@ impl Provider for AnthropicProvider { return Err(super::api_error("Anthropic", response).await); } - let chat_response: ChatResponse = response.json().await?; - Self::parse_text_response(chat_response) + let chat_response: NativeChatResponse = response.json().await?; + let parsed = Self::parse_native_response(chat_response); + parsed + .text + .ok_or_else(|| anyhow::anyhow!("No response from Anthropic")) } async fn chat( @@ -586,6 +629,13 @@ impl Provider for AnthropicProvider { Self::apply_cache_to_last_message(&mut messages); } + // For OAuth tokens, prepend Claude Code identity to system prompt + let system_prompt = if Self::is_setup_token(credential) { + Self::apply_oauth_system_prompt(system_prompt) + } else { + system_prompt + }; + let native_request = NativeChatRequest { model: model.to_string(), max_tokens: 4096, @@ -785,7 +835,14 @@ mod tests { .headers() .get("anthropic-beta") .and_then(|v| v.to_str().ok()), - Some("oauth-2025-04-20") + Some("claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14") + ); + assert_eq!( + request + .headers() + .get("anthropic-dangerous-direct-browser-access") + .and_then(|v| v.to_str().ok()), + Some("true") ); assert!(request.headers().get("x-api-key").is_none()); }