From 8bfee42d951373b97f48ccf198ffe9bb91cefc8a Mon Sep 17 00:00:00 2001 From: Tomas Ward <35110844+TomasWard1@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:24:10 -0300 Subject: [PATCH] fix(provider): complete Anthropic OAuth setup-token authentication (#4053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setup tokens (sk-ant-oat01-*) from Claude Pro/Max subscriptions require specific headers and a system prompt to authenticate successfully. Without these, the API returns 400 Bad Request. Changes to apply_auth(): - Add claude-code-20250219 and interleaved-thinking-2025-05-14 beta headers alongside existing oauth-2025-04-20 - Add anthropic-dangerous-direct-browser-access: true header New apply_oauth_system_prompt() method: - Prepends required "You are Claude Code" identity to system prompt - Handles String, Blocks, and None system prompt variants Changes to chat_with_system() and chat(): - Inject OAuth system prompt when using setup tokens - Use NativeChatRequest/NativeChatResponse for proper SystemPrompt enum support in chat_with_system Test updates: - Updated apply_auth test to verify new beta headers and browser-access header Tested with real OAuth token via `zeroclaw agent -m` — confirmed working end-to-end. Co-authored-by: Claude Opus 4.6 --- src/providers/anthropic.rs | 73 +++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 8 deletions(-) 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()); }