fix(#3263): provider-compatible default match — reject same-bare-name on different providers (Codex over-match MUST-FIX)

Prior round stripped provider prefixes from both sides and matched bare-only,
which over-matched: openai/gpt-4o would match default openrouter/gpt-4o. Now the
matcher compares bare model ids AND rejects when both sides identify DIFFERENT
providers (from provider/ prefix, @provider: qualifier, or the explicit provider
arg). Same-provider / unknown-session-provider still match. Added cross-provider
rejection regression tests.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-06-01 01:19:49 +00:00
parent 4147e7f3df
commit 7e9f674edf
2 changed files with 52 additions and 21 deletions
+41 -21
View File
@@ -1763,10 +1763,20 @@ def _model_matches_configured_default(
- bare: ``claude-opus-4.8``
- slash-prefixed: ``anthropic/claude-opus-4.8`` (OpenRouter-style)
- @provider:model: ``@anthropic:claude-opus-4.8``
Treat any of these as a match so a provider-prefixed default config still
receives its cap (and a bare default still matches a prefixed session model).
Empty default no match (caller falls back to applying the cap broadly only
when no default is configured at all).
Matching rule (correct in both directions):
1. Identical strings match.
2. Otherwise compare BARE model ids BUT only after a provider-compatibility
check: if BOTH sides carry an identifiable provider (from a ``provider/``
prefix, an ``@provider:`` qualifier, or the explicit ``provider`` arg for
the session side) and those providers DIFFER, it is NOT a match. This
stops a non-default model on a different provider that happens to share a
bare name (``openai/gpt-4o`` vs default ``openrouter/gpt-4o``) from being
treated as the default and wrongly receiving its cap.
3. When a provider can't be identified on one side, fall through to the bare
comparison (lenient-when-unknown a bare default config still matches a
bare/prefixed session model).
Empty default no match.
"""
sess = str(session_model or "").strip()
default = str(cfg_default or "").strip()
@@ -1774,23 +1784,33 @@ def _model_matches_configured_default(
return False
if sess == default:
return True
# Normalize both to their bare model name, stripping a leading
# ``@provider:`` qualifier or a single ``provider/`` slash prefix.
def _bare(value: str, prov: str | None = None) -> set[str]:
forms = {value}
unq, _q = _split_provider_qualified_model(value)
if unq:
forms.add(unq)
# Strip a single leading "provider/" segment (OpenRouter-style).
if "/" in unq:
forms.add(unq.split("/", 1)[1].strip())
# Also consider a provider-prefixed form when an explicit provider is known.
if prov:
p = str(prov).strip().lower()
if p and "/" not in unq and not unq.startswith("@"):
forms.add(f"{p}/{unq}")
return {f for f in forms if f}
return bool(_bare(sess, provider) & _bare(default))
def _split(value: str) -> tuple[str, str | None]:
"""Return (bare_model, provider_or_None) for any of the 3 shapes."""
value = str(value or "").strip()
# @provider:model
unq, q_prov = _split_provider_qualified_model(value)
if q_prov:
return unq.strip(), str(q_prov).strip().lower()
# provider/model (single leading slash segment)
if "/" in value:
prefix, rest = value.split("/", 1)
return rest.strip(), prefix.strip().lower()
return value, None
sess_bare, sess_prov = _split(sess)
default_bare, default_prov = _split(default)
# The explicit provider arg is the session side's provider when the model
# string itself didn't carry one.
if not sess_prov and provider:
sess_prov = str(provider).strip().lower() or None
if not sess_bare or not default_bare or sess_bare != default_bare:
return False
# Bare ids match. Reject only when both sides name DIFFERENT providers.
if sess_prov and default_prov and sess_prov != default_prov:
return False
return True
@@ -130,6 +130,17 @@ def test_default_match_distinct_models_do_not_match():
assert _matcher()("gpt-5.5", "claude-opus-4.8", "openai") is False
def test_default_match_same_bare_different_provider_does_NOT_match():
"""Same bare model id on a DIFFERENT provider is NOT the configured default
and must not receive its cap (Codex final-gate over-match finding)."""
assert _matcher()("openai/gpt-4o", "openrouter/gpt-4o", "openai") is False
assert _matcher()("@openai:gpt-4o", "@openrouter:gpt-4o") is False
assert _matcher()("gpt-4o", "openrouter/gpt-4o", "openai") is False
# but the SAME provider (or unknown session provider) still matches:
assert _matcher()("openrouter/gpt-4o", "openrouter/gpt-4o") is True
assert _matcher()("gpt-4o", "openrouter/gpt-4o", "openrouter") is True
def test_default_match_empty_inputs_are_false():
assert _matcher()("claude-opus-4.8", "") is False
assert _matcher()("", "claude-opus-4.8") is False