diff --git a/.gitignore b/.gitignore index 2082a02d..b811526e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,7 @@ bindings/node/artifacts/ bindings/node/npm/ bindings/node/bench/node_modules/ bindings/node/bench/package-lock.json -bindings/python/**/__pycache__/ +__pycache__/ bindings/python/uv.lock bindings/kotlin/build/ diff --git a/Cargo.toml b/Cargo.toml index 4ce588ad..d66751c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ members = [ "aimux-provider-utils", "aimux-providers", "aimux-stream", - "scripts/fix_tool", "tools/aimux-cli", "tools/aimux-replay", "tools/aimux-web", @@ -67,14 +66,6 @@ url = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -# Schema / validation -schemars = "0.8" - -# Proc-macro support -proc-macro2 = "1" -syn = { version = "2", features = ["full"] } -quote = "1" - # Internal crates aimux-core = { path = "aimux-core", version = "0.3.0" } aimux-providers = { path = "aimux-providers", version = "0.3.0" } diff --git a/aimux-providers/src/provider.rs b/aimux-providers/src/provider.rs index aee64572..a22fc334 100644 --- a/aimux-providers/src/provider.rs +++ b/aimux-providers/src/provider.rs @@ -1,7 +1,7 @@ //! Registry-backed provider construction (RFC-0017 phase 4). //! -//! Single source of truth: [`provider_registry.json`](provider_registry.json) -//! (generated by `scripts/gen_provider_registry.py`). All built-in +//! Single source of truth: [`provider_registry.json`](provider_registry.json), +//! edited by hand (one row per provider). All built-in //! OpenAI-compatible providers are looked up by name at runtime — there are no //! per-provider `XxxConfig`/`XxxProvider` types anymore (retired in phase 4). //! @@ -83,12 +83,8 @@ static REGISTRY: OnceLock> = OnceLock::new(); fn registry() -> &'static [RegistryEntry] { REGISTRY.get_or_init(|| { let raw = include_str!("provider_registry.json"); - let entries: Vec = serde_json::from_str(raw).unwrap_or_else(|e| { - panic!( - "provider_registry.json is invalid: {e} — regenerate with \ - scripts/gen_provider_registry.py" - ) - }); + let entries: Vec = serde_json::from_str(raw) + .unwrap_or_else(|e| panic!("provider_registry.json is invalid: {e}")); for entry in &entries { assert!(!entry.name.is_empty(), "registry entry missing name"); assert!( diff --git a/scripts/check_missing.py b/scripts/check_missing.py deleted file mode 100644 index ff2d8bdc..00000000 --- a/scripts/check_missing.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -"""Check which inventory providers still have no .rs implementation.""" - -import re -from pathlib import Path - -REPO = Path(__file__).resolve().parent.parent -INV = REPO / "rfc" / "0004-provider-inventory.md" -SRC = REPO / "aimux-providers" / "src" - -# Get all existing .rs module names -existing_mods = {f.stem for f in SRC.glob("*.rs") if f.stem != "lib"} - -# Also check lib.rs for pub mod declarations -lib_rs = (SRC / "lib.rs").read_text(encoding="utf-8") -declared_mods = set(re.findall(r'^pub mod (\w+)', lib_rs, re.MULTILINE)) - -# Name mapping: inventory name -> possible .rs module names -# (inventory names that don't directly map to a filename) -NAME_ALIASES = { - "ollama": ["ollama"], - "openrouter": ["openrouter"], - "mistralrs": ["mistralrs"], - "llamafile": ["llamafile"], - "doubleword": ["doubleword"], - "copilot (github)": ["copilot"], - "github_copilot": ["copilot"], - "github (models)": ["github"], - "chatgpt (订阅)": ["chatgpt"], - "zai (智谱/z.ai)": ["zai"], - "zhipu (智谱/glm)": ["bigmodel", "zai"], - "baidu (文心/ernie)": ["baidu"], - "tencent (混元/hunyuan)": ["tencent"], - "xunfei (讯飞/spark)": ["xunfei"], - "baichuan": ["baichuan"], - "stepfun (阶跃)": ["stepfun"], - "minimax": ["minimax"], - "moonshot (kimi)": ["moonshotai"], - "lingyiwanwu (零一/yi)": ["lingyiwanwu"], - "yi (零一)": ["lingyiwanwu"], - "360 (ai360/zhinao)": ["qihoo360"], - "coze (扣子)": ["coze"], - "siliconflow (硅基流动)": ["siliconflow"], - "qiniu-ai (七牛)": ["qiniu_ai"], - "longcat": ["longcat"], - "bigmodel (智谱)": ["bigmodel"], - "aihubmix": ["aihubmix"], - "mira": ["mira"], - "xiaomimimo (小米)": ["xiaomimimo"], - "byteplus": ["byteplus"], - "perfxcloud": ["perfxcloud"], - "llama.cpp": ["llamacpp"], - "lmstudio": ["lmstudio"], - "vllm": ["vllm"], - "sgl (sglang)": ["sglang"], - "sglang": ["sglang"], - "xinference": ["xinference"], - "omlx / mlx_lm": ["omlx"], - "mlx": ["mlx"], - "local (通用本地)": ["local"], - "cybertron": ["cybertron"], - "jlama": ["jlama"], - "localai": ["localai"], - "onnx": ["onnx"], - "openvino": ["openvino"], - "gaudi": ["gaudi"], - "jina": ["jina"], - "jina_ai": ["jina"], - "nomic": ["nomic"], - "mixedbread": ["mixedbread"], - "clip": ["clip"], - "pg_vector": ["pg_vector"], - "s3_vectors": ["s3_vectors"], - "milvus (向量库)": ["milvus"], - "qdrant (向量库)": ["qdrant"], - "fastembed": ["fastembed"], - "stability": ["stability_ai"], - "stability-ai": ["stability_ai"], - "recraft": ["recraft"], - "runwayml": ["runwayml"], - "runway": ["runwayml"], - "aws_polly": ["aws_polly"], - "nvidia_riva": ["nvidia_riva"], - "soniox": ["soniox"], - "midjourney": ["midjourney"], - "sora": ["sora"], - "vidu (视频)": ["vidu"], - "vidu": ["vidu"], - "jimeng (即梦)": ["jimeng"], - "ideogram": ["ideogram"], - "flux": ["flux"], - "suno (音乐)": ["suno"], - "suno": ["suno"], - "meshy": ["meshy"], - "tripo3d": ["tripo3d"], - "segmind": ["segmind"], - "runware": ["runware"], - "sarvam": ["sarvam"], - "murf": ["murf"], - "playai": ["playai"], - "speechify": ["speechify"], - "inworld": ["inworld"], - "ai21": ["ai21"], - "anyscale": ["anyscale"], - "sambanova": ["sambanova"], - "predibase": ["predibase"], - "triton (nvidia)": ["triton"], - "triton": ["triton"], - "databricks": ["databricks"], - "sagemaker (aws)": ["sagemaker"], - "watsonx (ibm)": ["watsonx"], - "scaleway": ["scaleway"], - "snowflake": ["snowflake"], - "sap": ["sap"], - "oci (oracle)": ["oci"], - "nlp_cloud": ["nlp_cloud"], - "friendliai": ["friendliai"], - "clarifai": ["clarifai"], - "gigachat (三星)": ["gigachat"], - "codestral": ["codestral"], - "morph": ["morph"], - "v0": ["v0"], - "aiml": ["aiml"], - "heroku": ["heroku"], - "hosted_vllm": ["hosted_vllm"], - "nvidia_nim": ["nvidia_nim"], - "nscale": ["nscale"], - "lambda_ai": ["lambda_ai"], - "petals": ["petals"], - "oobabooba": ["oobabooba"], - "inception": ["inception"], - "galadriel": ["galadriel"], - "gdc": ["gdc"], - "datarobot": ["datarobot"], - "infinity": ["infinity"], - "kluster-ai": ["kluster_ai"], - "krutrim": ["krutrim"], - "bytez": ["bytez"], - "upstage": ["upstage"], - "deepbricks": ["deepbricks"], - "lemonfox-ai": ["lemonfox_ai"], - "inference-net": ["inference_net"], - "302ai": ["ai302"], - "matterai": ["matterai"], - "nextbit": ["nextbit"], - "modal": ["modal"], - "aibadgr": ["aibadgr"], - "ncompass": ["ncompass"], - "reka-ai": ["reka_ai"], - "sgl": ["sglang"], - "parasail": ["parasail"], - "wafer": ["wafer"], - "bedrock_mantle": ["bedrock_mantle"], - "nous (nousresearch)": ["nous_research"], - "opencode-go": ["opencode_go"], - "opencode-zen": ["opencode_zen"], - "kiro": ["kiro"], - "pioneer": ["pioneer"], - "kilo": ["kilo"], - "cline-pass": ["cline_pass"], - "commandcode": ["commandcode"], - "portkey": ["portkey"], - "helicone": ["helicone"], - "requesty": ["requesty"], - "cometapi": ["cometapi"], - "novita": ["novita"], - "submodel": ["submodel"], - "api2d": ["api2d"], - "ohmygpt": ["ohmygpt"], - "closeai": ["closeai"], - "openaisb": ["openaisb"], - "openaimax": ["openaimax"], - "ails": ["ails"], - "api2gpt": ["api2gpt"], - "aigc2d": ["aigc2d"], - "fastgpt": ["fastgpt"], - "tokenpony": ["tokenpony"], - "fastrouter": ["fastrouter"], - "orcarouter": ["orcarouter"], - "ovhcloud": ["ovhcloud"], - "nebius": ["nebius"], - "hyperbolic": ["hyperbolic"], - "featherless-ai": ["featherless_ai"], - "meta": ["meta_llama"], - "meta_llama": ["meta_llama"], - "modelscope (魔搭)": ["modelscope"], - "docker_model_runner": ["docker_model_runner"], - "ollama_cloud": ["ollama_cloud"], - "litellm_proxy": ["litellm_proxy"], - "compactifai": ["compactifai"], - "doubaoaudio": ["doubaoaudio"], - "mokaai": ["mokaai"], - "skylark (云雀)": ["skylark"], - "bing (new bing)": ["bing"], - "slack (slack claude)": ["slack"], - "deepl": ["deepl"], - "dify": ["dify"], - "doc2x": ["doc2x"], - "sangforaicp": ["sangforaicp"], - "streamlake": ["streamlake"], - "antling": ["antling"], - "text_embeddings_inference": ["text_embeddings_inference", "tei"], - "tei (text embeddings inference)": ["tei", "text_embeddings_inference"], - "sakana": ["sakana"], - "scx-ai": ["scx_ai"], - "atlascloud": ["atlascloud"], - "canopywave": ["canopywave"], - "embercloud": ["embercloud"], - "tundra": ["tundra"], - "reve": ["reve"], - "gonka24": ["gonka24"], - "albert": ["albert"], - "fastcrw": ["fastcrw"], - "apiserpent": ["apiserpent"], - "nanogpt": ["nanogpt"], - "vercel (ai gateway)": ["vercel"], - "gradient_ai": ["gradient_ai"], - "azure_ai": ["azure_ai"], -} - -# Parse inventory for all provider names in tables -content = INV.read_text(encoding="utf-8") -cross = "\u274C" -check = "\u2705" - -missing = [] -for line in content.split("\n"): - if not line.startswith("|"): - continue - parts = [p.strip() for p in line.split("|")] - if len(parts) < 4: - continue - name = parts[1] - if not name or name in ("厂商", "厂商/类型", "模块"): - continue - if name.startswith("-"): - continue - - # Check if aimux column (parts[2]) still has ❌ - if cross not in parts[2]: - continue # already has some ✅ - - # This provider is marked ❌ in aimux column - # Check if we have an implementation - aliases = NAME_ALIASES.get(name, [name.replace("-", "_").replace(".", "_").replace("/", "_").replace(" ", "_").lower()]) - # Also try the raw name - aliases = list(set(aliases + [name.replace("-", "_").replace(".", "_").replace(" ", "_").lower()])) - - found = False - for alias in aliases: - if alias in existing_mods or alias in declared_mods: - found = True - break - if not found: - missing.append(name) - -print(f"Still missing (cross mark in inventory, no .rs found): {len(missing)}") -for m in missing: - print(f" {m}") diff --git a/scripts/fix_profiles.rs b/scripts/fix_profiles.rs deleted file mode 100644 index ef12e044..00000000 --- a/scripts/fix_profiles.rs +++ /dev/null @@ -1,105 +0,0 @@ -// 批量给薄封装加 OpenAICompatProfile::full() -// 用法: cargo run --release -p fix-profiles -// 或者直接用 rustc 跑 -use std::fs; -use std::path::PathBuf; - -fn main() { - let dir = PathBuf::from("aimux-providers/src"); - let mut fixed = 0; - let mut skipped = 0; - - for entry in fs::read_dir(&dir).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - let name = path.file_name().unwrap().to_str().unwrap().to_string(); - - // 跳过非 .rs 文件和已处理的文件 - if !name.ends_with(".rs") || name == "lib.rs" || name == "open_responses.rs" { - continue; - } - if name == "groq.rs" || name == "alibaba.rs" { - continue; - } - - let content = fs::read_to_string(&path).unwrap(); - - // 只处理薄封装(包含 OpenAIProvider::new 但不含 OpenAICompatProfile) - if !content.contains("OpenAIProvider::new") || content.contains("OpenAICompatProfile") { - skipped += 1; - continue; - } - - let mut new_content = content.clone(); - - // 1. 加 import:把 use crate::openai::{OpenAIConfig, ...} 改成包含 OpenAICompatProfile - // 情况 A: use crate::openai::{OpenAIConfig, OpenAIModel, OpenAIProvider}; - if new_content.contains("use crate::openai::{OpenAIConfig") { - new_content = new_content.replace( - "use crate::openai::{OpenAIConfig", - "use crate::openai::{OpenAICompatProfile, OpenAIConfig", - ); - } - // 情况 B: use crate::openai::OpenAIConfig; (单独一行) - else if new_content.contains("use crate::openai::OpenAIConfig;") { - new_content = new_content.replace( - "use crate::openai::OpenAIConfig;", - "use crate::openai::{OpenAICompatProfile, OpenAIConfig};", - ); - } - - // 2. 在 OpenAIConfig::new(api_key) 链的末尾加 .with_profile(OpenAICompatProfile::full()) - // 模式 A: .with_provider(PROVIDER_NAME)\n ) → 加 .with_profile 在 ) 前 - // 模式 B: .with_base_url(DEFAULT_BASE_URL)\n ) → 加 .with_provider + .with_profile - - // 先试模式 A:已有 .with_provider(PROVIDER_NAME) - if new_content.contains(".with_provider(PROVIDER_NAME)") { - // 在 .with_provider(PROVIDER_NAME) 后面加 .with_profile - // 需要找缩进 - if let Some(pos) = new_content.find(".with_provider(PROVIDER_NAME)") { - let line_start = &new_content[pos..]; - // 找这行结尾的换行 - if let Some(nl) = line_start.find('\n') { - let after = &line_start[nl+1..]; - // 找下一个非空白行 - let indent: String = after.chars().take_while(|c| c.is_whitespace() && *c != '\n').collect(); - let insert = format!("\n{}.with_profile(OpenAICompatProfile::full())", indent); - // 只在后面是 ) 时插入 - let rest = after.trim_start(); - if rest.starts_with(')') { - let split = pos + line_start[..nl].len(); - new_content = format!("{}{}{}", &new_content[..split], insert, &new_content[split..]); - } - } - } - } - // 模式 B:只有 .with_base_url(DEFAULT_BASE_URL),没有 .with_provider - else if new_content.contains(".with_base_url(DEFAULT_BASE_URL)") { - // 在 .with_base_url(DEFAULT_BASE_URL) 后加 .with_provider(PROVIDER_NAME) 和 .with_profile - if let Some(pos) = new_content.find(".with_base_url(DEFAULT_BASE_URL)") { - let line_start = &new_content[pos..]; - if let Some(nl) = line_start.find('\n') { - let after = &line_start[nl+1..]; - let indent: String = after.chars().take_while(|c| c.is_whitespace() && *c != '\n').collect(); - let rest = after.trim_start(); - if rest.starts_with(')') { - let insert = format!("\n{}.with_provider(PROVIDER_NAME)\n{}.with_profile(OpenAICompatProfile::full())", indent, indent); - let split = pos + line_start[..nl].len(); - new_content = format!("{}{}{}", &new_content[..split], insert, &new_content[split..]); - } - } - } - } - - if new_content != content { - fs::write(&path, new_content).unwrap(); - fixed += 1; - println!("Fixed: {}", name); - } else { - println!("SKIP (no pattern matched): {}", name); - skipped += 1; - } - } - - println!("\n总计: {} fixed, {} skipped", fixed, skipped); -} diff --git a/scripts/fix_profiles2.rs b/scripts/fix_profiles2.rs deleted file mode 100644 index 46c37ca4..00000000 --- a/scripts/fix_profiles2.rs +++ /dev/null @@ -1,68 +0,0 @@ -// 修复 with_profile 插入位置错误 -use std::fs; -use std::path::PathBuf; - -fn main() { - let dir = PathBuf::from("aimux-providers/src"); - let mut fixed = 0; - - for entry in fs::read_dir(&dir).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - let name = path.file_name().unwrap().to_str().unwrap().to_string(); - if !name.ends_with(".rs") || name == "lib.rs" { - continue; - } - - let content = fs::read_to_string(&path).unwrap(); - if !content.contains("OpenAICompatProfile") { - continue; - } - - let mut new_content = content.clone(); - - // 修复模式 A: .with_provider(PROVIDER_NAME),\n .with_profile(...)\n ) - // 改成: .with_provider(PROVIDER_NAME)\n .with_profile(...),\n ) - new_content = new_content.replace( - ".with_provider(PROVIDER_NAME),\n .with_profile(OpenAICompatProfile::full())\n )", - ".with_provider(PROVIDER_NAME)\n .with_profile(OpenAICompatProfile::full()),\n )", - ); - - // 修复模式 B: .with_base_url(DEFAULT_BASE_URL),\n .with_provider(PROVIDER_NAME)\n .with_profile(...)\n ) - // 改成: .with_base_url(DEFAULT_BASE_URL)\n .with_provider(PROVIDER_NAME)\n .with_profile(...),\n ) - new_content = new_content.replace( - ".with_base_url(DEFAULT_BASE_URL),\n .with_provider(PROVIDER_NAME)\n .with_profile(OpenAICompatProfile::full())\n )", - ".with_base_url(DEFAULT_BASE_URL)\n .with_provider(PROVIDER_NAME)\n .with_profile(OpenAICompatProfile::full()),\n )", - ); - - // 通用修复:任何 .with_profile(OpenAICompatProfile::full())\n ) - // 前面缺逗号且缩进不对的,修成正确格式 - // 模式: ,\n .with_profile(OpenAICompatProfile::full())\n ) - // 这种在 ) 前面没逗号的情况 - if new_content.contains(".with_profile(OpenAICompatProfile::full())\n )") { - // 检查是不是已经有了正确的逗号 - if !new_content.contains(".with_profile(OpenAICompatProfile::full()),\n )") { - new_content = new_content.replace( - ".with_profile(OpenAICompatProfile::full())\n )", - ".with_profile(OpenAICompatProfile::full()),\n )", - ); - } - } - - // 修复模式: .with_profile(...) 出现在 ) 之后(独立行以 . 开头) - // 即: ),\n .with_profile(...) - // 这种应该是: .with_profile(...),\n ) - if new_content.contains("),\n .with_profile(OpenAICompatProfile::full())") { - // 这种是错误的——需要把 .with_profile 移到 ) 前面 - // 但这个比较复杂,先标记 - } - - if new_content != content { - fs::write(&path, new_content).unwrap(); - fixed += 1; - println!("Fixed: {}", name); - } - } - - println!("\n总计: {} fixed", fixed); -} diff --git a/scripts/fix_profiles3.rs b/scripts/fix_profiles3.rs deleted file mode 100644 index a12003da..00000000 --- a/scripts/fix_profiles3.rs +++ /dev/null @@ -1,42 +0,0 @@ -// 修复残留的 with_profile 位置错误 -use std::fs; -use std::path::PathBuf; - -fn main() { - let dir = PathBuf::from("aimux-providers/src"); - let mut fixed = 0; - - for entry in fs::read_dir(&dir).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - let name = path.file_name().unwrap().to_str().unwrap().to_string(); - if !name.ends_with(".rs") || name == "lib.rs" { - continue; - } - - let content = fs::read_to_string(&path).unwrap(); - let mut new_content = content.clone(); - - // 修复: .with_provider(PROVIDER_NAME),\n .with_profile(OpenAICompatProfile::full()),\n ) - // → .with_provider(PROVIDER_NAME)\n .with_profile(OpenAICompatProfile::full()),\n ) - new_content = new_content.replace( - ".with_provider(PROVIDER_NAME),\n .with_profile(OpenAICompatProfile::full()),\n )", - ".with_provider(PROVIDER_NAME)\n .with_profile(OpenAICompatProfile::full()),\n )", - ); - - // 修复: .with_base_url(DEFAULT_BASE_URL),\n .with_provider(PROVIDER_NAME),\n .with_profile(...) - // → 正确格式 - new_content = new_content.replace( - ".with_base_url(DEFAULT_BASE_URL),\n .with_provider(PROVIDER_NAME),\n .with_profile(OpenAICompatProfile::full()),\n )", - ".with_base_url(DEFAULT_BASE_URL)\n .with_provider(PROVIDER_NAME)\n .with_profile(OpenAICompatProfile::full()),\n )", - ); - - if new_content != content { - fs::write(&path, new_content).unwrap(); - fixed += 1; - println!("Fixed: {}", name); - } - } - - println!("\n总计: {} fixed", fixed); -} diff --git a/scripts/fix_profiles4.rs b/scripts/fix_profiles4.rs deleted file mode 100644 index b866ad83..00000000 --- a/scripts/fix_profiles4.rs +++ /dev/null @@ -1,49 +0,0 @@ -// 修复残留的 with_profile 位置错误 - 更通用的匹配 -use std::fs; -use std::path::PathBuf; -use regex::Regex; - -fn main() { - let dir = PathBuf::from("aimux-providers/src"); - let mut fixed = 0; - - // 匹配: .with_provider(PROVIDER_NAME),\n.with_profile(OpenAICompatProfile::full()),\n) - let re = Regex::new( - r"\.with_provider\(PROVIDER_NAME\),\n\s+\.with_profile\(OpenAICompatProfile::full\(\)\),\n\s+\)" - ).unwrap(); - - // 也匹配: .with_base_url(DEFAULT_BASE_URL),\n.with_provider(PROVIDER_NAME),\n.with_profile(...) - let re2 = Regex::new( - r"\.with_base_url\(DEFAULT_BASE_URL\),\n\s+\.with_provider\(PROVIDER_NAME\),\n\s+\.with_profile\(OpenAICompatProfile::full\(\)\),\n\s+\)" - ).unwrap(); - - for entry in fs::read_dir(&dir).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - let name = path.file_name().unwrap().to_str().unwrap().to_string(); - if !name.ends_with(".rs") || name == "lib.rs" { - continue; - } - - let content = fs::read_to_string(&path).unwrap(); - let mut new_content = content.clone(); - - // 先修 re2(更长的模式) - new_content = re2.replace_all(&new_content, - ".with_base_url(DEFAULT_BASE_URL)\n .with_provider(PROVIDER_NAME)\n .with_profile(OpenAICompatProfile::full()),\n )" - ).to_string(); - - // 再修 re - new_content = re.replace_all(&new_content, - ".with_provider(PROVIDER_NAME)\n .with_profile(OpenAICompatProfile::full()),\n )" - ).to_string(); - - if new_content != content { - fs::write(&path, new_content).unwrap(); - fixed += 1; - println!("Fixed: {}", name); - } - } - - println!("\n总计: {} fixed", fixed); -} diff --git a/scripts/fix_profiles5.rs b/scripts/fix_profiles5.rs deleted file mode 100644 index 60678e81..00000000 --- a/scripts/fix_profiles5.rs +++ /dev/null @@ -1,81 +0,0 @@ -// 修复残留的 with_profile 位置错误 - 纯字符串匹配 -use std::fs; -use std::path::PathBuf; - -fn main() { - let dir = PathBuf::from("aimux-providers/src"); - let mut fixed = 0; - - for entry in fs::read_dir(&dir).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - let name = path.file_name().unwrap().to_str().unwrap().to_string(); - if !name.ends_with(".rs") || name == "lib.rs" { - continue; - } - - let content = fs::read_to_string(&path).unwrap(); - if !content.contains("with_profile") { - continue; - } - - let mut new_content = content.clone(); - - // 通用修复:找到 .with_provider(PROVIDER_NAME), 后面跟着换行+空格+.with_profile 的模式 - // 把它改成 .with_provider(PROVIDER_NAME)\n .with_profile(...),\n ) - // 通过逐行处理 - let lines: Vec<&str> = new_content.lines().collect(); - let mut result = String::new(); - let mut i = 0; - while i < lines.len() { - let line = lines[i]; - // 检查当前行是否以 .with_provider(PROVIDER_NAME), 结尾 - if line.trim_end().ends_with(".with_provider(PROVIDER_NAME),") { - // 去掉末尾逗号 - let trimmed = line.trim_end(); - let without_comma = &trimmed[..trimmed.len() - 1]; - result.push_str(without_comma); - result.push('\n'); - i += 1; - // 检查下一行是否是 .with_profile(OpenAICompatProfile::full()), - if i < lines.len() { - let next = lines[i].trim(); - if next.starts_with(".with_profile(OpenAICompatProfile::full())") { - // 加正确的缩进(16空格) - result.push_str(" .with_profile(OpenAICompatProfile::full()),\n"); - i += 1; - // 跳过下一个 ) 行,加正确的 ) - if i < lines.len() && lines[i].trim() == ")" { - result.push_str(" )\n"); - i += 1; - } - continue; - } - } - // 如果不是预期模式,恢复逗号 - result = result.trim_end_matches(without_comma).to_string(); - result.push_str(line); - result.push('\n'); - } else { - result.push_str(line); - result.push('\n'); - i += 1; - } - } - - // 同样修复 .with_base_url(DEFAULT_BASE_URL), 后跟 .with_provider 的模式 - // 但这更复杂,先只处理上面的 - - if new_content.trim_end_matches('\n') != result.trim_end_matches('\n') && result.contains(".with_profile(OpenAICompatProfile::full()),\n )") { - new_content = result; - } - - if new_content != content { - fs::write(&path, &new_content).unwrap(); - fixed += 1; - println!("Fixed: {}", name); - } - } - - println!("\n总计: {} fixed", fixed); -} diff --git a/scripts/fix_tool/Cargo.toml b/scripts/fix_tool/Cargo.toml deleted file mode 100644 index e1e9f7de..00000000 --- a/scripts/fix_tool/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "fix_tool" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -description.workspace = true -rust-version.workspace = true - -[dependencies] - -[lints] -workspace = true diff --git a/scripts/fix_tool/src/main.rs b/scripts/fix_tool/src/main.rs deleted file mode 100644 index e7a11a96..00000000 --- a/scripts/fix_tool/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/scripts/gen_provider_registry.py b/scripts/gen_provider_registry.py deleted file mode 100644 index d53bcd0d..00000000 --- a/scripts/gen_provider_registry.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -"""Generate provider-registry.json from openai_compat_registry.rs (RFC-0017 phase 4). - -Single source of truth: the registry declaration file. This script extracts -name/display/base_url/env_var/profile for all 250 OpenAI-compatible providers -and writes aimux-providers/src/provider_registry.json (embedded via include_str!). -Also regenerates the ProviderName derived types (Rust enum / TS union / ...). - -Usage: uv run python scripts/gen_provider_registry.py -Idempotent: overwrites the output files. -""" - -import json -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -REGISTRY = ROOT / "aimux-providers" / "src" / "openai_compat_registry.rs" -OUT_JSON = ROOT / "aimux-providers" / "src" / "provider_registry.json" - -BLOCK_RE = re.compile( - r"declare_openai_compat_provider!\((.*?)\);", re.DOTALL -) -PROFILE_RE = re.compile( - r"OpenAICompatProfile::(\w+)\(\)(.*?)$", re.DOTALL -) -MAX_TOKENS_RE = re.compile(r'\.with_max_tokens_key\("([^"]+)"\)') - - -def parse_profile(expr: str) -> dict: - """Translate a profile expression into its JSON form (non-default fields only).""" - m = PROFILE_RE.search(expr.strip()) - if not m: - raise ValueError(f"unparseable profile expr: {expr!r}") - kind = m.group(1) - rest = m.group(2) - - profile: dict = {} - if kind == "groq": - # Full groq() semantics (mod.rs OpenAICompatProfile::groq()): - # supports_top_k=false, stream_usage_key="x_groq", - # max_tokens_key="max_completion_tokens" (stage2-002 wiring). - profile.update({ - "supports_top_k": False, - "stream_usage_key": "x_groq", - "max_tokens_key": "max_completion_tokens", - }) - elif kind == "deepseek": - # deepseek() now returns full() semantics (RFC-0017 phase 2 retirements) - pass - elif kind != "full": - raise ValueError(f"unknown profile kind: {kind}") - - mt = MAX_TOKENS_RE.search(rest) - if mt: - profile["max_tokens_key"] = mt.group(1) - - return profile - - -def main() -> int: - text = REGISTRY.read_text(encoding="utf-8") - entries = [] - for block in BLOCK_RE.finditer(text): - body = re.sub(r"\s+", " ", block.group(1)).strip() - parts = body.split(", ") - if len(parts) < 7: - raise ValueError(f"malformed block: {block.group(0)[:80]!r}") - name = parts[0].strip('"') - display = parts[3].strip('"') - # base_url part may carry a preceding `//` comment line (freemodel); - # take the LAST quoted string in the part. xpersona's "/v1" is a known - # unresolved base_url (research backlog), kept as-is. - base_urls = re.findall(r'"([^"]*)"', parts[4]) - if not base_urls: - raise ValueError(f"no string literal in base_url part: {parts[4]!r}") - base_url = base_urls[-1] - env_var = re.match(r'"([^"]*)"', parts[5]).group(1) - profile = parse_profile(parts[6]) - entries.append({ - "name": name, - "display": display, - "base_url": base_url, - "env_var": env_var, - "profile": profile, - }) - - entries.sort(key=lambda e: e["name"]) - OUT_JSON.write_text( - json.dumps(entries, ensure_ascii=False, indent=2) + "\n", - encoding="utf-8", - ) - print(f"provider_registry.json: {len(entries)} providers -> {OUT_JSON.relative_to(ROOT)}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/gen_responses_convert.py b/scripts/gen_responses_convert.py deleted file mode 100644 index 794a4391..00000000 --- a/scripts/gen_responses_convert.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python3 -"""Generate `openai/responses/responses_convert.rs` (RFC-0012 §3.5 shared framework). - -This was a ONE-TIME extraction script: it copied the byte-identical -non-streaming parser and streaming SSE reducer out of a pre-extraction -`openai/responses/mod.rs` (which Azure duplicated verbatim) into the shared -module. That extraction has been applied and the bodies no longer exist in -`mod.rs` — `responses_convert.rs` is now the hand-maintained source of truth -(it has since gained `status`/`raw_body` parameters for §2.2 in-band 2xx error -classification, which no pre-extraction source ever contained). - -Re-running therefore fails with a clear error unless you point it at a -pre-extraction source tree via AIMUX_SRC_ROOT or argv[1]. Identifier rewrites -applied to the verbatim bodies: - - `convert::parse_usage` -> `parse_usage` (imported here) - - `provider_key_stream` -> `provider_key` (the param) - - `request_result.warnings` -> `request_warnings` (the param) -""" - -import os -import sys -from pathlib import Path - -# Repo root: overridable for a historical/pre-extraction checkout; defaults to -# the repo containing this script. -REPO = Path(os.environ.get("AIMUX_SRC_ROOT", sys.argv[1] if len(sys.argv) > 1 else Path(__file__).resolve().parents[1])) -SRC = REPO / "aimux-providers/src/openai/responses/mod.rs" -OUT = REPO / "aimux-providers/src/openai/responses/responses_convert.rs" - -if not SRC.is_file(): - sys.exit(f"error: source file not found: {SRC}") - -lines = SRC.read_text(encoding="utf-8").splitlines(keepends=True) - - -def find(marker: str, after: int = 0) -> int: - """Return the 1-indexed line number of the first line whose stripped - content equals `marker`, searching from line `after` (1-indexed) onward. - """ - for i in range(after, len(lines)): - if lines[i].strip() == marker: - return i + 1 - sys.exit( - f"error: extraction marker not found in {SRC}: {marker!r}\n" - "The RFC-0012 §3.5 extraction has already been applied — mod.rs no\n" - "longer contains the parser/reducer bodies, and the checked-in\n" - "responses_convert.rs is hand-maintained (it now threads the observed\n" - "HTTP status and raw body, which the pre-extraction source lacked).\n" - "Edit responses_convert.rs directly; only re-run this script against a\n" - "pre-extraction checkout (AIMUX_SRC_ROOT= or argv[1])." - ) - - -# do_generate core: from the "Top-level error field" comment through the `})` -# that closes `Ok(GenerateResult { ... })`. -gen_start = find("// Top-level error field.") -ok_line = find("Ok(GenerateResult {", after=gen_start) -gen_close = find("})", after=ok_line) -generate_body = "".join(lines[gen_start - 1 : gen_close]) - -# do_stream reducer: the interior of `async_stream::stream! { ... }`, i.e. from -# the line after the `stream! {` opener up to (excluding) the `};` closer. The -# closer is the `};` that immediately precedes `Ok(StreamResult {` — scanning -# backward from that return avoids matching inner `match { };` closers. -stream_open = find("let stream = async_stream::stream! {") -ok_stream = find("Ok(StreamResult {", after=stream_open) -sc = ok_stream - 1 -while sc > stream_open and lines[sc - 1].strip() != "};": - sc -= 1 -stream_close = sc -stream_body = "".join(lines[stream_open : stream_close - 1]) - -# Rewrite identifiers so the verbatim bodies resolve inside the shared module. -generate_body = generate_body.replace("convert::parse_usage", "parse_usage") -generate_body = generate_body.replace("request_result.warnings", "request_warnings") -stream_body = stream_body.replace("convert::parse_usage", "parse_usage") -stream_body = stream_body.replace("provider_key_stream", "provider_key") - - -def reindent(block: str, from_n: int, to_n: int) -> str: - """Strip `from_n` leading spaces and prepend `to_n` (net shift to_n-from_n).""" - out = [] - for line in block.splitlines(keepends=True): - if line.strip() == "": - out.append("\n") - continue - if line.startswith(" " * from_n): - out.append(" " * to_n + line[from_n:]) - else: - out.append(line) - return "".join(out) - - -HEADER = """//! Shared Responses API framework (RFC-0012 §3.5). -//! -//! Vendors whose Responses implementations speak the OpenAI wire format -//! (currently OpenAI and Azure OpenAI) share this module for the parts that are -//! byte-identical across them: -//! - non-streaming output parsing — [`build_responses_generate_result`], -//! - the streaming SSE event reducer — [`build_responses_event_stream`], -//! - common HTTP header list construction — [`build_header_list`]. -//! -//! Vendors with genuinely different protocols (xAI, HuggingFace, the generic -//! `open_responses` provider) keep their own request/streaming logic and reuse -//! only the small shared helpers where they are byte-identical. Per the RFC, -//! genuinely different streaming loops are **not** force-merged into one -//! function — only the shared framework is extracted. - -use std::collections::HashMap; -use std::pin::Pin; - -use futures::{Stream, StreamExt}; -use serde_json::{Value, json}; - -use aimux_core::error::AiMuxError; -use aimux_core::error::ApiCallError; -use aimux_core::result::{GenerateContent, GenerateResult}; -use aimux_core::stream_part::StreamPart; -use aimux_core::types::{FinishReason, FinishReasonUnified, ResponseMetadata, Usage, Warning}; -use aimux_stream::{SseError, SseEvent}; - -use super::convert::{convert_responses_usage, map_responses_finish_reason, parse_usage}; -use super::types::ResponsesUsage; - -/// Pinned, boxed stream of model stream parts. -/// -/// Matches the `stream` field of [`aimux_core::result::StreamResult`]. Used as -/// the return type of the shared streaming reducer so the boxed trait object -/// does not leak a complex type into call sites. -pub type ResponsesEventStream = Pin> + Send>>; - -/// Build the `Vec<(String, String)>` header list for an `HttpRequest`, appending -/// `Content-Type: application/json`. -/// -/// Byte-identical copies previously lived in the OpenAI, HuggingFace and xAI -/// responses modules; they now route through this single implementation. -pub fn build_header_list(headers: &HashMap) -> Vec<(String, String)> { - let mut list: Vec<(String, String)> = - headers.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); - list.push(("Content-Type".to_string(), "application/json".to_string())); - list -} - -// -- Non-streaming output parsing -------------------------------------------- - -/// Parse a non-streaming Responses API JSON body into a [`GenerateResult`]. -/// -/// Shared verbatim by the OpenAI and Azure providers: both speak the same -/// Responses wire format for non-streaming output (top-level error, `output` -/// array of `message`/`function_call`/`custom_tool_call`/`reasoning` items, -/// `incomplete_details`, `usage`, provider metadata with `responseId` / -/// `reasoningContext` / `serviceTier`). Vendor callers supply the parsed `data`, -/// the observed HTTP `status` and full `raw_body` (evidence for in-band 2xx -/// errors), the request `body`/`response_headers` to attach, and the -/// provider-metadata namespace `provider_key` ("openai" / "azure"). -pub fn build_responses_generate_result( - data: &Value, - status: u16, - raw_body: &str, - request_warnings: Vec, - provider_key: String, - body: Value, - response_headers: HashMap, -) -> Result { -""" - -generate_body_i = reindent(generate_body, 8, 4) -FOOTER_GENERATE = "}\n\n" - -STREAM_HEADER = """// -- Streaming SSE event reducer --------------------------------------------- - -/// A tool call being streamed (tracked by `output_index`). -#[allow(dead_code)] -struct OngoingToolCall { - tool_name: String, - tool_call_id: String, -} - -/// A reasoning item being streamed (tracked by `item_id`). -struct ReasoningState { - encrypted_content: Option, - /// summary_index → status. - summary_parts: HashMap, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -enum SummaryStatus { - Active, - CanConclude, - Concluded, -} - -/// Build the streaming event reducer shared by the OpenAI and Azure providers. -/// -/// Both speak the same Responses streaming wire format (the -/// `response.created -> output_item.added -> output_text.delta -> -/// output_item.done -> response.completed` main path, plus -/// `function_call_arguments.delta`, `custom_tool_call_input.delta`, -/// `reasoning_summary_part.added/done` and `reasoning_summary_text.delta`). -/// -/// The caller performs the HTTP send (`send_stream`) and hands the observed -/// HTTP `status` plus the peeked `first_event` and remainder `sse_stream` to -/// this reducer; an early `error` / `response.failed` surfaces as a clean -/// `Err` here. -pub fn build_responses_event_stream( - first_event: Option>, - sse_stream: S, - status: u16, - provider_key: String, - warnings: Vec, - store_flag: bool, -) -> Result -where - S: Stream> + Unpin + Send + 'static, -{ - // Peek at the first SSE event to detect early errors (before any output). - if let Some(Ok(ref event)) = first_event - && let Ok(val) = serde_json::from_str::(&event.data) - { - let etype = val.get("type").and_then(|v| v.as_str()).unwrap_or(""); - if etype == "error" || etype == "response.failed" { - let message = val - .get("response") - .and_then(|r| r.get("error")) - .and_then(|e| e.get("message")) - .and_then(|v| v.as_str()) - .or_else(|| { - val.get("error") - .and_then(|e| e.get("message")) - .and_then(|v| v.as_str()) - }) - .unwrap_or("Responses API stream error"); - return Err(AiMuxError::ApiCall(ApiCallError { - // Mid-stream provider failure arrives on a successful HTTP - // response: keep the observed 2xx status (§2.2). - status_code: Some(status), - provider_code: val - .get("response") - .and_then(|r| r.get("error")) - .or_else(|| val.get("error")) - .and_then(|e| e.get("type").or_else(|| e.get("code"))) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - message: message.to_string(), - response_body: Some(event.data.clone()), - ..Default::default() - })); - } - } - - let stream = async_stream::stream! { -""" - -stream_body_i = reindent(stream_body, 12, 8) -STREAM_FOOTER = """ }; - Ok(Box::pin(stream)) -} -""" - -content = ( - HEADER - + generate_body_i - + FOOTER_GENERATE - + STREAM_HEADER - + stream_body_i - + STREAM_FOOTER -) - -OUT.write_text(content, encoding="utf-8") -print(f"wrote {OUT} ({len(content.splitlines())} lines)") diff --git a/scripts/gen_vertex_maas_providers.py b/scripts/gen_vertex_maas_providers.py deleted file mode 100644 index 7bc982a5..00000000 --- a/scripts/gen_vertex_maas_providers.py +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/env python3 -"""Generate the 10 vertex_ai_*_models providers. - -Each provider is a thin OpenAI-compatible wrapper over `OpenAIProvider` that -points at the Vertex AI MaaS OpenAPI endpoint and authenticates with a Google -Cloud Bearer token. This script writes the 10 `.rs` files and patches -`lib.rs` (module + re-export registration) and the shared -`openai_compatible_test.rs` (imports + macro invocations). - -Re-runnable: the `.rs` files are overwritten and the lib/test patches are -applied idempotently (anchored string replacement that asserts a single match). -""" - -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -SRC = ROOT / "aimux-providers" / "src" -LIB = SRC / "lib.rs" -TEST = ROOT / "aimux-providers" / "tests" / "openai_compatible_test.rs" - -BOM = "\ufeff" - -# Ordered alphabetically by Pascal name (ai21 < anthropic < ... < zai), which -# also matches module-name order so rustfmt's reorder_modules/imports is a no-op. -PROVIDERS = [ - { - "canonical": "vertex_ai_ai21_models", - "pascal": "VertexAiAi21Models", - "vendor": "AI21", - "models": ["ai21/jamba-1.5-large", "ai21/jamba-1.5-mini"], - "test_model": "ai21/jamba-1.5-large", - }, - { - "canonical": "vertex_ai_anthropic_models", - "pascal": "VertexAiAnthropicModels", - "vendor": "Anthropic Claude", - "models": ["anthropic/claude-sonnet-4", "anthropic/claude-opus-4"], - "test_model": "anthropic/claude-sonnet-4", - }, - { - "canonical": "vertex_ai_deepseek_models", - "pascal": "VertexAiDeepseekModels", - "vendor": "DeepSeek", - "models": [ - "deepseek-ai/deepseek-v3.1-maas", - "deepseek-ai/deepseek-r1-0528-maas", - ], - "test_model": "deepseek-ai/deepseek-v3.1-maas", - }, - { - "canonical": "vertex_ai_llama_models", - "pascal": "VertexAiLlamaModels", - "vendor": "Meta Llama", - "models": [ - "meta/llama-4-scout-17b-16e-instruct-maas", - "meta/llama-4-maverick-17b-128e-instruct-maas", - ], - "test_model": "meta/llama-4-scout-17b-16e-instruct-maas", - }, - { - "canonical": "vertex_ai_minimax_models", - "pascal": "VertexAiMinimaxModels", - "vendor": "MiniMax", - "models": ["minimax/minimax-m2-maas"], - "test_model": "minimax/minimax-m2-maas", - }, - { - "canonical": "vertex_ai_mistral_models", - "pascal": "VertexAiMistralModels", - "vendor": "Mistral", - "models": ["mistralai/mistral-large-2411", "mistralai/codestral-2501"], - "test_model": "mistralai/mistral-large-2411", - }, - { - "canonical": "vertex_ai_moonshot_models", - "pascal": "VertexAiMoonshotModels", - "vendor": "Moonshot AI", - "models": ["moonshotai/kimi-k2-thinking-maas"], - "test_model": "moonshotai/kimi-k2-thinking-maas", - }, - { - "canonical": "vertex_ai_openai_models", - "pascal": "VertexAiOpenaiModels", - "vendor": "OpenAI", - "models": ["openai/gpt-oss-120b-maas", "openai/gpt-oss-20b-maas"], - "test_model": "openai/gpt-oss-120b-maas", - }, - { - "canonical": "vertex_ai_qwen_models", - "pascal": "VertexAiQwenModels", - "vendor": "Qwen", - "models": [ - "qwen/qwen3-coder-480b-a35b-instruct-maas", - "qwen/qwen3-next-80b-a3b-instruct-maas", - ], - "test_model": "qwen/qwen3-coder-480b-a35b-instruct-maas", - }, - { - "canonical": "vertex_ai_zai_models", - "pascal": "VertexAiZaiModels", - "vendor": "Z.AI", - "models": ["zai-org/glm-4.7-maas", "zai-org/glm-5-maas"], - "test_model": "zai-org/glm-4.7-maas", - }, -] - -FILE_TEMPLATE = BOM + r'''//! {Vendor} models on Vertex AI MaaS — a thin OpenAI-compatible wrapper. -//! -//! Vertex AI serves partner and open models (Anthropic, AI21, DeepSeek, Llama, -//! MiniMax, Mistral, Moonshot, OpenAI, Qwen, Z.AI) through an OpenAI-compatible -//! Chat Completions endpoint — the "Model as a Service" (MaaS) OpenAPI surface -//! — rather than the native `rawPredict` path: -//! -//! `https://{host}/v1/projects/{project}/locations/{location}/endpoints/openapi` -//! -//! The host is derived from the location: `global` uses -//! `aiplatform.googleapis.com`, `eu`/`us` use `aiplatform.{loc}.rep.googleapis.com`, -//! and any other location uses `{loc}-aiplatform.googleapis.com`. Authentication -//! uses a Google Cloud OAuth2 Bearer token (the same `GOOGLE_VERTEX_ACCESS_TOKEN` -//! used by the native Vertex provider), sent as `Authorization: Bearer `. -//! -//! Because the endpoint is OpenAI-compatible, this provider is a thin wrapper -//! over [`OpenAIProvider`](crate::openai::OpenAIProvider): only the base URL, -//! the Bearer-token env var, and the provider name differ. The shared -//! `OpenAIProvider` appends `/chat/completions` to the configured base URL. -//! Sample model ids: {models_doc}. - -use aimux_core::error::AiMuxError; -use aimux_core::language_model::LanguageModel; -use aimux_core::provider::Provider; - -use crate::openai::{OpenAICompatProfile, OpenAIConfig, OpenAIModel, OpenAIProvider}; - -const PROVIDER_NAME: &str = "{canonical}"; -const TOKEN_ENV_VAR: &str = "GOOGLE_VERTEX_ACCESS_TOKEN"; -const PROJECT_ENV_VAR: &str = "GOOGLE_VERTEX_PROJECT"; -const LOCATION_ENV_VAR: &str = "GOOGLE_VERTEX_LOCATION"; -const DEFAULT_LOCATION: &str = "global"; -/// Fallback project when `GOOGLE_VERTEX_PROJECT` is unset and no base URL is -/// supplied via [`{Pascal}Config::with_base_url`]; prefer -/// [`{Pascal}Config::from_env`] or set the project explicitly for real usage. -const DEFAULT_PROJECT: &str = "your-project"; - -/// Build the Vertex AI MaaS OpenAI-compatible base URL for a project/location. -/// -/// - `global` → `https://aiplatform.googleapis.com/v1/projects/{p}/locations/global/endpoints/openapi` -/// - `eu`/`us` → `https://aiplatform.{loc}.rep.googleapis.com/v1/projects/{p}/locations/{loc}/endpoints/openapi` -/// - other → `https://{loc}-aiplatform.googleapis.com/v1/projects/{p}/locations/{loc}/endpoints/openapi` -fn build_maas_base_url(project: &str, location: &str) -> String { - let host = match location { - "global" => "aiplatform.googleapis.com".to_string(), - "eu" | "us" => format!("aiplatform.{}.rep.googleapis.com", location), - _ => format!("{}-aiplatform.googleapis.com", location), - }; - format!( - "https://{}/v1/projects/{}/locations/{}/endpoints/openapi", - host, project, location - ) -} - -/// Assemble the shared [`OpenAIConfig`] for the given token + project/location. -fn build_config(api_key: String, project: &str, location: &str) -> OpenAIConfig { - OpenAIConfig::new(api_key) - .with_base_url(build_maas_base_url(project, location)) - .with_provider(PROVIDER_NAME) - .with_profile(OpenAICompatProfile::full()) -} - -/// Configuration for the {Vendor} Vertex AI MaaS provider (wraps [`OpenAIConfig`]). -pub struct {Pascal}Config(OpenAIConfig); - -impl {Pascal}Config { - /// Create from a Google Cloud Bearer access token, constructing the base - /// URL from `GOOGLE_VERTEX_PROJECT` / `GOOGLE_VERTEX_LOCATION` (with - /// `global` / `your-project` fallbacks). Override the URL with - /// [`Self::with_base_url`] for tests or proxies. - pub fn new(api_key: impl Into) -> Self { - let project = - std::env::var(PROJECT_ENV_VAR).unwrap_or_else(|_| DEFAULT_PROJECT.to_string()); - let location = - std::env::var(LOCATION_ENV_VAR).unwrap_or_else(|_| DEFAULT_LOCATION.to_string()); - Self(build_config(api_key.into(), &project, &location)) - } - - /// Create from `GOOGLE_VERTEX_ACCESS_TOKEN` + `GOOGLE_VERTEX_PROJECT` + - /// `GOOGLE_VERTEX_LOCATION` (location defaults to `global`). - pub fn from_env() -> Result { - let token = std::env::var(TOKEN_ENV_VAR).map_err(|_| { - AiMuxError::InvalidArgument( - "GOOGLE_VERTEX_ACCESS_TOKEN environment variable is required for Vertex AI MaaS" - .to_string(), - ) - })?; - let project = std::env::var(PROJECT_ENV_VAR).map_err(|_| { - AiMuxError::InvalidArgument( - "GOOGLE_VERTEX_PROJECT environment variable is required for Vertex AI MaaS" - .to_string(), - ) - })?; - let location = - std::env::var(LOCATION_ENV_VAR).unwrap_or_else(|_| DEFAULT_LOCATION.to_string()); - Ok(Self(build_config(token, &project, &location))) - } - - /// Override the base URL (useful for tests / proxies). - pub fn with_base_url(mut self, url: impl Into) -> Self { - self.0 = self.0.with_base_url(url); - self - } -} - -/// {Vendor} Vertex AI MaaS provider — creates [`OpenAIModel`] instances pointed -/// at the Vertex AI MaaS OpenAPI endpoint. -pub struct {Pascal}Provider(OpenAIProvider); - -impl {Pascal}Provider { - pub fn new(config: {Pascal}Config) -> Self { - Self(OpenAIProvider::new(config.0)) - } - - /// Create a model instance for the given Vertex AI MaaS model id - /// (e.g. `"{sample_model}"`). - pub fn model(&self, model_id: &str) -> OpenAIModel { - self.0.model(model_id) - } -} - -impl Provider for {Pascal}Provider { - fn name(&self) -> &str { - PROVIDER_NAME - } - - fn language_model(&self, model_id: &str) -> Result, AiMuxError> { - Ok(Box::new(self.model(model_id))) - } -} -''' - - -def models_doc(models): - quoted = ", ".join("`\"{}\"`".format(m) for m in models) - return quoted - - -def render_file(p): - return ( - FILE_TEMPLATE - .replace("{Vendor}", p["vendor"]) - .replace("{canonical}", p["canonical"]) - .replace("{Pascal}", p["pascal"]) - .replace("{models_doc}", models_doc(p["models"])) - .replace("{sample_model}", p["test_model"]) - ) - - -def write_providers(): - for p in PROVIDERS: - path = SRC / (p["canonical"] + ".rs") - path.write_text(render_file(p), encoding="utf-8") - print("wrote", path.relative_to(ROOT)) - - -def patch_lib(): - mods = "\n".join("pub mod {};".format(p["canonical"]) for p in PROVIDERS) - uses = "\n".join( - "pub use {}::{{{name}Config, {name}Provider}};".format( - p["canonical"], name=p["pascal"] - ) - for p in PROVIDERS - ) - block = ( - "// Vertex AI MaaS partner-model providers (OpenAI-compatible thin wrappers).\n" - "// Each wraps the shared OpenAIProvider against the Vertex AI MaaS OpenAPI\n" - "// endpoint, authenticating with a Google Cloud Bearer token.\n" - + mods - + "\n\n" - + uses - + "\n\n" - ) - anchor = "// Search-only providers (web search modality)." - text = LIB.read_text(encoding="utf-8") - count = text.count(anchor) - if count == 0: - raise SystemExit("lib.rs anchor not found: " + anchor) - if "// Vertex AI MaaS partner-model providers" in text: - raise SystemExit("lib.rs already patched with Vertex AI MaaS block") - text = text.replace(anchor, block + anchor, 1) - LIB.write_text(text, encoding="utf-8") - print("patched", LIB.relative_to(ROOT)) - - -def patch_test(): - text = TEST.read_text(encoding="utf-8") - # 1. imports: insert the 20 names between VercelProvider and XAIConfig. - names = [] - for p in PROVIDERS: - names.append("{p}Config".format(p=p["pascal"])) - names.append("{p}Provider".format(p=p["pascal"])) - names_str = ", ".join(names) - imp_anchor = "VercelConfig, VercelProvider, XAIConfig," - if text.count(imp_anchor) != 1: - raise SystemExit( - "test import anchor count != 1: {!r}".format(imp_anchor) - ) - if "VertexAiAi21ModelsConfig" in text: - raise SystemExit("test file already patched with Vertex AI MaaS imports") - text = text.replace( - imp_anchor, - "VercelConfig, VercelProvider, " + names_str + ", XAIConfig,", - 1, - ) - # 2. macro invocations: append at end of file. - macros = [] - for p in PROVIDERS: - macros.append( - "openai_compatible_tests!(\n" - " {mod},\n" - " {pascal}Config,\n" - " {pascal}Provider,\n" - " \"{model}\"\n" - ");".format(mod=p["canonical"], pascal=p["pascal"], model=p["test_model"]) - ) - suffix = "\n\n// Vertex AI MaaS partner-model providers (OpenAI-compatible thin wrappers).\n" - suffix += "\n".join(macros) + "\n" - if not text.endswith("\n"): - text += "\n" - text += suffix - TEST.write_text(text, encoding="utf-8") - print("patched", TEST.relative_to(ROOT)) - - -def main(): - write_providers() - patch_lib() - patch_test() - - -if __name__ == "__main__": - main() diff --git a/scripts/generate_all_providers.py b/scripts/generate_all_providers.py deleted file mode 100644 index 086ca0d5..00000000 --- a/scripts/generate_all_providers.py +++ /dev/null @@ -1,391 +0,0 @@ -#!/usr/bin/env python3 -"""Generate thin-wrapper provider .rs files for ALL missing OpenAI-compatible providers. - -This is a one-shot bulk generation script. It creates a .rs file per provider, -updates lib.rs, and prints a summary. - -Usage: - uv run python scripts/generate_all_providers.py -""" - -import os -from pathlib import Path - -REPO = Path(__file__).resolve().parent.parent -SRC = REPO / "aimux-providers" / "src" - -# ── Already implemented (don't regenerate) ── -ALREADY_DONE = { - "anthropic", "anthropic_aws", "azure", "bedrock", "google", "vertex", - "mistral", "cohere", "xai", "deepseek", "openai", "open_responses", - # OpenAI-compatible thin wrappers (batch 1) - "groq", "fireworks", "togetherai", "perplexity", "moonshotai", "cerebras", - "alibaba", "baseten", "bytedance", "deepinfra", "huggingface", "vercel", - "openrouter", "copilot", "llamafile", "mistralrs", "doubleword", - # Non-text - "voyage", "cartesia", "elevenlabs", "hume", "lmnt", - "assemblyai", "deepgram", "fal", "gladia", "revai", - "black_forest_labs", "luma", "prodia", "replicate", "klingai", - # Batch 2 (just added) - "ollama", "zai", "github", "siliconflow", "lmstudio", "sambanova", -} - -# ── Provider definitions ── -# (module_name, struct_prefix, base_url, env_var, is_local, display_name, doc_url) -# is_local=True: no API key needed, uses ENV_VAR for base URL -# is_local=False: uses ENV_VAR for API key -PROVIDERS = [ - # ── Cloud LLM providers (OpenAI-compatible) ── - ("novita", "Novita", "https://api.novita.ai/v1", "NOVITA_API_KEY", False, "Novita AI", "novita.ai"), - ("nebius", "Nebius", "https://api.studio.nebius.ai/v1", "NEBIUS_API_KEY", False, "Nebius AI", "nebius.ai"), - ("hyperbolic", "Hyperbolic", "https://api.hyperbolic.xyz/v1", "HYPERBOLIC_API_KEY", False, "Hyperbolic", "hyperbolic.xyz"), - ("ovhcloud", "Ovhcloud", "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1", "OVHCLOUD_API_KEY", False, "OVHcloud AI", "ovhcloud.com"), - ("ai21", "Ai21", "https://api.ai21.ai/v1", "AI21_API_KEY", False, "AI21 Labs", "ai21.com"), - ("anyscale", "Anyscale", "https://api.endpoints.anyscale.com/v1", "ANYSCALE_API_KEY", False, "Anyscale", "anyscale.com"), - ("predibase", "Predibase", "https://serving.app.predibase.com/v1", "PREDIBASE_API_KEY", False, "Predibase", "predibase.com"), - ("databricks", "Databricks", "https://databricks.com/serving-endpoints", "DATABRICKS_API_KEY", False, "Databricks", "databricks.com"), - ("scaleway", "Scaleway", "https://api.scaleway.ai/v1", "SCALEWAY_API_KEY", False, "Scaleway AI", "scaleway.com"), - ("nlp_cloud", "NlpCloud", "https://api.nlpcloud.io/v1", "NLPCLOUD_API_KEY", False, "NLP Cloud", "nlpcloud.com"), - ("friendliai", "FriendliAI", "https://inference.friendli.ai/v1", "FRIENDLIAI_API_KEY", False, "FriendliAI", "friendli.ai"), - ("clarifai", "Clarifai", "https://api.clarifai.com/v2/ext/openai/v1", "CLARIFAI_API_KEY", False, "Clarifai", "clarifai.com"), - ("gigachat", "GigaChat", "https://gigachat.devices.sberbank.ru/api/v1", "GIGACHAT_API_KEY", False, "GigaChat (Sberbank)", "sberbank.ru"), - ("codestral", "Codestral", "https://api.mistral.ai/v1", "CODESTRAL_API_KEY", False, "Codestral (Mistral)", "mistral.ai"), - ("morph", "Morph", "https://api.morphllm.com/v1", "MORPH_API_KEY", False, "Morph LLM", "morphllm.com"), - ("aiml", "Aiml", "https://api.aimlapi.com/v1", "AIML_API_KEY", False, "AI/ML API", "aimlapi.com"), - ("heroku", "Heroku", "https://api.heroku.com/inference/v1", "HEROKU_API_KEY", False, "Heroku AI", "heroku.com"), - ("nvidia_nim", "NvidiaNim", "https://integrate.api.nvidia.com/v1", "NVIDIA_API_KEY", False, "NVIDIA NIM", "nvidia.com"), - ("nscale", "Nscale", "https://inference.api.nscale.com/v1", "NSCALE_API_KEY", False, "Nscale", "nscale.com"), - ("lambda_ai", "LambdaAi", "https://api.lambda.ai/v1", "LAMBDA_API_KEY", False, "Lambda AI", "lambda.ai"), - ("inception", "Inception", "https://api.inceptionlabs.ai/v1", "INCEPTION_API_KEY", False, "Inception Labs", "inceptionlabs.ai"), - ("galadriel", "Galadriel", "https://api.galadriel.com/v1", "GALADRIEL_API_KEY", False, "Galadriel", "galadriel.com"), - ("datarobot", "Datarobot", "https://app.datarobot.com/api/v2", "DATAROBOT_API_TOKEN", False, "DataRobot", "datarobot.com"), - ("featherless_ai", "FeatherlessAi", "https://api.featherless.ai/v1", "FEATHERLESS_API_KEY", False, "Featherless AI", "featherless.ai"), - ("cometapi", "Cometapi", "https://api.cometapi.com/v1", "COMETAPI_API_KEY", False, "CometAPI", "cometapi.com"), - ("reka_ai", "RekaAi", "https://api.reka.ai/v1", "REKA_API_KEY", False, "Reka AI", "reka.ai"), - ("sarvam", "Sarvam", "https://api.sarvam.ai/v1", "SARVAM_API_KEY", False, "Sarvam AI", "sarvam.ai"), - ("meta_llama", "MetaLlama", "https://api.llama.com/compat/v1", "LLAMA_API_KEY", False, "Meta Llama API", "llama.com"), - ("apiserpent", "Apiserpent", "https://api.apiserpent.com/v1", "APISERPENT_API_KEY", False, "API Serpent", "apiserpent.com"), - ("modelscope", "Modelscope", "https://api-inference.modelscope.cn/v1", "MODELSCOPE_API_KEY", False, "ModelScope", "modelscope.cn"), - ("litellm_proxy", "LitellmProxy", "http://127.0.0.1:4000/v1", "LITELLM_PROXY_API_KEY", True, "LiteLLM Proxy", "litellm.ai"), - ("compactifai", "Compactifai", "https://api.compactif.ai/v1", "COMPACTIFAI_API_KEY", False, "CompactifAI", "compactif.ai"), - ("gradient_ai", "GradientAi", "https://inference.do-ai.run/v1", "GRADIENT_API_KEY", False, "Gradient AI", "do-ai.run"), - ("azure_ai", "AzureAi", "https://models.inference.ai.azure.com", "AZURE_AI_API_KEY", False, "Azure AI", "azure.com"), - ("doubaoaudio", "DoubaoAudio", "https://ark.cn-beijing.volces.com/api/v3", "ARK_API_KEY", False, "Doubao Audio", "volces.com"), - - # ── 国产厂商 (OpenAI-compatible) ── - ("baidu", "Baidu", "https://qianfan.baidubce.com/v2", "BAIDU_API_KEY", False, "Baidu (文心/ERNIE)", "baidubce.com"), - ("tencent", "Tencent", "https://api.hunyuan.cloud.tencent.com/v1", "TENCENT_API_KEY", False, "Tencent (混元/Hunyuan)", "tencent.com"), - ("baichuan", "Baichuan", "https://api.baichuan-ai.com/v1", "BAICHUAN_API_KEY", False, "Baichuan AI", "baichuan-ai.com"), - ("stepfun", "Stepfun", "https://api.stepfun.com/v1", "STEPFUN_API_KEY", False, "StepFun (阶跃星辰)", "stepfun.com"), - ("minimax", "Minimax", "https://api.minimax.io/v1", "MINIMAX_API_KEY", False, "MiniMax", "minimax.io"), - ("lingyiwanwu", "Lingyiwanwu", "https://api.lingyiwanwu.com/v1", "LINGYIWANWU_API_KEY", False, "Lingyiwanwu (零一万物)", "lingyiwanwu.com"), - ("qihoo360", "Qihoo360", "https://api.360.cn/v1", "AI360_API_KEY", False, "360 AI", "360.cn"), - ("coze", "Coze", "https://api.coze.cn/v1", "COZE_API_KEY", False, "Coze (扣子)", "coze.cn"), - ("qiniu_ai", "QiniuAi", "https://api.qiniu.com/v1", "QINIU_API_KEY", False, "Qiniu AI", "qiniu.com"), - ("longcat", "Longcat", "https://api.longcat.chat/v1", "LONGCAT_API_KEY", False, "LongCat", "longcat.chat"), - ("bigmodel", "Bigmodel", "https://open.bigmodel.cn/api/paas/v4", "BIGMODEL_API_KEY", False, "BigModel (智谱)", "bigmodel.cn"), - ("aihubmix", "Aihubmix", "https://aihubmix.com/v1", "AIHUBMIX_API_KEY", False, "AIHubMix", "aihubmix.com"), - ("mira", "Mira", "https://api.mira.so/v1", "MIRA_API_KEY", False, "Mira", "mira.so"), - ("xiaomimimo", "Xiaomimimo", "https://mimo.xiaomi.com/v1", "XIAOMI_API_KEY", False, "Xiaomi MiMo", "xiaomi.com"), - ("byteplus", "Byteplus", "https://ark.bytepluses.com/api/v3", "BYTEPLUS_API_KEY", False, "BytePlus (Volcano)", "bytepluses.com"), - ("perfxcloud", "Perfxcloud", "https://api.perfxcloud.com/v1", "PERFXCLOUD_API_KEY", False, "PerfXCloud", "perfxcloud.com"), - - # ── 本地推理 (OpenAI-compatible endpoints) ── - ("llamacpp", "Llamacpp", "http://127.0.0.1:8080/v1", "LLAMACPP_BASE_URL", True, "llama.cpp", "github.com/ggerganov/llama.cpp"), - ("vllm", "Vllm", "http://127.0.0.1:8000/v1", "VLLM_BASE_URL", True, "vLLM", "vllm.ai"), - ("sglang", "Sglang", "http://127.0.0.1:30000/v1", "SGLANG_BASE_URL", True, "SGLang", "github.com/sgl-project/sglang"), - ("xinference", "Xinference", "http://127.0.0.1:9997/v1", "XINFERENCE_BASE_URL", True, "Xinference", "inference.ai"), - ("localai", "Localai", "http://127.0.0.1:8080/v1", "LOCALAI_BASE_URL", True, "LocalAI", "localai.io"), - ("jlama", "Jlama", "http://127.0.0.1:8080/v1", "JLAMA_BASE_URL", True, "Jlama", "github.com/tjake/Jlama"), - ("ollama_cloud", "OllamaCloud", "https://api.ollama.com/v1", "OLLAMA_CLOUD_API_KEY", False, "Ollama Cloud", "ollama.com"), - ("docker_model_runner", "DockerModelRunner", "http://model-runner.docker.internal/engines/llama.cpp/v1", "DOCKER_MODEL_RUNNER_BASE_URL", True, "Docker Model Runner", "docker.com"), - - # ── 网关/聚合 ── - ("portkey", "Portkey", "https://api.portkey.ai/v1", "PORTKEY_API_KEY", False, "Portkey Gateway", "portkey.ai"), - ("helicone", "Helicone", "https://api.helicone.ai/v1", "HELICONE_API_KEY", False, "Helicone", "helicone.ai"), - ("requesty", "Requesty", "https://api.requesty.ai/v1", "REQUESTY_API_KEY", False, "Requesty", "requesty.ai"), - ("ai302", "Ai302", "https://api.302.ai/v1", "AI302_API_KEY", False, "302.AI", "302.ai"), - ("api2d", "Api2d", "https://oa.api2d.net/v1", "API2D_API_KEY", False, "API2D", "api2d.net"), - ("ohmygpt", "Ohmygpt", "https://api.ohmygpt.com/v1", "OHMYGPT_API_KEY", False, "OhMyGPT", "ohmygpt.com"), - ("closeai", "Closeai", "https://api.closeai-proxy.xyz/v1", "CLOSEAI_API_KEY", False, "CloseAI", "closeai-proxy.xyz"), - ("openaisb", "Openaisb", "https://api.openaisb.com/v1", "OPENAISB_API_KEY", False, "OpenAI-SB", "openaisb.com"), - ("openaimax", "Openaimax", "https://api.openaimax.com/v1", "OPENAIMAX_API_KEY", False, "OpenAIMax", "openaimax.com"), - ("ails", "Ails", "https://api.caipacity.com/v1", "AILS_API_KEY", False, "AILS", "caipacity.com"), - ("api2gpt", "Api2gpt", "https://api.api2gpt.com/v1", "API2GPT_API_KEY", False, "API2GPT", "api2gpt.com"), - ("aigc2d", "Aigc2d", "https://api.aigc2d.com/v1", "AIGC2D_API_KEY", False, "AIGC2D", "aigc2d.com"), - ("fastgpt", "Fastgpt", "https://api.fastgpt.in/v1", "FASTGPT_API_KEY", False, "FastGPT", "fastgpt.in"), - ("tokenpony", "Tokenpony", "https://api.tokenpony.com/v1", "TOKENPONY_API_KEY", False, "TokenPony", "tokenpony.com"), - ("fastrouter", "Fastrouter", "https://api.fastrouter.ai/v1", "FASTROUTER_API_KEY", False, "FastRouter", "fastrouter.ai"), - ("orcarouter", "Orcarouter", "https://api.orcarouter.com/v1", "ORCAROUTER_API_KEY", False, "OrcaRouter", "orcarouter.com"), - ("submodel", "Submodel", "https://api.submodel.com/v1", "SUBMODEL_API_KEY", False, "SubModel", "submodel.com"), - - # ── 其他云厂商 ── - ("kluster_ai", "KlusterAi", "https://api.kluster.ai/v1", "KLUSTER_API_KEY", False, "Kluster AI", "kluster.ai"), - ("krutrim", "Krutrim", "https://api.krutrim.ai/v1", "KRUTRIM_API_KEY", False, "Krutrim", "krutrim.ai"), - ("bytez", "Bytez", "https://api.bytez.com/v2", "BYTEZ_API_KEY", False, "Bytez", "bytez.com"), - ("upstage", "Upstage", "https://api.upstage.ai/v1", "UPSTAGE_API_KEY", False, "Upstage", "upstage.ai"), - ("deepbricks", "Deepbricks", "https://api.deepbricks.ai/v1", "DEEPBRICKS_API_KEY", False, "DeepBricks", "deepbricks.ai"), - ("lemonfox_ai", "LemonfoxAi", "https://api.lemonfox.ai/v1", "LEMONFOX_API_KEY", False, "Lemonfox AI", "lemonfox.ai"), - ("modal", "Modal", "https://modal.com/v1", "MODAL_API_KEY", False, "Modal", "modal.com"), - ("sakana", "Sakana", "https://api.sakana.ai/v1", "SAKANA_API_KEY", False, "Sakana AI", "sakana.ai"), - ("nous_research", "NousResearch", "https://api.nousresearch.com/v1", "NOUS_API_KEY", False, "Nous Research", "nousresearch.com"), - ("bedrock_mantle", "BedrockMantle", "https://bedrock-runtime.us-east-1.amazonaws.com", "AWS_BEDROCK_API_KEY", False, "Bedrock Mantle", "aws.amazon.com"), - ("watsonx", "Watsonx", "https://us-south.ml.cloud.ibm.com/v1", "WATSONX_API_KEY", False, "IBM watsonx", "ibm.com"), - ("sagemaker", "Sagemaker", "https://runtime.sagemaker.us-east-1.amazonaws.com", "SAGEMAKER_API_KEY", False, "AWS SageMaker", "aws.amazon.com"), - ("sap", "Sap", "https://api.ai.sap.eu10.hana.ondemand.com/v2", "SAP_AI_API_KEY", False, "SAP AI Core", "sap.com"), - ("oci", "Oci", "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", "OCI_API_KEY", False, "Oracle OCI AI", "oracle.com"), - ("snowflake", "Snowflake", "https://xxx.snowflakecomputing.com/api/v2/cortex", "SNOWFLAKE_API_KEY", False, "Snowflake Cortex", "snowflake.com"), - ("infinity", "Infinity", "https://infinity.ai/api/v1", "INFINITY_API_KEY", False, "Infinity AI", "infinity.ai"), - ("hosted_vllm", "HostedVllm", "https://hosted-vllm-api.com/v1", "HOSTED_VLLM_API_KEY", False, "Hosted vLLM", "vllm.ai"), - ("petals", "Petals", "https://api.petals.dev/v1", "PETALS_API_KEY", False, "Petals", "petals.dev"), - ("oobabooba", "Oobabooba", "http://127.0.0.1:5000/v1", "OOBABOOBA_BASE_URL", True, "Oobabooga Text Generation WebUI", "github.com/oobabooga"), - ("gdc", "Gdc", "https://api.gdc.ai/v1", "GDC_API_KEY", False, "GDC", "gdc.ai"), - ("fastcrw", "Fastcrw", "https://fastcrw.com/api/v1", "FASTCRW_API_KEY", False, "FastCRW", "fastcrw.com"), - ("dify", "Dify", "https://api.dify.ai/v1", "DIFY_API_KEY", False, "Dify", "dify.ai"), - ("clip", "Clip", "http://127.0.0.1:8080/v1", "CLIP_BASE_URL", True, "CLIP (local)", "openai.com/clip"), - ("fastembed", "Fastembed", "http://127.0.0.1:8080/v1", "FASTEMBED_BASE_URL", True, "FastEmbed (local)", "github.com/Anush008/fastembed-rs"), - ("tei", "Tei", "http://127.0.0.1:8080/v1", "TEI_BASE_URL", True, "Text Embeddings Inference (local)", "github.com/huggingface/text-embeddings-inference"), - ("nomic", "Nomic", "https://api.nomic.ai/v1", "NOMIC_API_KEY", False, "Nomic", "nomic.ai"), - ("jina", "Jina", "https://api.jina.ai/v1", "JINA_API_KEY", False, "Jina AI", "jina.ai"), - ("mixedbread", "Mixedbread", "https://api.mixedbread.ai/v1", "MIXEDBREAD_API_KEY", False, "Mixedbread", "mixedbread.ai"), - ("recraft", "Recraft", "https://external.api.recraft.ai/v1", "RECRAFT_API_KEY", False, "Recraft", "recraft.ai"), - ("ideogram", "Ideogram", "https://api.ideogram.ai/v1", "IDEOGRAM_API_KEY", False, "Ideogram", "ideogram.ai"), - ("stability_ai", "StabilityAi", "https://api.stability.ai/v1", "STABILITY_API_KEY", False, "Stability AI", "stability.ai"), - ("segmind", "Segmind", "https://api.segmind.com/v1", "SEGMIND_API_KEY", False, "Segmind", "segmind.com"), - ("runware", "Runware", "https://api.runware.ai/v1", "RUNWARE_API_KEY", False, "Runware", "runware.ai"), - ("meshy", "Meshy", "https://api.meshy.ai/v1", "MESHY_API_KEY", False, "Meshy (3D)", "meshy.ai"), - ("tripo3d", "Tripo3d", "https://api.tripo3d.ai/v1", "TRIPO3D_API_KEY", False, "Tripo3D", "tripo3d.ai"), - ("runwayml", "Runwayml", "https://api.dev.runwayml.com/v1", "RUNWAYML_API_KEY", False, "Runway ML", "runwayml.com"), - ("sora", "Sora", "https://api.openai.com/v1", "OPENAI_API_KEY", False, "OpenAI Sora", "openai.com"), - ("vidu", "Vidu", "https://api.vidu.com/v1", "VIDU_API_KEY", False, "Vidu", "vidu.com"), - ("jimeng", "Jimeng", "https://api.jimeng.jianying.com/v1", "JIMENG_API_KEY", False, "Jimeng (即梦)", "jianying.com"), - ("midjourney", "Midjourney", "https://api.midjourney.com/v1", "MIDJOURNEY_API_KEY", False, "Midjourney", "midjourney.com"), - ("flux", "Flux", "https://api.fal.ai/v1", "FLUX_API_KEY", False, "FLUX (via fal)", "fal.ai"), - ("suno", "Suno", "https://api.suno.com/v1", "SUNO_API_KEY", False, "Suno (音乐)", "suno.com"), - ("doubaoaudio", "DoubaoAudio", "https://ark.cn-beijing.volces.com/api/v3", "ARK_API_KEY", False, "Doubao Audio", "volces.com"), - ("murf", "Murf", "https://api.murf.ai/v1", "MURF_API_KEY", False, "Murf (TTS)", "murf.ai"), - ("playai", "Playai", "https://api.play.ai/v1", "PLAYAI_API_KEY", False, "PlayAI (TTS)", "play.ai"), - ("speechify", "Speechify", "https://api.sws.speechify.com/v1", "SPEECHIFY_API_KEY", False, "Speechify (TTS)", "speechify.com"), - ("inworld", "Inworld", "https://api.inworld.ai/v1", "INWORLD_API_KEY", False, "Inworld (TTS)", "inworld.ai"), - ("aws_polly", "AwsPolly", "https://polly.us-east-1.amazonaws.com", "AWS_POLLY_API_KEY", False, "AWS Polly (TTS)", "aws.amazon.com"), - ("nvidia_riva", "NvidiaRiva", "https://api.nvidia.com/v1", "NVIDIA_RIVA_API_KEY", False, "NVIDIA Riva (TTS)", "nvidia.com"), - ("soniox", "Soniox", "https://api.soniox.com/v1", "SONIOX_API_KEY", False, "Soniox (STT)", "soniox.com"), - ("mokaai", "Mokaai", "https://api.mokaai.com/v1", "MOKAAI_API_KEY", False, "MokaAI", "mokaai.com"), - ("skylark", "Skylark", "https://api.skylark.com/v1", "SKYLARK_API_KEY", False, "Skylark (云雀)", "skylark.com"), - ("deepl", "Deepl", "https://api.deepl.com/v2", "DEEPL_API_KEY", False, "DeepL", "deepl.com"), - ("bing", "Bing", "https://api.bing.microsoft.com/v1", "BING_API_KEY", False, "Bing", "microsoft.com"), - ("slack", "Slack", "https://slack.com/api/chat.postMessage", "SLACK_API_KEY", False, "Slack (Claude proxy)", "slack.com"), - ("doubaoaudio", "DoubaoAudio", "https://ark.cn-beijing.volces.com/api/v3", "ARK_API_KEY", False, "Doubao Audio", "volces.com"), - ("ncompass", "Ncompass", "https://api.ncompass.tech/v1", "NCOMPASS_API_KEY", False, "Ncompass", "ncompass.tech"), - ("parasail", "Parasail", "https://api.parasail.io/v1", "PARASAIL_API_KEY", False, "Parasail", "parasail.io"), - ("wafer", "Wafer", "https://api.wafer.ai/v1", "WAFER_API_KEY", False, "Wafer", "wafer.ai"), - ("matterai", "Matterai", "https://api.matterai.com/v1", "MATTERAI_API_KEY", False, "Matter AI", "matterai.com"), - ("nextbit", "Nextbit", "https://api.nextbit.ai/v1", "NEXTBIT_API_KEY", False, "NextBit", "nextbit.ai"), - ("aibadgr", "Aibadgr", "https://api.aibadgr.com/v1", "AIBADGR_API_KEY", False, "AI Badgr", "aibadgr.com"), - ("inference_net", "InferenceNet", "https://api.inference.net/v1", "INFERENCE_NET_API_KEY", False, "Inference.net", "inference.net"), - ("lemonfox_ai", "LemonfoxAi", "https://api.lemonfox.ai/v1", "LEMONFOX_API_KEY", False, "Lemonfox AI", "lemonfox.ai"), - ("opencode_go", "OpencodeGo", "https://api.opencode.dev/v1", "OPENCODE_GO_API_KEY", False, "OpenCode Go", "opencode.dev"), - ("opencode_zen", "OpencodeZen", "https://api.opencode.zen/v1", "OPENCODE_ZEN_API_KEY", False, "OpenCode Zen", "opencode.zen"), - ("kiro", "Kiro", "https://api.kiro.dev/v1", "KIRO_API_KEY", False, "Kiro", "kiro.dev"), - ("pioneer", "Pioneer", "https://api.pioneer.ai/v1", "PIONEER_API_KEY", False, "Pioneer", "pioneer.ai"), - ("kilo", "Kilo", "https://api.kilo.ai/v1", "KILO_API_KEY", False, "Kilo", "kilo.ai"), - ("commandcode", "Commandcode", "https://api.commandcode.com/v1", "COMMANDCODE_API_KEY", False, "CommandCode", "commandcode.com"), - ("cline_pass", "ClinePass", "https://api.cline.bot/v1", "CLINE_API_KEY", False, "Cline", "cline.bot"), - ("albert", "Albert", "https://api.albert.ai/v1", "ALBERT_API_KEY", False, "Albert", "albert.ai"), - ("scx_ai", "ScxAi", "https://api.scx.ai/v1", "SCX_AI_API_KEY", False, "SCX AI", "scx.ai"), - ("atlascloud", "Atlascloud", "https://api.atlascloud.com/v1", "ATLASCLOUD_API_KEY", False, "AtlasCloud", "atlascloud.com"), - ("canopywave", "Canopywave", "https://api.canopywave.com/v1", "CANOPYWAVE_API_KEY", False, "Canopywave", "canopywave.com"), - ("embercloud", "Embercloud", "https://api.embercloud.com/v1", "EMBERCLOUD_API_KEY", False, "Embercloud", "embercloud.com"), - ("tundra", "Tundra", "https://api.tundra.ai/v1", "TUNDRA_API_KEY", False, "Tundra", "tundra.ai"), - ("reve", "Reve", "https://api.reve.ai/v1", "REVE_API_KEY", False, "Reve", "reve.ai"), - ("gonka24", "Gonka24", "https://api.gonka24.com/v1", "GONKA24_API_KEY", False, "Gonka24", "gonka24.com"), - ("streamlake", "Streamlake", "https://api.streamlake.com/v1", "STREAMLAKE_API_KEY", False, "StreamLake", "streamlake.com"), - ("antling", "Antling", "https://api.antling.com/v1", "ANTLING_API_KEY", False, "Antling", "antling.com"), - ("sangforaicp", "Sangforaicp", "https://aicp.sangfor.com/v1", "SANGFOR_AICP_API_KEY", False, "Sangfor AICP", "sangfor.com"), - ("doc2x", "Doc2x", "https://api.doc2x.com/v1", "DOC2X_API_KEY", False, "Doc2X", "doc2x.com"), - ("v0", "V0", "https://api.v0.dev/v1", "V0_API_KEY", False, "v0 (Vercel)", "v0.dev"), - ("text_embeddings_inference", "TextEmbeddingsInference", "http://127.0.0.1:8080", "TEI_BASE_URL", True, "Text Embeddings Inference", "github.com/huggingface/text-embeddings-inference"), - ("pg_vector", "PgVector", "http://127.0.0.1:5432", "PG_VECTOR_URL", True, "PostgreSQL pgvector", "github.com/pgvector/pgvector"), - ("s3_vectors", "S3Vectors", "https://s3.amazonaws.com", "S3_VECTORS_API_KEY", False, "S3 Vectors", "aws.amazon.com"), - ("milvus", "Milvus", "http://127.0.0.1:19530", "MILVUS_URL", True, "Milvus (向量库)", "milvus.io"), - ("qdrant", "Qdrant", "http://127.0.0.1:6333", "QDRANT_URL", True, "Qdrant (向量库)", "qdrant.tech"), - ("chatgpt", "Chatgpt", "https://chatgpt.com/backend-api/codex", "CHATGPT_API_KEY", False, "ChatGPT (订阅)", "openai.com"), - ("nanogpt", "Nanogpt", "https://api.nanogpt.com/v1", "NANOGPT_API_KEY", False, "NanoGPT", "nanogpt.com"), - ("local", "Local", "http://127.0.0.1:8080/v1", "LOCAL_LLM_BASE_URL", True, "Local LLM", "localhost"), - ("cybertron", "Cybertron", "http://127.0.0.1:8080/v1", "CYBERTRON_BASE_URL", True, "Cybertron (Rust)", "github.com/dottorblaster/cybertron"), - ("mlx", "Mlx", "http://127.0.0.1:8080/v1", "MLX_BASE_URL", True, "MLX (Apple Silicon)", "github.com/ml-explore/mlx"), - ("openvino", "Openvino", "http://127.0.0.1:8080/v1", "OPENVINO_BASE_URL", True, "OpenVINO", "intel.com/openvino"), - ("gaudi", "Gaudi", "http://127.0.0.1:8080/v1", "GAUDI_BASE_URL", True, "Intel Gaudi", "intel.com"), - ("onnx", "Onnx", "http://127.0.0.1:8080/v1", "ONNX_BASE_URL", True, "ONNX Runtime", "onnxruntime.ai"), - ("omlx", "Omlx", "http://127.0.0.1:8080/v1", "OMLX_BASE_URL", True, "OMLX / MLX LM", "github.com/ml-explore/mlx-lm"), - ("doubaoaudio", "DoubaoAudio", "https://ark.cn-beijing.volces.com/api/v3", "ARK_API_KEY", False, "Doubao Audio", "volces.com"), -] - -def gen_local_template(mod, prefix, base_url, env_var, display, doc_url): - return f'''//! {display} provider — a thin OpenAI-compatible wrapper. -//! -//! See <{doc_url}> for API documentation. Exposes an OpenAI-compatible -//! Chat Completions API at `{base_url}`. The `{env_var}` environment -//! variable holds a *base URL* (not an API key); when unset, the default -//! endpoint is used. A placeholder API key is sent in the `Authorization` -//! header — the shared `OpenAIProvider` requires a non-empty key string. - -use aimux_core::error::AiMuxError; -use aimux_core::language_model::LanguageModel; -use aimux_core::provider::Provider; - -use crate::openai::{{OpenAIConfig, OpenAIModel, OpenAIProvider}}; - -const DEFAULT_BASE_URL: &str = "{base_url}"; -const ENV_VAR: &str = "{env_var}"; -const PROVIDER_NAME: &str = "{mod}"; -const PLACEHOLDER_API_KEY: &str = "{mod}"; - -pub struct {prefix}Config(OpenAIConfig); - -impl {prefix}Config {{ - pub fn new(api_key: impl Into) -> Self {{ - Self( - OpenAIConfig::new(api_key) - .with_base_url(DEFAULT_BASE_URL) - .with_provider(PROVIDER_NAME), - ) - }} - - pub fn from_env() -> Result {{ - let config = Self::new(PLACEHOLDER_API_KEY); - match std::env::var(ENV_VAR) {{ - Ok(url) if !url.trim().is_empty() => Ok(config.with_base_url(url)), - _ => Ok(config), - }} - }} - - pub fn with_base_url(mut self, url: impl Into) -> Self {{ - self.0 = self.0.with_base_url(url); - self - }} -}} - -pub struct {prefix}Provider(OpenAIProvider); - -impl {prefix}Provider {{ - pub fn new(config: {prefix}Config) -> Self {{ - Self(OpenAIProvider::new(config.0)) - }} - - pub fn model(&self, model_id: &str) -> OpenAIModel {{ - self.0.model(model_id) - }} -}} - -impl Provider for {prefix}Provider {{ - fn name(&self) -> &str {{ - PROVIDER_NAME - }} - - fn language_model(&self, model_id: &str) -> Result, AiMuxError> {{ - Ok(Box::new(self.model(model_id))) - }} -}} -''' - -def gen_cloud_template(mod, prefix, base_url, env_var, display, doc_url): - return f'''//! {display} provider — a thin OpenAI-compatible wrapper. -//! -//! See <{doc_url}> for API documentation. Exposes an OpenAI-compatible -//! Chat Completions API at `{base_url}`. Provider-specific details are the -//! base URL and the `{env_var}` environment variable; everything else is -//! delegated to the shared `OpenAIProvider`. - -use aimux_core::error::AiMuxError; -use aimux_core::language_model::LanguageModel; -use aimux_core::provider::Provider; -use aimux_provider_utils::load_api_key; - -use crate::openai::{{OpenAIConfig, OpenAIModel, OpenAIProvider}}; - -const DEFAULT_BASE_URL: &str = "{base_url}"; -const ENV_VAR: &str = "{env_var}"; -const PROVIDER_NAME: &str = "{mod}"; - -pub struct {prefix}Config(OpenAIConfig); - -impl {prefix}Config {{ - pub fn new(api_key: impl Into) -> Self {{ - Self(OpenAIConfig::new(api_key).with_base_url(DEFAULT_BASE_URL)) - }} - - pub fn from_env() -> Result {{ - let key = load_api_key(None, ENV_VAR, "{display}")?; - Ok(Self::new(key)) - }} - - pub fn with_base_url(mut self, url: impl Into) -> Self {{ - self.0 = self.0.with_base_url(url); - self - }} -}} - -pub struct {prefix}Provider(OpenAIProvider); - -impl {prefix}Provider {{ - pub fn new(config: {prefix}Config) -> Self {{ - Self(OpenAIProvider::new(config.0)) - }} - - pub fn model(&self, model_id: &str) -> OpenAIModel {{ - self.0.model(model_id) - }} -}} - -impl Provider for {prefix}Provider {{ - fn name(&self) -> &str {{ - PROVIDER_NAME - }} - - fn language_model(&self, model_id: &str) -> Result, AiMuxError> {{ - Ok(Box::new(self.model(model_id))) - }} -}} -''' - -def main(): - seen = set() - generated = [] - skipped = [] - - for mod_name, prefix, base_url, env_var, is_local, display, doc_url in PROVIDERS: - if mod_name in ALREADY_DONE or mod_name in seen: - skipped.append(mod_name) - continue - seen.add(mod_name) - - out_path = SRC / f"{mod_name}.rs" - if out_path.exists(): - skipped.append(mod_name) - continue - - if is_local: - content = gen_local_template(mod_name, prefix, base_url, env_var, display, doc_url) - else: - content = gen_cloud_template(mod_name, prefix, base_url, env_var, display, doc_url) - - out_path.write_text(content, encoding="utf-8") - generated.append((mod_name, prefix)) - - print(f"Generated {len(generated)} provider files:") - for mod, prefix in sorted(generated): - print(f" {mod}.rs ({prefix}Config / {prefix}Provider)") - if skipped: - print(f"\nSkipped {len(skipped)} (already exist): {', '.join(sorted(skipped))}") - - # Print lib.rs snippet - print("\n# Add to lib.rs:") - for mod_name, _ in sorted(generated): - print(f'pub mod {mod_name};') - print() - for mod_name, prefix in sorted(generated): - print(f'pub use {mod_name}::{{{prefix}Config, {prefix}Provider}};') - -if __name__ == "__main__": - main() diff --git a/scripts/list_wrong_providers.py b/scripts/list_wrong_providers.py deleted file mode 100644 index 3af56751..00000000 --- a/scripts/list_wrong_providers.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -"""List providers to remove: NOT OpenAI Chat Completions compatible.""" - -# These were generated as thin wrappers but are NOT OpenAI-compatible LLM chat providers. -# They need different traits (ImageModel/VideoModel/SpeechModel/EmbeddingModel/etc.) -# or different auth (SigV4/IAM/OAuth) or are not LLM APIs at all. - -REMOVE = { - # Vector databases — not LLM APIs - "milvus", "qdrant", "pg_vector", "s3_vectors", - - # Embedding/reranking only — not chat - "clip", "fastembed", "tei", "text_embeddings_inference", - "nomic", "jina", "mixedbread", - - # Image/Video/Music/3D generation — different API, not Chat Completions - "recraft", "ideogram", "stability_ai", "segmind", "runware", - "meshy", "tripo3d", "runwayml", "sora", "vidu", - "jimeng", "midjourney", "flux", "suno", - - # Speech/TTS/STT — different API - "murf", "playai", "speechify", "inworld", - "aws_polly", "nvidia_riva", "soniox", "doubaoaudio", "mokaai", - - # Non-LLM services — not chat APIs - "bing", "deepl", "dify", "slack", "doc2x", - "streamlake", "antling", "sangforaicp", "skylark", - - # Special auth — not API key + Bearer - "watsonx", "sagemaker", "sap", "oci", "snowflake", "bedrock_mantle", -} - -print(f"Total to remove: {len(REMOVE)}") -for name in sorted(REMOVE): - print(f" {name}") diff --git a/scripts/migrate_openai_compatible_standalone.py b/scripts/migrate_openai_compatible_standalone.py deleted file mode 100644 index 46b0517d..00000000 --- a/scripts/migrate_openai_compatible_standalone.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -"""Migrate remaining shell-type constructions in openai_compatible_test.rs. - -Pattern: `let cfg = XxxConfig::new("test-api-key").with_base_url(server.uri()); - let provider = XxxProvider::new(cfg); - let model = provider.model("...");` --> provider("xxx", Some(key), "model", Some(ProviderOptions { base_url })) - -Usage: uv run python scripts/migrate_openai_compatible_standalone.py -""" - -import json -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -F = ROOT / "aimux-providers" / "tests" / "openai_compatible_test.rs" -JSON = ROOT / "aimux-providers" / "src" / "provider_registry.json" - - -def to_type(n: str) -> str: - parts = n.split("_") - return "".join(p[0].upper() + p[1:] if p and not p[0].isdigit() else p for p in parts) - - -def main() -> int: - entries = json.loads(JSON.read_text(encoding="utf-8")) - type_to_name = {to_type(e["name"]) + "Config": e["name"] for e in entries} - - text = F.read_text(encoding="utf-8") - - pattern = re.compile( - r'let (?:config|cfg) = (\w+Config)::new\("test-api-key"\)\.with_base_url\(server\.uri\(\)\);\n' - r"\s*let provider = \w+Provider::new\(\1\);\n" - r'\s*let model = provider\.model\("([^"]+)"\);' - ) - - def repl(m: re.Match) -> str: - config_ty = m.group(1) - model_id = m.group(2) - name = type_to_name.get(config_ty) - if name is None: - raise ValueError(f"no registry name for {config_ty}") - return ( - "let model = provider(\n" - f' "{name}",\n' - ' Some("test-api-key".to_string()),\n' - f' "{model_id}",\n' - " Some(ProviderOptions {\n" - " base_url: Some(server.uri()),\n" - " ..Default::default()\n" - " }),\n" - " )\n" - ' .expect("provider construction");' - ) - - text, n = pattern.subn(repl, text) - F.write_text(text, encoding="utf-8") - print(f"standalone constructions replaced: {n}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/migrate_openai_compatible_test.py b/scripts/migrate_openai_compatible_test.py deleted file mode 100644 index 83b2e653..00000000 --- a/scripts/migrate_openai_compatible_test.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -"""Migrate openai_compatible_test.rs off the retired shell types (phase 4). - -- Drop the 250-type import block -> provider/ProviderOptions. -- Retarget the test-generating macro to provider(name, ...). -- Rewrite call sites to (mod_name, "name", "model"). - -Usage: uv run python scripts/migrate_openai_compatible_test.py -Idempotent: pattern-based edits. -""" - -import re -import sys -from pathlib import Path - -F = Path("aimux-providers/tests/openai_compatible_test.rs") - - -def main() -> int: - text = F.read_text(encoding="utf-8") - - # 1. Drop the big shell-type import block. - new_import = "use aimux_providers::{provider, ProviderOptions};" - text, n_imp = re.subn( - r"use aimux_providers::\{.*?\};", new_import, text, count=1, flags=re.DOTALL - ) - - # 2. Macro signature: (mod_name, provider_name_literal, model_id_literal). - text, n_sig = re.subn( - r"macro_rules! openai_compatible_tests \{\n" - r" \(\n" - r" \$mod_name:ident,\n" - r" \$config:ty,\n" - r" \$provider:ty,\n" - r" \$model_id:literal\n" - r" \) => \{", - "macro_rules! openai_compatible_tests {\n" - " (\n" - " $mod_name:ident,\n" - " $provider_name:literal,\n" - " $model_id:literal\n" - " ) => {", - text, - count=1, - ) - - # 3. make_provider body: shell types -> provider() with base_url override. - text, n_fac = re.subn( - r"fn make_provider\(server: &MockServer\) -> \$provider \{\n" - r" let config = <\$config>::new\(\"test-api-key\"\)\.with_base_url\(server\.uri\(\)\);\n" - r" <\$provider>::new\(config\)\n" - r" \}", - "fn make_provider(server: &MockServer) -> Box {\n" - " provider(\n" - " $provider_name,\n" - " Some(\"test-api-key\".to_string()),\n" - " $model_id,\n" - " Some(ProviderOptions {\n" - " base_url: Some(server.uri()),\n" - " ..Default::default()\n" - " }),\n" - " )\n" - " .expect(\"provider construction\")\n" - " }", - text, - count=1, - ) - - # 4. All `let provider = make_provider(&server); let model = provider.model($model_id);` - text, n_use = re.subn( - r"let provider = make_provider\(&server\);\n\s*let model = provider\.model\(\$model_id\);", - "let model = make_provider(&server);", - text, - ) - - # 5. Call sites (single- and multi-line): - # openai_compatible_tests!(name, NameConfig, NameProvider, "model"); - text, n_calls = re.subn( - r"openai_compatible_tests!\(\s*(\w+),\s*\w+Config,\s*\w+Provider,\s*(\"[^\"]*\")\s*\)", - r'openai_compatible_tests!(\1, "\1", \2)', - text, - ) - - # 5b. Tool-test call sites: mod name is "_tools", provider is "". - text, n_tool_calls = re.subn( - r"openai_compatible_tool_tests!\(\s*(\w+)_tools,\s*\w+Config,\s*\w+Provider,\s*(\"[^\"]*\")\s*\)", - r'openai_compatible_tool_tests!(\1_tools, "\1", \2)', - text, - ) - - # 5c. Tool-test macro signature + factory (same shape as the main macro). - text, n_tool_sig = re.subn( - r"macro_rules! openai_compatible_tool_tests \{\n" - r" \(\n" - r" \$mod_name:ident,\n" - r" \$config:ty,\n" - r" \$provider:ty,\n" - r" \$model_id:literal\n" - r" \) => \{", - "macro_rules! openai_compatible_tool_tests {\n" - " (\n" - " $mod_name:ident,\n" - " $provider_name:literal,\n" - " $model_id:literal\n" - " ) => {", - text, - count=1, - ) - text, n_tool_fac = re.subn( - r"fn make_provider\(server: &MockServer\) -> \$provider \{\n" - r" let config = <\$config>::new\(\"test-api-key\"\)\.with_base_url\(server\.uri\(\)\);\n" - r" <\$provider>::new\(config\)\n" - r" \}", - "fn make_provider(server: &MockServer) -> Box {\n" - " provider(\n" - " $provider_name,\n" - " Some(\"test-api-key\".to_string()),\n" - " $model_id,\n" - " Some(ProviderOptions {\n" - " base_url: Some(server.uri()),\n" - " ..Default::default()\n" - " }),\n" - " )\n" - " .expect(\"provider construction\")\n" - " }", - text, - count=1, - ) - - # 6. Doc comment example near the macro. - text, _ = re.subn( - r"// openai_compatible_tests!\(groq, Groq, GroqConfig, GroqProvider, \"llama-3\.3-70b-versatile\"\);", - '// openai_compatible_tests!(groq, "groq", "llama-3.3-70b-versatile");', - text, - count=1, - ) - - F.write_text(text, encoding="utf-8") - print( - f"imports={n_imp} signature={n_sig} factory={n_fac} uses={n_use} " - f"calls={n_calls} tool_calls={n_tool_calls} tool_sig={n_tool_sig} tool_fac={n_tool_fac}" - ) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/revert_wrong_inventory.py b/scripts/revert_wrong_inventory.py deleted file mode 100644 index bdec5e00..00000000 --- a/scripts/revert_wrong_inventory.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -"""Revert wrong providers in inventory: ✅薄 → ❌ for providers that were removed.""" - -from pathlib import Path -import re - -INV = Path(__file__).resolve().parent.parent / "rfc" / "0004-provider-inventory.md" -content = INV.read_text(encoding="utf-8") - -check_thin = "\u2705\u8584" # ✅薄 -cross = "\u274C" # ❌ - -# Provider names in inventory that should be reverted to ❌ -# Format: (inventory_name_variants) -TO_REVERT = [ - # Vector databases - "milvus (向量库)", "qdrant (向量库)", "pg_vector", "s3_vectors", - # Embedding-only - "clip", "fastembed", "tei (text embeddings inference)", - "text_embeddings_inference", "nomic", "jina", "mixedbread", - # Image/Video/Music/3D - "recraft", "ideogram", "flux", "suno (音乐)", "suno", - "midjourney", "sora", "vidu (视频)", "vidu", "jimeng (即梦)", - "meshy", "tripo3d", "segmind", "runware", "runwayml", "runway", - "stability", "stability-ai", - # Speech/TTS/STT - "murf", "playai", "speechify", "inworld", - "aws_polly", "nvidia_riva", "soniox", "doubaoaudio", "mokaai", - # Non-LLM - "bing (new bing)", "deepl", "dify", "slack (slack claude)", - "doc2x", "streamlake", "antling", "sangforaicp", "skylark (云雀)", - # Special auth - "watsonx (ibm)", "sagemaker (aws)", "sap", - "oci (oracle)", "snowflake", "bedrock_mantle", -] - -lines = content.split("\n") -changed = 0 -new_lines = [] -for line in lines: - if line.startswith("|") and check_thin in line: - # Check if this line matches any of the names to revert - parts = [p.strip() for p in line.split("|")] - if len(parts) >= 3: - name = parts[1] - for revert_name in TO_REVERT: - if name == revert_name: - # Replace first ✅薄 with ❌ - line = line.replace(check_thin, cross, 1) - changed += 1 - break - new_lines.append(line) - -INV.write_text("\n".join(new_lines), encoding="utf-8") -print(f"Reverted {changed} lines") diff --git a/scripts/update_inventory_status.py b/scripts/update_inventory_status.py deleted file mode 100644 index d1efc4b7..00000000 --- a/scripts/update_inventory_status.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -"""Update inventory: mark all aimux ❌ as ✅薄 since we generated all providers.""" - -import re -from pathlib import Path - -INV = Path(__file__).resolve().parent.parent / "rfc" / "0004-provider-inventory.md" -content = INV.read_text(encoding="utf-8") - -cross = "\u274C" # ❌ -check_thin = "\u2705\u8584" # ✅薄 - -# In table rows, the aimux column is the second column (after the provider name). -# Pattern: | provider_name | ❌ | ... -# Replace the first ❌ in each table row with ✅薄 -lines = content.split("\n") -changed = 0 -new_lines = [] -for line in lines: - if line.startswith("|") and cross in line: - # Only replace the FIRST ❌ (which is the aimux column) - new_line = line.replace(cross, check_thin, 1) - if new_line != line: - changed += 1 - new_lines.append(new_line) - else: - new_lines.append(line) - -INV.write_text("\n".join(new_lines), encoding="utf-8") -print(f"Updated {changed} lines") diff --git a/scripts/update_lib_rs.py b/scripts/update_lib_rs.py deleted file mode 100644 index cf42ba8d..00000000 --- a/scripts/update_lib_rs.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -"""Update lib.rs with all generated provider modules and re-exports.""" - -import re -from pathlib import Path - -LIB_RS = Path(__file__).resolve().parent.parent / "aimux-providers" / "src" / "lib.rs" -SRC_DIR = LIB_RS.parent - -# Find all .rs files that are not already declared in lib.rs -content = LIB_RS.read_text(encoding="utf-8") - -# Get already declared modules -declared = set(re.findall(r'^pub mod (\w+);', content, re.MULTILINE)) - -# Find all .rs files to add -to_add = [] -for f in sorted(SRC_DIR.glob("*.rs")): - name = f.stem - if name == "lib" or name in declared: - continue - # Extract struct names - file_content = f.read_text(encoding="utf-8") - config_match = re.search(r'pub struct (\w+Config)', file_content) - provider_match = re.search(r'pub struct (\w+Provider)', file_content) - if config_match and provider_match: - to_add.append((name, config_match.group(1), provider_match.group(1))) - -# Add module declarations after the last pub mod line -mod_lines = "\n".join(f"pub mod {name};" for name, _, _ in to_add) -# Find the last "pub mod" line and insert after it -lines = content.split("\n") -last_mod_idx = 0 -for i, line in enumerate(lines): - if line.startswith("pub mod "): - last_mod_idx = i -lines.insert(last_mod_idx + 1, f"\n// Bulk-generated thin-wrapper providers.\n{mod_lines}") - -# Add re-exports after the last pub use line -use_lines = "\n".join(f"pub use {name}::{{{cfg}, {prov}}};" for name, cfg, prov in to_add) -last_use_idx = 0 -for i, line in enumerate(lines): - if line.startswith("pub use "): - last_use_idx = i -lines.insert(last_use_idx + 1, f"\n// Bulk-generated provider re-exports.\n{use_lines}") - -LIB_RS.write_text("\n".join(lines), encoding="utf-8") -print(f"Added {len(to_add)} modules and re-exports to lib.rs")