mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
refactor(auth): extract ApiCredential::from_api_key
The "Anthropic uses x-api-key header, everyone else uses Bearer" logic was written three times: env_source (env-based construction), resolve (vault-based construction), and provider_auth (CLI key validation). Any future header rename would need three edits. Add ApiCredential::from_api_key(provider, key) as a canonical constructor. Each callsite now builds via the helper and overrides only the fields specific to its path (env base URLs, vault-sourced org/project IDs, codex mode, etc.). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7e83cd38e5
commit
b4bc9a0506
3 changed files with 61 additions and 97 deletions
|
|
@ -1,11 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_model::Provider;
|
||||
|
||||
use crate::credential_source::{CredentialSource, ResolvedCredentials};
|
||||
use crate::{ApiCredential, ApiKeyHeader, EnvLookup};
|
||||
use crate::{ApiCredential, EnvLookup};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EnvCredentialSource {
|
||||
|
|
@ -33,66 +32,32 @@ impl EnvCredentialSource {
|
|||
.iter()
|
||||
.find_map(|var| self.lookup(var))?;
|
||||
|
||||
Some(match provider {
|
||||
Provider::Anthropic => ApiCredential {
|
||||
provider,
|
||||
auth_header: ApiKeyHeader::Custom {
|
||||
name: "x-api-key".to_string(),
|
||||
value: key,
|
||||
},
|
||||
extra_headers: HashMap::new(),
|
||||
base_url: self.lookup("ANTHROPIC_BASE_URL"),
|
||||
codex_mode: false,
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
},
|
||||
let mut cred = ApiCredential::from_api_key(provider, key);
|
||||
match provider {
|
||||
Provider::Anthropic => {
|
||||
cred.base_url = self.lookup("ANTHROPIC_BASE_URL");
|
||||
}
|
||||
Provider::OpenAi => {
|
||||
let mut extra_headers = HashMap::new();
|
||||
let mut base_url = self.lookup("OPENAI_BASE_URL");
|
||||
let mut codex_mode = false;
|
||||
cred.base_url = self.lookup("OPENAI_BASE_URL");
|
||||
cred.org_id = self.lookup("OPENAI_ORG_ID");
|
||||
cred.project_id = self.lookup("OPENAI_PROJECT_ID");
|
||||
if let Some(account_id) = self.lookup("CHATGPT_ACCOUNT_ID") {
|
||||
base_url = Some("https://chatgpt.com/backend-api/codex".to_string());
|
||||
codex_mode = true;
|
||||
extra_headers.insert("ChatGPT-Account-Id".to_string(), account_id);
|
||||
extra_headers.insert("originator".to_string(), "fabro".to_string());
|
||||
}
|
||||
ApiCredential {
|
||||
provider,
|
||||
auth_header: ApiKeyHeader::Bearer(key),
|
||||
extra_headers,
|
||||
base_url,
|
||||
codex_mode,
|
||||
org_id: self.lookup("OPENAI_ORG_ID"),
|
||||
project_id: self.lookup("OPENAI_PROJECT_ID"),
|
||||
cred.base_url = Some("https://chatgpt.com/backend-api/codex".to_string());
|
||||
cred.codex_mode = true;
|
||||
cred.extra_headers
|
||||
.insert("ChatGPT-Account-Id".to_string(), account_id);
|
||||
cred.extra_headers
|
||||
.insert("originator".to_string(), "fabro".to_string());
|
||||
}
|
||||
}
|
||||
Provider::Gemini => ApiCredential {
|
||||
provider,
|
||||
auth_header: ApiKeyHeader::Bearer(key),
|
||||
extra_headers: HashMap::new(),
|
||||
base_url: self.lookup("GEMINI_BASE_URL"),
|
||||
codex_mode: false,
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
},
|
||||
Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => {
|
||||
bearer_credential(provider, key)
|
||||
Provider::Gemini => {
|
||||
cred.base_url = self.lookup("GEMINI_BASE_URL");
|
||||
}
|
||||
Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => {}
|
||||
// OpenAiCompatible has no api_key_env_vars, so find_map returned None above.
|
||||
Provider::OpenAiCompatible => unreachable!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn bearer_credential(provider: Provider, key: String) -> ApiCredential {
|
||||
ApiCredential {
|
||||
provider,
|
||||
auth_header: ApiKeyHeader::Bearer(key),
|
||||
extra_headers: HashMap::new(),
|
||||
base_url: None,
|
||||
codex_mode: false,
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
}
|
||||
Some(cred)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,32 @@ pub struct ApiCredential {
|
|||
pub project_id: Option<String>,
|
||||
}
|
||||
|
||||
impl ApiCredential {
|
||||
/// Build an `ApiCredential` from just an API key. Picks the right
|
||||
/// auth header kind for the provider (Anthropic uses `x-api-key`;
|
||||
/// everyone else uses `Authorization: Bearer`). All other fields
|
||||
/// default to empty.
|
||||
#[must_use]
|
||||
pub fn from_api_key(provider: Provider, key: String) -> Self {
|
||||
let auth_header = match provider {
|
||||
Provider::Anthropic => ApiKeyHeader::Custom {
|
||||
name: "x-api-key".to_string(),
|
||||
value: key,
|
||||
},
|
||||
_ => ApiKeyHeader::Bearer(key),
|
||||
};
|
||||
Self {
|
||||
provider,
|
||||
auth_header,
|
||||
extra_headers: HashMap::new(),
|
||||
base_url: None,
|
||||
codex_mode: false,
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CliCredential {
|
||||
pub env_vars: HashMap<String, String>,
|
||||
|
|
@ -202,7 +228,6 @@ impl CredentialResolver {
|
|||
}
|
||||
|
||||
fn to_api_credential(&self, vault: &Vault, credential: &AuthCredential) -> ApiCredential {
|
||||
let mut extra_headers = HashMap::new();
|
||||
let base_url = match credential.provider {
|
||||
Provider::Anthropic => self.lookup_env_or_vault(vault, "ANTHROPIC_BASE_URL"),
|
||||
Provider::OpenAi => self.lookup_env_or_vault(vault, "OPENAI_BASE_URL"),
|
||||
|
|
@ -213,32 +238,19 @@ impl CredentialResolver {
|
|||
}
|
||||
};
|
||||
match &credential.details {
|
||||
AuthDetails::ApiKey { key } => ApiCredential {
|
||||
provider: credential.provider,
|
||||
auth_header: match credential.provider {
|
||||
Provider::Anthropic => ApiKeyHeader::Custom {
|
||||
name: "x-api-key".to_string(),
|
||||
value: key.clone(),
|
||||
},
|
||||
_ => ApiKeyHeader::Bearer(key.clone()),
|
||||
},
|
||||
extra_headers,
|
||||
base_url,
|
||||
codex_mode: false,
|
||||
org_id: if credential.provider == Provider::OpenAi {
|
||||
self.lookup_env_or_vault(vault, "OPENAI_ORG_ID")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
project_id: if credential.provider == Provider::OpenAi {
|
||||
self.lookup_env_or_vault(vault, "OPENAI_PROJECT_ID")
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
AuthDetails::ApiKey { key } => {
|
||||
let mut cred = ApiCredential::from_api_key(credential.provider, key.clone());
|
||||
cred.base_url = base_url;
|
||||
if credential.provider == Provider::OpenAi {
|
||||
cred.org_id = self.lookup_env_or_vault(vault, "OPENAI_ORG_ID");
|
||||
cred.project_id = self.lookup_env_or_vault(vault, "OPENAI_PROJECT_ID");
|
||||
}
|
||||
cred
|
||||
}
|
||||
AuthDetails::CodexOAuth {
|
||||
tokens, account_id, ..
|
||||
} => {
|
||||
let mut extra_headers = HashMap::new();
|
||||
if let Some(account_id) = account_id {
|
||||
extra_headers.insert("ChatGPT-Account-Id".to_string(), account_id.clone());
|
||||
extra_headers.insert("originator".to_string(), "fabro".to_string());
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ use dialoguer::console::Term;
|
|||
use dialoguer::theme::ColorfulTheme;
|
||||
use dialoguer::{Confirm, Password};
|
||||
use fabro_auth::{
|
||||
ApiCredential, ApiKeyHeader, AuthContextRequest, AuthContextResponse, AuthCredential,
|
||||
AuthMethod, codex_oauth_config, strategy_for,
|
||||
ApiCredential, AuthContextRequest, AuthContextResponse, AuthCredential, AuthMethod,
|
||||
codex_oauth_config, strategy_for,
|
||||
};
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_llm::generate::{GenerateParams, generate};
|
||||
|
|
@ -78,23 +78,10 @@ pub(crate) enum ApiKeySource {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub(crate) async fn validate_api_key(provider: Provider, api_key: &str) -> Result<(), String> {
|
||||
let auth_header = if provider == Provider::Anthropic {
|
||||
ApiKeyHeader::Custom {
|
||||
name: "x-api-key".to_string(),
|
||||
value: api_key.to_string(),
|
||||
}
|
||||
} else {
|
||||
ApiKeyHeader::Bearer(api_key.to_string())
|
||||
};
|
||||
let client = LlmClient::from_credentials(vec![ApiCredential {
|
||||
let client = LlmClient::from_credentials(vec![ApiCredential::from_api_key(
|
||||
provider,
|
||||
auth_header,
|
||||
extra_headers: std::collections::HashMap::new(),
|
||||
base_url: None,
|
||||
codex_mode: false,
|
||||
org_id: None,
|
||||
project_id: None,
|
||||
}])
|
||||
api_key.to_string(),
|
||||
)])
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue