Files
2026-07-13 13:12:33 +08:00

223 lines
8.5 KiB
Python

"""Explicit llm.base_url must win over provider-derived env overrides (#484).
The resolver distinguishes an operator-chosen base_url from a derived one by
value-vs-baseline: a stored value equal to the pydantic field default, the
provider spec default, or the current derived env value (``OPENAI_BASE_URL``
et al.) is *derived*, everything else is *explicit*. Explicit values beat the
env var; derived values keep today's env-first behavior so a fleet-wide
``*_BASE_URL`` still applies to configs that never chose an endpoint.
Everything here is offline and synthetic; env vars are scoped via monkeypatch.
"""
from __future__ import annotations
import tomllib
from types import SimpleNamespace
from opensquilla.gateway.config import GatewayConfig
from opensquilla.gateway.llm_runtime import resolve_llm_runtime_config
from opensquilla.gateway.rpc_config import _handle_config_set
from opensquilla.onboarding.config_store import persist_config
from opensquilla.onboarding.mutations import upsert_llm_provider
_ENV_URL = "https://corp-endpoint.example/v1"
_USER_URL = "https://user-proxy.example/v1"
_OPENAI_SPEC_DEFAULT = "https://api.openai.com/v1"
_FIELD_DEFAULT = "https://openrouter.ai/api/v1"
class _CapturingSelector:
def __init__(self) -> None:
self.synced = None
def sync_primary(self, cfg) -> None:
self.synced = cfg
def _admin_ctx(config, selector=None):
return SimpleNamespace(config=config, provider_selector=selector)
def test_explicit_config_base_url_beats_provider_env(monkeypatch) -> None:
"""#484 core: a user-chosen base_url must survive env-present resolution."""
monkeypatch.setenv("OPENAI_BASE_URL", _ENV_URL)
cfg = GatewayConfig(
llm={
"provider": "openai",
"model": "gpt-5.5",
"api_key": "sk-explicit",
"base_url": _USER_URL,
}
)
runtime = resolve_llm_runtime_config(cfg)
assert runtime.base_url == _USER_URL
assert runtime.base_url_from_env is False
# The live model must keep serving the explicit URL too (no env clobber).
assert cfg.llm.base_url == _USER_URL
def test_saved_base_url_survives_restart_with_env_set(tmp_path, monkeypatch) -> None:
"""#484 end-to-end: save via the provider mutation, persist, reboot, resolve."""
monkeypatch.setenv("OPENAI_BASE_URL", _ENV_URL)
config_path = tmp_path / "config.toml"
cfg = GatewayConfig(config_path=str(config_path))
resolve_llm_runtime_config(cfg) # boot-time resolution on the default config
res = upsert_llm_provider(
cfg,
provider_id="openai_responses",
model="gpt-5.5",
api_key="sk-user",
base_url=_USER_URL,
)
persist_config(res.config, path=config_path)
assert tomllib.loads(config_path.read_text())["llm"]["base_url"] == _USER_URL
rebooted = GatewayConfig.load_from_toml(config_path)
runtime = resolve_llm_runtime_config(rebooted)
assert runtime.base_url == _USER_URL
assert rebooted.llm.base_url == _USER_URL
assert runtime.base_url_from_env is False
def test_minimal_toml_resolves_spec_default_not_field_default(tmp_path, monkeypatch) -> None:
"""A config that never chose an endpoint must get the PROVIDER's default.
The pydantic field default is the openrouter URL; before value-vs-baseline
resolution it leaked into every minimal non-openrouter config.
"""
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
config_path = tmp_path / "config.toml"
config_path.write_text('[llm]\nprovider = "openai"\nmodel = "gpt-5.5"\napi_key = "sk-x"\n')
runtime = resolve_llm_runtime_config(GatewayConfig.load_from_toml(config_path))
assert runtime.base_url == _OPENAI_SPEC_DEFAULT
assert runtime.base_url_from_env is False
def test_spec_default_stored_value_yields_to_env(monkeypatch) -> None:
"""A stored value equal to the spec default is derived, not operator-chosen.
The Web UI seeds the endpoint field with the spec default and submits it
verbatim, so this shape is common on disk; a fleet-wide env override must
keep applying to it.
"""
monkeypatch.setenv("OPENAI_BASE_URL", _ENV_URL)
cfg = GatewayConfig(
llm={
"provider": "openai",
"model": "gpt-5.5",
"api_key": "sk-x",
"base_url": _OPENAI_SPEC_DEFAULT,
}
)
runtime = resolve_llm_runtime_config(cfg)
assert runtime.base_url == _ENV_URL
assert runtime.base_url_from_env is True
def test_field_default_stored_value_yields_to_env(tmp_path, monkeypatch) -> None:
"""Minimal TOML (field default in the loaded model) keeps env-first behavior."""
monkeypatch.setenv("OPENAI_BASE_URL", _ENV_URL)
config_path = tmp_path / "config.toml"
config_path.write_text('[llm]\nprovider = "openai"\nmodel = "gpt-5.5"\napi_key = "sk-x"\n')
runtime = resolve_llm_runtime_config(GatewayConfig.load_from_toml(config_path))
assert runtime.base_url == _ENV_URL
assert runtime.base_url_from_env is True
def test_stored_env_value_is_env_derived_and_idempotent(monkeypatch) -> None:
"""A stored value equal to the current env value counts as env-derived.
This is what keeps repeated in-process resolves stable: the first resolve
materializes the env URL into the model, and the second must classify it
exactly the same way (from_env stays True, no explicit promotion).
"""
monkeypatch.setenv("OPENAI_BASE_URL", _ENV_URL)
cfg = GatewayConfig(
llm={"provider": "openai", "model": "gpt-5.5", "api_key": "sk-x", "base_url": _ENV_URL}
)
first = resolve_llm_runtime_config(cfg)
second = resolve_llm_runtime_config(cfg)
assert first.base_url == _ENV_URL
assert first.base_url_from_env is True
assert second.base_url == _ENV_URL
assert second.base_url_from_env is True
def test_base_url_from_env_reports_false_without_env(monkeypatch) -> None:
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
explicit = GatewayConfig(
llm={"provider": "openai", "model": "gpt-5.5", "api_key": "sk-x", "base_url": _USER_URL}
)
derived = GatewayConfig(llm={"provider": "openai", "model": "gpt-5.5", "api_key": "sk-x"})
assert resolve_llm_runtime_config(explicit).base_url_from_env is False
assert resolve_llm_runtime_config(derived).base_url_from_env is False
def test_unknown_provider_preserves_explicit_base_url(monkeypatch) -> None:
"""Degraded boot (unregistered provider) must not lose the stored endpoint."""
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
cfg = GatewayConfig(
llm={"provider": "not-a-provider", "model": "m", "api_key": "k", "base_url": _USER_URL}
)
runtime = resolve_llm_runtime_config(cfg)
assert runtime.base_url == _USER_URL
assert runtime.base_url_from_env is False
async def test_config_set_persists_user_base_url_with_env_set(tmp_path, monkeypatch) -> None:
"""#484 via config.set: the user's URL must reach disk and the selector."""
monkeypatch.setenv("OPENAI_BASE_URL", _ENV_URL)
config_path = tmp_path / "config.toml"
cfg = GatewayConfig(
config_path=str(config_path),
llm={"provider": "openai", "model": "gpt-5.5", "api_key": "sk-x"},
)
resolve_llm_runtime_config(cfg) # boot
selector = _CapturingSelector()
ctx = _admin_ctx(cfg, selector)
await _handle_config_set({"path": "llm.base_url", "value": _USER_URL}, ctx)
assert tomllib.loads(config_path.read_text())["llm"]["base_url"] == _USER_URL
assert _ENV_URL not in config_path.read_text()
assert ctx.config.llm.base_url == _USER_URL
assert selector.synced.base_url == _USER_URL
rebooted = GatewayConfig.load_from_toml(config_path)
assert resolve_llm_runtime_config(rebooted).base_url == _USER_URL
async def test_config_set_unrelated_change_does_not_bake_env_url(tmp_path, monkeypatch) -> None:
"""An unrelated config.set on an env-resolved config must not write the env URL."""
monkeypatch.setenv("OPENAI_BASE_URL", _ENV_URL)
config_path = tmp_path / "config.toml"
config_path.write_text('[llm]\nprovider = "openai"\nmodel = "gpt-5.5"\napi_key = "sk-x"\n')
cfg = GatewayConfig.load_from_toml(config_path)
cfg.config_path = str(config_path)
resolve_llm_runtime_config(cfg) # boot materializes the env URL + records it
assert cfg.llm.base_url == _ENV_URL
ctx = _admin_ctx(cfg, _CapturingSelector())
await _handle_config_set({"path": "port", "value": 18795}, ctx)
text = config_path.read_text()
assert _ENV_URL not in text
assert tomllib.loads(text)["port"] == 18795