From 4f33b8e44fb6143824010b4b27eec90e66f874a1 Mon Sep 17 00:00:00 2001 From: Fini Date: Fri, 31 Jul 2026 20:48:31 +0800 Subject: [PATCH] fix(agent): drop hidden models from the codex app-server list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cache parser filters on `visibility`, but the app-server parser ignored the protocol's twin field `hidden`, so the two paths that feed the same picker disagreed on what counts as a listable model. Today the server withholds internal entries (`codex-auto-review`) from `model/list` on its own, so this changes nothing against the current build — it keeps a server that starts sending them from leaking an unusable model into the picker. Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x --- .../op-host-services/src/model_discovery.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/op-host-services/src/model_discovery.rs b/crates/op-host-services/src/model_discovery.rs index 3cafd9dfa..131051cdd 100644 --- a/crates/op-host-services/src/model_discovery.rs +++ b/crates/op-host-services/src/model_discovery.rs @@ -316,6 +316,18 @@ fn parse_codex_model_list(line: &str) -> Option> { Some( data.iter() .filter_map(|m| { + // `hidden` is the app-server's twin of the cache's + // `visibility` (which `parse_codex_models_cache` filters + // on): entries the CLI lists for internal use only — + // `codex-auto-review` and friends. Today the server + // already withholds them from `model/list`, so this + // filter is a no-op against the current build; it exists + // so a server that starts sending them cannot leak an + // unusable model into the picker, and so both parse paths + // apply the same rule. + if m.get("hidden").and_then(serde_json::Value::as_bool) == Some(true) { + return None; + } let value = m.get("model").or_else(|| m.get("id"))?.as_str()?; let name = m .get("displayName") @@ -606,6 +618,22 @@ mod tests { assert_eq!(models[1].display_name, "gpt-5.4"); } + #[test] + fn app_server_parser_drops_hidden_models_like_the_cache_parser() { + // Same rule the cache parser applies via `visibility` — an + // internal-use entry must never reach the picker. + let models = parse_codex_model_list( + r#"{"id":2,"result":{"data":[ + {"id":"gpt-5.6-sol","model":"gpt-5.6-sol","displayName":"GPT-5.6-Sol","hidden":false}, + {"id":"codex-auto-review","model":"codex-auto-review","displayName":"Codex Auto Review","hidden":true}, + {"id":"gpt-5.5","model":"gpt-5.5","displayName":"GPT-5.5"} + ]}}"#, + ) + .expect("id:2 parses"); + let values: Vec<&str> = models.iter().map(|m| m.value.as_str()).collect(); + assert_eq!(values, ["gpt-5.6-sol", "gpt-5.5"]); + } + #[test] fn copilot_response_parser_picks_id2_model_list() { // connect reply (id 1) — not the model list.