Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/autoskillit/cli/doctor/_doctor_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
atomic_write,
default_log_dir,
get_logger,
is_valid_codex_model_id,
Comment thread
Trecek marked this conversation as resolved.
)
from autoskillit.execution import QUOTA_CACHE_SCHEMA_VERSION

Expand All @@ -28,8 +29,6 @@

CODEX_MIN_VERSION: tuple[int, ...] = (0, 130, 0)

VALID_CODEX_MODEL_IDS: frozenset[str] = frozenset({"gpt-5.4", "gpt-5.4-mini", "gpt-5.5"})

_CODEX_ALIAS_STALENESS_DAYS: int = 90


Expand Down Expand Up @@ -390,14 +389,14 @@ def _check_codex_model_alias_staleness() -> DoctorResult:
f" (threshold {_CODEX_ALIAS_STALENESS_DAYS}d);"
f" re-verify alias targets and update CODEX_MODEL_ALIASES_LAST_VERIFIED",
)
invalid = {k: v for k, v in CODEX_MODEL_ALIASES.items() if v not in VALID_CODEX_MODEL_IDS}
invalid = {k: v for k, v in CODEX_MODEL_ALIASES.items() if not is_valid_codex_model_id(v)}
if invalid:
pairs = ", ".join(f"{k}={v!r}" for k, v in invalid.items())
return DoctorResult(
Severity.WARNING,
check_name,
f"CODEX_MODEL_ALIASES contains unrecognized model IDs: {pairs};"
f" update VALID_CODEX_MODEL_IDS or fix the alias",
f" update CODEX_VALID_MODEL_IDS or fix the alias",
)
return DoctorResult(
Severity.OK,
Expand Down
2 changes: 2 additions & 0 deletions src/autoskillit/core/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ from .types import CODEX_MODEL_ALIASES as CODEX_MODEL_ALIASES
from .types import CODEX_MODEL_ALIASES_LAST_VERIFIED as CODEX_MODEL_ALIASES_LAST_VERIFIED
from .types import CODEX_SCHEMA_VERSION as CODEX_SCHEMA_VERSION
from .types import CODEX_SESSIONS_SUBDIR as CODEX_SESSIONS_SUBDIR
from .types import CODEX_VALID_MODEL_IDS as CODEX_VALID_MODEL_IDS
from .types import CONFIG_AUTHORITY_KEYS as CONFIG_AUTHORITY_KEYS
from .types import CONTEXT_EXHAUSTION_MARKER as CONTEXT_EXHAUSTION_MARKER
from .types import CORE_PACKS as CORE_PACKS
Expand Down Expand Up @@ -368,6 +369,7 @@ from .types import extract_positional_args as extract_positional_args
from .types import extract_skill_name as extract_skill_name
from .types import fleet_error as fleet_error
from .types import is_path_like_token as is_path_like_token
from .types import is_valid_codex_model_id as is_valid_codex_model_id
from .types import model_class as model_class
from .types import resolve_payload_field as resolve_payload_field
from .types import resolve_skill_name as resolve_skill_name
Expand Down
49 changes: 36 additions & 13 deletions src/autoskillit/core/types/_type_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from collections.abc import Mapping
from dataclasses import dataclass, field
from pathlib import Path
from types import MappingProxyType
from typing import Any

from ._type_checkpoint import SessionCheckpoint
Expand All @@ -22,6 +23,7 @@
"CODEX_EFFORT_MAPPING",
"CODEX_MODEL_ALIASES",
"CODEX_MODEL_ALIASES_LAST_VERIFIED",
"CODEX_VALID_MODEL_IDS",
"CmdOrigin",
"CmdSpec",
"ModelTranslation",
Expand All @@ -30,6 +32,7 @@
"CodexEventData",
"SessionEvent",
"AgentSessionResult",
"is_valid_codex_model_id",
"model_class",
"strip_context_window_suffix",
]
Expand Down Expand Up @@ -177,27 +180,43 @@ class BackendCapabilities:
"haiku": "haiku",
}

CODEX_MODEL_ALIASES: dict[str, str] = {
"sonnet": "gpt-5.4",
"opus": "gpt-5.5",
"haiku": "gpt-5.4-mini",
}
CODEX_MODEL_ALIASES: Mapping[str, str] = MappingProxyType(
{
"sonnet": "gpt-5.5",
"opus": "gpt-5.5",
"haiku": "gpt-5.5",
}
)

CODEX_MODEL_ALIASES_LAST_VERIFIED: str = "2026-06-11"
CODEX_MODEL_ALIASES_LAST_VERIFIED: str = "2026-07-09"

CODEX_VALID_MODEL_IDS: frozenset[str] = frozenset({"gpt-5.5"})
Comment thread
Trecek marked this conversation as resolved.

assert set(CODEX_MODEL_ALIASES.values()).issubset(CODEX_VALID_MODEL_IDS), (
"CODEX_MODEL_ALIASES values must all be members of CODEX_VALID_MODEL_IDS; "
f"got {sorted(set(CODEX_MODEL_ALIASES.values()) - CODEX_VALID_MODEL_IDS)}"
)

CODEX_EFFORT_MAPPING: dict[str, str] = {
"sonnet": "high",
"opus": "xhigh",
"haiku": "medium",
}

_CODEX_MODEL_REVERSE: dict[str, str] = {v: k for k, v in CODEX_MODEL_ALIASES.items()}
_codex_alias_values = list(CODEX_MODEL_ALIASES.values())
assert len(_CODEX_MODEL_REVERSE) == len(CODEX_MODEL_ALIASES), (
"CODEX_MODEL_ALIASES values must be unique — duplicate makes an alias unreachable "
f"via model_class(). Duplicates: "
f"{[v for v in _codex_alias_values if _codex_alias_values.count(v) > 1]}"
)

Comment thread
Trecek marked this conversation as resolved.
def _codex_unique_model_reverse(aliases: Mapping[str, str]) -> Mapping[str, str]:
"""Return reverse aliases only for native model IDs used by one local class.

Shared native IDs are intentionally omitted so model_class() falls back to
the Codex model ID instead of projecting to an arbitrary local class.
"""
values = tuple(aliases.values())
return {model_id: alias for alias, model_id in aliases.items() if values.count(model_id) == 1}


# Reverse lookup is valid only for one-to-one native IDs. When multiple local
Comment thread
Trecek marked this conversation as resolved.
Comment thread
Trecek marked this conversation as resolved.
# classes share a Codex model, the class is carried by model_reasoning_effort.
_CODEX_MODEL_REVERSE: Mapping[str, str] = _codex_unique_model_reverse(CODEX_MODEL_ALIASES)


@dataclass(frozen=True, slots=True)
Expand All @@ -217,6 +236,10 @@ def strip_context_window_suffix(model: str) -> str:
return _CONTEXT_WINDOW_SUFFIX_RE.sub("", model)


def is_valid_codex_model_id(model_id: str) -> bool:
return model_id in CODEX_VALID_MODEL_IDS


def model_class(model: str) -> str:
base = strip_context_window_suffix(model)
if base in CLAUDE_MODEL_ALIASES:
Expand Down
8 changes: 5 additions & 3 deletions tests/arch/test_backend_translate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def test_translate_model_called_at_terminal_model_sites() -> None:
)


def test_translate_model_distinct_per_alias_class() -> None:
def test_translate_model_resolution_distinct_per_alias_class() -> None:
from autoskillit.core.types._type_backend import CLAUDE_MODEL_ALIASES

alias_keys = list(CLAUDE_MODEL_ALIASES.keys())
Expand All @@ -77,8 +77,10 @@ def test_translate_model_distinct_per_alias_class() -> None:
backend = cls()
except TypeError:
pytest.skip(f"{backend_name}: requires constructor args — cannot validate")
translated = [backend.translate_model(k) for k in alias_keys]
translated = [
(backend.translate_model(k), backend.model_config_overrides(k)) for k in alias_keys
]
assert len(translated) == len(set(translated)), (
f"{backend_name}: translate_model produced duplicate outputs for alias keys "
f"{backend_name}: model resolution produced duplicate outputs for alias keys "
f"{alias_keys}: {translated}"
)
4 changes: 2 additions & 2 deletions tests/cli/test_doctor_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_ok_when_within_threshold_and_valid_aliases(
monkeypatch.setattr(
mod,
"CODEX_MODEL_ALIASES",
{"sonnet": "gpt-5.4", "opus": "gpt-5.5", "haiku": "gpt-5.4-mini"},
{"sonnet": "gpt-5.5", "opus": "gpt-5.5", "haiku": "gpt-5.5"},
Comment thread
Trecek marked this conversation as resolved.
)
result = mod._check_codex_model_alias_staleness()
assert result.severity == Severity.OK
Expand All @@ -45,7 +45,7 @@ def test_warning_when_alias_value_invalid(self, monkeypatch: pytest.MonkeyPatch)
monkeypatch.setattr(
mod,
"CODEX_MODEL_ALIASES",
{"sonnet": "gpt-5.4", "opus": "BOGUS-MODEL"},
{"sonnet": "gpt-5.5", "opus": "BOGUS-MODEL"},
)
result = mod._check_codex_model_alias_staleness()
assert result.severity == Severity.WARNING
Expand Down
2 changes: 2 additions & 0 deletions tests/core/test_backend_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def test_backend_module_all_exhaustive():
"CODEX_EFFORT_MAPPING",
"CODEX_MODEL_ALIASES",
"CODEX_MODEL_ALIASES_LAST_VERIFIED",
"CODEX_VALID_MODEL_IDS",
"CmdOrigin",
"CmdSpec",
"ModelTranslation",
Expand All @@ -205,6 +206,7 @@ def test_backend_module_all_exhaustive():
"CodexEventData",
"SessionEvent",
"AgentSessionResult",
"is_valid_codex_model_id",
"model_class",
"strip_context_window_suffix",
}
Expand Down
6 changes: 3 additions & 3 deletions tests/execution/backends/test_codex_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,10 +499,10 @@ def test_preserves_foreign_sections_with_dotted_keys(self, tmp_path):
def test_preserves_nested_sections_with_special_keys(self, tmp_path):
p = tmp_path / "config.toml"
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text('[notice]\nmodel_migrations = {"gpt-5.3-codex" = "gpt-5.4"}\n')
p.write_text('[notice]\nmodel_migrations = {"gpt-5.3-codex" = "gpt-5.5"}\n')
ensure_codex_mcp_registered(config_path=p)
data = _read_codex_config(p).data
assert data["notice"]["model_migrations"]["gpt-5.3-codex"] == "gpt-5.4"
assert data["notice"]["model_migrations"]["gpt-5.3-codex"] == "gpt-5.5"

def test_updates_stale_tool_timeout(self, tmp_path):
p = tmp_path / "config.toml"
Expand Down Expand Up @@ -742,7 +742,7 @@ def test_round_trip_key_with_dot(self):
assert parsed == original

def test_round_trip_key_with_multiple_dots(self):
original = {"notice": {"model_migrations": {"gpt-5.3-codex": "gpt-5.4"}}}
original = {"notice": {"model_migrations": {"gpt-5.3-codex": "gpt-5.5"}}}
serialized = _serialize_toml(original)
parsed = tomllib.loads(serialized)
assert parsed == original
Expand Down
10 changes: 6 additions & 4 deletions tests/execution/backends/test_model_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,14 @@ def test_suffix_stripping(self) -> None:
assert model_class("sonnet[1m]") == "sonnet"
assert model_class("haiku[1m]") == "haiku"

def test_codex_reverse_map(self) -> None:
def test_codex_shared_model_id_stays_native_when_class_is_ambiguous(self) -> None:
Comment thread
Trecek marked this conversation as resolved.
from autoskillit.core import model_class

assert model_class(CODEX_MODEL_ALIASES["opus"]) == "opus"
assert model_class(CODEX_MODEL_ALIASES["sonnet"]) == "sonnet"
assert model_class(CODEX_MODEL_ALIASES["haiku"]) == "haiku"
shared_model_id = CODEX_MODEL_ALIASES["opus"]
assert {CODEX_MODEL_ALIASES[key] for key in ("sonnet", "opus", "haiku")} == {
shared_model_id
}
assert model_class(shared_model_id) == shared_model_id

def test_unknown_passthrough(self) -> None:
from autoskillit.core import model_class
Expand Down
10 changes: 4 additions & 6 deletions tests/execution/test_model_alias_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

pytestmark = [pytest.mark.layer("execution"), pytest.mark.small]

VALID_CODEX_MODEL_IDS: frozenset[str] = frozenset({"gpt-5.4", "gpt-5.4-mini", "gpt-5.5"})

VALID_CLAUDE_MODEL_IDS: frozenset[str] = frozenset({"sonnet", "opus", "haiku"})


Expand All @@ -27,12 +25,12 @@ def test_anomaly_detection_aliases_keys_match_shared() -> None:


def test_codex_alias_values_in_allowlist() -> None:
from autoskillit.core.types._type_backend import CODEX_MODEL_ALIASES
from autoskillit.core.types._type_backend import CODEX_MODEL_ALIASES, is_valid_codex_model_id

for key, value in CODEX_MODEL_ALIASES.items():
assert value in VALID_CODEX_MODEL_IDS, (
f"CODEX_MODEL_ALIASES[{key!r}] = {value!r} is not in VALID_CODEX_MODEL_IDS. "
"Update VALID_CODEX_MODEL_IDS if the intended target model changed."
assert is_valid_codex_model_id(value), (
f"CODEX_MODEL_ALIASES[{key!r}] = {value!r} is not a valid Codex model ID. "
"Update CODEX_VALID_MODEL_IDS if the intended target model changed."
)


Expand Down
Loading