@@ -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