mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
fix: support Claude OAuth token for model listing (#109)
This commit is contained in:
parent
a7b3881099
commit
a5a6492165
3 changed files with 188 additions and 1 deletions
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,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]
|
||||
|
|
@ -3318,6 +3344,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}"),
|
||||
});
|
||||
|
|
@ -3372,6 +3405,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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue