Skip to content

Commit 10bcaa8

Browse files
committed
fix(MOC-236): #477 bot review round-8(pool-miss 不退回被排除的 active provider)
round-6 pool-miss 退 default_provider()=active,但 active 本身可能未加入整合(整合时 set-default 锁定,active 可能是开整合前的遗留)→ pool-miss 仍把流量打到被排除的 active。 proxy_runner 新增 pool_default_provider_id:池化下 default = active(若在子集内)否则池首条 所属 provider(与 apply root-model 锚定一致),绝不退被排除的 active。+1 单测。
1 parent 3c4f412 commit 10bcaa8

1 file changed

Lines changed: 76 additions & 6 deletions

File tree

src-tauri/src/proxy_runner.rs

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
282316
mod 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

Comments
 (0)