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
19 changes: 19 additions & 0 deletions apps/backend/core/providers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,25 @@ def get_model_for(self, provider: str) -> str | None:
return self.ollama_model or None
return None

def coherent_session_model(self, requested_model: str | None) -> str | None:
"""Return a model id coherent with this config's provider.

The phase-model default is Claude-centric (``phase_config`` tiers
resolve to ``claude-*`` ids). When the configured provider is a
non-Claude provider and ``requested_model`` is a Claude-family id,
return the provider's OWN configured model instead, so a direct-API
session does not receive a Claude id it cannot serve (404
model_not_found). Otherwise return ``requested_model`` unchanged.

This is the QA-path analog of the coder fix in #337; both keep a
Claude-family phase default from leaking into a non-Claude session.
"""
if self.provider != "claude" and (requested_model or "").startswith("claude"):
provider_model = self.get_model_for(self.provider)
if provider_model:
return provider_model
return requested_model

def with_provider_model(self, provider: str, model: str) -> "ProviderConfig":
"""Return a copy configured to use the given provider/model pair."""
provider = provider.lower()
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/qa/fixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,12 @@ def _build_qa_fixer_runtime_session(
f"routed={provider_name}, env={config.provider}"
)
provider = create_engine_provider(config)
# Keep a Claude-family QA phase default from 404ing a direct provider
# (the QA-path analog of #337). See ProviderConfig.coherent_session_model.
session = provider.create_session(
SessionConfig(
name=f"qa_fixer-runtime-{fix_session}",
model=model,
model=config.coherent_session_model(model),
extra={"agent_type": "qa_fixer"},
)
)
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/qa/reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,10 +579,12 @@ async def run_qa_reviewer_runtime_session(
f"routed={provider_name}, env={config.provider}"
)
provider = create_engine_provider(config)
# Keep a Claude-family QA phase default from 404ing a direct provider
# (the QA-path analog of #337). See ProviderConfig.coherent_session_model.
session = provider.create_session(
SessionConfig(
name=f"qa_reviewer-runtime-{qa_session}",
model=model,
model=config.coherent_session_model(model),
extra={"agent_type": "qa_reviewer"},
)
)
Expand Down
47 changes: 47 additions & 0 deletions tests/test_provider_coherent_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Tests for ProviderConfig.coherent_session_model (QA/coder model coherence)."""

from core.providers.config import ProviderConfig


def test_claude_family_model_replaced_for_direct_provider():
"""A claude-family phase default must not reach a non-Claude session."""
config = ProviderConfig(provider="openai", openai_model="gpt-5.2")

assert (
config.coherent_session_model("claude-sonnet-4-5-20250929") == "gpt-5.2"
)


def test_explicit_provider_model_is_preserved():
config = ProviderConfig(provider="openai", openai_model="gpt-5.2")

assert config.coherent_session_model("gpt-4o") == "gpt-4o"


def test_none_model_passes_through():
config = ProviderConfig(provider="openai", openai_model="gpt-5.2")

assert config.coherent_session_model(None) is None


def test_claude_provider_keeps_claude_model():
config = ProviderConfig(
provider="claude", claude_model="claude-sonnet-4-5-20250929"
)

assert (
config.coherent_session_model("claude-sonnet-4-5-20250929")
== "claude-sonnet-4-5-20250929"
)


def test_direct_provider_without_configured_model_keeps_request():
"""If the provider has no configured model, fall back to the request
rather than returning an empty string."""
config = ProviderConfig(provider="openrouter", openrouter_model="")

# openrouter_model empty -> get_model_for returns "" (falsy) -> keep request
assert (
config.coherent_session_model("claude-sonnet-4-5-20250929")
== "claude-sonnet-4-5-20250929"
)
Loading