mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
Merge remote-tracking branch 'origin/main' into litellm_fips_jwt_algorithm_allowlists
This commit is contained in:
commit
6c0fbe7ef3
4 changed files with 399 additions and 8 deletions
|
|
@ -6159,6 +6159,7 @@
|
|||
]
|
||||
},
|
||||
"azure/gpt-realtime-whisper": {
|
||||
"deprecation_date": "2027-05-06",
|
||||
"input_cost_per_second": 0.0002833333333333333,
|
||||
"litellm_provider": "azure",
|
||||
"mode": "audio_transcription",
|
||||
|
|
@ -8060,6 +8061,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-05,
|
||||
"cache_read_input_token_cost": 1e-06,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 2e-06,
|
||||
"deprecation_date": "2028-01-11",
|
||||
"input_cost_per_token": 1e-05,
|
||||
"input_cost_per_token_above_272k_tokens": 2e-05,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8108,6 +8110,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-05,
|
||||
"cache_read_input_token_cost": 1e-06,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 2e-06,
|
||||
"deprecation_date": "2028-01-11",
|
||||
"input_cost_per_token": 1e-05,
|
||||
"input_cost_per_token_above_272k_tokens": 2e-05,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8156,6 +8159,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-07,
|
||||
"cache_read_input_token_cost": 1e-08,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 2e-08,
|
||||
"deprecation_date": "2028-03-11",
|
||||
"input_cost_per_token": 1e-07,
|
||||
"input_cost_per_token_above_272k_tokens": 2e-07,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8204,6 +8208,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-07,
|
||||
"cache_read_input_token_cost": 1e-08,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 2e-08,
|
||||
"deprecation_date": "2028-03-11",
|
||||
"input_cost_per_token": 1e-07,
|
||||
"input_cost_per_token_above_272k_tokens": 2e-07,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8252,6 +8257,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 5e-06,
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 4e-07,
|
||||
"deprecation_date": "2028-03-11",
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_272k_tokens": 4e-06,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8300,6 +8306,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 5e-06,
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 4e-07,
|
||||
"deprecation_date": "2028-03-11",
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_272k_tokens": 4e-06,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -56802,7 +56809,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_web_search": true
|
||||
"supports_web_search": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-sol.html"
|
||||
},
|
||||
"bedrock_mantle/openai.gpt-5.6-terra": {
|
||||
"input_cost_per_token": 2.2e-06,
|
||||
|
|
@ -56844,7 +56852,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_web_search": true
|
||||
"supports_web_search": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-terra.html"
|
||||
},
|
||||
"bedrock_mantle/openai.gpt-5.6-cyber": {
|
||||
"input_cost_per_token": 1.375e-05,
|
||||
|
|
@ -56951,7 +56960,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_web_search": true
|
||||
"supports_web_search": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-luna.html"
|
||||
},
|
||||
"us.openai.gpt-5.6-sol": {
|
||||
"input_cost_per_token": 4.4e-06,
|
||||
|
|
@ -57722,7 +57732,8 @@
|
|||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-6.html"
|
||||
},
|
||||
"bedrock_mantle/anthropic.claude-haiku-4-5": {
|
||||
"cache_creation_input_token_cost": 1.25e-06,
|
||||
|
|
|
|||
|
|
@ -6159,6 +6159,7 @@
|
|||
]
|
||||
},
|
||||
"azure/gpt-realtime-whisper": {
|
||||
"deprecation_date": "2027-05-06",
|
||||
"input_cost_per_second": 0.0002833333333333333,
|
||||
"litellm_provider": "azure",
|
||||
"mode": "audio_transcription",
|
||||
|
|
@ -8060,6 +8061,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-05,
|
||||
"cache_read_input_token_cost": 1e-06,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 2e-06,
|
||||
"deprecation_date": "2028-01-11",
|
||||
"input_cost_per_token": 1e-05,
|
||||
"input_cost_per_token_above_272k_tokens": 2e-05,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8108,6 +8110,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-05,
|
||||
"cache_read_input_token_cost": 1e-06,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 2e-06,
|
||||
"deprecation_date": "2028-01-11",
|
||||
"input_cost_per_token": 1e-05,
|
||||
"input_cost_per_token_above_272k_tokens": 2e-05,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8156,6 +8159,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-07,
|
||||
"cache_read_input_token_cost": 1e-08,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 2e-08,
|
||||
"deprecation_date": "2028-03-11",
|
||||
"input_cost_per_token": 1e-07,
|
||||
"input_cost_per_token_above_272k_tokens": 2e-07,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8204,6 +8208,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-07,
|
||||
"cache_read_input_token_cost": 1e-08,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 2e-08,
|
||||
"deprecation_date": "2028-03-11",
|
||||
"input_cost_per_token": 1e-07,
|
||||
"input_cost_per_token_above_272k_tokens": 2e-07,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8252,6 +8257,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 5e-06,
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 4e-07,
|
||||
"deprecation_date": "2028-03-11",
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_272k_tokens": 4e-06,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -8300,6 +8306,7 @@
|
|||
"cache_creation_input_token_cost_above_272k_tokens": 5e-06,
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_272k_tokens": 4e-07,
|
||||
"deprecation_date": "2028-03-11",
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_272k_tokens": 4e-06,
|
||||
"litellm_provider": "azure",
|
||||
|
|
@ -56802,7 +56809,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_web_search": true
|
||||
"supports_web_search": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-sol.html"
|
||||
},
|
||||
"bedrock_mantle/openai.gpt-5.6-terra": {
|
||||
"input_cost_per_token": 2.2e-06,
|
||||
|
|
@ -56844,7 +56852,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_web_search": true
|
||||
"supports_web_search": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-terra.html"
|
||||
},
|
||||
"bedrock_mantle/openai.gpt-5.6-cyber": {
|
||||
"input_cost_per_token": 1.375e-05,
|
||||
|
|
@ -56951,7 +56960,8 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_web_search": true
|
||||
"supports_web_search": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-luna.html"
|
||||
},
|
||||
"us.openai.gpt-5.6-sol": {
|
||||
"input_cost_per_token": 4.4e-06,
|
||||
|
|
@ -57722,7 +57732,8 @@
|
|||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
"supports_vision": true,
|
||||
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-6.html"
|
||||
},
|
||||
"bedrock_mantle/anthropic.claude-haiku-4-5": {
|
||||
"cache_creation_input_token_cost": 1.25e-06,
|
||||
|
|
|
|||
154
tests/integration/configuration/test_callback_settings_boot.py
Normal file
154
tests/integration/configuration/test_callback_settings_boot.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import json
|
||||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
from pydantic import JsonValue
|
||||
|
||||
from tests.integration._support.client import Gateway
|
||||
from tests.integration._support.process import owned_proxy
|
||||
|
||||
SERVING_CONSUMERS: Final = {
|
||||
"compression_interception": "CompressionInterceptionLogger",
|
||||
"code_interpreter_interception": "CodeInterpreterInterceptionLogger",
|
||||
"websearch_interception": "WebSearchInterceptionLogger",
|
||||
}
|
||||
OTEL_CONSUMER: Final = {"otel": "OpenTelemetry"}
|
||||
GUARDRAIL_CONSUMERS: Final = {
|
||||
"presidio": "_OPTIONAL_PresidioPIIMasking",
|
||||
"lakera_prompt_injection": "lakeraAI_Moderation",
|
||||
}
|
||||
|
||||
TOP_LEVEL_SHAPES: Final = (
|
||||
pytest.param({}, id="empty-object"),
|
||||
pytest.param(None, id="null"),
|
||||
pytest.param("otel", id="string"),
|
||||
pytest.param(["otel"], id="list"),
|
||||
pytest.param(True, id="bool"),
|
||||
pytest.param(0, id="zero"),
|
||||
)
|
||||
|
||||
CONSUMER_SHAPES: Final = (
|
||||
pytest.param({}, id="empty-object"),
|
||||
pytest.param(None, id="null"),
|
||||
pytest.param("on", id="string"),
|
||||
pytest.param(True, id="bool"),
|
||||
pytest.param([], id="empty-list"),
|
||||
pytest.param(["on"], id="list"),
|
||||
pytest.param(7, id="int"),
|
||||
)
|
||||
|
||||
TOP_LEVEL_BOOT_CRASH: Final = (
|
||||
"BUG: a non-object callback_settings is stored verbatim and proxy startup crashes calling .get on it"
|
||||
)
|
||||
TOP_LEVEL_BOOT_CRASH_IDS: Final = frozenset({"string", "list", "bool"})
|
||||
OTEL_DROPPED: Final = (
|
||||
"BUG: a non-object callback_settings.otel fails dict() and the otel callback is silently not registered"
|
||||
)
|
||||
OTEL_DROPPED_IDS: Final = frozenset({"null", "string", "bool", "int"})
|
||||
|
||||
|
||||
def _write_config(
|
||||
directory: Path, upstream_url: str, model: str, callbacks: tuple[str, ...], callback_settings: JsonValue
|
||||
) -> Path:
|
||||
config: Final = directory / f"callback_settings_{uuid.uuid4().hex}.yaml"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": model,
|
||||
"litellm_params": {
|
||||
"model": f"openai/{model}",
|
||||
"api_base": f"{upstream_url}/v1",
|
||||
"api_key": "integration-provider-key",
|
||||
},
|
||||
}
|
||||
],
|
||||
"litellm_settings": {"callbacks": list(callbacks)},
|
||||
"callback_settings": callback_settings,
|
||||
"general_settings": {
|
||||
"master_key": "os.environ/LITELLM_MASTER_KEY",
|
||||
"database_url": "os.environ/DATABASE_URL",
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _assert_registered(candidate: Gateway, consumers: Mapping[str, str]) -> None:
|
||||
response: Final = candidate.request("GET", "/active/callbacks")
|
||||
assert response.status_code == 200, response.text
|
||||
missing: Final = sorted(name for name, class_name in consumers.items() if class_name not in response.text)
|
||||
assert missing == [], response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("callback_settings", TOP_LEVEL_SHAPES)
|
||||
def test_top_level_callback_settings_shape_boots_registers_and_serves_chat(
|
||||
gateway: Gateway, tmp_path: Path, callback_settings: JsonValue, request: pytest.FixtureRequest
|
||||
) -> None:
|
||||
if request.node.callspec.id in TOP_LEVEL_BOOT_CRASH_IDS:
|
||||
pytest.skip(TOP_LEVEL_BOOT_CRASH)
|
||||
consumers: Final = {**SERVING_CONSUMERS, **OTEL_CONSUMER}
|
||||
model: Final = f"integration-callback-settings-{uuid.uuid4().hex}"
|
||||
config: Final = _write_config(tmp_path, gateway.upstream_url, model, tuple(consumers), callback_settings)
|
||||
with owned_proxy(gateway, tmp_path, {"STORE_MODEL_IN_DB": "False"}, config=config) as candidate:
|
||||
_assert_registered(candidate, consumers)
|
||||
reply: Final = candidate.chat(model, text=f"callback settings {uuid.uuid4().hex}")
|
||||
assert reply["model"] == model, reply
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", CONSUMER_SHAPES)
|
||||
def test_serving_consumer_settings_shape_boots_registers_and_serves_chat(
|
||||
gateway: Gateway, tmp_path: Path, value: JsonValue
|
||||
) -> None:
|
||||
model: Final = f"integration-callback-settings-{uuid.uuid4().hex}"
|
||||
config: Final = _write_config(
|
||||
tmp_path,
|
||||
gateway.upstream_url,
|
||||
model,
|
||||
tuple(SERVING_CONSUMERS),
|
||||
{consumer: value for consumer in SERVING_CONSUMERS},
|
||||
)
|
||||
with owned_proxy(gateway, tmp_path, {"STORE_MODEL_IN_DB": "False"}, config=config) as candidate:
|
||||
_assert_registered(candidate, SERVING_CONSUMERS)
|
||||
reply: Final = candidate.chat(model, text=f"callback settings {uuid.uuid4().hex}")
|
||||
assert reply["model"] == model, reply
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", CONSUMER_SHAPES)
|
||||
def test_otel_settings_shape_boots_registers_and_serves_chat(
|
||||
gateway: Gateway, tmp_path: Path, value: JsonValue, request: pytest.FixtureRequest
|
||||
) -> None:
|
||||
if request.node.callspec.id in OTEL_DROPPED_IDS:
|
||||
pytest.skip(OTEL_DROPPED)
|
||||
model: Final = f"integration-callback-settings-{uuid.uuid4().hex}"
|
||||
config: Final = _write_config(tmp_path, gateway.upstream_url, model, tuple(OTEL_CONSUMER), {"otel": value})
|
||||
with owned_proxy(gateway, tmp_path, {"STORE_MODEL_IN_DB": "False"}, config=config) as candidate:
|
||||
_assert_registered(candidate, OTEL_CONSUMER)
|
||||
reply: Final = candidate.chat(model, text=f"callback settings {uuid.uuid4().hex}")
|
||||
assert reply["model"] == model, reply
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", CONSUMER_SHAPES)
|
||||
def test_guardrail_consumer_settings_shape_boots_and_registers(
|
||||
gateway: Gateway, tmp_path: Path, value: JsonValue
|
||||
) -> None:
|
||||
model: Final = f"integration-callback-settings-{uuid.uuid4().hex}"
|
||||
config: Final = _write_config(
|
||||
tmp_path,
|
||||
gateway.upstream_url,
|
||||
model,
|
||||
tuple(GUARDRAIL_CONSUMERS),
|
||||
{consumer: value for consumer in GUARDRAIL_CONSUMERS},
|
||||
)
|
||||
environment: Final = {
|
||||
"STORE_MODEL_IN_DB": "False",
|
||||
"PRESIDIO_ANALYZER_API_BASE": gateway.upstream_url,
|
||||
"PRESIDIO_ANONYMIZER_API_BASE": gateway.upstream_url,
|
||||
}
|
||||
with owned_proxy(gateway, tmp_path, environment, config=config) as candidate:
|
||||
_assert_registered(candidate, GUARDRAIL_CONSUMERS)
|
||||
|
|
@ -1,9 +1,93 @@
|
|||
import json
|
||||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
from integration._support.client import Gateway, object_value
|
||||
from integration._support.process import owned_proxy
|
||||
from pydantic import JsonValue
|
||||
|
||||
SIBLING_LIMITS: Final = {"max_input_tokens": 4321, "max_output_tokens": 987}
|
||||
|
||||
NON_NUMERIC_LIMITS: Final = (
|
||||
pytest.param("", id="empty-string"),
|
||||
pytest.param(" ", id="blank-string"),
|
||||
pytest.param("128,000", id="thousands-separator"),
|
||||
pytest.param("unlimited", id="word"),
|
||||
pytest.param("NaN", id="nan-string"),
|
||||
pytest.param("inf", id="inf-string"),
|
||||
pytest.param([], id="empty-list"),
|
||||
pytest.param([4096], id="list"),
|
||||
pytest.param({}, id="empty-object"),
|
||||
pytest.param({"tokens": 4096}, id="object"),
|
||||
pytest.param(True, id="bool"),
|
||||
pytest.param(None, id="null"),
|
||||
)
|
||||
|
||||
NUMERIC_EDGE_LIMITS: Final = (
|
||||
pytest.param(0, id="zero"),
|
||||
pytest.param(-1, id="negative"),
|
||||
pytest.param(1.5, id="float"),
|
||||
pytest.param("1.5", id="float-string"),
|
||||
pytest.param("1e9", id="exponent-string"),
|
||||
pytest.param(10**12, id="huge"),
|
||||
)
|
||||
NUMERIC_EDGE_EXPECTED: Final = {
|
||||
"zero": 0,
|
||||
"negative": -1,
|
||||
"float": 1,
|
||||
"float-string": 1,
|
||||
"exponent-string": 1_000_000_000,
|
||||
"huge": 10**12,
|
||||
}
|
||||
|
||||
MODEL_GROUP_INFO_500: Final = (
|
||||
"BUG: /model_group/info returns 500 for every caller when one deployment's token limit is non-numeric"
|
||||
)
|
||||
CHAT_500: Final = (
|
||||
"BUG: chat completions return 500 from ModelGroupInfo validation when the deployment's token limit is non-numeric"
|
||||
)
|
||||
MODEL_GROUP_INFO_500_IDS: Final = frozenset(
|
||||
{"empty-string", "blank-string", "thousands-separator", "word", "nan-string", "inf-string"}
|
||||
| {"empty-list", "list", "empty-object", "object"}
|
||||
)
|
||||
CHAT_500_IDS: Final = frozenset(
|
||||
{"empty-string", "blank-string", "thousands-separator", "word", "empty-list", "list", "empty-object", "object"}
|
||||
)
|
||||
|
||||
|
||||
def _listed(gateway: Gateway, path: str) -> dict[str, dict[str, JsonValue]]:
|
||||
entries: Final = gateway.get(path)["data"]
|
||||
assert isinstance(entries, list)
|
||||
return {str(object_value(entry)["id"]): object_value(entry) for entry in entries}
|
||||
|
||||
|
||||
def _limits(entry: Mapping[str, JsonValue]) -> tuple[JsonValue, JsonValue]:
|
||||
return entry.get("max_input_tokens"), entry.get("max_output_tokens")
|
||||
|
||||
|
||||
def _assert_listing_spares_the_sibling(
|
||||
gateway: Gateway, broken: str, sibling: str, broken_limits: tuple[JsonValue, JsonValue]
|
||||
) -> None:
|
||||
for path in ("/v1/models", "/models"):
|
||||
listed: Final = _listed(gateway, path)
|
||||
assert _limits(listed[sibling]) == (4321, 987), (path, listed[sibling])
|
||||
assert _limits(listed[broken]) == broken_limits, (path, listed[broken])
|
||||
single: Final = gateway.get(f"/v1/models/{broken}")
|
||||
assert single["id"] == broken, single
|
||||
assert _limits(single) == broken_limits, single
|
||||
registered: Final = gateway.get("/model/info")["data"]
|
||||
assert isinstance(registered, list)
|
||||
assert {broken, sibling} <= {str(object_value(entry)["model_name"]) for entry in registered}
|
||||
|
||||
|
||||
def _assert_serves_chat(gateway: Gateway, *models: str) -> None:
|
||||
for model in models:
|
||||
reply: Final = gateway.chat(model, text=f"token limit edge {uuid.uuid4().hex}")
|
||||
assert reply["model"] == model, reply
|
||||
|
||||
|
||||
def _listed_model(gateway: Gateway, model: str) -> dict[str, JsonValue]:
|
||||
entries: Final = gateway.get("/v1/models")["data"]
|
||||
|
|
@ -27,3 +111,134 @@ def test_v1_models_carries_deployment_model_info_limits_for_an_unknown_model(gat
|
|||
listed: Final = _listed_model(gateway, model)
|
||||
assert listed["max_input_tokens"] == 4321, listed
|
||||
assert listed["max_output_tokens"] == 987, listed
|
||||
|
||||
|
||||
def test_numeric_string_token_limit_is_coerced_to_an_int(gateway: Gateway) -> None:
|
||||
with gateway.scenario() as scenario:
|
||||
model: Final = scenario.model(
|
||||
model=f"openai/custom-{uuid.uuid4().hex}",
|
||||
model_info={"max_input_tokens": "4096", "max_output_tokens": "512"},
|
||||
)
|
||||
assert _limits(_listed_model(gateway, model)) == (4096, 512)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", NON_NUMERIC_LIMITS)
|
||||
def test_non_numeric_token_limit_is_listed_as_absent_without_breaking_the_listing(
|
||||
gateway: Gateway, value: JsonValue
|
||||
) -> None:
|
||||
with gateway.scenario() as scenario:
|
||||
sibling: Final = scenario.model(model=f"openai/custom-{uuid.uuid4().hex}", model_info=SIBLING_LIMITS)
|
||||
broken: Final = scenario.model(
|
||||
model=f"openai/custom-{uuid.uuid4().hex}",
|
||||
model_info={"max_input_tokens": value, "max_output_tokens": value},
|
||||
)
|
||||
_assert_listing_spares_the_sibling(gateway, broken, sibling, (None, None))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", NON_NUMERIC_LIMITS)
|
||||
def test_non_numeric_token_limit_still_serves_chat(
|
||||
gateway: Gateway, value: JsonValue, request: pytest.FixtureRequest
|
||||
) -> None:
|
||||
if request.node.callspec.id in CHAT_500_IDS:
|
||||
pytest.skip(CHAT_500)
|
||||
with gateway.scenario() as scenario:
|
||||
sibling: Final = scenario.model(model=f"openai/custom-{uuid.uuid4().hex}", model_info=SIBLING_LIMITS)
|
||||
broken: Final = scenario.model(
|
||||
model=f"openai/custom-{uuid.uuid4().hex}",
|
||||
model_info={"max_input_tokens": value, "max_output_tokens": value},
|
||||
)
|
||||
_assert_serves_chat(gateway, broken, sibling)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", NUMERIC_EDGE_LIMITS)
|
||||
def test_numeric_edge_token_limit_is_listed_as_its_integer_without_breaking_the_listing(
|
||||
gateway: Gateway, value: JsonValue, request: pytest.FixtureRequest
|
||||
) -> None:
|
||||
expected: Final = NUMERIC_EDGE_EXPECTED[request.node.callspec.id]
|
||||
with gateway.scenario() as scenario:
|
||||
sibling: Final = scenario.model(model=f"openai/custom-{uuid.uuid4().hex}", model_info=SIBLING_LIMITS)
|
||||
broken: Final = scenario.model(
|
||||
model=f"openai/custom-{uuid.uuid4().hex}",
|
||||
model_info={"max_input_tokens": value, "max_output_tokens": value},
|
||||
)
|
||||
_assert_listing_spares_the_sibling(gateway, broken, sibling, (expected, expected))
|
||||
_assert_serves_chat(gateway, broken, sibling)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ("max_input_tokens", "max_output_tokens"))
|
||||
def test_one_malformed_limit_does_not_disturb_the_other(gateway: Gateway, field: str) -> None:
|
||||
other: Final = "max_output_tokens" if field == "max_input_tokens" else "max_input_tokens"
|
||||
with gateway.scenario() as scenario:
|
||||
model: Final = scenario.model(
|
||||
model=f"openai/custom-{uuid.uuid4().hex}", model_info={field: "128,000", other: 2048}
|
||||
)
|
||||
listed: Final = _listed_model(gateway, model)
|
||||
assert listed.get(field) is None, listed
|
||||
assert listed[other] == 2048, listed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", NON_NUMERIC_LIMITS + NUMERIC_EDGE_LIMITS)
|
||||
def test_malformed_token_limit_keeps_model_group_info_serving(
|
||||
gateway: Gateway, value: JsonValue, request: pytest.FixtureRequest
|
||||
) -> None:
|
||||
if request.node.callspec.id in MODEL_GROUP_INFO_500_IDS:
|
||||
pytest.skip(MODEL_GROUP_INFO_500)
|
||||
with gateway.scenario() as scenario:
|
||||
sibling: Final = scenario.model(model=f"openai/custom-{uuid.uuid4().hex}", model_info=SIBLING_LIMITS)
|
||||
broken: Final = scenario.model(
|
||||
model=f"openai/custom-{uuid.uuid4().hex}",
|
||||
model_info={"max_input_tokens": value, "max_output_tokens": value},
|
||||
)
|
||||
groups: Final = gateway.get("/model_group/info")["data"]
|
||||
assert isinstance(groups, list)
|
||||
assert {broken, sibling} <= {str(object_value(group)["model_group"]) for group in groups}
|
||||
single: Final = gateway.get("/model_group/info", {"model_group": broken})["data"]
|
||||
assert isinstance(single, list)
|
||||
assert [object_value(group)["model_group"] for group in single] == [broken]
|
||||
|
||||
|
||||
def _yaml_deployment(name: str, upstream_url: str, model_info: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
|
||||
return {
|
||||
"model_name": name,
|
||||
"litellm_params": {
|
||||
"model": f"openai/custom-{uuid.uuid4().hex}",
|
||||
"api_base": f"{upstream_url}/v1",
|
||||
"api_key": "integration-provider-key",
|
||||
},
|
||||
"model_info": dict(model_info),
|
||||
}
|
||||
|
||||
|
||||
def test_non_numeric_token_limits_in_config_yaml_are_listed_as_absent(gateway: Gateway, tmp_path: Path) -> None:
|
||||
run: Final = uuid.uuid4().hex
|
||||
sibling: Final = f"integration-yaml-sibling-{run}"
|
||||
broken: Final = {f"integration-yaml-{parameter.id}-{run}": parameter.values[0] for parameter in NON_NUMERIC_LIMITS}
|
||||
serving: Final = tuple(
|
||||
f"integration-yaml-{parameter.id}-{run}" for parameter in NON_NUMERIC_LIMITS if parameter.id not in CHAT_500_IDS
|
||||
)
|
||||
config: Final = tmp_path / "malformed_token_limits.yaml"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"model_list": [
|
||||
_yaml_deployment(sibling, gateway.upstream_url, SIBLING_LIMITS),
|
||||
*(
|
||||
_yaml_deployment(
|
||||
name, gateway.upstream_url, {"max_input_tokens": value, "max_output_tokens": value}
|
||||
)
|
||||
for name, value in broken.items()
|
||||
),
|
||||
],
|
||||
"general_settings": {
|
||||
"master_key": "os.environ/LITELLM_MASTER_KEY",
|
||||
"database_url": "os.environ/DATABASE_URL",
|
||||
"store_model_in_db": True,
|
||||
},
|
||||
"router_settings": {"disable_cooldowns": True},
|
||||
}
|
||||
)
|
||||
)
|
||||
with owned_proxy(gateway, tmp_path, {}, config=config) as candidate:
|
||||
for name in broken:
|
||||
_assert_listing_spares_the_sibling(candidate, name, sibling, (None, None))
|
||||
_assert_serves_chat(candidate, sibling, *serving)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue