litellm/tests/unit/llms/test_oss_decision.py
devin-ai-integration[bot] a93fee9f6c
feat(decisions): add Laya and Bespoke providers to the Add Model form (#45260)
* feat(ui): add decisions and classifier providers to the Add Model form

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(cost): use literal upstream decisions paths

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: drop duplicate TYPESAFE provider fields entry superseded by main's TypeSafe

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(ui): drop stray blank line in provider_info_helpers test

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): type the OSS decisions provider fields test

* fix(decisions): keep Laya and Bespoke env keys on their configured server

Route laya and bespoke Decisions deployments through the same oss_connection
and validate_oss_model rules the pass-through and the classifier router use,
so LAYA_API_KEY and BESPOKE_API_KEY only go to LAYA_API_BASE and BESPOKE_API_BASE,
an api_base with credentials, a query, or a non-HTTP scheme answers 400, and
an unknown checkpoint name answers 400 before any upstream call

* fix(decisions): send the Laya and Bespoke env key to a form-added model on the env server

A deployment added through Add Model always carries an API base, so the
env key only reached deployments with no base. The env key now goes to a
deployment whose base matches LAYA_API_BASE or BESPOKE_API_BASE, an empty
deployment key counts as no key, and the Bespoke placeholder is the name
Ollama serves.

* test(decisions): cover the env-base classifier case in the complexity router test

* fix(decisions): match the env server with or without /v1 and point Ollama users at bespoke/nimble

* fix(decisions): call /v1/systemone once from classifiers and pass-through on a /v1 base

* fix(decisions): strip /v1 from the API base path only, so a host named v1 still works

---------

Co-authored-by: kerry <kerry@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-14 08:11:11 -07:00

86 lines
3.6 KiB
Python

from typing import Final
import pytest
from litellm.llms.oss_decision import OssDecisionProvider, oss_connection, validate_oss_request
pytestmark: Final = pytest.mark.parametrize("provider", ["laya", "bespoke"])
@pytest.mark.parametrize(
("base", "key", "expected_base", "expected_key"),
[
(None, None, "http://decision.test/root", "oss-env-key"),
("http://custom.test/", None, "http://custom.test", None),
("http://custom.test/", "explicit-key", "http://custom.test", "explicit-key"),
("http://custom.test/", "", "http://custom.test", None),
("http://decision.test/root", None, "http://decision.test/root", "oss-env-key"),
("http://decision.test/root/", "", "http://decision.test/root", "oss-env-key"),
("http://decision.test/root", "explicit-key", "http://decision.test/root", "explicit-key"),
("http://decision.test/root/v1/", None, "http://decision.test/root", "oss-env-key"),
("http://decision.test/root/v2", None, "http://decision.test/root/v2", None),
("http://v1", None, "http://v1", None),
("http://v1/v1/", None, "http://v1", None),
("http://[::1]:8000/v1", None, "http://[::1]:8000", None),
],
)
def test_oss_credentials_stay_with_their_configured_destination(
monkeypatch: pytest.MonkeyPatch,
provider: OssDecisionProvider,
base: str | None,
key: str | None,
expected_base: str,
expected_key: str | None,
) -> None:
monkeypatch.setenv(f"{provider.upper()}_API_BASE", "http://decision.test/root/")
monkeypatch.setenv(f"{provider.upper()}_API_KEY", "oss-env-key")
monkeypatch.setenv("TYPESAFE_API_KEY", "never-send-this")
monkeypatch.setenv("NIMBLE_API_KEY", "never-send-nimble-search-key")
connection: Final = oss_connection(provider, base, key)
assert (connection.api_base, connection.api_key) == (expected_base, expected_key)
assert "key" not in repr(connection)
@pytest.mark.parametrize(
("env_base", "base"),
[
("http://decision.test/root/v1/", "http://decision.test/root"),
("http://decision.test/root", "http://decision.test/root/v1"),
],
)
def test_oss_env_key_follows_its_server_with_or_without_the_v1_suffix(
monkeypatch: pytest.MonkeyPatch, provider: OssDecisionProvider, env_base: str, base: str
) -> None:
monkeypatch.setenv(f"{provider.upper()}_API_BASE", env_base)
monkeypatch.setenv(f"{provider.upper()}_API_KEY", "oss-env-key")
assert oss_connection(provider, base).api_key == "oss-env-key"
@pytest.mark.parametrize(
"base",
["", "ftp://laya.test", "http://user:password@laya.test", "https://laya.test?key=x", "http://laya.test/#x"],
)
def test_oss_rejects_ambiguous_server_urls(provider: OssDecisionProvider, base: str) -> None:
with pytest.raises(ValueError, match=provider):
oss_connection(provider, base)
def test_oss_missing_server_does_not_fall_back_to_typesafe(
monkeypatch: pytest.MonkeyPatch, provider: OssDecisionProvider
) -> None:
monkeypatch.delenv(f"{provider.upper()}_API_BASE", raising=False)
monkeypatch.setenv("TYPESAFE_API_BASE", "https://typesafe.test")
monkeypatch.setenv("NIMBLE_API_BASE", "https://nimble-search.test")
with pytest.raises(ValueError, match=f"{provider.upper()}_API_BASE"):
oss_connection(provider)
def test_oss_request_accepts_the_name_ollama_serves_nimble_under_only_for_bespoke(
provider: OssDecisionProvider,
) -> None:
body: Final = {"model": "nimble"}
if provider == "bespoke":
assert validate_oss_request(provider, body) == "nimble"
return
with pytest.raises(ValueError, match=f"{provider} model must be one of"):
validate_oss_request(provider, body)