Skip to content
Open
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
12 changes: 10 additions & 2 deletions ductor_bot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,8 +578,16 @@ class ModelRegistry:

@staticmethod
def provider_for(model_id: str) -> str:
"""Return the provider for a model ID."""
if model_id in CLAUDE_MODELS:
"""Return the provider for a model ID.

Claude Code accepts both short aliases (opus/sonnet/haiku) and full
Claude model IDs such as ``claude-opus-4-7`` /
``claude-sonnet-4-6`` / ``claude-haiku-4-5-20251001``. The registry
previously matched only the short aliases, so a full Claude model
ID fell through to the codex branch and broke Claude routing for
clients passing the canonical model name.
"""
if model_id in CLAUDE_MODELS or model_id.startswith("claude-"):
return "claude"
if (
model_id in _GEMINI_ALIASES
Expand Down
37 changes: 37 additions & 0 deletions tests/test_provider_for_claude_full_ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Regression tests for ModelRegistry.provider_for full Claude model IDs.

Claude Code accepts both short aliases (opus/sonnet/haiku) and full Claude
model IDs (claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5-20251001).
The registry historically matched only the short aliases, so a full ID fell
through to the codex branch and broke Claude routing for clients that pass
the canonical model name.
"""

from __future__ import annotations

import pytest

from ductor_bot.config import ModelRegistry


@pytest.mark.parametrize(
"model_id",
[
"claude-opus-4-7",
"claude-sonnet-4-6",
"claude-haiku-4-5-20251001",
],
)
def test_provider_for_full_claude_model_ids(model_id: str) -> None:
assert ModelRegistry.provider_for(model_id) == "claude"


def test_provider_for_short_aliases_still_claude() -> None:
assert ModelRegistry.provider_for("opus") == "claude"
assert ModelRegistry.provider_for("sonnet") == "claude"
assert ModelRegistry.provider_for("haiku") == "claude"


def test_provider_for_non_claude_unchanged() -> None:
assert ModelRegistry.provider_for("gpt-5.2-codex") == "codex"
assert ModelRegistry.provider_for("gemini-2.5-pro") == "gemini"