use super::credential::AuthScheme;
use super::stores::XAI_LOGIN_HINT;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CredentialPolicy {
Key,
OAuth,
OAuthUnlessExplicit,
AwsChain,
AzureChain,
GcpChain,
}
impl CredentialPolicy {
pub fn as_str(self) -> &'static str {
match self {
CredentialPolicy::Key => "key",
CredentialPolicy::OAuth => "oauth",
CredentialPolicy::OAuthUnlessExplicit => "oauth-unless-explicit",
CredentialPolicy::AwsChain => "aws-chain",
CredentialPolicy::AzureChain => "azure-chain",
CredentialPolicy::GcpChain => "gcp-chain",
}
}
pub fn is_cloud_chain(self) -> bool {
matches!(
self,
CredentialPolicy::AwsChain | CredentialPolicy::AzureChain | CredentialPolicy::GcpChain
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EndpointSupport {
pub complete: bool,
pub stream: bool,
pub live: bool,
pub files: bool,
pub batches: bool,
pub images: bool,
pub speech: bool,
pub video: bool,
pub responses_api: bool,
pub models: bool,
pub caches: bool,
}
impl EndpointSupport {
pub const CHAT: EndpointSupport = EndpointSupport {
complete: true,
stream: true,
live: false,
files: false,
batches: false,
images: false,
speech: false,
video: false,
responses_api: false,
models: false,
caches: false,
};
pub const CHAT_MODELS: EndpointSupport = EndpointSupport {
models: true,
..EndpointSupport::CHAT
};
pub fn supports_endpoint(&self, name: &str) -> bool {
match name {
"complete" => self.complete,
"stream" => self.stream,
"live" => self.live,
"files" => self.files,
"batches" => self.batches,
"images" => self.images,
"speech" => self.speech,
"video" => self.video,
"responses_api" => self.responses_api,
"models" => self.models,
"caches" => self.caches,
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModelPlacement {
Body,
Path,
}
impl ModelPlacement {
pub fn as_str(self) -> &'static str {
match self {
ModelPlacement::Body => "body",
ModelPlacement::Path => "path",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StreamFraming {
Sse,
AwsEventStream,
}
impl StreamFraming {
pub fn as_str(self) -> &'static str {
match self {
StreamFraming::Sse => "sse",
StreamFraming::AwsEventStream => "aws-event-stream",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AnthropicVersionIn {
Header,
Body(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostSetting {
pub name: &'static str,
pub env: &'static [&'static str],
pub default: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HostSpec {
pub base_url: &'static str,
pub endpoint_env: &'static [&'static str],
pub settings: &'static [HostSetting],
pub paths: &'static [(&'static str, &'static str)],
pub model_in: ModelPlacement,
pub anthropic_version_in: AnthropicVersionIn,
pub stream_framing: StreamFraming,
pub required_headers: &'static [(&'static str, &'static str)],
pub sigv4_service: Option<&'static str>,
}
impl HostSpec {
pub const fn new(base_url: &'static str) -> HostSpec {
HostSpec {
base_url,
endpoint_env: &[],
settings: &[],
paths: &[],
model_in: ModelPlacement::Body,
anthropic_version_in: AnthropicVersionIn::Header,
stream_framing: StreamFraming::Sse,
required_headers: &[],
sigv4_service: None,
}
}
pub fn path_for(&self, endpoint: &str) -> Option<&'static str> {
self.paths
.iter()
.find(|(name, _)| *name == endpoint)
.map(|(_, path)| *path)
}
pub fn setting_names(&self) -> impl Iterator<Item = &'static str> + '_ {
self.settings.iter().map(|s| s.name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccessPolicy {
pub provider: &'static str,
pub supports: EndpointSupport,
pub credential_policy: CredentialPolicy,
pub auth_modes: &'static [&'static str],
pub enterprise_variants: &'static [&'static str],
pub env_keys: &'static [&'static str],
pub auth_scheme: &'static [AuthScheme],
pub headers: &'static [(&'static str, &'static str)],
pub host: Option<HostSpec>,
pub login_hint: Option<&'static str>,
pub backend: &'static str,
pub backend_options: &'static [(&'static str, &'static str)],
pub system_prefix: Option<&'static str>,
pub base_url: Option<&'static str>,
pub placeholder_key: Option<&'static str>,
pub backend_settings: &'static [HostSetting],
}
impl AccessPolicy {
pub fn auth_header(&self) -> AuthScheme {
for scheme in self.auth_scheme {
match scheme {
AuthScheme::Bearer => return AuthScheme::Bearer,
AuthScheme::XApiKey | AuthScheme::ApiKey => return AuthScheme::XApiKey,
_ => {}
}
}
AuthScheme::Bearer
}
pub fn is_cloud_chain(&self) -> bool {
self.credential_policy.is_cloud_chain()
}
pub fn backend_option(&self, name: &str) -> Option<&'static str> {
self.backend_options
.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| *value)
}
pub fn resolve_backend_settings(
&self,
given: &crate::cloud::hosts::HostSettings,
env: Option<&std::collections::BTreeMap<String, String>>,
mut sources: Option<&mut std::collections::BTreeMap<String, String>>,
) -> Result<crate::cloud::hosts::HostSettings, crate::errors::Lm15Error> {
let known: Vec<&str> = self.backend_settings.iter().map(|s| s.name).collect();
let mut unknown: Vec<&String> = given
.keys()
.filter(|k| !known.contains(&k.as_str()))
.collect();
unknown.sort();
if !unknown.is_empty() {
let names: Vec<String> = unknown.iter().map(|n| format!("'{n}'")).collect();
let (hint, fix) = if known.is_empty() {
(
"this door takes no settings".to_string(),
format!("Remove the settings entry for {}", self.provider),
)
} else {
(
format!("known: {}", known.join(", ")),
format!("Pass only {} for {}", known.join(", "), self.provider),
)
};
let mut meta = crate::errors::ErrorMeta::new(format!(
"{}: unknown setting(s) {}; {hint}\n\n To fix:\n - {fix}\n",
self.provider,
names.join(", ")
));
meta.provider = Some(self.provider.to_string());
return Err(crate::errors::Lm15Error::NotConfiguredError(meta));
}
let mut out = crate::cloud::hosts::HostSettings::new();
for setting in self.backend_settings {
let mut value = given.get(setting.name).filter(|v| !v.is_empty()).cloned();
let mut origin = value.as_ref().map(|_| "explicit".to_string());
if value.is_none() {
if let Some(env) = env {
if let Some(var) = setting
.env
.iter()
.find(|var| env.get(**var).is_some_and(|v| !v.is_empty()))
{
value = env.get(*var).cloned();
origin = Some(format!("env:{var}"));
}
}
}
let value = value.unwrap_or_else(|| {
self.backend_option(setting.name)
.unwrap_or_default()
.to_string()
});
if let Some(sources) = sources.as_deref_mut() {
sources.insert(
setting.name.to_string(),
origin.unwrap_or_else(|| "default".into()),
);
}
out.insert(setting.name.to_string(), value);
}
Ok(out)
}
}
pub fn claude_code_version_guidance(message: &str) -> String {
const HEAD: &str = "Claude Code ";
const MIDDLE: &str = " does not support this model; version ";
const TAIL: &str = " or newer is required";
if message.contains("\n\n To fix:") {
return message.to_string();
}
let required = message.find(HEAD).and_then(|start| {
let rest = &message[start + HEAD.len()..];
let sent_end = rest.find(char::is_whitespace)?;
let rest = rest[sent_end..].strip_prefix(MIDDLE)?;
let required_end = rest.find(char::is_whitespace)?;
let required = &rest[..required_end];
(!required.is_empty() && rest[required_end..].starts_with(TAIL) && sent_end > 0)
.then_some(required)
});
match required {
None => message.to_string(),
Some(required) => format!(
"{message}\n\n To fix:\n - lm15 sends this version itself; updating Claude Code does not change it\n - Set the claude-code setting client_version to {required} or newer (or {CLAUDE_CODE_VERSION_ENV}={required})\n"
),
}
}
use crate::generated::tables;
pub const ANTHROPIC_API: AccessPolicy = tables::ANTHROPIC;
pub const CLAUDE_CODE: AccessPolicy = tables::CLAUDE_CODE;
pub const OPENAI_API: AccessPolicy = tables::OPENAI;
pub const OPENAI_CODEX: AccessPolicy = tables::OPENAI_CODEX;
pub const OPENAI_CHAT_API: AccessPolicy = tables::OPENAI_CHAT;
pub const XAI: AccessPolicy = AccessPolicy {
login_hint: Some(XAI_LOGIN_HINT),
..tables::XAI
};
pub const KIMI_CODE: AccessPolicy = tables::KIMI_CODE;
pub const GITHUB_COPILOT: AccessPolicy = tables::GITHUB_COPILOT;
pub const GEMINI_API: AccessPolicy = tables::GEMINI;
pub const META: AccessPolicy = tables::META;
pub const GROQ: AccessPolicy = tables::GROQ;
pub const OPENROUTER: AccessPolicy = tables::OPENROUTER;
pub const DEEPSEEK: AccessPolicy = tables::DEEPSEEK;
pub const ZAI: AccessPolicy = tables::ZAI;
pub const DEEPINFRA: AccessPolicy = tables::DEEPINFRA;
pub const TOGETHER: AccessPolicy = tables::TOGETHER;
pub const FIREWORKS: AccessPolicy = tables::FIREWORKS;
pub const PARASAIL: AccessPolicy = tables::PARASAIL;
pub const MOONSHOTAI: AccessPolicy = tables::MOONSHOTAI;
pub const MOONSHOTAI_RESPONSES: AccessPolicy = tables::MOONSHOTAI_RESPONSES;
pub const META_CHAT: AccessPolicy = tables::META_CHAT;
pub const DEEPSEEK_ANTHROPIC: AccessPolicy = tables::DEEPSEEK_ANTHROPIC;
pub const META_ANTHROPIC: AccessPolicy = tables::META_ANTHROPIC;
pub const MOONSHOTAI_ANTHROPIC: AccessPolicy = tables::MOONSHOTAI_ANTHROPIC;
pub const AWS_ANTHROPIC: AccessPolicy = tables::AWS_ANTHROPIC;
pub const BEDROCK_ANTHROPIC: AccessPolicy = tables::BEDROCK_ANTHROPIC;
pub const BEDROCK_CHAT: AccessPolicy = tables::BEDROCK_CHAT;
pub const BEDROCK_MANTLE_CHAT: AccessPolicy = tables::BEDROCK_MANTLE_CHAT;
pub const AZURE: AccessPolicy = tables::AZURE;
pub const AZURE_CHAT: AccessPolicy = tables::AZURE_CHAT;
pub const AZURE_ANTHROPIC: AccessPolicy = tables::AZURE_ANTHROPIC;
pub const VERTEX: AccessPolicy = tables::VERTEX;
pub const VERTEX_EXPRESS: AccessPolicy = tables::VERTEX_EXPRESS;
pub const VERTEX_ANTHROPIC: AccessPolicy = tables::VERTEX_ANTHROPIC;
pub const OLLAMA: AccessPolicy = tables::OLLAMA;
pub const VLLM: AccessPolicy = tables::VLLM;
pub const SGLANG: AccessPolicy = tables::SGLANG;
pub const TYPESAFE: AccessPolicy = tables::TYPESAFE;
pub const ACCESS_POLICIES: &[AccessPolicy] = tables::ACCESS_POLICIES;
const fn str_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
const fn entry(table: &'static [(&'static str, &'static str)], key: &str) -> &'static str {
let mut i = 0;
while i < table.len() {
if str_eq(table[i].0, key) {
return table[i].1;
}
i += 1;
}
panic!("a table value this crate names is missing from src/generated/tables.rs")
}
const fn some(value: Option<&'static str>) -> &'static str {
match value {
Some(v) => v,
None => panic!("a table value this crate names is missing from src/generated/tables.rs"),
}
}
const fn setting_env(settings: &'static [HostSetting], name: &str) -> &'static str {
let mut i = 0;
while i < settings.len() {
if str_eq(settings[i].name, name) && !settings[i].env.is_empty() {
return settings[i].env[0];
}
i += 1;
}
panic!("a backend setting this crate names is missing from src/generated/tables.rs")
}
pub const DEFAULT_CLAUDE_CODE_VERSION: &str = entry(CLAUDE_CODE.backend_options, "client_version");
pub const CLAUDE_CODE_VERSION_ENV: &str =
setting_env(CLAUDE_CODE.backend_settings, "client_version");
pub const CODEX_CLIENT_VERSION_ENV: &str =
setting_env(OPENAI_CODEX.backend_settings, "client_version");
pub const DEFAULT_CLAUDE_CODE_SYSTEM_PROMPT: &str = some(CLAUDE_CODE.system_prefix);
pub const DEFAULT_CODEX_BASE_URL: &str = some(OPENAI_CODEX.base_url);
pub const DEFAULT_CODEX_ORIGINATOR: &str = entry(OPENAI_CODEX.headers, "originator");
pub const DEFAULT_CODEX_INSTRUCTIONS: &str = some(OPENAI_CODEX.system_prefix);
pub const DEFAULT_CODEX_CLIENT_VERSION: &str =
entry(OPENAI_CODEX.backend_options, "client_version");
pub const DEFAULT_XAI_BASE_URL: &str = some(XAI.base_url);
pub use crate::registry::canonical_provider;
pub fn known_providers() -> Vec<&'static str> {
let mut names: Vec<&'static str> = ACCESS_POLICIES.iter().map(|p| p.provider).collect();
names.sort_unstable();
names
}
pub fn access_policy(provider: &str) -> Option<&'static AccessPolicy> {
let canonical = canonical_provider(provider);
ACCESS_POLICIES.iter().find(|p| p.provider == canonical)
}
pub fn shared_api_key_source<'a>(
entries: impl IntoIterator<Item = &'a str>,
provider: &str,
) -> Result<Option<&'a str>, Vec<&'a str>> {
let target = canonical_provider(provider);
let entries: Vec<&str> = entries.into_iter().collect();
let exact: Vec<&str> = entries
.iter()
.copied()
.filter(|e| canonical_provider(e) == target)
.collect();
let mut candidates = exact;
if candidates.is_empty() {
if let Some(policy) = access_policy(&target) {
if !policy.env_keys.is_empty() {
candidates = entries
.iter()
.copied()
.filter(|e| {
access_policy(e).is_some_and(|other| other.env_keys == policy.env_keys)
})
.collect();
}
}
}
match candidates.len() {
0 => Ok(None),
1 => Ok(Some(candidates[0])),
_ => {
candidates.sort_unstable();
Err(candidates)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use AuthScheme::{Bearer, SigV4, XApiKey};
use CredentialPolicy::OAuth;
#[test]
fn provider_strings_are_unique() {
let mut names = known_providers();
names.dedup();
assert_eq!(names.len(), ACCESS_POLICIES.len());
assert_eq!(ACCESS_POLICIES.len(), 36); }
#[test]
fn oauth_policies_declare_no_env_keys() {
for policy in ACCESS_POLICIES {
if policy.credential_policy == OAuth {
assert!(policy.env_keys.is_empty(), "{}", policy.provider);
assert!(policy.login_hint.is_some(), "{}", policy.provider);
}
}
}
#[test]
fn placeholder_presets_declare_no_env_keys() {
for policy in ACCESS_POLICIES {
if policy.placeholder_key.is_some() {
assert!(policy.env_keys.is_empty(), "{}", policy.provider);
}
}
}
#[test]
fn underscore_alias_resolves() {
assert_eq!(
access_policy("openai_chat").unwrap().provider,
"openai-chat"
);
assert!(access_policy("nope").is_none());
}
#[test]
fn sigv4_and_cloud_chains_name_a_host() {
for policy in ACCESS_POLICIES {
if policy.auth_scheme.contains(&SigV4) {
assert!(
policy.host.and_then(|h| h.sigv4_service).is_some(),
"{}",
policy.provider
);
}
if policy.is_cloud_chain() {
assert!(policy.host.is_some(), "{}", policy.provider);
}
assert!(!policy.auth_scheme.is_empty(), "{}", policy.provider);
}
}
#[test]
fn claude_code_user_agent_carries_the_version() {
let ua = CLAUDE_CODE
.headers
.iter()
.find(|(k, _)| *k == "user-agent")
.map(|(_, v)| *v)
.unwrap();
assert_eq!(ua, format!("claude-cli/{DEFAULT_CLAUDE_CODE_VERSION}"));
}
#[test]
fn auth_header_projection() {
assert_eq!(ANTHROPIC_API.auth_header(), XApiKey);
assert_eq!(AZURE.auth_header(), XApiKey);
assert_eq!(BEDROCK_CHAT.auth_header(), Bearer);
assert_eq!(VERTEX_EXPRESS.auth_header(), Bearer);
assert_eq!(
OPENAI_CODEX.backend_option("client_version"),
Some("0.147.0")
);
assert_eq!(
VERTEX_ANTHROPIC.host.unwrap().path_for("messages/stream"),
Some("/publishers/anthropic/models/{model}:streamRawPredict")
);
}
#[test]
fn bound_base_urls_match_the_compat_tables() {
assert_eq!(GROQ.base_url, Some("https://api.groq.com/openai/v1"));
assert_eq!(DEEPSEEK.base_url, Some("https://api.deepseek.com"));
assert_eq!(
DEEPSEEK_ANTHROPIC.base_url,
Some("https://api.deepseek.com/anthropic/v1")
);
assert_eq!(
MOONSHOTAI_RESPONSES.base_url,
Some("https://api.moonshot.ai/v1")
);
assert_eq!(OLLAMA.base_url, Some("http://localhost:11434/v1"));
}
}