mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 07:03:31 +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 |
|
| 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` |
|
| Codex | `OPENAI_API_KEY`, `CODEX_API_KEY` |
|
||||||
| OpenCode | `OPENAI_API_KEY` |
|
| OpenCode | `OPENAI_API_KEY` |
|
||||||
| Amp | `ANTHROPIC_API_KEY` |
|
| Amp | `ANTHROPIC_API_KEY` |
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,22 @@ pub fn extract_all_credentials(options: &CredentialExtractionOptions) -> Extract
|
||||||
auth_type: AuthType::ApiKey,
|
auth_type: AuthType::ApiKey,
|
||||||
provider: "anthropic".to_string(),
|
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") {
|
if let Ok(value) = std::env::var("OPENAI_API_KEY") {
|
||||||
|
|
@ -376,3 +392,134 @@ fn is_expired_rfc3339(value: &str) -> bool {
|
||||||
Err(_) => false,
|
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_MODELS_URL: &str = "https://api.anthropic.com/v1/models?beta=true";
|
||||||
const ANTHROPIC_VERSION: &str = "2023-06-01";
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub enum BrandingMode {
|
pub enum BrandingMode {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -3318,6 +3344,13 @@ impl SessionManager {
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let body = response.text().await.unwrap_or_default();
|
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 {
|
return Err(SandboxError::StreamError {
|
||||||
message: format!("Anthropic models request failed {status}: {body}"),
|
message: format!("Anthropic models request failed {status}: {body}"),
|
||||||
});
|
});
|
||||||
|
|
@ -3372,6 +3405,13 @@ impl SessionManager {
|
||||||
default_model = models.first().map(|model| model.id.clone());
|
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 {
|
Ok(AgentModelsResponse {
|
||||||
models,
|
models,
|
||||||
default_model,
|
default_model,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue