Merge branch 'litellm_internal_staging' into pragnyan/fix-xiaomi-output-config

This commit is contained in:
Pragnyan Ramtha 2026-05-19 01:07:51 +05:30 • committed by GitHub
commit 4c7d856864
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 556 additions and 105 deletions

View file

@ -22,7 +22,7 @@ class BedrockCohereEmbeddingConfig:
) -> dict:
for k, v in non_default_params.items():
if k == "encoding_format":
optional_params["embedding_types"] = v
optional_params["embedding_types"] = v if isinstance(v, list) else [v]
elif k == "dimensions":
optional_params["output_dimension"] = v
return optional_params

View file

@ -1,6 +1,7 @@
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from fastapi import HTTPException, status
from pydantic import BaseModel
from litellm._logging import verbose_proxy_logger
from litellm.caching import DualCache
@ -53,6 +54,37 @@ def require_caller_user_id_for_non_admin(
return user_api_key_dict.user_id
def _check_passthrough_routes_caller_permission(
data: BaseModel,
user_api_key_dict: UserAPIKeyAuth,
*,
entity: str = "key",
) -> None:
"""
Only proxy admins may set `allowed_passthrough_routes` (top-level or under
`metadata`) — it short-circuits the role-based route gate, so keys and teams
must be gated identically.
"""
# view-only admins excluded by design; blocked upstream from writes anyway
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
return
if getattr(data, "allowed_passthrough_routes", None):
raise HTTPException(
status_code=403,
detail={
"error": f"Only proxy admins can set `allowed_passthrough_routes` on a {entity}."
},
)
metadata = getattr(data, "metadata", None)
if isinstance(metadata, dict) and metadata.get("allowed_passthrough_routes"):
raise HTTPException(
status_code=403,
detail={
"error": f"Only proxy admins can set `metadata.allowed_passthrough_routes` on a {entity}."
},
)
def _is_user_team_admin(
user_api_key_dict: UserAPIKeyAuth, team_obj: LiteLLM_TeamTable
) -> bool:

View file

@ -55,6 +55,7 @@ from litellm.proxy.common_utils.rbac_utils import check_org_admin_can_generate_k
from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks
from litellm.proxy.management_endpoints.common_utils import (
_check_passthrough_routes_caller_permission,
_is_user_org_admin_for_team,
_is_user_team_admin,
_set_object_metadata_field,
@ -548,36 +549,6 @@ def _check_allowed_routes_caller_permission(
)
def _check_passthrough_routes_caller_permission(
data: BaseModel,
user_api_key_dict: UserAPIKeyAuth,
) -> None:
"""
Only proxy admins may set `allowed_passthrough_routes` on a key, either at
the top level of the request or nested under `metadata`.
The route gate evaluates passthrough access ahead of the standard role
gate, so the field is restricted to admins to keep that ordering safe.
"""
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
return
if getattr(data, "allowed_passthrough_routes", None):
raise HTTPException(
status_code=403,
detail={
"error": "Only proxy admins can set `allowed_passthrough_routes` on a key."
},
)
metadata = getattr(data, "metadata", None)
if isinstance(metadata, dict) and metadata.get("allowed_passthrough_routes"):
raise HTTPException(
status_code=403,
detail={
"error": "Only proxy admins can set `metadata.allowed_passthrough_routes` on a key."
},
)
async def validate_team_id_used_in_service_account_request(
team_id: Optional[str],
prisma_client: Optional[PrismaClient],

View file

@ -73,6 +73,7 @@ from litellm.proxy.auth.auth_checks import (
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.management_endpoints.common_utils import (
_check_passthrough_routes_caller_permission,
_is_user_org_admin_for_team,
_is_user_team_admin,
_set_object_metadata_field,
@ -1049,6 +1050,10 @@ async def new_team( # noqa: PLR0915
Member(role="admin", user_id=user_api_key_dict.user_id)
)
_check_passthrough_routes_caller_permission(
data, user_api_key_dict, entity="team"
)
## ADD TO MODEL TABLE
_model_id = None
if data.model_aliases is not None and isinstance(data.model_aliases, dict):
@ -1646,6 +1651,10 @@ async def update_team( # noqa: PLR0915
user_api_key_dict=user_api_key_dict,
)
_check_passthrough_routes_caller_permission(
data, user_api_key_dict, entity="team"
)
if data.soft_budget is not None:
max_budget_to_check = (
data.max_budget

View file

@ -36,6 +36,75 @@ SAFE_BODY_MATCHER_NAME = "safe_body"
KEY_FINGERPRINT_MATCHER_NAME = "key_fingerprint"
KEY_FINGERPRINT_HEADER = "x-litellm-key-fp"
VCR_DIAG_DIR_ENV = "LITELLM_VCR_DIAG_DIR"
VCR_DIAG_DIR_DEFAULT = "test-results/vcr-diagnostics"
def _vcr_diag_dir() -> str:
return os.environ.get(VCR_DIAG_DIR_ENV) or VCR_DIAG_DIR_DEFAULT
def vcr_diag_write_line(msg: str) -> None:
try:
directory = _vcr_diag_dir()
os.makedirs(directory, exist_ok=True)
path = os.path.join(directory, f"{os.getpid()}.log")
with open(path, "a", encoding="utf-8") as fh:
fh.write(msg.rstrip("\n") + "\n")
except OSError:
pass
def reset_vcr_diag_dir() -> None:
if os.environ.get("PYTEST_XDIST_WORKER"):
return
directory = _vcr_diag_dir()
if not os.path.isdir(directory):
return
try:
names = os.listdir(directory)
except OSError:
return
for name in names:
if name.endswith(".log"):
try:
os.remove(os.path.join(directory, name))
except OSError:
pass
def emit_vcr_diagnostic_log(terminalreporter) -> None:
directory = _vcr_diag_dir()
if not os.path.isdir(directory):
return
try:
files = sorted(f for f in os.listdir(directory) if f.endswith(".log"))
except OSError:
return
if not files:
return
terminalreporter.write_sep("=", "VCR DIAGNOSTIC LOG", bold=True)
terminalreporter.write_line(
f" source dir: {directory} (also archived as a CI artifact)"
)
for name in files:
path = os.path.join(directory, name)
try:
with open(path, "r", encoding="utf-8") as fh:
content = fh.read()
except OSError as exc:
terminalreporter.write_line(
f" [failed to read {name}: {type(exc).__name__}: {exc}]"
)
continue
if not content.strip():
continue
terminalreporter.write_sep("-", name, bold=False)
for line in content.splitlines():
terminalreporter.write_line(line)
terminalreporter.write_sep("=", bold=True)
# Intentionally narrower than ``FILTERED_REQUEST_HEADERS``: AWS SigV4 headers
# carry secrets but their values rotate on every call, so fingerprinting them
# would defeat caching.
@ -91,6 +160,32 @@ VCR_IMAGE_B64_PLACEHOLDER = "dGVzdA=="
VCR_FIXED_MULTIPART_BOUNDARY = "vcr-static-boundary"
def pin_httpx_multipart_boundary(monkeypatch) -> None:
try:
import httpx._multipart as _httpx_multipart
except ImportError:
return
_original_init = _httpx_multipart.MultipartStream.__init__
def _init_with_fixed_boundary(self, data, files, boundary=None, **kwargs):
if boundary is None:
boundary = VCR_FIXED_MULTIPART_BOUNDARY.encode("ascii")
return _original_init(self, data=data, files=files, boundary=boundary, **kwargs)
monkeypatch.setattr(
_httpx_multipart.MultipartStream, "__init__", _init_with_fixed_boundary
)
@pytest.fixture(scope="session", autouse=True)
def _pin_multipart_boundary():
monkeypatch = pytest.MonkeyPatch()
pin_httpx_multipart_boundary(monkeypatch)
yield
monkeypatch.undo()
def _scrub_response(response):
if not isinstance(response, dict):
return response
@ -139,9 +234,17 @@ def _strip_image_b64_payloads(response):
preserves all those checks while shrinking cassettes by ~99%.
"""
if not isinstance(response, dict):
vcr_diag_write_line(
f"[vcr-strip-b64] response is {type(response).__name__!r}, not "
"dict; skipping b64 scrub"
)
return response
body = response.get("body")
if not isinstance(body, dict):
vcr_diag_write_line(
f"[vcr-strip-b64] response['body'] is {type(body).__name__!r}, "
"not dict; skipping b64 scrub"
)
return response
raw = body.get("string")
if raw is None:
@ -151,12 +254,20 @@ def _strip_image_b64_payloads(response):
try:
text = bytes(raw).decode("utf-8")
except UnicodeDecodeError:
vcr_diag_write_line(
"[vcr-strip-b64] response body bytes are not valid UTF-8; "
"skipping b64 scrub"
)
return response
was_bytes = True
elif isinstance(raw, str):
text = raw
was_bytes = False
else:
vcr_diag_write_line(
f"[vcr-strip-b64] response['body']['string'] is "
f"{type(raw).__name__!r}, not bytes/str; skipping b64 scrub"
)
return response
try:
@ -186,6 +297,35 @@ def _before_record_response(response):
return filter_non_2xx_response(_scrub_response(_strip_image_b64_payloads(response)))
def _canonical_body(request) -> tuple[bytes, str]:
pre_type = type(getattr(request, "body", None)).__name__
_materialize_iterable_body(request)
body = getattr(request, "body", None)
if body is None:
return b"", pre_type
if isinstance(body, bytes):
return body, pre_type
if isinstance(body, bytearray):
return bytes(body), pre_type
if isinstance(body, str):
return body.encode("utf-8"), pre_type
if isinstance(body, (dict, list)):
try:
return (
json.dumps(body, sort_keys=True, separators=(",", ":")).encode("utf-8"),
pre_type,
)
except (TypeError, ValueError):
pass
method = getattr(request, "method", "?")
uri = getattr(request, "uri", getattr(request, "url", "?"))
vcr_diag_write_line(
f"[vcr-canonical-body] FALLBACK: {method} {uri} body type "
f"{type(body).__name__!r} not coerced to bytes; comparing as b''"
)
return b"", pre_type
def _safe_body_matcher(r1, r2) -> None:
"""Compare request bodies as bytes; never invokes ``json.loads``.
@ -195,27 +335,47 @@ def _safe_body_matcher(r1, r2) -> None:
This matcher is strictly more conservative — the only equivalence
it gives up vs. the default is "JSON key order doesn't matter".
"""
body1 = getattr(r1, "body", None)
body2 = getattr(r2, "body", None)
body1, pre1 = _canonical_body(r1)
body2, pre2 = _canonical_body(r2)
if body1 == body2:
return
def _to_bytes(b):
if b is None:
return b""
if isinstance(b, bytes):
return b
if isinstance(b, str):
return b.encode("utf-8")
return None
n1 = _to_bytes(body1)
n2 = _to_bytes(body2)
if n1 is not None and n2 is not None and n1 == n2:
return
_emit_body_mismatch_diagnostic(r1, r2, body1, body2, pre1, pre2)
raise AssertionError("request bodies differ")
def _emit_body_mismatch_diagnostic(r1, r2, body1, body2, pre1, pre2) -> None:
def _describe(label, asbytes, pre_type):
return (
f" {label}: pre_canonical_type={pre_type!r} length={len(asbytes)} "
f"sha256={hashlib.sha256(asbytes).hexdigest()} "
f"preview={asbytes[:120]!r}"
)
method_a = getattr(r1, "method", "?")
method_b = getattr(r2, "method", "?")
url_a = getattr(r1, "uri", getattr(r1, "url", "?"))
url_b = getattr(r2, "uri", getattr(r2, "url", "?"))
lines = [
"[vcr-safe-body-matcher] request body mismatch",
f" request[a]: {method_a} {url_a}",
f" request[b]: {method_b} {url_b}",
_describe("body[a]", body1, pre1),
_describe("body[b]", body2, pre2),
]
if body1 != body2:
offset = next(
(i for i in range(min(len(body1), len(body2))) if body1[i] != body2[i]),
min(len(body1), len(body2)),
)
start = max(0, offset - 100)
end_a = min(len(body1), offset + 100)
end_b = min(len(body2), offset + 100)
lines.append(f" first divergent byte offset: {offset}")
lines.append(f" window[a] @ {start}..{end_a}: {body1[start:end_a]!r}")
lines.append(f" window[b] @ {start}..{end_b}: {body2[start:end_b]!r}")
vcr_diag_write_line("\n".join(lines))
def _iter_header_values(headers, name: str):
if headers is None:
return
@ -271,6 +431,13 @@ def _compute_key_fingerprint(request) -> str:
stable = _stable_key_value(header_name, text)
parts.append(f"{header_name}={stable}")
if not parts:
method = getattr(request, "method", "?")
uri = getattr(request, "uri", getattr(request, "url", "?"))
vcr_diag_write_line(
f"[vcr-key-fingerprint] no API key header found on {method} "
f"{uri}; falling back to 'no-key'. If this request should have "
"carried auth, something earlier in the pipeline stripped it."
)
return "no-key"
digest = hashlib.sha256("\n".join(parts).encode("utf-8")).hexdigest()
return digest[:16]
@ -360,6 +527,13 @@ def _normalize_multipart_boundary(request) -> None:
elif isinstance(body, str):
new_body = body.replace(current_boundary, VCR_FIXED_MULTIPART_BOUNDARY)
else:
vcr_diag_write_line(
f"[vcr-multipart-normalize] body normalization SKIPPED: "
f"body type {type(body).__name__!r} is not bytes/bytearray/str. "
f"content-type={content_type_value!r}. "
f"Recorded body will retain the random boundary substring "
f"and the safe_body matcher will miss on the next run."
)
return
try:
@ -389,6 +563,7 @@ def _before_record_request(request):
headers = getattr(request, "headers", None)
if headers is None:
return request
_materialize_iterable_body(request)
if not any(_iter_header_values(headers, KEY_FINGERPRINT_HEADER)):
fingerprint = _compute_key_fingerprint(request)
try:
@ -400,6 +575,56 @@ def _before_record_request(request):
return request
def _materialize_iterable_body(request) -> None:
body = getattr(request, "body", None)
if body is None or isinstance(body, (bytes, bytearray, str)):
return
if not hasattr(body, "__next__"):
return
try:
chunks = list(body)
except TypeError:
return
out = _coalesce_chunks_to_bytes(chunks)
if out is None:
method = getattr(request, "method", "?")
uri = getattr(request, "uri", getattr(request, "url", "?"))
first_type = type(chunks[0]).__name__ if chunks else "empty"
vcr_diag_write_line(
f"[vcr-materialize] FALLBACK: {method} {uri} chunk type "
f"{first_type!r} not coerced to bytes; storing b''"
)
out = b""
try:
request.body = out
except (AttributeError, TypeError):
pass
for attr in ("_was_iter", "_was_file"):
try:
setattr(request, attr, False)
except (AttributeError, TypeError):
pass
def _coalesce_chunks_to_bytes(chunks):
if not chunks:
return b""
first = chunks[0]
try:
if isinstance(first, int):
return bytes(chunks)
if isinstance(first, (bytes, bytearray)):
return b"".join(c if isinstance(c, bytes) else bytes(c) for c in chunks)
if isinstance(first, str):
return "".join(chunks).encode("utf-8")
except (TypeError, ValueError):
return None
return None
def _key_fingerprint_matcher(r1, r2) -> None:
def _fp(req):
for value in _iter_header_values(
@ -410,7 +635,17 @@ def _key_fingerprint_matcher(r1, r2) -> None:
return value if isinstance(value, str) else str(value)
return "no-key"
if _fp(r1) != _fp(r2):
fp1, fp2 = _fp(r1), _fp(r2)
if fp1 != fp2:
method_a = getattr(r1, "method", "?")
method_b = getattr(r2, "method", "?")
url_a = getattr(r1, "uri", getattr(r1, "url", "?"))
url_b = getattr(r2, "uri", getattr(r2, "url", "?"))
vcr_diag_write_line(
"[vcr-key-fingerprint-matcher] API key fingerprints differ\n"
f" request[a]: {method_a} {url_a} fingerprint={fp1!r}\n"
f" request[b]: {method_b} {url_b} fingerprint={fp2!r}"
)
raise AssertionError("API key fingerprints differ")

View file

@ -5,14 +5,17 @@ import pytest
sys.path.insert(0, os.path.abspath("../.."))
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -44,6 +47,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -57,3 +61,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -23,12 +23,21 @@ pwd = os.path.dirname(os.path.realpath(__file__))
print(pwd)
file_path = os.path.join(pwd, "gettysburg.wav")
audio_file = open(file_path, "rb")
file2_path = os.path.join(pwd, "eagle.wav")
audio_file2 = open(file2_path, "rb")
with open(file_path, "rb") as _f:
_GETTYSBURG_BYTES = _f.read()
with open(file2_path, "rb") as _f:
_EAGLE_BYTES = _f.read()
def _audio_file():
return ("gettysburg.wav", _GETTYSBURG_BYTES, "audio/wav")
def _audio_file2():
return ("eagle.wav", _EAGLE_BYTES, "audio/wav")
load_dotenv()
@ -44,7 +53,7 @@ async def _run_transcription(
):
transcript = await litellm.atranscription(
model=model,
file=audio_file,
file=_audio_file(),
api_key=api_key,
api_base=api_base,
response_format=response_format,
@ -101,7 +110,7 @@ async def test_transcription_caching():
response_1 = await litellm.atranscription(
model="whisper-1",
file=audio_file,
file=_audio_file(),
)
await asyncio.sleep(5)
@ -110,7 +119,7 @@ async def test_transcription_caching():
response_2 = await litellm.atranscription(
model="whisper-1",
file=audio_file,
file=_audio_file(),
)
print("response_1", response_1)
@ -122,7 +131,7 @@ async def test_transcription_caching():
response_3 = await litellm.atranscription(
model="whisper-1",
file=audio_file2,
file=_audio_file2(),
)
print("response_3", response_3)
print("response3 hidden params", response_3._hidden_params)
@ -146,7 +155,7 @@ async def test_whisper_log_pre_call():
with patch.object(custom_logger, "log_pre_api_call") as mock_log_pre_call:
await litellm.atranscription(
model="whisper-1",
file=audio_file,
file=_audio_file(),
)
mock_log_pre_call.assert_called_once()
@ -165,7 +174,7 @@ async def test_whisper_log_pre_call():
with patch.object(custom_logger, "log_pre_api_call") as mock_log_pre_call:
await litellm.atranscription(
model="whisper-1",
file=audio_file,
file=_audio_file(),
)
mock_log_pre_call.assert_called_once()
@ -177,7 +186,7 @@ async def test_gpt_4o_transcribe():
from unittest.mock import patch, MagicMock
await litellm.atranscription(
model="openai/gpt-4o-transcribe", file=audio_file, response_format="json"
model="openai/gpt-4o-transcribe", file=_audio_file(), response_format="json"
)
@ -187,7 +196,9 @@ async def test_gpt_4o_transcribe_model_mapping():
# Test GPT-4o mini transcribe
response = await litellm.atranscription(
model="openai/gpt-4o-mini-transcribe", file=audio_file, response_format="json"
model="openai/gpt-4o-mini-transcribe",
file=_audio_file(),
response_format="json",
)
# Check that the response contains the correct model in hidden params
@ -198,7 +209,7 @@ async def test_gpt_4o_transcribe_model_mapping():
# Test GPT-4o transcribe
response2 = await litellm.atranscription(
model="openai/gpt-4o-transcribe", file=audio_file, response_format="json"
model="openai/gpt-4o-transcribe", file=_audio_file(), response_format="json"
)
# Check that the response contains the correct model in hidden params
@ -209,7 +220,7 @@ async def test_gpt_4o_transcribe_model_mapping():
# Test traditional whisper-1 still works
response3 = await litellm.atranscription(
model="openai/whisper-1", file=audio_file, response_format="json"
model="openai/whisper-1", file=_audio_file(), response_format="json"
)
# Check that the response contains the correct model in hidden params
@ -262,7 +273,7 @@ async def test_azure_transcribe_model_mapping():
# Make the transcription call
response = await litellm.atranscription(
model="azure/whisper-1",
file=audio_file,
file=_audio_file(),
response_format="json",
api_key="test-api-key",
api_base="https://my-endpoint-europe-berri-992.openai.azure.com/",

View file

@ -16,14 +16,17 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -55,6 +58,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -160,3 +164,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -9,14 +9,17 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm # noqa: E402,F401
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -58,6 +61,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -71,3 +75,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -103,12 +103,6 @@ class BaseLLMImageEditTest(ABC):
pwd = os.path.dirname(os.path.realpath(__file__))
# Image fixtures must be regenerated per access — module-level
# ``open(...)`` handles get consumed after a single multipart upload, leaving
# subsequent tests in the same process to send empty bodies. That non-determinism
# (a) blows the recorded cassette past ``MAX_EPISODES_PER_CASSETTE`` so the
# persister refuses to save (see ``tests/_vcr_redis_persister.py``), and
# (b) re-bills the live image edit endpoint on every CI run.
def _read_image_bytes(filename: str) -> bytes:
with open(os.path.join(pwd, filename), "rb") as f:
return f.read()
@ -119,32 +113,20 @@ _LITELLM_SITE_BYTES = _read_image_bytes("litellm_site.png")
def _make_test_images() -> list:
"""Return a fresh pair of image streams seeded with the fixture bytes.
return [_ISHAAN_GITHUB_BYTES, _LITELLM_SITE_BYTES]
Use this everywhere you'd previously have used the module-level
``TEST_IMAGES``. Each call returns brand new ``BytesIO`` objects whose
file pointers start at 0, so multipart uploads encode the full image
bytes on every test invocation. Parametrized and ``flaky``-retried
test methods call ``get_base_image_edit_call_args`` once per
invocation, so a fresh stream per call is sufficient — the factory
must not auto-rewind on EOF or the SDK's multipart writer will read
the same bytes forever (worker OOM).
"""
def _make_single_test_image() -> bytes:
return _ISHAAN_GITHUB_BYTES
def get_test_images_as_bytesio():
return [
BytesIO(_ISHAAN_GITHUB_BYTES),
BytesIO(_LITELLM_SITE_BYTES),
]
def _make_single_test_image() -> BytesIO:
return BytesIO(_ISHAAN_GITHUB_BYTES)
def get_test_images_as_bytesio():
"""Helper function to get test images as BytesIO objects"""
return _make_test_images()
class TestOpenAIImageEditGPTImage1(BaseLLMImageEditTest):
"""
Concrete implementation of BaseLLMImageEditTest for OpenAI image edits.
@ -710,10 +692,9 @@ async def test_multiple_image_edit_with_different_formats():
try:
prompt = "Create a cohesive artistic style across all images"
# Test with mixed BytesIO and file objects
mixed_images = [
_make_single_test_image(), # File object
get_test_images_as_bytesio()[1], # BytesIO object
_make_single_test_image(),
get_test_images_as_bytesio()[1],
]
result = await aimage_edit(

View file

@ -12,14 +12,17 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm # noqa: E402,F401
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -86,6 +89,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -116,3 +120,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -13,14 +13,17 @@ sys.path.insert(
import litellm # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -52,6 +55,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -116,3 +120,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -18,14 +18,17 @@ sys.path.insert(
import litellm # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -73,6 +76,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -82,6 +86,7 @@ def pytest_runtest_logreport(report):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)
# ---------------------------------------------------------------------------

View file

@ -22,14 +22,17 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -84,6 +87,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -93,6 +97,7 @@ def pytest_runtest_logreport(report):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)
# ---------------------------------------------------------------------------

View file

@ -19,14 +19,17 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -79,6 +82,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -229,3 +233,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -12,14 +12,17 @@ import pytest
sys.path.insert(0, os.path.abspath("../.."))
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -51,6 +54,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -64,3 +68,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -5,14 +5,17 @@ import pytest
sys.path.insert(0, os.path.abspath("../.."))
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -56,6 +59,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -71,3 +75,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -12,14 +12,17 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm # noqa: E402,F401
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -97,6 +100,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -123,3 +127,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -13,14 +13,17 @@ import pytest
sys.path.insert(0, os.path.abspath("../.."))
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -52,6 +55,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -65,3 +69,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)

View file

@ -957,3 +957,50 @@ def test_titan_image_embedding_cost_uses_per_image_rate():
assert response.usage is not None
assert response.usage.prompt_tokens_details is not None
assert response.usage.prompt_tokens_details.image_count == 1
@pytest.mark.parametrize(
"encoding_format,expected_embedding_types",
[
("float", ["float"]),
("base64", ["base64"]),
(["float", "int8"], ["float", "int8"]),
],
)
def test_bedrock_cohere_embedding_types_wrapped_as_list(
encoding_format, expected_embedding_types
):
"""
Bedrock Cohere expects `embedding_types` as a JSON array, not a raw string.
Regression test for: Bedrock returns
Malformed input request: #/embedding_types: expected type: JSONArray, found: String
when `encoding_format` is passed as a string.
"""
litellm.set_verbose = True
client = HTTPHandler()
model = "bedrock/cohere.embed-multilingual-v3"
with patch.object(client, "post") as mock_post:
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = json.dumps(cohere_embedding_response)
mock_response.json = lambda: json.loads(mock_response.text)
mock_post.return_value = mock_response
response = litellm.embedding(
model=model,
input=test_input,
encoding_format=encoding_format,
client=client,
aws_region_name="us-east-1",
aws_bedrock_runtime_endpoint="https://bedrock-runtime.us-east-1.amazonaws.com",
api_key="test-bearer-token-12345",
)
assert isinstance(response, litellm.EmbeddingResponse)
request_body = json.loads(mock_post.call_args.kwargs.get("data", "{}"))
assert "embedding_types" in request_body
assert request_body["embedding_types"] == expected_embedding_types
assert isinstance(request_body["embedding_types"], list)

View file

@ -7943,3 +7943,103 @@ async def test_team_member_me_returns_404_for_unknown_team(mock_db_client):
user_api_key_dict=caller_auth,
)
assert exc_info.value.status_code == 404
def _non_admin_auth():
return UserAPIKeyAuth(
user_id="u-team-admin", user_role=LitellmUserRoles.INTERNAL_USER
)
def test_check_passthrough_routes_caller_permission_team():
from litellm.proxy._types import NewTeamRequest
from litellm.proxy.management_endpoints.common_utils import (
_check_passthrough_routes_caller_permission,
)
admin = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)
non_admin = _non_admin_auth()
_check_passthrough_routes_caller_permission(
NewTeamRequest(allowed_passthrough_routes=["/foo/*"]), admin, entity="team"
)
_check_passthrough_routes_caller_permission(
NewTeamRequest(), non_admin, entity="team"
)
_check_passthrough_routes_caller_permission(
NewTeamRequest(allowed_passthrough_routes=[]), non_admin, entity="team"
)
with pytest.raises(HTTPException) as exc:
_check_passthrough_routes_caller_permission(
NewTeamRequest(allowed_passthrough_routes=["/admin/*"]),
non_admin,
entity="team",
)
assert exc.value.status_code == 403
assert "allowed_passthrough_routes" in str(exc.value.detail)
assert "team" in str(exc.value.detail)
with pytest.raises(HTTPException) as exc:
_check_passthrough_routes_caller_permission(
NewTeamRequest(metadata={"allowed_passthrough_routes": ["/admin/*"]}),
non_admin,
entity="team",
)
assert exc.value.status_code == 403
assert "metadata.allowed_passthrough_routes" in str(exc.value.detail)
@pytest.mark.asyncio
async def test_new_team_blocks_non_admin_passthrough_routes(mock_db_client):
"""A non-proxy-admin cannot self-grant pass-through routes via /team/new."""
mock_db_client.db.litellm_teamtable.count = AsyncMock(return_value=0)
from fastapi import Request
from litellm.proxy._types import NewTeamRequest, ProxyException
from litellm.proxy.management_endpoints.team_endpoints import new_team
with patch(
"litellm.proxy.management_endpoints.team_endpoints._check_user_team_limits",
AsyncMock(return_value=None),
):
with pytest.raises(ProxyException) as exc:
await new_team(
data=NewTeamRequest(
team_alias="t", allowed_passthrough_routes=["/admin/*"]
),
http_request=MagicMock(spec=Request),
user_api_key_dict=_non_admin_auth(),
)
assert str(exc.value.code) == "403"
assert "allowed_passthrough_routes" in str(exc.value.message)
@pytest.mark.asyncio
async def test_update_team_blocks_non_admin_passthrough_routes(mock_db_client):
"""Even a team manager (non-proxy-admin) cannot set pass-through routes via
/team/update — the gate runs after _verify_team_access."""
from fastapi import Request
from litellm.proxy._types import ProxyException, UpdateTeamRequest
from litellm.proxy.management_endpoints.team_endpoints import update_team
existing = MagicMock()
existing.model_dump.return_value = {"team_id": "t1"}
mock_db_client.db.litellm_teamtable.find_unique = AsyncMock(return_value=existing)
with patch(
"litellm.proxy.management_endpoints.team_endpoints._verify_team_access",
AsyncMock(return_value=None),
):
with pytest.raises(ProxyException) as exc:
await update_team(
data=UpdateTeamRequest(
team_id="t1", allowed_passthrough_routes=["/admin/*"]
),
http_request=MagicMock(spec=Request),
user_api_key_dict=_non_admin_auth(),
)
assert str(exc.value.code) == "403"
assert "allowed_passthrough_routes" in str(exc.value.message)

View file

@ -12,14 +12,17 @@ sys.path.insert(
) # Adds the parent directory to the system path
import litellm # noqa: E402,F401
from tests._vcr_conftest_common import ( # noqa: E402
from tests._vcr_conftest_common import ( # noqa: E402,F401
VerboseReporterState,
_pin_multipart_boundary,
apply_vcr_auto_marker_to_items,
emit_cassette_cache_session_banner,
emit_vcr_classification_summary,
emit_vcr_diagnostic_log,
install_live_call_probe,
record_vcr_outcome,
register_persister_if_enabled,
reset_vcr_diag_dir,
vcr_config_dict,
)
@ -84,6 +87,7 @@ def _vcr_outcome_gate(request, vcr):
def pytest_configure(config):
_verbose_state.remember_pluginmanager(config)
reset_vcr_diag_dir()
def pytest_runtest_logreport(report):
@ -110,3 +114,4 @@ def pytest_collection_modifyitems(config, items):
def pytest_terminal_summary(terminalreporter, exitstatus, config):
emit_cassette_cache_session_banner(terminalreporter)
emit_vcr_classification_summary(terminalreporter)
emit_vcr_diagnostic_log(terminalreporter)