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
1 change: 1 addition & 0 deletions src/autoskillit/core/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,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 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
from .types import resolve_target_skill as resolve_target_skill
Expand Down
20 changes: 18 additions & 2 deletions src/autoskillit/core/types/_type_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"CodexEventData",
"SessionEvent",
"AgentSessionResult",
"model_class",
"strip_context_window_suffix",
]

Expand Down Expand Up @@ -169,9 +170,9 @@ class BackendCapabilities:
}

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

CODEX_EFFORT_MAPPING: dict[str, str] = {
Expand All @@ -180,6 +181,14 @@ class BackendCapabilities:
"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]}"
)


@dataclass(frozen=True, slots=True)
class ModelTranslation:
Expand All @@ -198,6 +207,13 @@ def strip_context_window_suffix(model: str) -> str:
return _CONTEXT_WINDOW_SUFFIX_RE.sub("", model)


def model_class(model: str) -> str:
base = strip_context_window_suffix(model)
if base in CLAUDE_MODEL_ALIASES:
return base
return _CODEX_MODEL_REVERSE.get(base, base)


CLAUDE_CODE_CAPABILITIES: BackendCapabilities = BackendCapabilities(
channel_b_capable=True,
pty_required=True,
Expand Down
16 changes: 16 additions & 0 deletions tests/arch/test_backend_translate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,19 @@ def test_translate_model_called_at_terminal_model_sites() -> None:
f"{backend_name}.{method_name} appends --model but does not call "
f"self.translate_model"
)


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

alias_keys = list(CLAUDE_MODEL_ALIASES.keys())
for backend_name, cls in BACKEND_REGISTRY.items():
try:
backend = cls()
except TypeError:
pytest.skip(f"{backend_name}: requires constructor args β€” cannot validate")
translated = [backend.translate_model(k) for k in alias_keys]
assert len(translated) == len(set(translated)), (
f"{backend_name}: translate_model produced duplicate outputs for alias keys "
f"{alias_keys}: {translated}"
)
1 change: 1 addition & 0 deletions tests/core/test_backend_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ def test_backend_module_all_exhaustive():
"CodexEventData",
"SessionEvent",
"AgentSessionResult",
"model_class",
"strip_context_window_suffix",
}

Expand Down
28 changes: 28 additions & 0 deletions tests/execution/backends/test_model_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,31 @@ def test_interactive_cmd_has_effort(self) -> None:
def test_no_effort_for_native_model_in_headless_cmd(self) -> None:
spec = CodexBackend().build_headless_cmd("test prompt", model="gpt-5.5")
assert "model_reasoning_effort" not in " ".join(spec.cmd)


class TestModelClass:
def test_canonical_keys(self) -> None:
from autoskillit.core import model_class

assert model_class("opus") == "opus"
assert model_class("sonnet") == "sonnet"
assert model_class("haiku") == "haiku"

def test_suffix_stripping(self) -> None:
from autoskillit.core import model_class

assert model_class("opus[1m]") == "opus"
assert model_class("sonnet[1m]") == "sonnet"
assert model_class("haiku[1m]") == "haiku"

def test_codex_reverse_map(self) -> None:
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"

def test_unknown_passthrough(self) -> None:
from autoskillit.core import model_class

assert model_class("custom-model-xyz") == "custom-model-xyz"
2 changes: 1 addition & 1 deletion tests/execution/test_model_alias_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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

VALID_CODEX_MODEL_IDS: frozenset[str] = frozenset({"gpt-5.5"})
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 Down
Loading