fix(claude-native): cold-resume a session on its persisted canonical model (#5167)

A pane's /model persists the exact id it runs (claude-opus-4-8), but the launch gate only accepted an exact catalog row, and a direct-login catalog spells that family as alias rows (opus -> claude-opus-5, the appended claude-opus-4-8[1m] default). Every live switch to a non-default model therefore armed a resume failure: "not in this host's current model list".

Accept a canonical Anthropic id when the endpoint serves canonical spellings and the catalog lists the id's family — the same fold /model already applies to an unpinned canonical id — and keep refusing gateways, Bedrock, and unlisted families so a stale pick still fails fast.

Covers the cold resume of a persisted pick for claude and codex in the live model-flows suite (red on claude before the fix, green after; codex has no alias layer and passes both ways), plus unit coverage of the fold and the launch gate.

Closes #5158
This commit is contained in:
Corey Zumar
2026-08-21 00:47:49 -07:00
committed by GitHub
parent 67f9db8bc8
commit 3d537bba59
6 changed files with 536 additions and 7 deletions
+51
View File
@@ -415,6 +415,57 @@ def _serves_canonical_anthropic_ids(claude_config: ClaudeNativeUcodeConfig) -> b
return host == "anthropic.com" or host.endswith(".anthropic.com")
def _claude_family(token: str) -> str | None:
"""
The family alias a model id or alias folds onto, bracket markers dropped.
:param token: A picker id or model id, e.g. ``"opus[1m]"``,
``"claude-opus-4-8"``.
:returns: The family alias, e.g. ``"opus"``, or ``None`` for none.
"""
from omnigent.claude_model_vocabulary import claude_model_alias
alias = claude_model_alias(token, {})
return alias.partition("[")[0] if alias else None
def claude_catalog_serves_model(
rows: list[dict[str, object]],
model: str,
claude_config: ClaudeNativeUcodeConfig | None,
) -> bool:
"""
Whether a launch of *model* is backed by this config's catalog.
An exact row — a picker id or its wire model — always serves. A canonical
Anthropic id no row spells exactly still launches when the endpoint takes
canonical spellings (``--model`` passes any string through, and a pane's
``/model`` persists exactly this id) and the catalog lists the id's
family: the same family fold ``/model`` applies to an unpinned canonical
id. A gateway that routes only its own ids, and a family the catalog
does not list, refuse — a genuinely stale pick still fails fast.
:param rows: Catalog rows, e.g.
``[{"id": "opus", "model": "claude-opus-5"}]``.
:param model: A picker id or model id, e.g. ``"claude-opus-4-8"``.
:param claude_config: The resolved launch config, or ``None`` (Claude's
own login).
:returns: ``True`` when the launch can run *model* against this catalog.
"""
from omnigent.model_catalog_store import catalog_contains
if catalog_contains(rows, model):
return True
if claude_config is not None and not _serves_canonical_anthropic_ids(claude_config):
return False
if not model.lower().startswith("claude-"):
return False
family = _claude_family(model)
return family is not None and any(
_claude_family(str(row.get("id") or row.get("model") or "")) == family for row in rows
)
def resolve_claude_native_model_selection(
model: str | None,
claude_config: ClaudeNativeUcodeConfig | None,
+6 -4
View File
@@ -6486,8 +6486,8 @@ async def _auto_create_claude_terminal(
# or to resolve a Default launch that would otherwise pass no ``--model``
# and leave the model to invisible CLI-private state.
if session_model_override or launch_model is None:
from omnigent.claude_native import claude_launch_catalog
from omnigent.model_catalog_store import catalog_contains, default_row
from omnigent.claude_native import claude_catalog_serves_model, claude_launch_catalog
from omnigent.model_catalog_store import default_row
launch_catalog: list[dict[str, object]] | None = None
try:
@@ -6501,9 +6501,11 @@ async def _auto_create_claude_terminal(
resolve_claude_native_model_selection(session_model_override, claude_config)
or session_model_override
)
# A pane's ``/model`` persists the exact id it runs; the catalog
# may spell that model only by its family alias.
if not (
catalog_contains(launch_catalog, session_model_override)
or catalog_contains(launch_catalog, resolved_request)
claude_catalog_serves_model(launch_catalog, session_model_override, claude_config)
or claude_catalog_serves_model(launch_catalog, resolved_request, claude_config)
):
raise click.ClickException(
f"the requested model {session_model_override!r} is not in this "