Merge remote-tracking branch 'origin/main' into feat/support-pi

This commit is contained in:
Franklin 2026-02-06 23:22:50 -05:00
commit 8b068eb1ae
3 changed files with 188 additions and 1 deletions

View file

@ -261,7 +261,7 @@ All agents receive API keys via environment variables:
| Agent | Environment Variables |
|-------|----------------------|
| Claude | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` |
| Claude | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN` |
| Codex | `OPENAI_API_KEY`, `CODEX_API_KEY` |
| OpenCode | `OPENAI_API_KEY` |
| Amp | `ANTHROPIC_API_KEY` |

View file

@ -282,6 +282,22 @@ pub fn extract_all_credentials(options: &CredentialExtractionOptions) -> Extract
auth_type: AuthType::ApiKey,
provider: "anthropic".to_string(),
});
} else if options.include_oauth {
if let Ok(value) = std::env::var("CLAUDE_CODE_OAUTH_TOKEN") {
result.anthropic = Some(ProviderCredentials {
api_key: value,
source: "environment".to_string(),
auth_type: AuthType::Oauth,
provider: "anthropic".to_string(),
});
} else if let Ok(value) = std::env::var("ANTHROPIC_AUTH_TOKEN") {
result.anthropic = Some(ProviderCredentials {
api_key: value,
source: "environment".to_string(),
auth_type: AuthType::Oauth,
provider: "anthropic".to_string(),
});
}
}
if let Ok(value) = std::env::var("OPENAI_API_KEY") {
@ -376,3 +392,134 @@ fn is_expired_rfc3339(value: &str) -> bool {
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
static ENV_LOCK: Mutex<()> = Mutex::new(());
const ANTHROPIC_ENV_KEYS: [&str; 5] = [
"ANTHROPIC_API_KEY",
"CLAUDE_API_KEY",
"CLAUDE_CODE_OAUTH_TOKEN",
"ANTHROPIC_AUTH_TOKEN",
"OPENAI_API_KEY",
];
fn with_env(mutations: &[(&str, Option<&str>)], test_fn: impl FnOnce()) {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let mut snapshot: HashMap<String, Option<String>> = HashMap::new();
for key in ANTHROPIC_ENV_KEYS {
snapshot.insert(key.to_string(), std::env::var(key).ok());
}
for (key, value) in mutations {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
test_fn();
for (key, value) in snapshot {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
fn empty_home_dir() -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_nanos();
let path =
std::env::temp_dir().join(format!("sandbox-agent-agent-credentials-test-{nanos}"));
fs::create_dir_all(&path).expect("failed to create temp home dir");
path
}
#[test]
fn extract_all_credentials_reads_claude_code_oauth_env() {
with_env(
&[
("ANTHROPIC_API_KEY", None),
("CLAUDE_API_KEY", None),
("CLAUDE_CODE_OAUTH_TOKEN", Some("oauth-token-123")),
("ANTHROPIC_AUTH_TOKEN", None),
],
|| {
let options = CredentialExtractionOptions {
home_dir: Some(empty_home_dir()),
include_oauth: true,
};
let creds = extract_all_credentials(&options);
let anthropic = creds
.anthropic
.expect("expected anthropic credentials from oauth env");
assert_eq!(anthropic.api_key, "oauth-token-123");
assert_eq!(anthropic.source, "environment");
assert_eq!(anthropic.auth_type, AuthType::Oauth);
assert_eq!(anthropic.provider, "anthropic");
},
);
}
#[test]
fn extract_all_credentials_ignores_oauth_env_when_disabled() {
with_env(
&[
("ANTHROPIC_API_KEY", None),
("CLAUDE_API_KEY", None),
("CLAUDE_CODE_OAUTH_TOKEN", Some("oauth-token-123")),
("ANTHROPIC_AUTH_TOKEN", None),
],
|| {
let options = CredentialExtractionOptions {
home_dir: Some(empty_home_dir()),
include_oauth: false,
};
let creds = extract_all_credentials(&options);
assert!(
creds.anthropic.is_none(),
"oauth env should be ignored when include_oauth is false"
);
},
);
}
#[test]
fn extract_all_credentials_prefers_api_key_over_oauth_env() {
with_env(
&[
("ANTHROPIC_API_KEY", Some("sk-ant-priority")),
("CLAUDE_API_KEY", None),
("CLAUDE_CODE_OAUTH_TOKEN", Some("oauth-token-123")),
("ANTHROPIC_AUTH_TOKEN", Some("oauth-token-456")),
],
|| {
let options = CredentialExtractionOptions {
home_dir: Some(empty_home_dir()),
include_oauth: true,
};
let creds = extract_all_credentials(&options);
let anthropic = creds
.anthropic
.expect("expected anthropic credentials from api key env");
assert_eq!(anthropic.api_key, "sk-ant-priority");
assert_eq!(anthropic.auth_type, AuthType::ApiKey);
},
);
}
}

View file

@ -56,6 +56,32 @@ static USER_MESSAGE_COUNTER: AtomicU64 = AtomicU64::new(1);
const ANTHROPIC_MODELS_URL: &str = "https://api.anthropic.com/v1/models?beta=true";
const ANTHROPIC_VERSION: &str = "2023-06-01";
fn claude_oauth_fallback_models() -> AgentModelsResponse {
AgentModelsResponse {
models: vec![
AgentModelInfo {
id: "default".to_string(),
name: Some("Default (recommended)".to_string()),
variants: None,
default_variant: None,
},
AgentModelInfo {
id: "opus".to_string(),
name: Some("Opus".to_string()),
variants: None,
default_variant: None,
},
AgentModelInfo {
id: "haiku".to_string(),
name: Some("Haiku".to_string()),
variants: None,
default_variant: None,
},
],
default_model: Some("default".to_string()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BrandingMode {
#[default]
@ -4007,6 +4033,13 @@ impl SessionManager {
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
if matches!(cred.auth_type, AuthType::Oauth) {
tracing::warn!(
status = %status,
"Anthropic model list rejected OAuth credentials; using Claude OAuth fallback models"
);
return Ok(claude_oauth_fallback_models());
}
return Err(SandboxError::StreamError {
message: format!("Anthropic models request failed {status}: {body}"),
});
@ -4061,6 +4094,13 @@ impl SessionManager {
default_model = models.first().map(|model| model.id.clone());
}
if models.is_empty() && matches!(cred.auth_type, AuthType::Oauth) {
tracing::warn!(
"Anthropic model list was empty for OAuth credentials; using Claude OAuth fallback models"
);
return Ok(claude_oauth_fallback_models());
}
Ok(AgentModelsResponse {
models,
default_model,