diff --git a/api/routes.py b/api/routes.py index 27e3c912..b6c611ed 100644 --- a/api/routes.py +++ b/api/routes.py @@ -7189,9 +7189,27 @@ def _context_length_lookup_inputs_for_model( # _context_length_config_api_key_for_provider). A built-in registry # provider (empty base_url by design) must keep the slot empty so # the registry endpoint resolves instead of another provider's URL. + # + # Two shapes cannot own the slot and therefore cannot conflict, so + # they keep master's backfill: a config that declares no provider + # at all (the profile-setup path writes model.base_url without one) + # and the two spellings of the same built-in id (opencode_go == + # opencode-go), folded through api.config._canonicalise_provider_id + # so distinct custom:* slugs stay distinct. _model_cfg_provider = _canonical_context_provider(model_cfg.get("provider")) - if not effective_provider or _providers_match_for_context( - _model_cfg_provider, effective_provider + _owner_provider = _model_cfg_provider + _session_provider = effective_provider + try: + from api.config import _canonicalise_provider_id as _canon_provider_id + + _owner_provider = _canon_provider_id(_owner_provider) or _owner_provider + _session_provider = _canon_provider_id(_session_provider) or _session_provider + except Exception: + pass + if ( + not effective_provider + or not _model_cfg_provider + or _providers_match_for_context(_owner_provider, _session_provider) ): effective_base_url = str(model_cfg.get("base_url") or "").strip() diff --git a/tests/test_issue3256_context_length_default_only_guard.py b/tests/test_issue3256_context_length_default_only_guard.py index 34f4b50f..b7bb3288 100644 --- a/tests/test_issue3256_context_length_default_only_guard.py +++ b/tests/test_issue3256_context_length_default_only_guard.py @@ -704,3 +704,92 @@ def test_same_provider_session_keeps_global_base_url_backfill(): assert lookup.base_url == ark_base, ( "same-owner sessions must keep the model.base_url backfill (#7535 compat)" ) + + +def test_base_url_only_config_without_provider_keeps_endpoint(): + """Regression (PR #7743 gate): a ``model`` section that declares + ``base_url`` but NO ``provider`` has no owner that could conflict, so any + session must still receive the URL — exactly as master did. The profile + setup path writes this shape (api/profiles.py), and dropping the URL made + the lookup skip the endpoint and silently fall back to 256,000.""" + from api.routes import _context_length_lookup_inputs_for_model + + only_base_url = "http://127.0.0.1:9/v1" + cfg = { + "model": { + "base_url": only_base_url, + "default": "some-model", + }, + } + + for session_provider in ("custom", "openrouter"): + lookup = _context_length_lookup_inputs_for_model( + "some-model", + session_provider, + cfg=cfg, + ) + + assert lookup.provider == session_provider + assert lookup.base_url == only_base_url, ( + f"a config with no declared provider owner cannot conflict with the " + f"{session_provider!r} session, so the base_url must survive the " + f"ownership guard" + ) + + +def test_underscore_provider_owner_matches_hyphen_session(): + """Regression (PR #7743 gate): ``opencode_go`` and ``opencode-go`` are two + spellings of the SAME built-in provider, so the owner session must still + receive ``model.base_url``. Comparing the ids through the repo's + canonicaliser folds the underscore without collapsing distinct + ``custom:*`` slugs.""" + from api.routes import _context_length_lookup_inputs_for_model + + owner_base = "http://127.0.0.1:9/v1" + cfg = { + "model": { + "provider": "opencode_go", + "base_url": owner_base, + "default": "some-model", + }, + } + + lookup = _context_length_lookup_inputs_for_model( + "some-model", + "opencode-go", + cfg=cfg, + ) + + assert lookup.provider == "opencode-go" + assert lookup.base_url == owner_base, ( + "opencode_go and opencode-go are the same built-in provider, so the " + "owner session must keep the base_url backfill" + ) + + +def test_underscore_provider_still_blocks_distinct_custom_owner(): + """Counterpart to test_underscore_provider_owner_matches_hyphen_session: + folding underscores must not open the #7535 guard. ``custom:my_box`` and a + session on the unrelated built-in ``opencode-go`` are still different + providers, so the custom owner's URL stays out of the registry slot.""" + from api.routes import _context_length_lookup_inputs_for_model + + cfg = { + "model": { + "provider": "custom:my_box", + "base_url": "http://127.0.0.1:9/v1", + "default": "some-model", + }, + } + + lookup = _context_length_lookup_inputs_for_model( + "some-model", + "opencode-go", + cfg=cfg, + ) + + assert lookup.provider == "opencode-go" + assert lookup.base_url == "", ( + "underscore folding must not let a distinct custom provider's " + "base_url leak onto another provider's session (#7535 guard)" + )