refactor(config): extract provider metadata into data-driven registry

This commit is contained in:
shenbowen
2026-06-10 16:21:21 +08:00
parent 0181493c79
commit f64cf6f0ff
5 changed files with 232 additions and 233 deletions
+10 -54
View File
@@ -171,8 +171,8 @@ impl ProviderKind {
Self::Novita,
Self::Fireworks,
Self::Siliconflow,
Self::SiliconflowCN,
Self::Arcee,
Self::SiliconflowCN,
Self::Moonshot,
Self::Sglang,
Self::Vllm,
@@ -185,63 +185,19 @@ impl ProviderKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Deepseek => "deepseek",
Self::NvidiaNim => "nvidia-nim",
Self::Openai => "openai",
Self::Atlascloud => "atlascloud",
Self::WanjieArk => "wanjie-ark",
Self::Volcengine => "volcengine",
Self::Openrouter => "openrouter",
Self::XiaomiMimo => "xiaomi-mimo",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
Self::Siliconflow => "siliconflow",
Self::SiliconflowCN => "siliconflow-CN",
Self::Arcee => "arcee",
Self::Moonshot => "moonshot",
Self::Sglang => "sglang",
Self::Vllm => "vllm",
Self::Ollama => "ollama",
Self::Huggingface => "huggingface",
Self::Together => "together",
Self::OpenaiCodex => "openai-codex",
Self::Anthropic => "anthropic",
}
self.provider().id()
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"deepseek" | "deep-seek" | "deepseek-cn" | "deepseek_china" | "deepseekcn"
| "deepseek-china" => Some(Self::Deepseek),
"nvidia" | "nvidia-nim" | "nvidia_nim" | "nim" => Some(Self::NvidiaNim),
"openai" | "open-ai" => Some(Self::Openai),
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => Some(Self::Atlascloud),
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => Some(Self::WanjieArk),
"volcengine" | "volcengine-ark" | "volcengine_ark" | "ark" | "volc-ark"
| "volcengineark" => Some(Self::Volcengine),
"openrouter" | "open_router" => Some(Self::Openrouter),
"xiaomi-mimo" | "xiaomi_mimo" | "xiaomimimo" | "mimo" | "xiaomi" => {
Some(Self::XiaomiMimo)
}
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
"siliconflow" | "silicon-flow" | "silicon_flow" => Some(Self::Siliconflow),
"siliconflow-cn" | "siliconflow-CN" => Some(Self::SiliconflowCN),
"arcee" | "arcee-ai" | "arcee_ai" => Some(Self::Arcee),
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => Some(Self::Moonshot),
"sglang" | "sg-lang" => Some(Self::Sglang),
"vllm" | "v-llm" => Some(Self::Vllm),
"ollama" | "ollama-local" => Some(Self::Ollama),
"huggingface" | "hugging-face" | "hugging_face" | "hf" => Some(Self::Huggingface),
"together" | "together-ai" | "together_ai" => Some(Self::Together),
"anthropic" | "claude" => Some(Self::Anthropic),
"openai-codex" | "openai_codex" | "openaicodex" | "codex" | "chatgpt"
| "chatgpt-codex" | "chatgpt_codex" | "chatgptcodex" => Some(Self::OpenaiCodex),
_ => None,
}
let trimmed = value.trim();
provider::all_providers()
.iter()
.find(|p| {
trimmed.eq_ignore_ascii_case(p.id())
|| p.aliases().iter().any(|a| trimmed.eq_ignore_ascii_case(a))
})
.map(|p| p.kind())
}
#[must_use]
+110 -49
View File
@@ -56,6 +56,11 @@ pub trait Provider: Send + Sync {
/// TOML table key under `[providers.<key>]`.
fn provider_config_key(&self) -> &'static str;
/// Alternate names accepted during provider resolution.
fn aliases(&self) -> &'static [&'static str] {
&[]
}
/// Wire format used by the provider.
fn wire(&self) -> WireFormat {
WireFormat::ChatCompletions
@@ -66,16 +71,22 @@ macro_rules! provider {
(
$struct_name:ident,
$kind:ident,
$id:literal,
$display_name:literal,
$base_url:ident,
$model:ident,
[$($env_var:literal),* $(,)?],
$config_key:literal
$config_key:literal,
aliases: [$($alias:literal),* $(,)?]
) => {
/// Zero-sized metadata entry for this built-in provider.
pub struct $struct_name;
impl Provider for $struct_name {
fn id(&self) -> &'static str {
$id
}
fn kind(&self) -> ProviderKind {
ProviderKind::$kind
}
@@ -99,6 +110,10 @@ macro_rules! provider {
fn provider_config_key(&self) -> &'static str {
$config_key
}
fn aliases(&self) -> &'static [&'static str] {
&[$($alias),*]
}
}
};
}
@@ -106,42 +121,51 @@ macro_rules! provider {
provider!(
Deepseek,
Deepseek,
"deepseek",
"DeepSeek",
DEFAULT_DEEPSEEK_BASE_URL,
DEFAULT_DEEPSEEK_MODEL,
["DEEPSEEK_API_KEY"],
"deepseek"
"deepseek",
aliases: ["deep-seek", "deepseek-cn", "deepseek_china", "deepseekcn", "deepseek-china"]
);
provider!(
NvidiaNim,
NvidiaNim,
"nvidia-nim",
"NVIDIA NIM",
DEFAULT_NVIDIA_NIM_BASE_URL,
DEFAULT_NVIDIA_NIM_MODEL,
["NVIDIA_API_KEY", "NVIDIA_NIM_API_KEY", "DEEPSEEK_API_KEY"],
"nvidia_nim"
"nvidia_nim",
aliases: ["nvidia", "nvidia_nim", "nim"]
);
provider!(
Openai,
Openai,
"openai",
"OpenAI-compatible",
DEFAULT_OPENAI_BASE_URL,
DEFAULT_OPENAI_MODEL,
["OPENAI_API_KEY"],
"openai"
"openai",
aliases: ["open-ai"]
);
provider!(
Atlascloud,
Atlascloud,
"atlascloud",
"AtlasCloud",
DEFAULT_ATLASCLOUD_BASE_URL,
DEFAULT_ATLASCLOUD_MODEL,
["ATLASCLOUD_API_KEY"],
"atlascloud"
"atlascloud",
aliases: ["atlas-cloud", "atlas_cloud", "atlas"]
);
provider!(
WanjieArk,
WanjieArk,
"wanjie-ark",
"Wanjie Ark",
DEFAULT_WANJIE_ARK_BASE_URL,
DEFAULT_WANJIE_ARK_MODEL,
@@ -150,11 +174,13 @@ provider!(
"WANJIE_API_KEY",
"WANJIE_MAAS_API_KEY"
],
"wanjie_ark"
"wanjie_ark",
aliases: ["wanjie", "wanjie_ark", "ark-wanjie", "ark_wanjie", "wanjieark", "wanjie-maas", "wanjie_maas", "wanjiemaas"]
);
provider!(
Volcengine,
Volcengine,
"volcengine",
"Volcengine Ark",
DEFAULT_VOLCENGINE_BASE_URL,
DEFAULT_VOLCENGINE_MODEL,
@@ -163,20 +189,24 @@ provider!(
"VOLCENGINE_ARK_API_KEY",
"ARK_API_KEY"
],
"volcengine"
"volcengine",
aliases: ["volcengine-ark", "volcengine_ark", "ark", "volc-ark", "volcengineark"]
);
provider!(
Openrouter,
Openrouter,
"openrouter",
"OpenRouter",
DEFAULT_OPENROUTER_BASE_URL,
DEFAULT_OPENROUTER_MODEL,
["OPENROUTER_API_KEY"],
"openrouter"
"openrouter",
aliases: ["open_router"]
);
provider!(
XiaomiMimo,
XiaomiMimo,
"xiaomi-mimo",
"Xiaomi MiMo",
DEFAULT_XIAOMI_MIMO_BASE_URL,
DEFAULT_XIAOMI_MIMO_MODEL,
@@ -187,112 +217,145 @@ provider!(
"XIAOMI_API_KEY",
"MIMO_API_KEY",
],
"xiaomi_mimo"
"xiaomi_mimo",
aliases: ["xiaomi_mimo", "xiaomimimo", "mimo", "xiaomi"]
);
provider!(
Novita,
Novita,
"Novita",
"novita",
"Novita AI",
DEFAULT_NOVITA_BASE_URL,
DEFAULT_NOVITA_MODEL,
["NOVITA_API_KEY"],
"novita"
"novita",
aliases: []
);
provider!(
Fireworks,
Fireworks,
"Fireworks",
"fireworks",
"Fireworks AI",
DEFAULT_FIREWORKS_BASE_URL,
DEFAULT_FIREWORKS_MODEL,
["FIREWORKS_API_KEY"],
"fireworks"
"fireworks",
aliases: ["fireworks-ai"]
);
provider!(
Siliconflow,
Siliconflow,
"siliconflow",
"SiliconFlow",
DEFAULT_SILICONFLOW_BASE_URL,
DEFAULT_SILICONFLOW_MODEL,
["SILICONFLOW_API_KEY"],
"siliconflow"
"siliconflow",
aliases: ["silicon-flow", "silicon_flow"]
);
provider!(
SiliconflowCN,
SiliconflowCN,
"SiliconFlow CN",
"siliconflow-CN",
"SiliconFlow (China)",
DEFAULT_SILICONFLOW_CN_BASE_URL,
DEFAULT_SILICONFLOW_MODEL,
["SILICONFLOW_API_KEY"],
"siliconflow"
"siliconflow",
aliases: [
"silicon-flow-cn",
"silicon-flow-CN",
"silicon_flow_cn",
"silicon_flow_CN",
"siliconflow-china",
]
);
provider!(
Arcee,
Arcee,
"Arcee",
"arcee",
"Arcee AI",
DEFAULT_ARCEE_BASE_URL,
DEFAULT_ARCEE_MODEL,
["ARCEE_API_KEY"],
"arcee"
"arcee",
aliases: ["arcee-ai", "arcee_ai"]
);
provider!(
Moonshot,
Moonshot,
"Moonshot",
"moonshot",
"Moonshot/Kimi",
DEFAULT_MOONSHOT_BASE_URL,
DEFAULT_MOONSHOT_MODEL,
["MOONSHOT_API_KEY", "KIMI_API_KEY"],
"moonshot"
"moonshot",
aliases: ["moonshot-ai", "kimi", "kimi-k2"]
);
provider!(
Sglang,
Sglang,
"sglang",
"SGLang",
DEFAULT_SGLANG_BASE_URL,
DEFAULT_SGLANG_MODEL,
["SGLANG_API_KEY"],
"sglang"
"sglang",
aliases: ["sg-lang"]
);
provider!(
Vllm,
Vllm,
"vllm",
"vLLM",
DEFAULT_VLLM_BASE_URL,
DEFAULT_VLLM_MODEL,
["VLLM_API_KEY"],
"vllm"
"vllm",
aliases: ["v-llm"]
);
provider!(
Ollama,
Ollama,
"ollama",
"Ollama",
DEFAULT_OLLAMA_BASE_URL,
DEFAULT_OLLAMA_MODEL,
["OLLAMA_API_KEY"],
"ollama"
"ollama",
aliases: ["ollama-local"]
);
provider!(
Huggingface,
Huggingface,
"huggingface",
"Hugging Face",
DEFAULT_HUGGINGFACE_BASE_URL,
DEFAULT_HUGGINGFACE_MODEL,
["HUGGINGFACE_API_KEY", "HF_TOKEN"],
"huggingface"
"huggingface",
aliases: ["hugging-face", "hugging_face", "hf"]
);
provider!(
Together,
Together,
"together",
"Together AI",
DEFAULT_TOGETHER_BASE_URL,
DEFAULT_TOGETHER_MODEL,
["TOGETHER_API_KEY"],
"together"
"together",
aliases: ["together-ai", "together_ai"]
);
/// OpenAI Codex / ChatGPT OAuth provider using the Responses API.
pub struct OpenaiCodex;
impl Provider for OpenaiCodex {
fn id(&self) -> &'static str {
"openai-codex"
}
fn kind(&self) -> ProviderKind {
ProviderKind::OpenaiCodex
}
@@ -317,6 +380,18 @@ impl Provider for OpenaiCodex {
"openai_codex"
}
fn aliases(&self) -> &'static [&'static str] {
&[
"openai_codex",
"openaicodex",
"codex",
"chatgpt",
"chatgpt-codex",
"chatgpt_codex",
"chatgptcodex",
]
}
fn wire(&self) -> WireFormat {
WireFormat::Responses
}
@@ -326,6 +401,10 @@ impl Provider for OpenaiCodex {
pub struct Anthropic;
impl Provider for Anthropic {
fn id(&self) -> &'static str {
"anthropic"
}
fn kind(&self) -> ProviderKind {
ProviderKind::Anthropic
}
@@ -389,8 +468,8 @@ static PROVIDER_REGISTRY: [&dyn Provider; 21] = [
&NOVITA,
&FIREWORKS,
&SILICONFLOW,
&SILICONFLOW_CN,
&ARCEE,
&SILICONFLOW_CN,
&MOONSHOT,
&SGLANG,
&VLLM,
@@ -426,27 +505,9 @@ pub fn resolve_provider(id_or_alias: &str) -> Option<&'static dyn Provider> {
/// Return metadata for a known provider kind.
#[must_use]
pub fn provider_for_kind(kind: ProviderKind) -> &'static dyn Provider {
match kind {
ProviderKind::Deepseek => &DEEPSEEK,
ProviderKind::NvidiaNim => &NVIDIA_NIM,
ProviderKind::Openai => &OPENAI,
ProviderKind::Atlascloud => &ATLASCLOUD,
ProviderKind::WanjieArk => &WANJIE_ARK,
ProviderKind::Volcengine => &VOLCENGINE,
ProviderKind::Openrouter => &OPENROUTER,
ProviderKind::XiaomiMimo => &XIAOMI_MIMO,
ProviderKind::Novita => &NOVITA,
ProviderKind::Fireworks => &FIREWORKS,
ProviderKind::Siliconflow => &SILICONFLOW,
ProviderKind::SiliconflowCN => &SILICONFLOW_CN,
ProviderKind::Arcee => &ARCEE,
ProviderKind::Moonshot => &MOONSHOT,
ProviderKind::Sglang => &SGLANG,
ProviderKind::Vllm => &VLLM,
ProviderKind::Ollama => &OLLAMA,
ProviderKind::Huggingface => &HUGGINGFACE,
ProviderKind::Together => &TOGETHER,
ProviderKind::OpenaiCodex => &OPENAI_CODEX,
ProviderKind::Anthropic => &ANTHROPIC,
}
PROVIDER_REGISTRY
.iter()
.find(|p| p.kind() == kind)
.copied()
.expect("ProviderKind variant missing from PROVIDER_REGISTRY")
}
+82 -103
View File
@@ -212,126 +212,105 @@ impl ApiProvider {
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"deepseek" | "deep-seek" => Some(Self::Deepseek),
"deepseek-cn" | "deepseek_china" | "deepseekcn" | "deepseek-china" => {
Some(Self::DeepseekCN)
}
"nvidia" | "nvidia-nim" | "nvidia_nim" | "nim" => Some(Self::NvidiaNim),
"openai" | "open-ai" => Some(Self::Openai),
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => Some(Self::Atlascloud),
"wanjie" | "wanjie-ark" | "wanjie_ark" | "ark-wanjie" | "ark_wanjie" | "wanjieark"
| "wanjie-maas" | "wanjie_maas" | "wanjiemaas" => Some(Self::WanjieArk),
"volcengine" | "volcengine-ark" | "volcengine_ark" | "ark" | "volc-ark"
| "volcengineark" => Some(Self::Volcengine),
"openrouter" | "open_router" => Some(Self::Openrouter),
"xiaomi-mimo" | "xiaomi_mimo" | "xiaomimimo" | "mimo" | "xiaomi" => {
Some(Self::XiaomiMimo)
}
"novita" => Some(Self::Novita),
"fireworks" | "fireworks-ai" => Some(Self::Fireworks),
"siliconflow" | "silicon-flow" | "silicon_flow" => Some(Self::Siliconflow),
"siliconflow-cn" | "siliconflow-CN" | "silicon-flow-cn" | "silicon-flow-CN"
| "silicon_flow_cn" | "silicon_flow_CN" | "siliconflow-china" => {
Some(Self::SiliconflowCn)
}
"arcee" | "arcee-ai" | "arcee_ai" => Some(Self::Arcee),
"moonshot" | "moonshot-ai" | "kimi" | "kimi-k2" => Some(Self::Moonshot),
"sglang" | "sg-lang" => Some(Self::Sglang),
"vllm" | "v-llm" => Some(Self::Vllm),
"ollama" | "ollama-local" => Some(Self::Ollama),
"huggingface" | "hugging-face" | "hugging_face" | "hf" => Some(Self::Huggingface),
"together" | "together-ai" | "together_ai" => Some(Self::Together),
"anthropic" | "claude" => Some(Self::Anthropic),
"openai-codex" | "openai_codex" | "openaicodex" | "codex" | "chatgpt"
| "chatgpt-codex" | "chatgpt_codex" | "chatgptcodex" => Some(Self::OpenaiCodex),
_ => None,
let trimmed = value.trim();
// ApiProvider-specific: "deepseek-cn" is a legacy variant here,
// while ProviderKind treats it as a Deepseek alias.
if trimmed.eq_ignore_ascii_case("deepseek-cn")
|| trimmed.eq_ignore_ascii_case("deepseek_china")
|| trimmed.eq_ignore_ascii_case("deepseekcn")
|| trimmed.eq_ignore_ascii_case("deepseek-china")
{
return Some(Self::DeepseekCN);
}
codewhale_config::ProviderKind::parse(value).map(Self::from_kind)
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Deepseek => "deepseek",
Self::DeepseekCN => "deepseek-cn",
Self::NvidiaNim => "nvidia-nim",
Self::Openai => "openai",
Self::Atlascloud => "atlascloud",
Self::WanjieArk => "wanjie-ark",
Self::Volcengine => "volcengine",
Self::Openrouter => "openrouter",
Self::XiaomiMimo => "xiaomi-mimo",
Self::Novita => "novita",
Self::Fireworks => "fireworks",
Self::Siliconflow => "siliconflow",
Self::SiliconflowCn => "siliconflow-CN",
Self::Arcee => "arcee",
Self::Moonshot => "moonshot",
Self::Sglang => "sglang",
Self::Vllm => "vllm",
Self::Ollama => "ollama",
Self::Huggingface => "huggingface",
Self::Together => "together",
Self::OpenaiCodex => "openai-codex",
Self::Anthropic => "anthropic",
match self.kind() {
Some(kind) => kind.as_str(),
None => "deepseek-cn",
}
}
/// Human-friendly label for picker UIs / status chips.
#[must_use]
pub fn display_name(self) -> &'static str {
match self {
Self::Deepseek => "DeepSeek",
Self::DeepseekCN => "DeepSeek (legacy alias)",
Self::NvidiaNim => "NVIDIA NIM",
Self::Openai => "OpenAI-compatible",
Self::Atlascloud => "AtlasCloud",
Self::WanjieArk => "Wanjie Ark",
Self::Volcengine => "Volcengine Ark",
Self::Openrouter => "OpenRouter",
Self::XiaomiMimo => "Xiaomi MiMo",
Self::Novita => "Novita AI",
Self::Fireworks => "Fireworks AI",
Self::Siliconflow => "SiliconFlow",
Self::SiliconflowCn => "SiliconFlow (China)",
Self::Arcee => "Arcee AI",
Self::Moonshot => "Moonshot/Kimi",
Self::Sglang => "SGLang",
Self::Vllm => "vLLM",
Self::Ollama => "Ollama",
Self::Huggingface => "Hugging Face",
Self::Together => "Together AI",
Self::OpenaiCodex => "OpenAI Codex (ChatGPT)",
Self::Anthropic => "Anthropic",
match self.kind() {
Some(kind) => kind.provider().display_name(),
None => "DeepSeek (legacy alias)",
}
}
/// All providers, in the order shown in the picker.
#[must_use]
pub fn all() -> &'static [Self] {
&[
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
Self::WanjieArk,
Self::Volcengine,
Self::Openrouter,
Self::XiaomiMimo,
Self::Novita,
Self::Fireworks,
Self::Siliconflow,
Self::SiliconflowCn,
Self::Arcee,
Self::Moonshot,
Self::Sglang,
Self::Vllm,
Self::Ollama,
Self::Huggingface,
Self::Together,
Self::OpenaiCodex,
Self::Anthropic,
]
&Self::FROM_KIND_LOOKUP
}
/// `ApiProvider` discriminant → `ProviderKind` lookup.
/// Index 1 is `None` for the legacy `DeepseekCN` variant.
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 22] = [
Some(codewhale_config::ProviderKind::Deepseek),
None, // DeepseekCN
Some(codewhale_config::ProviderKind::NvidiaNim),
Some(codewhale_config::ProviderKind::Openai),
Some(codewhale_config::ProviderKind::Atlascloud),
Some(codewhale_config::ProviderKind::WanjieArk),
Some(codewhale_config::ProviderKind::Volcengine),
Some(codewhale_config::ProviderKind::Openrouter),
Some(codewhale_config::ProviderKind::XiaomiMimo),
Some(codewhale_config::ProviderKind::Novita),
Some(codewhale_config::ProviderKind::Fireworks),
Some(codewhale_config::ProviderKind::Siliconflow),
Some(codewhale_config::ProviderKind::SiliconflowCN),
Some(codewhale_config::ProviderKind::Arcee),
Some(codewhale_config::ProviderKind::Moonshot),
Some(codewhale_config::ProviderKind::Sglang),
Some(codewhale_config::ProviderKind::Vllm),
Some(codewhale_config::ProviderKind::Ollama),
Some(codewhale_config::ProviderKind::Huggingface),
Some(codewhale_config::ProviderKind::Together),
Some(codewhale_config::ProviderKind::OpenaiCodex),
Some(codewhale_config::ProviderKind::Anthropic),
];
/// `ProviderKind` discriminant → `ApiProvider` lookup.
const FROM_KIND_LOOKUP: [Self; 21] = [
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
Self::WanjieArk,
Self::Volcengine,
Self::Openrouter,
Self::XiaomiMimo,
Self::Novita,
Self::Fireworks,
Self::Siliconflow,
Self::Arcee,
Self::SiliconflowCn,
Self::Moonshot,
Self::Sglang,
Self::Vllm,
Self::Ollama,
Self::Huggingface,
Self::Together,
Self::OpenaiCodex,
Self::Anthropic,
];
/// Map to the config-level `ProviderKind`.
/// Returns `None` for the legacy `DeepseekCN` variant.
#[must_use]
pub fn kind(self) -> Option<codewhale_config::ProviderKind> {
Self::KIND_LOOKUP[self as usize]
}
/// Construct from a config-level `ProviderKind`.
#[must_use]
pub fn from_kind(kind: codewhale_config::ProviderKind) -> Self {
Self::FROM_KIND_LOOKUP[kind as usize]
}
}
+1 -1
View File
@@ -505,8 +505,8 @@ mod tests {
"Novita AI",
"Fireworks AI",
"SiliconFlow",
"SiliconFlow (China)",
"Arcee AI",
"SiliconFlow (China)",
"Moonshot/Kimi",
"SGLang",
"vLLM",
+29 -26
View File
@@ -21,6 +21,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CONFIG_RS = ROOT / "crates" / "config" / "src" / "lib.rs"
PROVIDER_RS = ROOT / "crates" / "config" / "src" / "provider.rs"
TUI_CONFIG_RS = ROOT / "crates" / "tui" / "src" / "config.rs"
AGENT_RS = ROOT / "crates" / "agent" / "src" / "lib.rs"
PROVIDERS_MD = ROOT / "docs" / "PROVIDERS.md"
@@ -69,35 +70,37 @@ def extract_match_block(
def provider_kind_ids(config_rs: str) -> dict[str, str]:
impl_start = require_index(
config_rs, "impl ProviderKind", "crates/config/src/lib.rs"
provider_rs = read(PROVIDER_RS)
pairs = re.findall(
r"provider!\(\s*\n\s*\w+,\s*\n\s*(\w+),\s*\n\s*\"([^\"]+)\"",
provider_rs,
)
block = extract_match_block(
config_rs,
"pub fn as_str(self) -> &'static str",
"crates/config/src/lib.rs",
impl_start,
)
pairs = re.findall(r"Self::(\w+)\s*=>\s*\"([^\"]+)\"", block)
if not pairs:
raise ValueError("ProviderKind::as_str returned no providers")
return {variant: provider_id for variant, provider_id in pairs}
ids: dict[str, str] = {variant: provider_id for variant, provider_id in pairs}
# OpenaiCodex and Anthropic use manual impls rather than the provider!() macro
for variant_name, id_literal in [
("OpenaiCodex", "openai-codex"),
("Anthropic", "anthropic"),
]:
match = re.search(
rf'impl\s+Provider\s+for\s+{variant_name}.*?fn\s+id.*?\"({id_literal})\"',
provider_rs, re.DOTALL,
)
if match:
ids[variant_name] = match.group(1)
if not ids:
raise ValueError("provider!() invocations returned no providers")
return ids
def api_provider_ids(tui_config_rs: str) -> dict[str, str]:
impl_start = require_index(
tui_config_rs, "impl ApiProvider", "crates/tui/src/config.rs"
)
block = extract_match_block(
tui_config_rs,
"pub fn as_str(self) -> &'static str",
"crates/tui/src/config.rs",
impl_start,
)
pairs = re.findall(r"Self::(\w+)\s*=>\s*\"([^\"]+)\"", block)
if not pairs:
raise ValueError("ApiProvider::as_str returned no providers")
return {variant: provider_id for variant, provider_id in pairs}
# ApiProvider ids derive from ProviderKind ids (via delegation to .kind().as_str())
# plus the legacy "deepseek-cn" variant that exists only in ApiProvider.
variant_to_id = provider_kind_ids("")
# ApiProvider::SiliconflowCn maps to ProviderKind::SiliconflowCN
if "SiliconflowCN" in variant_to_id:
variant_to_id["SiliconflowCn"] = variant_to_id["SiliconflowCN"]
variant_to_id["DeepseekCN"] = "deepseek-cn"
return variant_to_id
def provider_tables(config_rs: str) -> set[str]:
@@ -253,4 +256,4 @@ def main() -> int:
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())