Skip to content

Commit a6e6fef

Browse files
committed
fix(models): allow distinct protocols on one provider base URL
1 parent 7d99226 commit a6e6fef

2 files changed

Lines changed: 43 additions & 6 deletions

File tree

‎src/crates/assembly/core/AGENTS.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,12 @@ or test-target layout. Workspace checks and product-wide tests are CI-backed and
212212
are not the default Core precheck. For documentation-only changes, run
213213
`git diff --check`.
214214

215+
For built-in provider overlay, trusted endpoint validation, and reasoning catalog changes:
216+
217+
```bash
218+
cargo test -p openbitfun-core --no-default-features --features ai-adapter-runtime --lib infrastructure::ai::
219+
```
220+
215221
Configuration persistence, account settings import, backup restore, legacy
216222
field/deletion compatibility, local-change notifications, and save/reload/model
217223
concurrency regressions have feature-free fixtures:

‎src/crates/assembly/core/src/infrastructure/ai/provider_catalog.rs‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ fn validate_overlay(overlay: &ProviderOverlayDocument) -> Result<(), String> {
212212
let mut provider_ids = BTreeSet::new();
213213
let mut catalog_provider_owners = BTreeMap::<String, String>::new();
214214
let mut trusted_urls = BTreeMap::<String, String>::new();
215+
let mut trusted_routes = BTreeSet::new();
215216
for provider in &overlay.providers {
216217
if provider.id.trim().is_empty() || !provider_ids.insert(provider.id.as_str()) {
217218
return Err(format!("duplicate or empty provider ID '{}'", provider.id));
@@ -339,11 +340,19 @@ fn validate_overlay(overlay: &ProviderOverlayDocument) -> Result<(), String> {
339340
provider.id, endpoint.id
340341
));
341342
}
343+
// One provider may expose multiple protocols at the same base,
344+
// but ownership and each protocol's binding must be unambiguous.
345+
let unique_route = trusted_routes.insert((
346+
normalized.clone(),
347+
endpoint.api_format.trim().to_ascii_lowercase(),
348+
));
342349
if let Some(previous) = trusted_urls.insert(normalized, provider.id.clone()) {
343-
return Err(format!(
344-
"trusted endpoint is claimed by providers '{previous}' and '{}'",
345-
provider.id
346-
));
350+
if previous != provider.id || !unique_route {
351+
return Err(format!(
352+
"trusted endpoint is claimed by providers '{previous}' and '{}'",
353+
provider.id
354+
));
355+
}
347356
}
348357
}
349358
}
@@ -572,10 +581,32 @@ mod tests {
572581
use super::{parse_overlay, resolve_builtin_provider_catalog, trusted_models_dev_binding};
573582
use openbitfun_ai_adapters::models_dev::ModelsDevCatalog;
574583

584+
#[test]
585+
fn trusted_urls_allow_distinct_protocols_only_within_one_provider() {
586+
let overlay = parse_overlay().expect("valid overlay with MiMo protocols");
587+
let index = overlay
588+
.providers
589+
.iter()
590+
.position(|p| p.id == "xiaomi")
591+
.unwrap();
592+
let mut duplicate = overlay.clone();
593+
// A second spelling of the same protocol/normalized URL is ambiguous.
594+
duplicate.providers[index].endpoints[1].api_format = " OpenAI ".into();
595+
assert!(super::validate_overlay(&duplicate).is_err());
596+
597+
let mut cross_provider = overlay.clone();
598+
let mut other = overlay.providers[index].clone();
599+
other.id = "other-provider".into();
600+
other.endpoints.truncate(1);
601+
other.endpoints[0].api_format = "gemini".into();
602+
cross_provider.providers.push(other);
603+
assert!(super::validate_overlay(&cross_provider).is_err());
604+
}
605+
575606
#[test]
576607
fn overlay_is_valid_and_keeps_product_endpoint_decisions() {
577608
let overlay = parse_overlay().expect("valid overlay");
578-
assert_eq!(overlay.providers.len(), 14);
609+
assert_eq!(overlay.providers.len(), 15);
579610
let go = overlay
580611
.providers
581612
.iter()
@@ -812,7 +843,7 @@ mod tests {
812843
"bundle".to_string(),
813844
ProviderCatalogSource::Bundle,
814845
);
815-
assert_eq!(resolved.providers.len(), 14);
846+
assert_eq!(resolved.providers.len(), 15);
816847
let go = resolved
817848
.providers
818849
.iter()

0 commit comments

Comments
 (0)