From fffda7a4642950b9cfb91c8bcc808351e2322542 Mon Sep 17 00:00:00 2001 From: aesxu2345 Date: Sun, 6 Sep 2026 01:28:28 -0700 Subject: [PATCH 1/7] feat: add GPT-6 model family --- README.md | 4 +++ chatmock/fast_mode.py | 6 ++++- chatmock/model_registry.py | 28 ++++++++++++++++++++ tests/test_routes.py | 52 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0df3503a..02c2e5b7 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,10 @@ The server runs at `http://127.0.0.1:8000` by default. Use `http://127.0.0.1:800 ChatMock automatically discovers the models available to the signed-in ChatGPT account. The current catalog commonly includes: +- `gpt-6` +- `gpt-6-mini` +- `gpt-6-nano` +- `gpt-6-pro` - `gpt-5.6-sol` - `gpt-5.6-terra` - `gpt-5.6-luna` diff --git a/chatmock/fast_mode.py b/chatmock/fast_mode.py index 498b70b8..170b8f70 100644 --- a/chatmock/fast_mode.py +++ b/chatmock/fast_mode.py @@ -8,6 +8,10 @@ PRIORITY_SUPPORTED_MODELS = frozenset( ( + "gpt-6", + "gpt-6-mini", + "gpt-6-nano", + "gpt-6-pro", "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", @@ -78,7 +82,7 @@ def resolve_service_tier( normalized = normalize_model_name(model) message = ( f"Fast mode is not supported for model '{normalized}'. " - "Use a supported GPT-5 priority-processing model or disable fast mode for this request." + "Use a supported GPT priority-processing model or disable fast mode for this request." ) if explicit_request: return ServiceTierResolution( diff --git a/chatmock/model_registry.py b/chatmock/model_registry.py index 0768b5ee..4cc59692 100644 --- a/chatmock/model_registry.py +++ b/chatmock/model_registry.py @@ -20,6 +20,34 @@ class ModelSpec: _MODEL_SPECS = ( + ModelSpec( + public_id="gpt-6", + upstream_id="gpt-6", + aliases=("gpt6", "gpt-6-latest"), + allowed_efforts=frozenset(("none", "low", "medium", "high", "xhigh", "max", "ultra")), + variant_efforts=("none", "low", "medium", "high", "xhigh", "max", "ultra"), + ), + ModelSpec( + public_id="gpt-6-mini", + upstream_id="gpt-6-mini", + aliases=("gpt6-mini", "gpt-6-mini-latest"), + allowed_efforts=frozenset(("none", "low", "medium", "high", "xhigh", "max", "ultra")), + variant_efforts=("none", "low", "medium", "high", "xhigh", "max", "ultra"), + ), + ModelSpec( + public_id="gpt-6-nano", + upstream_id="gpt-6-nano", + aliases=("gpt6-nano", "gpt-6-nano-latest"), + allowed_efforts=frozenset(("none", "low", "medium", "high", "xhigh", "max", "ultra")), + variant_efforts=("none", "low", "medium", "high", "xhigh", "max", "ultra"), + ), + ModelSpec( + public_id="gpt-6-pro", + upstream_id="gpt-6-pro", + aliases=("gpt6-pro", "gpt-6-pro-latest"), + allowed_efforts=frozenset(("none", "low", "medium", "high", "xhigh", "max", "ultra")), + variant_efforts=("none", "low", "medium", "high", "xhigh", "max", "ultra"), + ), ModelSpec( public_id="gpt-5", upstream_id="gpt-5", diff --git a/tests/test_routes.py b/tests/test_routes.py index a4906705..f14855b0 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -8,6 +8,7 @@ from unittest.mock import patch from chatmock.app import create_app +from chatmock.model_registry import extract_reasoning_from_model_name, normalize_model_name from chatmock.session import reset_session_state from websockets.sync.client import connect as ws_connect @@ -59,6 +60,10 @@ def test_openai_models_list(self) -> None: body = response.get_json() self.assertEqual(response.status_code, 200) model_ids = [item["id"] for item in body["data"]] + self.assertIn("gpt-6", model_ids) + self.assertIn("gpt-6-mini", model_ids) + self.assertIn("gpt-6-nano", model_ids) + self.assertIn("gpt-6-pro", model_ids) self.assertIn("gpt-5.4", model_ids) self.assertIn("gpt-5.4-mini", model_ids) self.assertIn("gpt-5.3-codex-spark", model_ids) @@ -71,12 +76,36 @@ def test_ollama_tags_list(self) -> None: body = response.get_json() self.assertEqual(response.status_code, 200) model_names = [item["name"] for item in body["models"]] + self.assertIn("gpt-6", model_names) + self.assertIn("gpt-6-mini", model_names) + self.assertIn("gpt-6-nano", model_names) + self.assertIn("gpt-6-pro", model_names) self.assertIn("gpt-5.4", model_names) self.assertIn("gpt-5.4-mini", model_names) self.assertIn("gpt-5.6-sol", model_names) self.assertIn("gpt-5.6-terra", model_names) self.assertIn("gpt-5.6-luna", model_names) + def test_gpt_6_aliases_and_reasoning_levels(self) -> None: + self.assertEqual(normalize_model_name("gpt6"), "gpt-6") + self.assertEqual(normalize_model_name("gpt-6-mini-latest"), "gpt-6-mini") + self.assertEqual(normalize_model_name("gpt6-nano-ultra"), "gpt-6-nano") + self.assertEqual( + extract_reasoning_from_model_name("gpt-6-pro:max"), + {"effort": "max"}, + ) + + def test_gpt_6_reasoning_models_list(self) -> None: + app = create_app(expose_reasoning_models=True, model_sync=False) + client = app.test_client() + response = client.get("/v1/models") + model_ids = [item["id"] for item in response.get_json()["data"]] + self.assertEqual(response.status_code, 200) + self.assertIn("gpt-6-none", model_ids) + self.assertIn("gpt-6-mini-xhigh", model_ids) + self.assertIn("gpt-6-nano-max", model_ids) + self.assertIn("gpt-6-pro-ultra", model_ids) + @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions(self, mock_start) -> None: mock_start.return_value = ( @@ -181,6 +210,29 @@ def test_chat_completions_fast_mode_sets_priority_service_tier(self, mock_start) self.assertEqual(response.status_code, 200) self.assertEqual(mock_start.call_args.kwargs["service_tier"], "priority") + @patch("chatmock.routes_openai.start_upstream_request") + def test_gpt_6_fast_mode_sets_priority_service_tier(self, mock_start) -> None: + mock_start.return_value = ( + FakeUpstream( + [ + {"type": "response.output_text.delta", "delta": "hello"}, + {"type": "response.completed", "response": {"id": "resp-openai"}}, + ] + ), + None, + ) + response = self.client.post( + "/v1/chat/completions", + json={ + "model": "gpt-6-pro", + "fast_mode": True, + "messages": [{"role": "user", "content": "hi"}], + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(mock_start.call_args.args[0], "gpt-6-pro") + self.assertEqual(mock_start.call_args.kwargs["service_tier"], "priority") + @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions_fast_mode_false_overrides_server_default(self, mock_start) -> None: app = create_app(fast_mode=True, model_sync=False) From 04fe3b05b94d966e9918892c8aa2b27c3c2f75e7 Mon Sep 17 00:00:00 2001 From: aesxu2345 Date: Sun, 6 Sep 2026 04:28:38 -0700 Subject: [PATCH 2/7] fix: merge synced and static model catalogs --- chatmock/model_registry.py | 31 ++++++++++++------ tests/test_routes.py | 67 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/chatmock/model_registry.py b/chatmock/model_registry.py index 4cc59692..2141fcf4 100644 --- a/chatmock/model_registry.py +++ b/chatmock/model_registry.py @@ -273,22 +273,33 @@ def extract_reasoning_from_model_name(model: str | None) -> dict[str, str] | Non def list_public_models(expose_reasoning_models: bool = False) -> list[str]: + model_ids: list[str] = [] + seen_ids: set[str] = set() + + def append_model(model_id: str) -> None: + if model_id not in seen_ids: + seen_ids.add(model_id) + model_ids.append(model_id) + catalog = current_model_catalog() if catalog is not None: remote_models = catalog.visible_models(wait_for_refresh=True) - if remote_models: - model_ids: list[str] = [] - for model in remote_models: - model_ids.append(model.slug) - if expose_reasoning_models: - model_ids.extend(f"{model.slug}-{effort}" for effort in model.reasoning_efforts) - return model_ids + for model in remote_models: + append_model(model.slug) + if expose_reasoning_models: + for effort in model.reasoning_efforts: + append_model(f"{model.slug}-{effort}") - model_ids: list[str] = [] for spec in _MODEL_SPECS: - model_ids.append(spec.public_id) + base_was_seen = spec.public_id in seen_ids + append_model(spec.public_id) if expose_reasoning_models: - model_ids.extend(f"{spec.public_id}-{effort}" for effort in spec.variant_efforts) + # Prefer account-specific reasoning metadata when the remote catalog + # contains this model; static variants are only a compatibility fallback. + if base_was_seen: + continue + for effort in spec.variant_efforts: + append_model(f"{spec.public_id}-{effort}") return model_ids diff --git a/tests/test_routes.py b/tests/test_routes.py index f14855b0..6ef9b905 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -8,6 +8,7 @@ from unittest.mock import patch from chatmock.app import create_app +from chatmock.model_catalog import CatalogModel, ModelCatalog from chatmock.model_registry import extract_reasoning_from_model_name, normalize_model_name from chatmock.session import reset_session_state from websockets.sync.client import connect as ws_connect @@ -106,6 +107,72 @@ def test_gpt_6_reasoning_models_list(self) -> None: self.assertIn("gpt-6-nano-max", model_ids) self.assertIn("gpt-6-pro-ultra", model_ids) + def test_remote_catalog_is_merged_with_static_models(self) -> None: + app = create_app(model_sync=False) + catalog = ModelCatalog(enabled=False) + remote_slugs = ( + "gpt-5.3-codex-spark", + "gpt-5.4-mini", + "gpt-5.5", + "gpt-5.6-luna", + "gpt-5.6-sol", + "gpt-5.6-terra", + ) + catalog._models = tuple( + CatalogModel( + slug=slug, + reasoning_efforts=("low", "medium", "high"), + service_tiers=frozenset(), + priority=index, + visibility="list", + supported_in_api=True, + ) + for index, slug in enumerate(remote_slugs) + ) + app.extensions["chatmock_model_catalog"] = catalog + client = app.test_client() + + openai_response = client.get("/v1/models") + ollama_response = client.get("/api/tags") + openai_ids = [item["id"] for item in openai_response.get_json()["data"]] + ollama_ids = [item["name"] for item in ollama_response.get_json()["models"]] + + self.assertEqual(openai_response.status_code, 200) + self.assertEqual(ollama_response.status_code, 200) + self.assertEqual(openai_ids, ollama_ids) + self.assertEqual(openai_ids[: len(remote_slugs)], list(remote_slugs)) + self.assertEqual(len(openai_ids), len(set(openai_ids))) + self.assertIn("gpt-6", openai_ids) + self.assertIn("gpt-6-mini", openai_ids) + self.assertIn("gpt-6-nano", openai_ids) + self.assertIn("gpt-6-pro", openai_ids) + self.assertIn("gpt-5", openai_ids) + self.assertIn("codex-mini", openai_ids) + + def test_remote_reasoning_variants_override_static_variants(self) -> None: + app = create_app(expose_reasoning_models=True, model_sync=False) + catalog = ModelCatalog(enabled=False) + catalog._models = ( + CatalogModel( + slug="gpt-5.5", + reasoning_efforts=("medium",), + service_tiers=frozenset(), + priority=0, + visibility="list", + supported_in_api=True, + ), + ) + app.extensions["chatmock_model_catalog"] = catalog + + response = app.test_client().get("/v1/models") + model_ids = [item["id"] for item in response.get_json()["data"]] + + self.assertEqual(response.status_code, 200) + self.assertEqual(model_ids.count("gpt-5.5"), 1) + self.assertIn("gpt-5.5-medium", model_ids) + self.assertNotIn("gpt-5.5-high", model_ids) + self.assertIn("gpt-6-ultra", model_ids) + @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions(self, mock_start) -> None: mock_start.return_value = ( From 1b372457588179610aaa81749a3aee4c4c13dcbf Mon Sep 17 00:00:00 2001 From: aesxu2345 Date: Sun, 6 Sep 2026 04:52:00 -0700 Subject: [PATCH 3/7] fix: only advertise verified GPT-6 models --- README.md | 4 -- chatmock/fast_mode.py | 4 -- chatmock/model_registry.py | 28 ----------- chatmock/routes_openai.py | 35 +++++++++++--- tests/test_routes.py | 99 ++++++++++++++++---------------------- 5 files changed, 70 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 02c2e5b7..0df3503a 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,6 @@ The server runs at `http://127.0.0.1:8000` by default. Use `http://127.0.0.1:800 ChatMock automatically discovers the models available to the signed-in ChatGPT account. The current catalog commonly includes: -- `gpt-6` -- `gpt-6-mini` -- `gpt-6-nano` -- `gpt-6-pro` - `gpt-5.6-sol` - `gpt-5.6-terra` - `gpt-5.6-luna` diff --git a/chatmock/fast_mode.py b/chatmock/fast_mode.py index 170b8f70..56adb332 100644 --- a/chatmock/fast_mode.py +++ b/chatmock/fast_mode.py @@ -8,10 +8,6 @@ PRIORITY_SUPPORTED_MODELS = frozenset( ( - "gpt-6", - "gpt-6-mini", - "gpt-6-nano", - "gpt-6-pro", "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", diff --git a/chatmock/model_registry.py b/chatmock/model_registry.py index 2141fcf4..a4633895 100644 --- a/chatmock/model_registry.py +++ b/chatmock/model_registry.py @@ -20,34 +20,6 @@ class ModelSpec: _MODEL_SPECS = ( - ModelSpec( - public_id="gpt-6", - upstream_id="gpt-6", - aliases=("gpt6", "gpt-6-latest"), - allowed_efforts=frozenset(("none", "low", "medium", "high", "xhigh", "max", "ultra")), - variant_efforts=("none", "low", "medium", "high", "xhigh", "max", "ultra"), - ), - ModelSpec( - public_id="gpt-6-mini", - upstream_id="gpt-6-mini", - aliases=("gpt6-mini", "gpt-6-mini-latest"), - allowed_efforts=frozenset(("none", "low", "medium", "high", "xhigh", "max", "ultra")), - variant_efforts=("none", "low", "medium", "high", "xhigh", "max", "ultra"), - ), - ModelSpec( - public_id="gpt-6-nano", - upstream_id="gpt-6-nano", - aliases=("gpt6-nano", "gpt-6-nano-latest"), - allowed_efforts=frozenset(("none", "low", "medium", "high", "xhigh", "max", "ultra")), - variant_efforts=("none", "low", "medium", "high", "xhigh", "max", "ultra"), - ), - ModelSpec( - public_id="gpt-6-pro", - upstream_id="gpt-6-pro", - aliases=("gpt6-pro", "gpt-6-pro-latest"), - allowed_efforts=frozenset(("none", "low", "medium", "high", "xhigh", "max", "ultra")), - variant_efforts=("none", "low", "medium", "high", "xhigh", "max", "ultra"), - ), ModelSpec( public_id="gpt-5", upstream_id="gpt-5", diff --git a/chatmock/routes_openai.py b/chatmock/routes_openai.py index 673e22fb..8258a651 100644 --- a/chatmock/routes_openai.py +++ b/chatmock/routes_openai.py @@ -71,6 +71,32 @@ def _gen(): return _gen() +def _upstream_error_message(upstream: Any) -> str: + raw = getattr(upstream, "content", b"") or b"" + text = getattr(upstream, "text", "") or "" + try: + body = json.loads(raw.decode("utf-8", errors="replace")) if raw else None + except (TypeError, ValueError): + body = None + + if isinstance(body, dict): + error = body.get("error") + if isinstance(error, dict): + message = error.get("message") + if isinstance(message, str) and message.strip(): + return message.strip() + elif isinstance(error, str) and error.strip(): + return error.strip() + for key in ("message", "detail", "title"): + value = body.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + + if isinstance(text, str) and text.strip(): + return text.strip() + return "Upstream error" + + def _service_tier_from_payload( model: str, payload: Dict[str, Any], @@ -237,11 +263,6 @@ def chat_completions() -> Response: created = int(time.time()) if upstream.status_code >= 400: - try: - raw = upstream.content - err_body = json.loads(raw.decode("utf-8", errors="ignore")) if raw else {"raw": upstream.text} - except Exception: - err_body = {"raw": upstream.text} if had_responses_tools: if verbose: print("[Passthrough] Upstream rejected tools; retrying without extra tools (args redacted)") @@ -262,7 +283,7 @@ def chat_completions() -> Response: else: err = { "error": { - "message": (err_body.get("error", {}) or {}).get("message", "Upstream error"), + "message": _upstream_error_message(upstream2 or upstream), "code": "RESPONSES_TOOLS_REJECTED", } } @@ -272,7 +293,7 @@ def chat_completions() -> Response: else: if verbose: print("Upstream error status=", upstream.status_code) - err = {"error": {"message": (err_body.get("error", {}) or {}).get("message", "Upstream error")}} + err = {"error": {"message": _upstream_error_message(upstream)}} if verbose: _log_json("OUT POST /v1/chat/completions", err) return jsonify(err), upstream.status_code diff --git a/tests/test_routes.py b/tests/test_routes.py index 6ef9b905..df026e90 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -9,7 +9,6 @@ from chatmock.app import create_app from chatmock.model_catalog import CatalogModel, ModelCatalog -from chatmock.model_registry import extract_reasoning_from_model_name, normalize_model_name from chatmock.session import reset_session_state from websockets.sync.client import connect as ws_connect @@ -61,10 +60,6 @@ def test_openai_models_list(self) -> None: body = response.get_json() self.assertEqual(response.status_code, 200) model_ids = [item["id"] for item in body["data"]] - self.assertIn("gpt-6", model_ids) - self.assertIn("gpt-6-mini", model_ids) - self.assertIn("gpt-6-nano", model_ids) - self.assertIn("gpt-6-pro", model_ids) self.assertIn("gpt-5.4", model_ids) self.assertIn("gpt-5.4-mini", model_ids) self.assertIn("gpt-5.3-codex-spark", model_ids) @@ -77,36 +72,12 @@ def test_ollama_tags_list(self) -> None: body = response.get_json() self.assertEqual(response.status_code, 200) model_names = [item["name"] for item in body["models"]] - self.assertIn("gpt-6", model_names) - self.assertIn("gpt-6-mini", model_names) - self.assertIn("gpt-6-nano", model_names) - self.assertIn("gpt-6-pro", model_names) self.assertIn("gpt-5.4", model_names) self.assertIn("gpt-5.4-mini", model_names) self.assertIn("gpt-5.6-sol", model_names) self.assertIn("gpt-5.6-terra", model_names) self.assertIn("gpt-5.6-luna", model_names) - def test_gpt_6_aliases_and_reasoning_levels(self) -> None: - self.assertEqual(normalize_model_name("gpt6"), "gpt-6") - self.assertEqual(normalize_model_name("gpt-6-mini-latest"), "gpt-6-mini") - self.assertEqual(normalize_model_name("gpt6-nano-ultra"), "gpt-6-nano") - self.assertEqual( - extract_reasoning_from_model_name("gpt-6-pro:max"), - {"effort": "max"}, - ) - - def test_gpt_6_reasoning_models_list(self) -> None: - app = create_app(expose_reasoning_models=True, model_sync=False) - client = app.test_client() - response = client.get("/v1/models") - model_ids = [item["id"] for item in response.get_json()["data"]] - self.assertEqual(response.status_code, 200) - self.assertIn("gpt-6-none", model_ids) - self.assertIn("gpt-6-mini-xhigh", model_ids) - self.assertIn("gpt-6-nano-max", model_ids) - self.assertIn("gpt-6-pro-ultra", model_ids) - def test_remote_catalog_is_merged_with_static_models(self) -> None: app = create_app(model_sync=False) catalog = ModelCatalog(enabled=False) @@ -142,13 +113,30 @@ def test_remote_catalog_is_merged_with_static_models(self) -> None: self.assertEqual(openai_ids, ollama_ids) self.assertEqual(openai_ids[: len(remote_slugs)], list(remote_slugs)) self.assertEqual(len(openai_ids), len(set(openai_ids))) - self.assertIn("gpt-6", openai_ids) - self.assertIn("gpt-6-mini", openai_ids) - self.assertIn("gpt-6-nano", openai_ids) - self.assertIn("gpt-6-pro", openai_ids) self.assertIn("gpt-5", openai_ids) self.assertIn("codex-mini", openai_ids) + def test_gpt_6_is_listed_when_remote_catalog_provides_it(self) -> None: + app = create_app(expose_reasoning_models=True, model_sync=False) + catalog = ModelCatalog(enabled=False) + catalog._models = ( + CatalogModel( + slug="gpt-6", + reasoning_efforts=("low", "high"), + service_tiers=frozenset(("priority",)), + priority=0, + visibility="list", + supported_in_api=True, + ), + ) + app.extensions["chatmock_model_catalog"] = catalog + + response = app.test_client().get("/v1/models") + model_ids = [item["id"] for item in response.get_json()["data"]] + + self.assertEqual(response.status_code, 200) + self.assertEqual(model_ids[:3], ["gpt-6", "gpt-6-low", "gpt-6-high"]) + def test_remote_reasoning_variants_override_static_variants(self) -> None: app = create_app(expose_reasoning_models=True, model_sync=False) catalog = ModelCatalog(enabled=False) @@ -171,7 +159,6 @@ def test_remote_reasoning_variants_override_static_variants(self) -> None: self.assertEqual(model_ids.count("gpt-5.5"), 1) self.assertIn("gpt-5.5-medium", model_ids) self.assertNotIn("gpt-5.5-high", model_ids) - self.assertIn("gpt-6-ultra", model_ids) @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions(self, mock_start) -> None: @@ -193,6 +180,27 @@ def test_chat_completions(self, mock_start) -> None: self.assertEqual(body["choices"][0]["message"]["content"], "hello") self.assertEqual(body["model"], "gpt5.4-mini") + @patch("chatmock.routes_openai.start_upstream_request") + def test_chat_completions_preserves_upstream_error_message(self, mock_start) -> None: + mock_start.return_value = ( + FakeUpstream( + status_code=400, + content=json.dumps({"detail": "The requested model is not available"}).encode("utf-8"), + ), + None, + ) + + response = self.client.post( + "/v1/chat/completions", + json={"model": "unavailable-model", "messages": [{"role": "user", "content": "hi"}]}, + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.get_json()["error"]["message"], + "The requested model is not available", + ) + @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions_honors_debug_model_override(self, mock_start) -> None: app = create_app(debug_model="gpt-5.4", model_sync=False) @@ -277,29 +285,6 @@ def test_chat_completions_fast_mode_sets_priority_service_tier(self, mock_start) self.assertEqual(response.status_code, 200) self.assertEqual(mock_start.call_args.kwargs["service_tier"], "priority") - @patch("chatmock.routes_openai.start_upstream_request") - def test_gpt_6_fast_mode_sets_priority_service_tier(self, mock_start) -> None: - mock_start.return_value = ( - FakeUpstream( - [ - {"type": "response.output_text.delta", "delta": "hello"}, - {"type": "response.completed", "response": {"id": "resp-openai"}}, - ] - ), - None, - ) - response = self.client.post( - "/v1/chat/completions", - json={ - "model": "gpt-6-pro", - "fast_mode": True, - "messages": [{"role": "user", "content": "hi"}], - }, - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(mock_start.call_args.args[0], "gpt-6-pro") - self.assertEqual(mock_start.call_args.kwargs["service_tier"], "priority") - @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions_fast_mode_false_overrides_server_default(self, mock_start) -> None: app = create_app(fast_mode=True, model_sync=False) From 275d03824aa74dc28a394b0d2ef182308ab021dd Mon Sep 17 00:00:00 2001 From: aesxu2345 Date: Sun, 6 Sep 2026 06:04:47 -0700 Subject: [PATCH 4/7] Add GPT-6 Astra model support --- README.md | 1 + chatmock/model_catalog.py | 4 +- chatmock/model_registry.py | 7 +++ tests/test_routes.py | 105 ++++++++++++++++++++++++++++++++++++- 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0df3503a..c2fd69e0 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ The server runs at `http://127.0.0.1:8000` by default. Use `http://127.0.0.1:800 ChatMock automatically discovers the models available to the signed-in ChatGPT account. The current catalog commonly includes: +- `gpt-6-astra` - `gpt-5.6-sol` - `gpt-5.6-terra` - `gpt-5.6-luna` diff --git a/chatmock/model_catalog.py b/chatmock/model_catalog.py index 89459608..d157b2ac 100644 --- a/chatmock/model_catalog.py +++ b/chatmock/model_catalog.py @@ -26,7 +26,7 @@ FETCH_TIMEOUT_SECONDS = 5 MODEL_CACHE_FILE = "chatmock_models_cache.json" # Bump only after verifying ChatMock against a newer Codex catalog contract. -CODEX_MODELS_CLIENT_VERSION = "0.146.0" +CODEX_MODELS_CLIENT_VERSION = "0.153.4" @dataclass(frozen=True) @@ -248,6 +248,8 @@ def _load_cache(self) -> None: return if not isinstance(payload, dict): return + if payload.get("client_version") != CODEX_MODELS_CLIENT_VERSION: + return cached_account_id = payload.get("account_id") current_account_id = _account_id_from_auth_file() diff --git a/chatmock/model_registry.py b/chatmock/model_registry.py index a4633895..0a303bd5 100644 --- a/chatmock/model_registry.py +++ b/chatmock/model_registry.py @@ -83,6 +83,13 @@ class ModelSpec: allowed_efforts=frozenset(("low", "medium", "high", "xhigh", "max")), variant_efforts=("low", "medium", "high", "xhigh", "max"), ), + ModelSpec( + public_id="gpt-6-astra", + upstream_id="gpt-6-astra", + aliases=("gpt6-astra", "gpt-6-astra-latest"), + allowed_efforts=frozenset(("low", "medium", "high", "xhigh", "max", "ultra")), + variant_efforts=("low", "medium", "high", "xhigh", "max", "ultra"), + ), ModelSpec( public_id="gpt-5.3-codex", upstream_id="gpt-5.3-codex", diff --git a/tests/test_routes.py b/tests/test_routes.py index df026e90..77f5e3fb 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -2,13 +2,15 @@ import json import socket +import tempfile import threading import time import unittest +from pathlib import Path from unittest.mock import patch from chatmock.app import create_app -from chatmock.model_catalog import CatalogModel, ModelCatalog +from chatmock.model_catalog import CODEX_MODELS_CLIENT_VERSION, CatalogModel, ModelCatalog from chatmock.session import reset_session_state from websockets.sync.client import connect as ws_connect @@ -66,6 +68,7 @@ def test_openai_models_list(self) -> None: self.assertIn("gpt-5.6-sol", model_ids) self.assertIn("gpt-5.6-terra", model_ids) self.assertIn("gpt-5.6-luna", model_ids) + self.assertIn("gpt-6-astra", model_ids) def test_ollama_tags_list(self) -> None: response = self.client.get("/api/tags") @@ -77,6 +80,7 @@ def test_ollama_tags_list(self) -> None: self.assertIn("gpt-5.6-sol", model_names) self.assertIn("gpt-5.6-terra", model_names) self.assertIn("gpt-5.6-luna", model_names) + self.assertIn("gpt-6-astra", model_names) def test_remote_catalog_is_merged_with_static_models(self) -> None: app = create_app(model_sync=False) @@ -160,6 +164,84 @@ def test_remote_reasoning_variants_override_static_variants(self) -> None: self.assertIn("gpt-5.5-medium", model_ids) self.assertNotIn("gpt-5.5-high", model_ids) + def test_remote_astra_reasoning_variants_override_static_variants(self) -> None: + app = create_app(expose_reasoning_models=True, model_sync=False) + catalog = ModelCatalog(enabled=False) + catalog._models = ( + CatalogModel( + slug="gpt-6-astra", + reasoning_efforts=("low", "ultra"), + service_tiers=frozenset(("priority",)), + priority=0, + visibility="list", + supported_in_api=True, + ), + ) + app.extensions["chatmock_model_catalog"] = catalog + + response = app.test_client().get("/v1/models") + model_ids = [item["id"] for item in response.get_json()["data"]] + + self.assertEqual(response.status_code, 200) + self.assertEqual(model_ids.count("gpt-6-astra"), 1) + self.assertIn("gpt-6-astra-low", model_ids) + self.assertIn("gpt-6-astra-ultra", model_ids) + self.assertNotIn("gpt-6-astra-medium", model_ids) + + @patch("chatmock.model_catalog._account_id_from_auth_file", return_value="acct") + def test_model_catalog_ignores_cache_from_old_client_version(self, _mock_account) -> None: + with tempfile.TemporaryDirectory() as directory: + cache_path = Path(directory) / "models.json" + cache_path.write_text( + json.dumps( + { + "client_version": "0.146.0", + "account_id": "acct", + "fetched_at": "2026-01-01T00:00:00Z", + "models": [ + { + "slug": "stale-model", + "visibility": "list", + "supported_in_api": True, + } + ], + } + ), + encoding="utf-8", + ) + + catalog = ModelCatalog(cache_path=cache_path) + + self.assertEqual(CODEX_MODELS_CLIENT_VERSION, "0.153.4") + self.assertEqual(catalog._models, ()) + + @patch("chatmock.model_catalog._account_id_from_auth_file", return_value="acct") + def test_model_catalog_loads_current_client_version_cache(self, _mock_account) -> None: + with tempfile.TemporaryDirectory() as directory: + cache_path = Path(directory) / "models.json" + cache_path.write_text( + json.dumps( + { + "client_version": CODEX_MODELS_CLIENT_VERSION, + "account_id": "acct", + "fetched_at": "2026-01-01T00:00:00Z", + "models": [ + { + "slug": "gpt-6-astra", + "supported_reasoning_levels": [{"effort": "ultra"}], + "visibility": "list", + "supported_in_api": True, + } + ], + } + ), + encoding="utf-8", + ) + + catalog = ModelCatalog(cache_path=cache_path) + + self.assertEqual([model.slug for model in catalog._models], ["gpt-6-astra"]) + @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions(self, mock_start) -> None: mock_start.return_value = ( @@ -180,6 +262,27 @@ def test_chat_completions(self, mock_start) -> None: self.assertEqual(body["choices"][0]["message"]["content"], "hello") self.assertEqual(body["model"], "gpt5.4-mini") + @patch("chatmock.routes_openai.start_upstream_request") + def test_chat_completions_maps_astra_alias_and_reasoning_effort(self, mock_start) -> None: + mock_start.return_value = ( + FakeUpstream( + [ + {"type": "response.output_text.delta", "delta": "hello"}, + {"type": "response.completed", "response": {"id": "resp-astra"}}, + ] + ), + None, + ) + + response = self.client.post( + "/v1/chat/completions", + json={"model": "gpt6-astra-ultra", "messages": [{"role": "user", "content": "hi"}]}, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(mock_start.call_args.args[0], "gpt-6-astra") + self.assertEqual(mock_start.call_args.kwargs["reasoning_param"]["effort"], "ultra") + @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions_preserves_upstream_error_message(self, mock_start) -> None: mock_start.return_value = ( From 6d271d506aa3cf10737d27ec5e64274c215e4582 Mon Sep 17 00:00:00 2001 From: aesxu2345 Date: Sun, 6 Sep 2026 06:51:59 -0700 Subject: [PATCH 5/7] Expose GPT-6 Astra reasoning variants --- DOCKER.md | 2 +- README.md | 9 +++++++-- chatmock/model_registry.py | 8 ++++++-- tests/test_routes.py | 9 ++++++++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index caf2b22f..332387b0 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -30,7 +30,7 @@ Set options in `.env` or pass environment variables: - `CHATGPT_LOCAL_REASONING_COMPAT`: legacy|o3|think-tags|current - `CHATGPT_LOCAL_FAST_MODE`: `true|false` to enable fast mode by default for supported models - `CHATGPT_LOCAL_CLIENT_ID`: OAuth client id override (rarely needed) -- `CHATGPT_LOCAL_EXPOSE_REASONING_MODELS`: `true|false` to add reasoning model variants to `/v1/models` +- `CHATGPT_LOCAL_EXPOSE_REASONING_MODELS`: `true|false` to add reasoning variants for all models to `/v1/models`; `gpt-6-astra` variants are always listed - `CHATGPT_LOCAL_ENABLE_WEB_SEARCH`: `true|false` to enable default web search tool - `CHATGPT_LOCAL_MODEL_SYNC`: `true|false` to discover account models automatically (default `true`) - `CHATGPT_LOCAL_MODEL_REFRESH_INTERVAL`: model catalog refresh interval in seconds (default `3600`) diff --git a/README.md b/README.md index c2fd69e0..08250fb4 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,11 @@ account. The current catalog commonly includes: - `gpt-5.4-mini` - `gpt-5.3-codex-spark` +`gpt-6-astra` always exposes each supported reasoning effort as a separate +model ID, such as `gpt-6-astra-low`, `gpt-6-astra-medium`, and +`gpt-6-astra-xhigh`. The signed-in account catalog determines the exact set; +the static fallback also includes `high`, `max`, and `ultra`. +
## Features @@ -111,7 +116,7 @@ account. The current catalog commonly includes: - Web search tool - OpenAI-compatible `/v1/responses` (HTTP + WebSocket) - Ollama-compatible endpoints -- Reasoning effort exposed as separate models (optional) +- Reasoning effort exposed as separate models (`gpt-6-astra` always; other models optional)
@@ -126,7 +131,7 @@ All flags go after `chatmock serve`. These can also be set as environment variab | `--reasoning-compat` | `CHATGPT_LOCAL_REASONING_COMPAT` | legacy, o3, think-tags | think-tags | How reasoning is returned to the client | | `--fast-mode` | `CHATGPT_LOCAL_FAST_MODE` | true/false | false | Priority processing for supported models | | `--enable-web-search` | `CHATGPT_LOCAL_ENABLE_WEB_SEARCH` | true/false | false | Allow the model to search the web | -| `--expose-reasoning-models` | `CHATGPT_LOCAL_EXPOSE_REASONING_MODELS` | true/false | false | List each reasoning level as its own model | +| `--expose-reasoning-models` | `CHATGPT_LOCAL_EXPOSE_REASONING_MODELS` | true/false | false | List each reasoning level for all models; `gpt-6-astra` variants are always listed | | `--model-sync` | `CHATGPT_LOCAL_MODEL_SYNC` | true/false | true | Discover account models automatically | | `--model-refresh-interval` | `CHATGPT_LOCAL_MODEL_REFRESH_INTERVAL` | seconds | 3600 | Refresh interval for model discovery | diff --git a/chatmock/model_registry.py b/chatmock/model_registry.py index 0a303bd5..e8618f10 100644 --- a/chatmock/model_registry.py +++ b/chatmock/model_registry.py @@ -8,6 +8,7 @@ ALL_REASONING_EFFORTS = ("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra") DEFAULT_REASONING_EFFORTS = frozenset(ALL_REASONING_EFFORTS) +ALWAYS_EXPOSE_REASONING_VARIANTS = frozenset(("gpt-6-astra",)) @dataclass(frozen=True) @@ -260,19 +261,22 @@ def append_model(model_id: str) -> None: seen_ids.add(model_id) model_ids.append(model_id) + def expose_variants_for(model_id: str) -> bool: + return expose_reasoning_models or model_id in ALWAYS_EXPOSE_REASONING_VARIANTS + catalog = current_model_catalog() if catalog is not None: remote_models = catalog.visible_models(wait_for_refresh=True) for model in remote_models: append_model(model.slug) - if expose_reasoning_models: + if expose_variants_for(model.slug): for effort in model.reasoning_efforts: append_model(f"{model.slug}-{effort}") for spec in _MODEL_SPECS: base_was_seen = spec.public_id in seen_ids append_model(spec.public_id) - if expose_reasoning_models: + if expose_variants_for(spec.public_id): # Prefer account-specific reasoning metadata when the remote catalog # contains this model; static variants are only a compatibility fallback. if base_was_seen: diff --git a/tests/test_routes.py b/tests/test_routes.py index 77f5e3fb..53c8f93d 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -69,6 +69,9 @@ def test_openai_models_list(self) -> None: self.assertIn("gpt-5.6-terra", model_ids) self.assertIn("gpt-5.6-luna", model_ids) self.assertIn("gpt-6-astra", model_ids) + for effort in ("low", "medium", "high", "xhigh", "max", "ultra"): + self.assertIn(f"gpt-6-astra-{effort}", model_ids) + self.assertNotIn("gpt-5.6-sol-low", model_ids) def test_ollama_tags_list(self) -> None: response = self.client.get("/api/tags") @@ -81,6 +84,9 @@ def test_ollama_tags_list(self) -> None: self.assertIn("gpt-5.6-terra", model_names) self.assertIn("gpt-5.6-luna", model_names) self.assertIn("gpt-6-astra", model_names) + for effort in ("low", "medium", "high", "xhigh", "max", "ultra"): + self.assertIn(f"gpt-6-astra-{effort}", model_names) + self.assertNotIn("gpt-5.6-sol-low", model_names) def test_remote_catalog_is_merged_with_static_models(self) -> None: app = create_app(model_sync=False) @@ -165,7 +171,7 @@ def test_remote_reasoning_variants_override_static_variants(self) -> None: self.assertNotIn("gpt-5.5-high", model_ids) def test_remote_astra_reasoning_variants_override_static_variants(self) -> None: - app = create_app(expose_reasoning_models=True, model_sync=False) + app = create_app(model_sync=False) catalog = ModelCatalog(enabled=False) catalog._models = ( CatalogModel( @@ -187,6 +193,7 @@ def test_remote_astra_reasoning_variants_override_static_variants(self) -> None: self.assertIn("gpt-6-astra-low", model_ids) self.assertIn("gpt-6-astra-ultra", model_ids) self.assertNotIn("gpt-6-astra-medium", model_ids) + self.assertNotIn("gpt-5.5-medium", model_ids) @patch("chatmock.model_catalog._account_id_from_auth_file", return_value="acct") def test_model_catalog_ignores_cache_from_old_client_version(self, _mock_account) -> None: From bcbd73cdd06a20722cc8596ab23ff3b097268276 Mon Sep 17 00:00:00 2001 From: aesxu2345 Date: Sun, 6 Sep 2026 23:41:50 -0700 Subject: [PATCH 6/7] Hide unverified GPT-6 Astra ultra model entry --- README.md | 8 +++++-- chatmock/model_registry.py | 7 +++++- tests/test_routes.py | 46 +++++++++++++++++++++++++++++++++----- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 08250fb4..939f4b02 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,14 @@ account. The current catalog commonly includes: - `gpt-5.4-mini` - `gpt-5.3-codex-spark` -`gpt-6-astra` always exposes each supported reasoning effort as a separate +`gpt-6-astra` exposes reasoning efforts as separate model ID, such as `gpt-6-astra-low`, `gpt-6-astra-medium`, and `gpt-6-astra-xhigh`. The signed-in account catalog determines the exact set; -the static fallback also includes `high`, `max`, and `ultra`. +the static fallback also includes `high` and `max`. +`gpt-6-astra-ultra` is excluded from both model lists, including cached or +remote catalog entries, because its upstream support has not been verified +and requests have been reported to fail. This removes discovery only: manually +specified requests are not remapped to another effort or blocked by this change.
diff --git a/chatmock/model_registry.py b/chatmock/model_registry.py index e8618f10..9b5e49f0 100644 --- a/chatmock/model_registry.py +++ b/chatmock/model_registry.py @@ -9,6 +9,9 @@ ALL_REASONING_EFFORTS = ("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra") DEFAULT_REASONING_EFFORTS = frozenset(ALL_REASONING_EFFORTS) ALWAYS_EXPOSE_REASONING_VARIANTS = frozenset(("gpt-6-astra",)) +# Do not advertise this unverified upstream model/effort combination, even +# when it appears in a cached or remote catalog. +HIDDEN_MODEL_IDS = frozenset(("gpt-6-astra-ultra",)) @dataclass(frozen=True) @@ -89,7 +92,7 @@ class ModelSpec: upstream_id="gpt-6-astra", aliases=("gpt6-astra", "gpt-6-astra-latest"), allowed_efforts=frozenset(("low", "medium", "high", "xhigh", "max", "ultra")), - variant_efforts=("low", "medium", "high", "xhigh", "max", "ultra"), + variant_efforts=("low", "medium", "high", "xhigh", "max"), ), ModelSpec( public_id="gpt-5.3-codex", @@ -257,6 +260,8 @@ def list_public_models(expose_reasoning_models: bool = False) -> list[str]: seen_ids: set[str] = set() def append_model(model_id: str) -> None: + if model_id in HIDDEN_MODEL_IDS: + return if model_id not in seen_ids: seen_ids.add(model_id) model_ids.append(model_id) diff --git a/tests/test_routes.py b/tests/test_routes.py index 53c8f93d..9349d286 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -69,8 +69,9 @@ def test_openai_models_list(self) -> None: self.assertIn("gpt-5.6-terra", model_ids) self.assertIn("gpt-5.6-luna", model_ids) self.assertIn("gpt-6-astra", model_ids) - for effort in ("low", "medium", "high", "xhigh", "max", "ultra"): + for effort in ("low", "medium", "high", "xhigh", "max"): self.assertIn(f"gpt-6-astra-{effort}", model_ids) + self.assertNotIn("gpt-6-astra-ultra", model_ids) self.assertNotIn("gpt-5.6-sol-low", model_ids) def test_ollama_tags_list(self) -> None: @@ -84,8 +85,9 @@ def test_ollama_tags_list(self) -> None: self.assertIn("gpt-5.6-terra", model_names) self.assertIn("gpt-5.6-luna", model_names) self.assertIn("gpt-6-astra", model_names) - for effort in ("low", "medium", "high", "xhigh", "max", "ultra"): + for effort in ("low", "medium", "high", "xhigh", "max"): self.assertIn(f"gpt-6-astra-{effort}", model_names) + self.assertNotIn("gpt-6-astra-ultra", model_names) self.assertNotIn("gpt-5.6-sol-low", model_names) def test_remote_catalog_is_merged_with_static_models(self) -> None: @@ -191,10 +193,44 @@ def test_remote_astra_reasoning_variants_override_static_variants(self) -> None: self.assertEqual(response.status_code, 200) self.assertEqual(model_ids.count("gpt-6-astra"), 1) self.assertIn("gpt-6-astra-low", model_ids) - self.assertIn("gpt-6-astra-ultra", model_ids) + self.assertNotIn("gpt-6-astra-ultra", model_ids) self.assertNotIn("gpt-6-astra-medium", model_ids) self.assertNotIn("gpt-5.5-medium", model_ids) + def test_astra_ultra_is_hidden_from_both_catalog_endpoints(self) -> None: + for expose in (False, True): + with self.subTest(expose_reasoning_models=expose): + app = create_app(expose_reasoning_models=expose, model_sync=False) + catalog = ModelCatalog(enabled=False) + catalog._models = tuple( + CatalogModel( + slug=slug, + reasoning_efforts=efforts, + service_tiers=frozenset(), + priority=index, + visibility="list", + supported_in_api=True, + ) + for index, (slug, efforts) in enumerate(( + ("gpt-6-astra", ("low", "xhigh", "ultra")), + ("gpt-6-astra-ultra", ()), + ("gpt-5.6-sol", ("ultra",)), + )) + ) + app.extensions["chatmock_model_catalog"] = catalog + client = app.test_client() + openai = client.get("/v1/models") + ollama = client.get("/api/tags") + self.assertEqual(openai.status_code, 200) + self.assertEqual(ollama.status_code, 200) + ids = [item["id"] for item in openai.get_json()["data"]] + self.assertEqual(ids, [item["name"] for item in ollama.get_json()["models"]]) + self.assertNotIn("gpt-6-astra-ultra", ids) + self.assertIn("gpt-6-astra-low", ids) + self.assertIn("gpt-6-astra-xhigh", ids) + self.assertEqual(len(ids), len(set(ids))) + self.assertEqual("gpt-5.6-sol-ultra" in ids, expose) + @patch("chatmock.model_catalog._account_id_from_auth_file", return_value="acct") def test_model_catalog_ignores_cache_from_old_client_version(self, _mock_account) -> None: with tempfile.TemporaryDirectory() as directory: @@ -283,12 +319,12 @@ def test_chat_completions_maps_astra_alias_and_reasoning_effort(self, mock_start response = self.client.post( "/v1/chat/completions", - json={"model": "gpt6-astra-ultra", "messages": [{"role": "user", "content": "hi"}]}, + json={"model": "gpt6-astra-xhigh", "messages": [{"role": "user", "content": "hi"}]}, ) self.assertEqual(response.status_code, 200) self.assertEqual(mock_start.call_args.args[0], "gpt-6-astra") - self.assertEqual(mock_start.call_args.kwargs["reasoning_param"]["effort"], "ultra") + self.assertEqual(mock_start.call_args.kwargs["reasoning_param"]["effort"], "xhigh") @patch("chatmock.routes_openai.start_upstream_request") def test_chat_completions_preserves_upstream_error_message(self, mock_start) -> None: From 8927205571427c1dc7a70369753db64c06c541b5 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 20 Sep 2026 10:29:08 +0800 Subject: [PATCH 7/7] docs: list Astra reasoning variants and ignore macOS metadata --- .gitignore | 2 ++ README.md | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cc9f3177..02fba157 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist/ # OS clutter .DS_Store +MAC_OSX/ +__MACOSX/ diff --git a/README.md b/README.md index 939f4b02..87120eb8 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,10 @@ ChatMock automatically discovers the models available to the signed-in ChatGPT account. The current catalog commonly includes: - `gpt-6-astra` +- `gpt-6-astra-high` +- `gpt-6-astra-low` +- `gpt-6-astra-medium` +- `gpt-6-astra-xhigh` - `gpt-5.6-sol` - `gpt-5.6-terra` - `gpt-5.6-luna` @@ -100,7 +104,7 @@ account. The current catalog commonly includes: - `gpt-5.3-codex-spark` `gpt-6-astra` exposes reasoning efforts as separate -model ID, such as `gpt-6-astra-low`, `gpt-6-astra-medium`, and +model IDs, such as `gpt-6-astra-low`, `gpt-6-astra-medium`, and `gpt-6-astra-xhigh`. The signed-in account catalog determines the exact set; the static fallback also includes `high` and `max`. `gpt-6-astra-ultra` is excluded from both model lists, including cached or