mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
fix(decisions): honor litellm OpenAI key and base settings for the openai provider
OpenAI decisions now resolve the key from litellm.api_key, litellm.openai_key, then OPENAI_API_KEY, and the base from litellm.api_base, OPENAI_BASE_URL, then OPENAI_API_BASE, matching other OpenAI calls. Providers own their configured key and base lookup through the endpoint config.
This commit is contained in:
parent
39012f1c22
commit
e465919004
5 changed files with 91 additions and 34 deletions
|
|
@ -25,7 +25,6 @@ from litellm.llms.openrouter.decisions.transformation import OPENROUTER_DECISION
|
||||||
from litellm.llms.perplexity.decisions.transformation import PERPLEXITY_DECISIONS_ENDPOINT
|
from litellm.llms.perplexity.decisions.transformation import PERPLEXITY_DECISIONS_ENDPOINT
|
||||||
from litellm.llms.strands_decider.decisions.transformation import STRANDS_DECIDER_DECISIONS_ENDPOINT
|
from litellm.llms.strands_decider.decisions.transformation import STRANDS_DECIDER_DECISIONS_ENDPOINT
|
||||||
from litellm.llms.typesafe.decisions.transformation import TYPESAFE_DECISIONS_ENDPOINT
|
from litellm.llms.typesafe.decisions.transformation import TYPESAFE_DECISIONS_ENDPOINT
|
||||||
from litellm.secret_managers.main import get_secret_str
|
|
||||||
from litellm.types.decisions import (
|
from litellm.types.decisions import (
|
||||||
DecisionQuestion,
|
DecisionQuestion,
|
||||||
DecisionsIRRequest,
|
DecisionsIRRequest,
|
||||||
|
|
@ -103,21 +102,16 @@ def _resolve_api_key(
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
if api_key is not None:
|
if api_key is not None:
|
||||||
return api_key
|
return api_key
|
||||||
|
configured_api_key: Final = endpoint.configured_api_key()
|
||||||
server_api_key: Final = next(
|
if configured_api_key:
|
||||||
(key for key in (get_secret_str(name) for name in endpoint.api_key_env) if key),
|
return configured_api_key
|
||||||
None,
|
if not endpoint.api_key_required:
|
||||||
|
return None
|
||||||
|
raise litellm.AuthenticationError(
|
||||||
|
message=f"Missing API key for Decisions provider '{provider}'",
|
||||||
|
model=model,
|
||||||
|
llm_provider=provider,
|
||||||
)
|
)
|
||||||
if server_api_key is None:
|
|
||||||
if not endpoint.api_key_required:
|
|
||||||
return None
|
|
||||||
raise litellm.AuthenticationError(
|
|
||||||
message=f"Missing API key for Decisions provider '{provider}'",
|
|
||||||
model=model,
|
|
||||||
llm_provider=provider,
|
|
||||||
)
|
|
||||||
|
|
||||||
return server_api_key
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_request(
|
def _validate_request(
|
||||||
|
|
@ -175,9 +169,7 @@ def _prepare_request(
|
||||||
) from error
|
) from error
|
||||||
|
|
||||||
endpoint: Final = DECISIONS_ENDPOINTS[provider]
|
endpoint: Final = DECISIONS_ENDPOINTS[provider]
|
||||||
env_api_base: Final = get_secret_str(endpoint.api_base_env)
|
resolved_api_base: Final = api_base or endpoint.configured_api_base()
|
||||||
default_api_base: Final = endpoint.default_api_base()
|
|
||||||
resolved_api_base: Final = api_base or env_api_base or default_api_base
|
|
||||||
if resolved_api_base is None:
|
if resolved_api_base is None:
|
||||||
raise litellm.BadRequestError(
|
raise litellm.BadRequestError(
|
||||||
message=endpoint.missing_api_base_message(provider),
|
message=endpoint.missing_api_base_message(provider),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from typing import Final, Protocol
|
||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
from typing_extensions import assert_never
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
|
from litellm.secret_managers.main import get_secret_str
|
||||||
from litellm.types.decisions import (
|
from litellm.types.decisions import (
|
||||||
ChoiceAnswer,
|
ChoiceAnswer,
|
||||||
ChoiceQuestion,
|
ChoiceQuestion,
|
||||||
|
|
@ -299,8 +300,11 @@ class JevCompatibleDecisionsEndpoint:
|
||||||
api_base_env: str
|
api_base_env: str
|
||||||
api_key_required: bool = True
|
api_key_required: bool = True
|
||||||
|
|
||||||
def default_api_base(self) -> str | None:
|
def configured_api_key(self) -> str | None:
|
||||||
return self.default_api_base_value
|
return next((key for key in (get_secret_str(name) for name in self.api_key_env) if key), None)
|
||||||
|
|
||||||
|
def configured_api_base(self) -> str | None:
|
||||||
|
return get_secret_str(self.api_base_env) or self.default_api_base_value
|
||||||
|
|
||||||
def missing_api_base_message(self, provider: str) -> str:
|
def missing_api_base_message(self, provider: str) -> str:
|
||||||
return f"api_base is required for Decisions provider '{provider}'"
|
return f"api_base is required for Decisions provider '{provider}'"
|
||||||
|
|
@ -324,16 +328,12 @@ class JevCompatibleDecisionsEndpoint:
|
||||||
|
|
||||||
|
|
||||||
class DecisionsProviderConfig(Protocol):
|
class DecisionsProviderConfig(Protocol):
|
||||||
@property
|
|
||||||
def api_key_env(self) -> tuple[str, ...]: ...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def api_base_env(self) -> str: ...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_key_required(self) -> bool: ...
|
def api_key_required(self) -> bool: ...
|
||||||
|
|
||||||
def default_api_base(self) -> str | None: ...
|
def configured_api_key(self) -> str | None: ...
|
||||||
|
|
||||||
|
def configured_api_base(self) -> str | None: ...
|
||||||
|
|
||||||
def missing_api_base_message(self, provider: str) -> str: ...
|
def missing_api_base_message(self, provider: str) -> str: ...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,15 @@ _RESPONSE_MAPPING_ADAPTER: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapte
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class CloudflareDecisionsEndpoint:
|
class CloudflareDecisionsEndpoint:
|
||||||
api_key_env: tuple[str, ...] = ("CLOUDFLARE_API_KEY",)
|
|
||||||
api_base_env: str = "CLOUDFLARE_API_BASE"
|
|
||||||
api_key_required: bool = True
|
api_key_required: bool = True
|
||||||
|
|
||||||
def default_api_base(self) -> str | None:
|
def configured_api_key(self) -> str | None:
|
||||||
|
return get_secret_str("CLOUDFLARE_API_KEY") or None
|
||||||
|
|
||||||
|
def configured_api_base(self) -> str | None:
|
||||||
|
env_api_base: Final = get_secret_str("CLOUDFLARE_API_BASE")
|
||||||
|
if env_api_base:
|
||||||
|
return env_api_base
|
||||||
account_id: Final = normalize_nonempty_secret_str(get_secret_str("CLOUDFLARE_ACCOUNT_ID"))
|
account_id: Final = normalize_nonempty_secret_str(get_secret_str("CLOUDFLARE_ACCOUNT_ID"))
|
||||||
if account_id is None:
|
if account_id is None:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ from typing import Final
|
||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
from typing_extensions import assert_never
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
|
import litellm
|
||||||
from litellm.llms.base_llm.decisions.transformation import decisions_text
|
from litellm.llms.base_llm.decisions.transformation import decisions_text
|
||||||
|
from litellm.secret_managers.main import get_secret_str
|
||||||
from litellm.types.decisions import (
|
from litellm.types.decisions import (
|
||||||
DecisionsIRAnswer,
|
DecisionsIRAnswer,
|
||||||
DecisionsIRChoiceAnswer,
|
DecisionsIRChoiceAnswer,
|
||||||
|
|
@ -271,12 +273,18 @@ def ir_to_openai_response(
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class OpenAIDecisionsEndpoint:
|
class OpenAIDecisionsEndpoint:
|
||||||
api_key_env: tuple[str, ...] = ("OPENAI_API_KEY",)
|
|
||||||
api_base_env: str = "OPENAI_BASE_URL"
|
|
||||||
api_key_required: bool = True
|
api_key_required: bool = True
|
||||||
|
|
||||||
def default_api_base(self) -> str | None:
|
def configured_api_key(self) -> str | None:
|
||||||
return "https://api.openai.com"
|
return litellm.api_key or litellm.openai_key or get_secret_str("OPENAI_API_KEY")
|
||||||
|
|
||||||
|
def configured_api_base(self) -> str | None:
|
||||||
|
return (
|
||||||
|
litellm.api_base
|
||||||
|
or get_secret_str("OPENAI_BASE_URL")
|
||||||
|
or get_secret_str("OPENAI_API_BASE")
|
||||||
|
or "https://api.openai.com"
|
||||||
|
)
|
||||||
|
|
||||||
def missing_api_base_message(self, provider: str) -> str:
|
def missing_api_base_message(self, provider: str) -> str:
|
||||||
return f"api_base is required for Decisions provider '{provider}'"
|
return f"api_base is required for Decisions provider '{provider}'"
|
||||||
|
|
|
||||||
|
|
@ -673,7 +673,9 @@ async def test_openai_decisions_translate_systemone_to_the_openai_wire_contract_
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
respx_mock: respx.MockRouter,
|
respx_mock: respx.MockRouter,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
monkeypatch.setattr(litellm, "api_base", None)
|
||||||
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
|
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
|
||||||
route: Final = respx_mock.post(url).respond(json=_OPENAI_RESPONSE)
|
route: Final = respx_mock.post(url).respond(json=_OPENAI_RESPONSE)
|
||||||
|
|
||||||
response: Final = await litellm.adecisions(
|
response: Final = await litellm.adecisions(
|
||||||
|
|
@ -720,6 +722,57 @@ async def test_openai_decisions_translate_systemone_to_the_openai_wire_contract_
|
||||||
assert litellm.completion_cost(completion_response=response) == pytest.approx(expected_cost)
|
assert litellm.completion_cost(completion_response=response) == pytest.approx(expected_cost)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("settings", "env", "url", "authorization"),
|
||||||
|
(
|
||||||
|
({"openai_key": "sdk-key"}, {}, "https://api.openai.com/v1/decisions", "Bearer sdk-key"),
|
||||||
|
(
|
||||||
|
{"api_key": "global-key", "openai_key": "sdk-key"},
|
||||||
|
{"OPENAI_API_KEY": "env-key"},
|
||||||
|
"https://api.openai.com/v1/decisions",
|
||||||
|
"Bearer global-key",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{},
|
||||||
|
{"OPENAI_API_KEY": "env-key", "OPENAI_API_BASE": "https://legacy.example/v1"},
|
||||||
|
"https://legacy.example/v1/decisions",
|
||||||
|
"Bearer env-key",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"api_base": "https://sdk.example"},
|
||||||
|
{"OPENAI_API_KEY": "env-key", "OPENAI_BASE_URL": "https://env.example"},
|
||||||
|
"https://sdk.example/v1/decisions",
|
||||||
|
"Bearer env-key",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ids=("openai_key", "api_key_before_env", "openai_api_base_env", "api_base_before_env"),
|
||||||
|
)
|
||||||
|
def test_openai_decisions_use_the_same_settings_as_other_openai_calls(
|
||||||
|
settings: Mapping[str, str],
|
||||||
|
env: Mapping[str, str],
|
||||||
|
url: str,
|
||||||
|
authorization: str,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
respx_mock: respx.MockRouter,
|
||||||
|
) -> None:
|
||||||
|
for name in ("api_key", "openai_key", "api_base"):
|
||||||
|
monkeypatch.setattr(litellm, name, settings.get(name))
|
||||||
|
for name in ("OPENAI_API_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE"):
|
||||||
|
monkeypatch.delenv(name, raising=False)
|
||||||
|
for name, value in env.items():
|
||||||
|
monkeypatch.setenv(name, value)
|
||||||
|
route: Final = respx_mock.post(url).respond(json=_OPENAI_RESPONSE)
|
||||||
|
|
||||||
|
litellm.decisions(
|
||||||
|
model="openai/gpt-6-luna",
|
||||||
|
state="review",
|
||||||
|
questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert route.call_count == 1
|
||||||
|
assert route.calls[0].request.headers["authorization"] == authorization
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_openai_format_calls_to_a_systemone_provider_get_openai_format_answers(
|
async def test_openai_format_calls_to_a_systemone_provider_get_openai_format_answers(
|
||||||
respx_mock: respx.MockRouter,
|
respx_mock: respx.MockRouter,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue