Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: ProviderName generator drift (checked-in files must match generator output)
run: python3 scripts/gen_provider_names.py --check
- name: Providers doc generator drift (docs/api/providers.md must match generator output)
run: python3 scripts/gen_providers_doc.py --check
- uses: dtolnay/rust-toolchain@stable
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ tsconfig.tsbuildinfo

# 竞品 bad-case 挖矿副产物(数据 + 临时脚本),不进产品仓库
quirks-corpus/

# Generated audit artifacts — keep conclusions in Markdown, not raw output.
/docs/quality-audit/**/lcov.info
/docs/quality-audit/**/*.log
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to aimux are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Breaking

- Removed the generated `ProviderName` type in every binding (Rust enum, TS
const object, Go/Java/Kotlin consts, Swift enum, Dart consts, Python
`Literal`) and `scripts/gen_provider_names.py`. Provider names are plain
strings: `provider("groq", ...)`. Built-in and overlay-registered names now
share one string path. The provider list lives in
[docs/api/providers.md](docs/api/providers.md); Rust also gains
`provider_names()`.

## [0.5.0] - 2026-09-27

**Breaking release.** 13 PRs since 0.3.0: the cross-language error model
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,18 +275,16 @@ session.close()
```rust
// OpenAI → DeepSeek: only the provider name changes (registry-backed;
// key read from the provider's env var)
use aimux_providers::{provider, provider_from_env, ProviderName};
use aimux_providers::{provider, provider_from_env};

// 推荐:类型化 ProviderName(IDE 补全 + 编译期检查)
let model = provider(ProviderName::Deepseek, None, "deepseek-chat", None)?;
// 字符串形式同样可用:
let model = provider("deepseek", None, "deepseek-chat", None)?;
let model = provider_from_env("deepseek", "deepseek-chat", None)?;
// model usage is identical — it's all dyn LanguageModel
```

All registry-backed OpenAI-compatible providers share one entry:
`provider(name, ...)` in every binding, with typed `ProviderName`
(enum/union/consts per language).
`provider(name, ...)` in every binding. Provider names are runtime strings;
the generated provider list is the discovery surface.
The retired per-provider shell types (`XxxConfig`/`XxxProvider`) are gone —
see [docs/API.md](docs/API.md#providers).

Expand Down
3 changes: 1 addition & 2 deletions aimux-providers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ macro_rules! delegate_list_models {
// The 250 per-provider `XxxConfig`/`XxxProvider` shell types were retired in
// phase 4 — use [`provider`] / [`provider_from_env`] instead.
pub mod provider;
pub mod provider_name;
pub mod replay;
pub use provider::{
ExternalProviderEntry, ProviderOptions, ProviderProfile, is_external_provider,
load_providers_from_json, provider, provider_from_env, provider_handle,
load_providers_from_json, provider, provider_from_env, provider_handle, provider_names,
provider_registry_entry, register_provider,
};
pub use replay::rebuild_provider;
Expand Down
63 changes: 14 additions & 49 deletions aimux-providers/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ pub struct ProviderOptions {
/// - `options` overrides individual fields of the resolved entry
/// (replaces the retired `with_base_url` etc.).
/// - Unknown names return [`AiMuxError::NoSuchProvider`] naming the requested
/// provider; built-in names are enumerated by the generated `ProviderName`
/// provider; built-in names are available through [`provider_names`]
/// (overlay-registered names are not).
///
/// # Errors
Expand Down Expand Up @@ -387,7 +387,7 @@ pub fn provider_handle(
let entry = registry().iter().find(|e| e.name == name).ok_or_else(|| {
AiMuxError::NoSuchProvider {
// Display derives from the id alone; valid names are discoverable
// via the generated `ProviderName` — listing 250 names here would
// via `provider_names()` — listing 250 names here would
// ride along in every error, across the C ABI.
provider_id: name.to_string(),
}
Expand Down Expand Up @@ -550,6 +550,11 @@ pub fn provider_from_env(
provider(name, None, model_id, options)
}

/// Names of all built-in registry providers.
pub fn provider_names() -> impl Iterator<Item = &'static str> {
registry().iter().map(|entry| entry.name.as_str())
}

/// Public lookup of a registered provider's runtime profile — used by tests
/// that assert registry wiring (e.g. `max_tokens_key`) without constructing a
/// model. Returns `None` for unknown provider names.
Expand All @@ -564,7 +569,6 @@ pub fn provider_registry_entry(name: &str) -> Option<OpenAICompatProfile> {
#[cfg(test)]
mod tests {
use super::*;
use crate::provider_name::ProviderName;

#[test]
fn provider_builds_groq_model() {
Expand Down Expand Up @@ -593,23 +597,12 @@ mod tests {
}

#[test]
fn provider_accepts_typed_and_string_names() {
// Both spellings work: typed ProviderName (recommended) and string.
let typed = match provider(
ProviderName::Groq,
Some("sk-test".into()),
"llama-3.3-70b",
None,
) {
Ok(m) => m,
Err(e) => panic!("typed name should construct: {e}"),
};
assert_eq!(typed.model_id(), "llama-3.3-70b");
let string = match provider("groq", Some("sk-test".into()), "llama-3.3-70b", None) {
fn provider_accepts_string_names() {
let model = match provider("groq", Some("sk-test".into()), "llama-3.3-70b", None) {
Ok(m) => m,
Err(e) => panic!("string name should construct: {e}"),
};
assert_eq!(string.model_id(), "llama-3.3-70b");
assert_eq!(model.model_id(), "llama-3.3-70b");
}

#[test]
Expand Down Expand Up @@ -740,38 +733,10 @@ mod tests {
}

#[test]
fn provider_name_roundtrip() {
assert_eq!(ProviderName::Groq.as_str(), "groq");
assert_eq!(
"deepseek".parse::<ProviderName>().ok(),
Some(ProviderName::Deepseek)
);
assert_eq!("nope".parse::<ProviderName>().ok(), None);
assert!(ProviderName::all_names().contains("groq"));
assert_eq!(ProviderName::ALL.len(), 251);
}

#[test]
fn provider_name_matches_registry_json() {
// Anti-drift: every registry name must exist as a ProviderName variant
// and round-trip; counts must match (guards against editing the JSON
// without regenerating provider_name.rs).
let registry: serde_json::Value =
serde_json::from_str(include_str!("provider_registry.json"))
.expect("registry JSON is valid");
let names: Vec<&str> = registry
.as_array()
.expect("registry is an array")
.iter()
.map(|e| e["name"].as_str().expect("name is a string"))
.collect();
assert_eq!(ProviderName::ALL.len(), names.len());
for name in &names {
let variant = name
.parse::<ProviderName>()
.unwrap_or_else(|_| panic!("registry name {name} missing from ProviderName"));
assert_eq!(variant.as_str(), *name);
}
fn provider_names_list_the_registry() {
let names: Vec<_> = provider_names().collect();
assert_eq!(names.len(), registry().len());
assert!(names.contains(&"groq"));
}

// ── RFC-0020: external provider overlay ────────────────────────────────
Expand Down
Loading
Loading