fix(nadir): scope NADIR_API_KEY on endpoint-inferred calls

A call with no provider prefix and api_base set to the hosted Nadir API
(e.g. model="bucket") was inferred as nadir by get_llm_provider's
endpoint matching, which loaded NADIR_API_KEY unconditionally. With
NADIR_API_BASE pointing at a self-hosted Nadir, that internal key was
sent to the hosted API, bypassing the base scoping the explicit nadir
branch and NadirDecisionsConfig.resolve_connection apply.

The inference branch now loads the env key only when the configured
base (NADIR_API_BASE, else the hosted default) is the hosted API. This
covers both the chat and Decisions paths, which share the inference.
Adds an asystemone integration test for both inferred cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
doramirdor 2026-11-04 20:23:28 -05:00
parent 3f59c7b99a
commit 65f03bf7c0
3 changed files with 32 additions and 3 deletions

View file

@ -315,8 +315,14 @@ def get_llm_provider(
dynamic_api_key = get_secret_str("CEREBRAS_API_KEY")
elif endpoint == NADIR_DEFAULT_API_BASE:
custom_llm_provider = "nadir" # rebind-ok: mirrors sibling endpoint branches
# NADIR_API_KEY belongs to NADIR_API_BASE, the same scoping as the explicit nadir branch:
# it reaches the hosted API only when that is the configured base.
dynamic_api_key = (
get_secret_str("NADIR_API_KEY") if api_base.lower().startswith("https://") else None
get_secret_str("NADIR_API_KEY")
if api_base.lower().startswith("https://")
and (get_secret_str("NADIR_API_BASE") or NADIR_DEFAULT_API_BASE).rstrip("/")
== NADIR_DEFAULT_API_BASE
else None
)
elif endpoint == "https://inference.baseten.co/v1":
custom_llm_provider = "baseten"

View file

@ -142,8 +142,9 @@ class NadirDecisionsConfig(BaseDecisionsConfig):
"""Nadir as a Decisions provider: ``/v1/bucket`` answers a single simple/medium/complex choice question.
Nadir is a tier classifier, not a general judge, so any other question set is rejected before a call.
``NADIR_API_KEY`` is only sent to ``NADIR_API_BASE`` or the hosted API, the same scoping as the ``nadir``
chat provider; with no key the call is anonymous, which Nadir serves on a per-IP rate-limited tier.
``NADIR_API_KEY`` is only sent to ``NADIR_API_BASE`` (the hosted API when unset), whether the provider is named
or inferred from the base, the same scoping as the ``nadir`` chat provider; with no key the call is anonymous,
which Nadir serves on a per-IP rate-limited tier.
"""
path = "/v1/bucket"

View file

@ -727,6 +727,28 @@ async def test_nadir_answers_a_tier_question_from_its_bucket_verdict_and_keeps_i
assert "authorization" not in elsewhere.calls.last.request.headers
@pytest.mark.asyncio
async def test_nadir_inferred_from_the_hosted_base_sends_only_a_hosted_key(
monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter
) -> None:
monkeypatch.setenv("NADIR_API_KEY", "ndr-env")
verdict: Final = {"routing_tier": "simple", "confidence": 0.8}
hosted: Final = respx_mock.post("https://api.getnadir.com/v1/bucket").respond(json=verdict)
questions: Final = {"tier": {"type": "choice", "criteria": {"simple": None, "medium": None, "complex": None}}}
monkeypatch.delenv("NADIR_API_BASE", raising=False)
_ = await litellm.asystemone(
model="bucket", state="hi", questions=questions, api_base="https://api.getnadir.com/v1"
)
monkeypatch.setenv("NADIR_API_BASE", "https://nadir.internal/v1")
_ = await litellm.asystemone(
model="bucket", state="hi", questions=questions, api_base="https://api.getnadir.com/v1"
)
assert hosted.calls[0].request.headers["authorization"] == "Bearer ndr-env"
assert "authorization" not in hosted.calls[1].request.headers
@pytest.mark.asyncio
async def test_nadir_refuses_a_question_it_cannot_answer_before_any_call(respx_mock: respx.MockRouter) -> None:
upstream: Final = respx_mock.post("https://api.getnadir.com/v1/bucket")