@@ -261,23 +261,57 @@ fn load_resolver_snapshot() -> Result<ResolverSnapshot, String> {
261261 . ok_or_else ( || "gateway api key was not generated" . to_owned ( ) ) ?;
262262
263263 // 池化反查表:仅 `exposeAllProviderModels` 开时构建(与 catalog 生成端共用
264- // `unique_pool_slugs`,保证 slug 逐字一致)。关 → 空表 → `decide_provider` 退回
265- // slug-split / 默认 provider,行为与池化前完全一致。
266- let pool_map = if cfg. settings . expose_all_provider_models {
267- build_catalog_slug_map ( & unique_pool_slugs ( & cfg. providers ) )
264+ // `unique_pool_slugs`,保证 slug 逐字一致)。关 → 空 entries → 空表 → `decide_provider`
265+ // 退回 slug-split / 默认 provider,行为与池化前完全一致。
266+ let pool_entries = if cfg. settings . expose_all_provider_models {
267+ unique_pool_slugs ( & cfg. providers )
268268 } else {
269- HashMap :: new ( )
269+ Vec :: new ( )
270+ } ;
271+ let pool_map = build_catalog_slug_map ( & pool_entries) ;
272+ // 默认 provider(resolver fallback):非池化 → active。池化 → active 若在整合子集内则 active,
273+ // 否则用池首条所属 provider —— **绝不**退回被排除的 active(否则 pool-miss 把流量打到用户
274+ // 移出整合的 provider,#477 P2 round-8;且与 apply root-model 锚到「池首条」保持一致)。
275+ let default_provider_id = if pool_entries. is_empty ( ) {
276+ cfg. active_provider . clone ( )
277+ } else {
278+ pool_default_provider_id (
279+ & cfg. providers ,
280+ cfg. active_provider . as_deref ( ) ,
281+ & pool_entries,
282+ )
270283 } ;
271284
272285 Ok ( ResolverSnapshot {
273286 provider_count : cfg. providers . len ( ) ,
274287 active_provider : cfg. active_provider . clone ( ) ,
275- resolver : StaticResolver :: new ( Some ( gateway_key) , cfg. providers , cfg . active_provider )
288+ resolver : StaticResolver :: new ( Some ( gateway_key) , cfg. providers , default_provider_id )
276289 . with_catalog_slug_map ( pool_map) ,
277290 gateway_auth : true ,
278291 } )
279292}
280293
294+ /// 池化模式下 resolver 的默认 provider id。active 在整合子集(`pool_entries`)内 → 用 active;
295+ /// 否则退回池首条 entry 所属 provider(与 `apply` 把 root `model` 锚到池首条 slug 一致),
296+ /// **绝不**返回被排除的 active —— 否则 pool-miss 的请求会路由到用户移出整合的 provider
297+ /// (子集语义违例,#477 P2 round-8)。
298+ fn pool_default_provider_id (
299+ providers : & [ codex_app_transfer_registry:: Provider ] ,
300+ active : Option < & str > ,
301+ pool_entries : & [ codex_app_transfer_registry:: PoolEntry ] ,
302+ ) -> Option < String > {
303+ let active_idx = active. and_then ( |aid| providers. iter ( ) . position ( |p| p. id == aid) ) ;
304+ let active_in_pool =
305+ active_idx. is_some_and ( |ai| pool_entries. iter ( ) . any ( |e| e. provider_idx == ai) ) ;
306+ if active_in_pool {
307+ return active. map ( str:: to_owned) ;
308+ }
309+ pool_entries
310+ . first ( )
311+ . and_then ( |e| providers. get ( e. provider_idx ) )
312+ . map ( |p| p. id . clone ( ) )
313+ }
314+
281315#[ cfg( test) ]
282316mod tests {
283317 use super :: * ;
@@ -289,6 +323,42 @@ mod tests {
289323 use crate :: admin:: handlers:: common:: test_support:: with_isolated_home;
290324 use crate :: admin:: registry_io:: { load as load_registry, save_for_test as save_registry} ;
291325
326+ #[ test]
327+ fn pool_default_provider_id_skips_excluded_active ( ) {
328+ use codex_app_transfer_registry:: { PoolEntry , Provider } ;
329+ let providers: Vec < Provider > = serde_json:: from_value ( json ! ( [
330+ { "id" : "a" , "name" : "A" , "baseUrl" : "https://a" , "apiFormat" : "openai_chat" , "apiKey" : "k" , "models" : { "default" : "ma" } } ,
331+ { "id" : "b" , "name" : "B" , "baseUrl" : "https://b" , "apiFormat" : "openai_chat" , "apiKey" : "k" , "models" : { "default" : "mb" } } ,
332+ { "id" : "c" , "name" : "C" , "baseUrl" : "https://c" , "apiFormat" : "openai_chat" , "apiKey" : "k" , "models" : { "default" : "mc" } }
333+ ] ) )
334+ . unwrap ( ) ;
335+ // 整合子集 = b, c(idx 1,2);a 被排除(未加入整合)。
336+ let entries = vec ! [
337+ PoolEntry {
338+ provider_idx: 1 ,
339+ slug: "b/mb" . into( ) ,
340+ real_model: "mb" . into( ) ,
341+ supports_one_m: false ,
342+ } ,
343+ PoolEntry {
344+ provider_idx: 2 ,
345+ slug: "c/mc" . into( ) ,
346+ real_model: "mc" . into( ) ,
347+ supports_one_m: false ,
348+ } ,
349+ ] ;
350+ // active=a 被排除 → 默认改用池首条所属 provider(b),绝不退回排除的 a。
351+ assert_eq ! (
352+ pool_default_provider_id( & providers, Some ( "a" ) , & entries) . as_deref( ) ,
353+ Some ( "b" )
354+ ) ;
355+ // active=b 在子集内 → 默认仍 b。
356+ assert_eq ! (
357+ pool_default_provider_id( & providers, Some ( "b" ) , & entries) . as_deref( ) ,
358+ Some ( "b" )
359+ ) ;
360+ }
361+
292362 fn config_with_gateway ( base_url : String , gateway : Value ) -> Value {
293363 json ! ( {
294364 "version" : "2.1.15" ,
0 commit comments