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:
@@ -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,
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user