Skip to content
Merged
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 @@ -547,8 +547,6 @@ jobs:
if: fromJSON(needs.changes.outputs.run)['contract-tests']
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### 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()`.

**Rust (aimux-stream)**

- `SseStream` is now a thin adapter over the `sse-stream` crate (WHATWG
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,18 +277,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
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ aimux 是 **provider 接入与治理的运行时**:

版本语义:
- **Rust 层 breaking**:A2 删枚举、#185 换类型。
- **绑定源码级 breaking**:A2 删除 Go / Java / Kotlin / Swift / Flutter / Node / Python 的 `ProviderName` 类型化常量,改为字符串(Node 由 `gen_ts_types.py` 生成 string-literal union 保留补全);CHANGELOG 需给迁移说明。
- **绑定源码级 breaking**:A2 删除 Go / Java / Kotlin / Swift / Flutter / Node / Python 的 `ProviderName` 类型化常量,改为字符串,Node 也不保留 string-literal union(overlay 注册的名字无法进入编译期联合类型,保留它就是保留第二条路径);CHANGELOG 需给迁移说明。
- binding wire 与 C ABI 不变。

### 0.7.0 ——「L1 协议层 + auth」(~5-6 周)
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,12 +35,11 @@ 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_api_key_env, provider_from_env, provider_handle,
provider_registry_entry, register_provider,
provider_names, provider_registry_entry, register_provider,
};
pub use replay::{rebuild_decision_provider, 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 @@ -417,7 +417,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 @@ -594,6 +594,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())
}

/// The environment variable [`provider`] / [`provider_handle`] read `name`'s
/// API key from when no key is passed: the runtime overlay's `api_key`
/// `env:` reference or its `env_var`, `{NAME}_API_KEY` for the local
Expand Down Expand Up @@ -639,7 +644,6 @@ pub fn provider_registry_entry(name: &str) -> Option<OpenAICompatProfile> {
#[cfg(test)]
mod tests {
use super::*;
use crate::provider_name::ProviderName;

#[test]
fn provider_api_key_env_names_the_variable_the_provider_reads() {
Expand Down Expand Up @@ -708,23 +712,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 @@ -855,38 +848,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