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:
parent
648b46b1d3
commit
8bfee42d95
@ -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());
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user