fix(codex): unify management model catalogs and refresh fingerprints

This commit is contained in:
ZheFox
2026-09-05 13:14:24 +08:00
parent 27b0381a9a
commit 5ca4f87951
16 changed files with 453 additions and 73 deletions
+2 -2
View File
@@ -22,8 +22,8 @@ pub use logic::{
};
pub use strategy::{
antigravity_model_id_is_routable, fetch_models_from_transports,
fetch_models_from_transports_for_client_version, ModelFetchStrategy, ModelFetchStrategyKind,
ModelsFetchOutcome, SelectedModelFetchStrategy,
fetch_models_from_transports_for_client_version, fetch_models_from_transports_for_management,
ModelFetchStrategy, ModelFetchStrategyKind, ModelsFetchOutcome, SelectedModelFetchStrategy,
};
pub use transport::{
build_antigravity_fetch_available_models_plan, build_antigravity_load_code_assist_plan,
+1 -1
View File
@@ -1339,7 +1339,7 @@ mod tests {
"https://chatgpt.com/backend-api/codex"
),
Some((
"https://chatgpt.com/backend-api/codex/models?client_version=0.144.1".to_string(),
"https://chatgpt.com/backend-api/codex/models?client_version=0.153.3".to_string(),
"openai:responses".to_string()
))
);
+66 -4
View File
@@ -53,6 +53,8 @@ pub struct ModelsFetchOutcome {
pub legacy_models: Vec<Value>,
pub errors: Vec<String>,
pub has_success: bool,
/// Only native `models` responses may populate the opaque Codex client catalog.
pub native_codex_catalog: bool,
pub upstream_metadata: Option<Value>,
pub etag: Option<String>,
pub upstream_status: Option<u16>,
@@ -143,7 +145,25 @@ pub async fn fetch_models_from_transports_for_client_version(
codex_client_version: Option<&str>,
) -> Result<ModelsFetchOutcome, String> {
let strategy = select_model_fetch_strategy(transports)?;
execute_model_fetch_strategy(runtime, transports, strategy, codex_client_version).await
execute_model_fetch_strategy(
runtime,
transports,
strategy,
codex_client_version,
codex_client_version.is_none(),
)
.await
}
/// Management also supports Codex-compatible proxies returning OpenAI `data` arrays,
/// independently of the fingerprint sent upstream. Public client catalogs stay strict.
pub async fn fetch_models_from_transports_for_management(
runtime: &(impl ModelFetchTransportRuntime + ?Sized),
transports: &[GatewayProviderTransportSnapshot],
codex_client_version: Option<&str>,
) -> Result<ModelsFetchOutcome, String> {
let strategy = select_model_fetch_strategy(transports)?;
execute_model_fetch_strategy(runtime, transports, strategy, codex_client_version, true).await
}
fn select_model_fetch_strategy(
@@ -213,6 +233,7 @@ async fn execute_model_fetch_strategy(
transports: &[GatewayProviderTransportSnapshot],
strategy: SelectedModelFetchStrategy,
codex_client_version: Option<&str>,
allow_codex_legacy_response: bool,
) -> Result<ModelsFetchOutcome, String> {
let Some(first_transport) = transports.first() else {
return Err("No transport snapshots available for models fetch".to_string());
@@ -230,6 +251,7 @@ async fn execute_model_fetch_strategy(
transports,
strategy.provider_id(),
codex_client_version,
allow_codex_legacy_response,
)
.await
}
@@ -255,6 +277,7 @@ async fn fetch_standard_models(
transports: &[GatewayProviderTransportSnapshot],
provider_type: &str,
codex_client_version: Option<&str>,
allow_codex_legacy_response: bool,
) -> Result<ModelsFetchOutcome, String> {
let mut all_models = Vec::new();
let mut successful_codex_catalogs = Vec::<(String, Vec<Value>)>::new();
@@ -263,10 +286,19 @@ async fn fetch_standard_models(
let mut etag = ConsistentValue::default();
let mut upstream_status = ConsistentValue::default();
let is_codex = provider_type.trim().eq_ignore_ascii_case("codex");
let mut native_codex_catalog = is_codex;
for transport in transports {
match fetch_standard_models_for_transport(runtime, transport, codex_client_version).await {
match fetch_standard_models_for_transport(
runtime,
transport,
codex_client_version,
allow_codex_legacy_response,
)
.await
{
Ok(outcome) => {
native_codex_catalog &= outcome.native_codex_catalog;
all_models.extend(outcome.cached_models.iter().cloned());
if is_codex && outcome.has_success {
successful_codex_catalogs
@@ -294,6 +326,7 @@ async fn fetch_standard_models(
let upstream_metadata =
crate::logic::model_catalog_upstream_metadata(provider_type, &merged_models);
let mut outcome = build_success_outcome(merged_models, upstream_metadata, has_success);
outcome.native_codex_catalog = native_codex_catalog && has_success;
if let Some(model_ids) = codex_model_ids {
outcome.fetched_model_ids = model_ids;
outcome.legacy_models = project_codex_models_for_legacy_cache(
@@ -312,6 +345,7 @@ async fn fetch_standard_models_for_transport(
runtime: &(impl ModelFetchTransportRuntime + ?Sized),
transport: &GatewayProviderTransportSnapshot,
codex_client_version: Option<&str>,
allow_codex_legacy_response: bool,
) -> Result<ModelsFetchOutcome, (String, Option<u16>)> {
let mut all_models = Vec::new();
let mut seen_ids = BTreeSet::new();
@@ -324,6 +358,7 @@ async fn fetch_standard_models_for_transport(
.provider_type
.trim()
.eq_ignore_ascii_case("codex");
let mut native_codex_catalog = is_codex;
for _ in 0..20 {
let plan = build_standard_models_fetch_execution_plan_for_client_version(
@@ -341,11 +376,16 @@ async fn fetch_standard_models_for_transport(
upstream_status.observe(Some(result.status_code));
let body_json =
execution_result_json_body(&result).map_err(|err| (err, Some(result.status_code)))?;
native_codex_catalog &= body_json.get("models").and_then(Value::as_array).is_some();
let parsed = if is_codex {
parse_codex_models_response_for_request(
&transport.endpoint.api_format,
&body_json,
codex_client_version,
if allow_codex_legacy_response {
None
} else {
codex_client_version
},
)
} else {
parse_models_response_page(&transport.endpoint.api_format, &body_json)
@@ -385,7 +425,9 @@ async fn fetch_standard_models_for_transport(
next_after_id = Some(next_cursor);
}
Ok(build_success_outcome(all_models, None, has_success)
let mut outcome = build_success_outcome(all_models, None, has_success);
outcome.native_codex_catalog = native_codex_catalog && has_success;
Ok(outcome
.with_etag(etag.finish())
.with_upstream_status(upstream_status.finish()))
}
@@ -459,6 +501,7 @@ async fn fetch_antigravity_models(
legacy_models: Vec::new(),
errors,
has_success: false,
native_codex_catalog: false,
upstream_metadata: None,
etag: None,
upstream_status: None,
@@ -642,6 +685,7 @@ async fn fetch_vertex_api_key_models(
legacy_models: Vec::new(),
errors: vec!["vertex_ai(api_key): missing api key".to_string()],
has_success: false,
native_codex_catalog: false,
upstream_metadata: None,
etag: None,
upstream_status: None,
@@ -702,6 +746,7 @@ async fn fetch_vertex_api_key_models(
legacy_models: Vec::new(),
errors,
has_success,
native_codex_catalog: false,
upstream_metadata: None,
etag: None,
upstream_status: None,
@@ -720,6 +765,7 @@ async fn fetch_vertex_service_account_models(
legacy_models: Vec::new(),
errors: vec!["vertex_ai(service_account): missing auth_config".to_string()],
has_success: false,
native_codex_catalog: false,
upstream_metadata: None,
etag: None,
upstream_status: None,
@@ -791,6 +837,7 @@ async fn fetch_vertex_service_account_models(
legacy_models: Vec::new(),
errors,
has_success,
native_codex_catalog: false,
upstream_metadata: None,
etag: None,
upstream_status: None,
@@ -1426,6 +1473,7 @@ fn build_success_outcome(
legacy_models,
errors: Vec::new(),
has_success,
native_codex_catalog: false,
upstream_metadata,
etag: None,
upstream_status: None,
@@ -2428,6 +2476,20 @@ mod tests {
assert!(outcome.has_success);
assert_eq!(outcome.fetched_model_ids, vec!["gpt-legacy-compatible"]);
assert_eq!(outcome.cached_models[0]["id"], "gpt-legacy-compatible");
assert!(!outcome.native_codex_catalog);
let versioned = crate::fetch_models_from_transports_for_management(
&runtime,
&[sample_codex_transport()],
Some("0.153.3"),
)
.await
.expect("management retains data-array compatibility with a new fingerprint");
assert!(versioned.has_success);
assert!(
!versioned.native_codex_catalog,
"generic responses cannot replace opaque catalogs"
);
assert_eq!(versioned.fetched_model_ids, outcome.fetched_model_ids);
assert_eq!(
outcome.legacy_models[0]["api_formats"],
json!(["openai:responses"])
+1 -1
View File
@@ -986,7 +986,7 @@ mod tests {
assert_eq!(
plan.url,
"https://chatgpt.com/backend-api/codex/models?client_version=0.144.1"
"https://chatgpt.com/backend-api/codex/models?client_version=0.153.3"
);
assert_eq!(
plan.headers.get("authorization").map(String::as_str),