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.strands_decider.decisions.transformation import STRANDS_DECIDER_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 (
|
||||
DecisionQuestion,
|
||||
DecisionsIRRequest,
|
||||
|
|
@ -103,21 +102,16 @@ def _resolve_api_key(
|
|||
) -> str | None:
|
||||
if api_key is not None:
|
||||
return api_key
|
||||
|
||||
server_api_key: Final = next(
|
||||
(key for key in (get_secret_str(name) for name in endpoint.api_key_env) if key),
|
||||
None,
|
||||
configured_api_key: Final = endpoint.configured_api_key()
|
||||
if configured_api_key:
|
||||
return configured_api_key
|
||||
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(
|
||||
|
|
@ -175,9 +169,7 @@ def _prepare_request(
|
|||
) from error
|
||||
|
||||
endpoint: Final = DECISIONS_ENDPOINTS[provider]
|
||||
env_api_base: Final = get_secret_str(endpoint.api_base_env)
|
||||
default_api_base: Final = endpoint.default_api_base()
|
||||
resolved_api_base: Final = api_base or env_api_base or default_api_base
|
||||
resolved_api_base: Final = api_base or endpoint.configured_api_base()
|
||||
if resolved_api_base is None:
|
||||
raise litellm.BadRequestError(
|
||||
message=endpoint.missing_api_base_message(provider),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from typing import Final, Protocol
|
|||
from pydantic import TypeAdapter
|
||||
from typing_extensions import assert_never
|
||||
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.decisions import (
|
||||
ChoiceAnswer,
|
||||
ChoiceQuestion,
|
||||
|
|
@ -299,8 +300,11 @@ class JevCompatibleDecisionsEndpoint:
|
|||
api_base_env: str
|
||||
api_key_required: bool = True
|
||||
|
||||
def default_api_base(self) -> str | None:
|
||||
return self.default_api_base_value
|
||||
def configured_api_key(self) -> str | None:
|
||||
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:
|
||||
return f"api_base is required for Decisions provider '{provider}'"
|
||||
|
|
@ -324,16 +328,12 @@ class JevCompatibleDecisionsEndpoint:
|
|||
|
||||
|
||||
class DecisionsProviderConfig(Protocol):
|
||||
@property
|
||||
def api_key_env(self) -> tuple[str, ...]: ...
|
||||
|
||||
@property
|
||||
def api_base_env(self) -> str: ...
|
||||
|
||||
@property
|
||||
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: ...
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,15 @@ _RESPONSE_MAPPING_ADAPTER: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapte
|
|||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CloudflareDecisionsEndpoint:
|
||||
api_key_env: tuple[str, ...] = ("CLOUDFLARE_API_KEY",)
|
||||
api_base_env: str = "CLOUDFLARE_API_BASE"
|
||||
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"))
|
||||
if account_id is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ from typing import Final
|
|||
from pydantic import TypeAdapter
|
||||
from typing_extensions import assert_never
|
||||
|
||||
import litellm
|
||||
from litellm.llms.base_llm.decisions.transformation import decisions_text
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.decisions import (
|
||||
DecisionsIRAnswer,
|
||||
DecisionsIRChoiceAnswer,
|
||||
|
|
@ -271,12 +273,18 @@ def ir_to_openai_response(
|
|||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OpenAIDecisionsEndpoint:
|
||||
api_key_env: tuple[str, ...] = ("OPENAI_API_KEY",)
|
||||
api_base_env: str = "OPENAI_BASE_URL"
|
||||
api_key_required: bool = True
|
||||
|
||||
def default_api_base(self) -> str | None:
|
||||
return "https://api.openai.com"
|
||||
def configured_api_key(self) -> str | None:
|
||||
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:
|
||||
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,
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "api_base", None)
|
||||
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
|
||||
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
|
||||
route: Final = respx_mock.post(url).respond(json=_OPENAI_RESPONSE)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
async def test_openai_format_calls_to_a_systemone_provider_get_openai_format_answers(
|
||||
respx_mock: respx.MockRouter,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue