fix: fix checking if provider is authenticated

This commit is contained in:
Nathan Flurry 2026-02-06 19:40:45 -08:00
parent b76d83577a
commit 80ce95f886
13 changed files with 801 additions and 6 deletions

View file

@ -63,7 +63,9 @@ pub fn extract_claude_credentials(
];
for path in config_paths {
let data = read_json_file(&path)?;
let Some(data) = read_json_file(&path) else {
continue;
};
for key_path in &key_paths {
if let Some(key) = read_string_field(&data, key_path) {
if key.starts_with("sk-ant-") {

View file

@ -21,10 +21,14 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::sync::{broadcast, Mutex};
use tokio::time::interval;
use tracing::warn;
use utoipa::{IntoParams, OpenApi, ToSchema};
use crate::router::{AgentModelInfo, AppState, CreateSessionRequest, PermissionReply};
use sandbox_agent_agent_management::agents::AgentId;
use sandbox_agent_agent_management::credentials::{
extract_all_credentials, CredentialExtractionOptions, ExtractedCredentials,
};
use sandbox_agent_error::SandboxError;
use sandbox_agent_universal_agent_schema::{
ContentPart, FileAction, ItemDeltaData, ItemEventData, ItemKind, ItemRole, ItemStatus,
@ -233,6 +237,8 @@ struct OpenCodeModelCache {
group_names: HashMap<String, String>,
default_group: String,
default_model: String,
/// Group IDs that have valid credentials available
connected: Vec<String>,
}
pub struct OpenCodeState {
@ -637,6 +643,21 @@ async fn opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCache {
}
async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCache {
// Check credentials upfront
let credentials = match tokio::task::spawn_blocking(|| {
extract_all_credentials(&CredentialExtractionOptions::new())
})
.await
{
Ok(creds) => creds,
Err(err) => {
warn!("Failed to extract credentials for model cache: {err}");
ExtractedCredentials::default()
}
};
let has_anthropic = credentials.anthropic.is_some();
let has_openai = credentials.openai.is_some();
let mut entries = Vec::new();
let mut model_lookup = HashMap::new();
let mut ambiguous_models = HashSet::new();
@ -735,6 +756,28 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
}
}
// Build connected list based on credential availability
let mut connected = Vec::new();
for group_id in group_names.keys() {
let is_connected = match group_agents.get(group_id) {
Some(AgentId::Claude) | Some(AgentId::Amp) => has_anthropic,
Some(AgentId::Codex) => has_openai,
Some(AgentId::Opencode) => {
// Check the specific provider for opencode groups (e.g., "opencode:anthropic")
match opencode_group_provider(group_id) {
Some("anthropic") => has_anthropic,
Some("openai") => has_openai,
_ => has_anthropic || has_openai,
}
}
Some(AgentId::Mock) => true,
None => false,
};
if is_connected {
connected.push(group_id.clone());
}
}
OpenCodeModelCache {
entries,
model_lookup,
@ -743,6 +786,7 @@ async fn build_opencode_model_cache(state: &OpenCodeAppState) -> OpenCodeModelCa
group_names,
default_group,
default_model,
connected,
}
}
@ -3962,7 +4006,6 @@ async fn oc_provider_list(State(state): State<Arc<OpenCodeAppState>>) -> impl In
}
let mut providers = Vec::new();
let mut defaults = serde_json::Map::new();
let mut connected = Vec::new();
for (group_id, entries) in grouped {
let mut models = serde_json::Map::new();
for entry in entries {
@ -3982,12 +4025,12 @@ async fn oc_provider_list(State(state): State<Arc<OpenCodeAppState>>) -> impl In
if let Some(default_model) = cache.group_defaults.get(&group_id) {
defaults.insert(group_id.clone(), Value::String(default_model.clone()));
}
connected.push(group_id);
}
// Use the connected list from cache (based on credential availability)
let providers = json!({
"all": providers,
"default": Value::Object(defaults),
"connected": connected
"connected": cache.connected
});
(StatusCode::OK, Json(providers))
}

View file

@ -1798,8 +1798,14 @@ impl SessionManager {
agent: AgentId,
) -> Result<AgentModelsResponse, SandboxError> {
match agent {
AgentId::Claude => self.fetch_claude_models().await,
AgentId::Codex => self.fetch_codex_models().await,
AgentId::Claude => match self.fetch_claude_models().await {
Ok(response) if !response.models.is_empty() => Ok(response),
_ => Ok(claude_fallback_models()),
},
AgentId::Codex => match self.fetch_codex_models().await {
Ok(response) if !response.models.is_empty() => Ok(response),
_ => Ok(codex_fallback_models()),
},
AgentId::Opencode => match self.fetch_opencode_models().await {
Ok(models) => Ok(models),
Err(_) => Ok(AgentModelsResponse {
@ -3927,6 +3933,8 @@ pub struct ServerStatusInfo {
pub struct AgentInfo {
pub id: String,
pub installed: bool,
/// Whether the agent's required provider credentials are available
pub credentials_available: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -4194,6 +4202,10 @@ async fn list_agents(
let agents =
tokio::task::spawn_blocking(move || {
let credentials = extract_all_credentials(&CredentialExtractionOptions::new());
let has_anthropic = credentials.anthropic.is_some();
let has_openai = credentials.openai.is_some();
all_agents()
.into_iter()
.map(|agent_id| {
@ -4202,6 +4214,13 @@ async fn list_agents(
let path = manager.resolve_binary(agent_id).ok();
let capabilities = agent_capabilities_for(agent_id);
let credentials_available = match agent_id {
AgentId::Claude | AgentId::Amp => has_anthropic,
AgentId::Codex => has_openai,
AgentId::Opencode => has_anthropic || has_openai,
AgentId::Mock => true,
};
// Add server_status for agents with shared processes
let server_status =
if capabilities.shared_process {
@ -4221,6 +4240,7 @@ async fn list_agents(
AgentInfo {
id: agent_id.as_str().to_string(),
installed,
credentials_available,
version,
path: path.map(|path| path.to_string_lossy().to_string()),
capabilities,
@ -4742,6 +4762,38 @@ fn mock_models_response() -> AgentModelsResponse {
}
}
fn claude_fallback_models() -> AgentModelsResponse {
let models = ["claude-sonnet-4-20250514", "claude-opus-4-20250514"]
.into_iter()
.map(|id| AgentModelInfo {
id: id.to_string(),
name: None,
variants: None,
default_variant: None,
})
.collect();
AgentModelsResponse {
models,
default_model: Some("claude-sonnet-4-20250514".to_string()),
}
}
fn codex_fallback_models() -> AgentModelsResponse {
let models = ["gpt-4o", "o3", "o4-mini"]
.into_iter()
.map(|id| AgentModelInfo {
id: id.to_string(),
name: None,
variants: Some(codex_variants()),
default_variant: Some("medium".to_string()),
})
.collect();
AgentModelsResponse {
models,
default_model: Some("gpt-4o".to_string()),
}
}
fn amp_variants() -> Vec<String> {
vec!["medium", "high", "xhigh"]
.into_iter()