Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions litellm-rust/crates/model-catalog/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub enum Mode {
AudioTranscription,
Chat,
Completion,
Decisions,
Embedding,
Evaluation,
Guardrail,
Expand Down
16 changes: 15 additions & 1 deletion litellm-rust/crates/model-catalog/tests/catalog.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};

use litellm_model_catalog::{AliasIssue, Catalog, Error, IntegrityLimits, Provenance};
use litellm_model_catalog::{AliasIssue, Catalog, Error, IntegrityLimits, Mode, Provenance};
use rstest::{fixture, rstest};
use serde_json::json;

Expand Down Expand Up @@ -42,6 +42,20 @@ fn fixture_catalog() -> Catalog {
.unwrap()
}

#[rstest]
#[case::decisions("decisions", Mode::Decisions)]
#[case::legacy_evaluation("evaluation", Mode::Evaluation)]
fn parses_decision_modes(#[case] mode: &str, #[case] expected: Mode) {
let source = json!({"Test":{"litellm_provider":"test","mode":mode}}).to_string();
let catalog = Catalog::parse(source.as_bytes(), Provenance::default()).unwrap();

assert_eq!(
catalog.lookup("Test").unwrap().entry.info().mode,
Some(expected)
);
assert_eq!(serde_json::to_value(expected).unwrap(), json!(mode));
}

#[rstest]
#[ignore]
fn preserves_fields_and_metadata(fixture_catalog: Catalog) {
Expand Down
4 changes: 2 additions & 2 deletions litellm/litellm_core_utils/health_check_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def get_mode_handlers(
"responses",
"anthropic_messages",
"ocr",
"evaluation",
"decisions",
],
Callable,
]:
Expand Down Expand Up @@ -311,7 +311,7 @@ def get_mode_handlers(
**_filter_model_params(model_params=model_params),
document=_ocr_health_check_document(model=model, custom_llm_provider=custom_llm_provider),
),
"evaluation": lambda: litellm.asystemone(
"decisions": lambda: litellm.asystemone(
**DECISIONS_CALL_PARAMS.validate_python(
{
"state": prompt or "health check",
Expand Down
6 changes: 4 additions & 2 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
ModelResponseStream,
RawRequestTypedDict,
StreamingChoices,
is_decisions_model_mode,
)
from litellm.types.workload_identity import ANTHROPIC_WIF_KWARGS_KEYS, OPENAI_WIF_KWARGS_KEYS
from litellm.utils import (
Expand Down Expand Up @@ -8821,8 +8822,9 @@ async def ahealth_check(
input=input,
)

if mode in mode_handlers:
_response: Final = await mode_handlers[mode]()
handler_mode: Final = "decisions" if is_decisions_model_mode(mode) else mode
if handler_mode in mode_handlers:
_response: Final = await mode_handlers[handler_mode]()
_response_headers: Final = cast( # cast-ok: provider headers are stored as a string-keyed mapping
Mapping[str, object], (get_hidden_params(_response) or {}).get("headers", {}) or {}
)
Expand Down
1 change: 1 addition & 0 deletions litellm/proxy/health_endpoints/_health_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2091,6 +2091,7 @@ async def test_model_connection(
"anthropic_messages",
"ocr",
"evaluation",
"decisions",
]
| None = fastapi.Body(
None,
Expand Down
5 changes: 5 additions & 0 deletions litellm/types/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ class LiteLLMCommonStrings(Enum):
llm_provider_not_provided = "Unmapped LLM provider for this endpoint. You passed model={model}, custom_llm_provider={custom_llm_provider}. Check supported provider and route: https://docs.litellm.ai/docs/providers"


def is_decisions_model_mode(mode: str | None) -> bool:
return mode in ("decisions", "evaluation")


SupportedCacheControls: Final = ["ttl", "s-maxage", "no-cache", "no-store"]


Expand Down Expand Up @@ -430,6 +434,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
"audio_transcription",
"audio_speech",
"responses",
"decisions",
"evaluation",
"ocr",
"realtime",
Expand Down
10 changes: 6 additions & 4 deletions tests/integration/management/test_model_health_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Final

import httpx
import pytest
from integration._support.client import Gateway, object_value, string_value
from integration._support.upstream import ScenarioHandle, delete_scenario, register_scenario
from integration.cost_calculation.cost_tracking_case import JsonResponse
Expand Down Expand Up @@ -93,7 +94,7 @@ def _probes_sent_to(gateway: Gateway, handle: ScenarioHandle) -> list[tuple[str,
]


def test_evaluation_mode_health_check_resolves_the_mode_from_the_cost_map_and_sends_the_default_probe(
def test_decisions_mode_health_check_resolves_the_mode_from_the_cost_map_and_sends_the_default_probe(
gateway: Gateway,
) -> None:
with gateway.scenario() as scenario:
Expand All @@ -114,15 +115,16 @@ def test_evaluation_mode_health_check_resolves_the_mode_from_the_cost_map_and_se
]


def test_evaluation_mode_health_check_sends_the_configured_state_and_questions(gateway: Gateway) -> None:
@pytest.mark.parametrize("mode", ("decisions", "evaluation"), ids=("decisions", "legacy-evaluation"))
def test_decisions_mode_health_check_sends_the_configured_state_and_questions(gateway: Gateway, mode: str) -> None:
with gateway.scenario() as scenario:
handle: Final = register_scenario(f"health-decisions-{uuid.uuid4().hex[:12]}", _CONFIGURED_PROBE_REPLY)
scenario.cleanups.callback(delete_scenario, handle)
model: Final = scenario.model(
model="typesafe/jev-custom",
api_base=handle.api_base(),
model_info={
"mode": "evaluation",
"mode": mode,
"health_check_params": {"state": _CONFIGURED_STATE, "questions": _CONFIGURED_QUESTIONS},
},
)
Expand All @@ -136,7 +138,7 @@ def test_evaluation_mode_health_check_sends_the_configured_state_and_questions(g
]


def test_evaluation_mode_health_check_of_the_self_hosted_strands_model_resolves_the_mode_from_the_cost_map(
def test_decisions_mode_health_check_of_the_self_hosted_strands_model_resolves_the_mode_from_the_cost_map(
gateway: Gateway,
) -> None:
with gateway.scenario() as scenario:
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/litellm_core_utils/test_health_check_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ def test_ocr_health_check_document_raises_without_the_extension():
("cloudflare/clef", "https://api.cloudflare.com/client/v4/accounts/acct-1/ai/run/@cf/cloudflare/clef"),
),
)
async def test_ahealth_check_probes_evaluation_models_through_the_decisions_api(
async def test_ahealth_check_probes_decisions_models_through_the_decisions_api(
model: str,
upstream_url: str,
monkeypatch: pytest.MonkeyPatch,
Expand Down Expand Up @@ -617,7 +617,9 @@ async def test_ahealth_check_probes_evaluation_models_through_the_decisions_api(


@pytest.mark.asyncio
async def test_ahealth_check_evaluation_uses_configured_probe_state_and_questions(
@pytest.mark.parametrize("mode", ("decisions", "evaluation"), ids=("decisions", "legacy-evaluation"))
async def test_ahealth_check_decisions_uses_configured_probe_state_and_questions(
mode: str,
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
Expand All @@ -638,7 +640,7 @@ async def test_ahealth_check_evaluation_uses_configured_probe_state_and_question
"state": "custom probe",
"questions": {"ok": {"type": "noul", "instructions": "Is it ok?"}},
},
mode=None,
mode=mode,
)

assert "error" not in result, result
Expand Down
12 changes: 7 additions & 5 deletions tests/unit/proxy/health_endpoints/test_health_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,8 @@ async def test_test_model_connection_request_mode_wins_over_resolved_mode():


@pytest.mark.asyncio
async def test_test_model_connection_evaluation_mode_uses_decisions_handler():
@pytest.mark.parametrize("mode", ("decisions", "evaluation"), ids=("decisions", "legacy-evaluation"))
async def test_test_model_connection_decision_modes_are_forwarded(mode: str):
deployment: Final = MappingProxyType(
{
"model_name": "typesafe/jev-latest",
Expand All @@ -893,14 +894,14 @@ async def test_test_model_connection_evaluation_mode_uses_decisions_handler():
with _test_connection_probe(deployment) as ahealth_check:
result: Final = await health_test_model_connection(
request=MagicMock(),
mode="evaluation",
mode=mode,
litellm_params={"model": "typesafe/jev-latest"},
model_info={"id": "typesafe-jev-id"},
user_api_key_dict=UserAPIKeyAuth(user_id="test-user", token="test-token"),
)

assert result["status"] == "success"
assert ahealth_check.call_args.kwargs["mode"] == "evaluation"
assert ahealth_check.call_args.kwargs["mode"] == mode


@pytest.mark.asyncio
Expand Down Expand Up @@ -5011,7 +5012,8 @@ def test_test_model_connection_accepts_image_edit_mode(monkeypatch):
assert response.json()["status"] == "success"


def test_test_model_connection_accepts_evaluation_mode(monkeypatch):
@pytest.mark.parametrize("mode", ("decisions", "evaluation"), ids=("decisions", "legacy-evaluation"))
def test_test_model_connection_accepts_decision_modes(monkeypatch, mode: str):
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()

Expand All @@ -5036,7 +5038,7 @@ def test_test_model_connection_accepts_evaluation_mode(monkeypatch):
response = client.post(
"/health/test_connection",
json={
"mode": "evaluation",
"mode": mode,
"litellm_params": {"model": "typesafe/jev-latest", "api_key": "sk-test"},
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def test_success_handler_dispatches_to_typesafe_handler():


@pytest.mark.asyncio
@pytest.mark.parametrize("mode", ("decisions", "evaluation"), ids=("decisions", "legacy-evaluation"))
@pytest.mark.parametrize("guardrail_cost", [0.0, 0.25])
@pytest.mark.parametrize("metadata_slot", ["metadata", "litellm_metadata"])
@pytest.mark.parametrize("provider,requested,routing_model", [
Expand All @@ -149,15 +150,15 @@ def test_success_handler_dispatches_to_typesafe_handler():
])
async def test_oss_gateway_accounts_for_checkpoint_usage_and_registered_cost(
monkeypatch: pytest.MonkeyPatch, routing_model: str | None, metadata_slot: str, guardrail_cost: float,
provider: str, requested: str
provider: str, requested: str, mode: str
) -> None:
checkpoint: Final = routing_model or requested
model: Final = f"{provider}/{checkpoint}"
input_rate: Final = 0.002
output_rate: Final = 0.005
monkeypatch.setitem(litellm.model_cost, model, {
"input_cost_per_token": input_rate, "output_cost_per_token": output_rate,
"litellm_provider": provider, "mode": "evaluation",
"litellm_provider": provider, "mode": mode,
})
start: Final = datetime.now()
logging_obj: Final = Logging(
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_model_prices_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ def test_deepseek_rows_bill_half_rate_outside_weekday_peak_hours(path: Path):


PROVIDER_LABELS_WITHOUT_A_MODEL_SET: Final = frozenset({"sagemaker", "bedrock_converse"})
MODES_SERVED_OUTSIDE_THE_LLM_PROVIDER_REGISTRY: Final = frozenset({"search", "evaluation"})
MODES_SERVED_OUTSIDE_THE_LLM_PROVIDER_REGISTRY: Final = frozenset({"search", "decisions", "evaluation"})
VERTEX_FAMILIES_A_VERTEX_WILDCARD_GRANT_DOES_NOT_LIST: Final = frozenset(
{
"vertex_ai-ai21_models",
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8403,7 +8403,7 @@ def test_models_by_provider():
or v["litellm_provider"] == "bedrock_converse"
):
continue
elif v.get("mode") in ("search", "evaluation"):
elif v.get("mode") in ("search", "decisions", "evaluation"):
continue
else:
providers.add(v["litellm_provider"])
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/types/test_types_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ModelResponseStream,
TextCompletionResponse,
all_litellm_params,
is_decisions_model_mode,
text_tokens_without_nested_reasoning,
)

Expand All @@ -41,6 +42,14 @@ def hidden_params(self) -> dict[str, object]: ...
def hidden_params(self, hidden_params: dict[str, object]) -> None: ...


@pytest.mark.parametrize(
("mode", "expected"),
(("decisions", True), ("evaluation", True), ("chat", False), (None, False)),
)
def test_is_decisions_model_mode(mode: str | None, expected: bool) -> None:
assert is_decisions_model_mode(mode) is expected


def test_rust_is_a_known_litellm_param():
assert "rust" in all_litellm_params

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
});

it("should return empty logo and dash display name when providerValue is undefined", () => {
const result = getProviderLogoAndName(undefined as any);

Check warning on line 42 in ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx

View workflow job for this annotation

GitHub Actions / ui

Unexpected any. Specify a different type
expect(result).toEqual({ logo: "", displayName: "-" });
});

Expand Down Expand Up @@ -367,7 +367,7 @@
});

it("should return default gpt-3.5-turbo placeholder for unknown provider", () => {
expect(getPlaceholder("UnknownProvider" as any)).toBe("gpt-3.5-turbo");

Check warning on line 370 in ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx

View workflow job for this annotation

GitHub Actions / ui

Unexpected any. Specify a different type
});

it("should return default gpt-3.5-turbo placeholder for OpenAI provider", () => {
Expand Down Expand Up @@ -412,17 +412,17 @@
describe("getProviderModels", () => {
it("should return empty array when provider is not provided", () => {
const modelMap = {};
const result = getProviderModels(undefined as any, modelMap);

Check warning on line 415 in ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx

View workflow job for this annotation

GitHub Actions / ui

Unexpected any. Specify a different type
expect(result).toEqual([]);
});

it("should return empty array when modelMap is not an object type", () => {
const result = getProviderModels(Providers.OpenAI, "not-an-object" as any);

Check warning on line 420 in ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx

View workflow job for this annotation

GitHub Actions / ui

Unexpected any. Specify a different type
expect(result).toEqual([]);
});

it("should return empty array when modelMap is undefined", () => {
const result = getProviderModels(Providers.OpenAI, undefined as any);

Check warning on line 425 in ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx

View workflow job for this annotation

GitHub Actions / ui

Unexpected any. Specify a different type
expect(result).toEqual([]);
});

Expand Down Expand Up @@ -506,9 +506,9 @@
it("should list only typesafe decision models for the 'TypeSafe' provider key, not the OpenRouter-hosted one", () => {
const modelMap = {
"typesafe/jev-latest": { litellm_provider: "typesafe", mode: "evaluation" },
"typesafe/jev-preview": { litellm_provider: "typesafe", mode: "evaluation" },
"typesafe/jev-preview": { litellm_provider: "typesafe", mode: "decisions" },
"openrouter/typesafe/jev-1.13": { litellm_provider: "openrouter", mode: "evaluation" },
"strands_decider/strands-decider-2B-hobson-v19": { litellm_provider: "strands_decider", mode: "evaluation" },
"strands_decider/strands-decider-2B-hobson-v19": { litellm_provider: "strands_decider", mode: "decisions" },
};
expect(getProviderModels("TypeSafe" as Providers, modelMap)).toEqual([
"typesafe/jev-latest",
Expand All @@ -518,7 +518,7 @@

it("should list only strands_decider models for the 'StrandsDecider' provider key", () => {
const modelMap = {
"strands_decider/strands-decider-2B-hobson-v19": { litellm_provider: "strands_decider", mode: "evaluation" },
"strands_decider/strands-decider-2B-hobson-v19": { litellm_provider: "strands_decider", mode: "decisions" },
"typesafe/jev-latest": { litellm_provider: "typesafe", mode: "evaluation" },
"openrouter/typesafe/jev-1.13": { litellm_provider: "openrouter", mode: "evaluation" },
};
Expand Down
3 changes: 2 additions & 1 deletion ui/litellm-dashboard/src/lib/decisionModels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const COST_MAP = {
};

describe("isDecisionMode", () => {
it("is true only for the evaluation mode", () => {
it("is true for both the decisions and evaluation modes", () => {
expect(isDecisionMode("decisions")).toBe(true);
expect(isDecisionMode("evaluation")).toBe(true);
expect(isDecisionMode("chat")).toBe(false);
expect(isDecisionMode(null)).toBe(false);
Expand Down
3 changes: 2 additions & 1 deletion ui/litellm-dashboard/src/lib/decisionModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export interface DecisionCatalog {

export const EMPTY_DECISION_CATALOG: DecisionCatalog = { models: new Set(), providers: new Map() };

export const isDecisionMode = (mode: string | null | undefined): boolean => mode === "evaluation";
export const isDecisionMode = (mode: string | null | undefined): boolean =>
mode === "decisions" || mode === "evaluation";

const isDecisionEntry = (entry: CostMapEntry): boolean =>
isDecisionMode(entry.mode) || (entry.supported_endpoints ?? []).some((endpoint) => DECISION_ENDPOINTS.has(endpoint));
Expand Down
2 changes: 1 addition & 1 deletion ui/litellm-dashboard/src/lib/http/schema.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading