fix(provider): complete Anthropic OAuth setup-token authentication (#4053)

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 <noreply@anthropic.com>
This commit is contained in:
Tomas Ward 2026-03-20 12:24:10 -03:00 committed by Roman Tataurov
parent 648b46b1d3
commit 8bfee42d95
No known key found for this signature in database
GPG Key ID: 70A51EF3185C334B

View File

@ -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<SystemPrompt>) -> Option<SystemPrompt> {
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());
}