mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
Merge branch 'litellm_internal_staging' into pragnyan/fix-xiaomi-output-config
This commit is contained in:
commit
4c7d856864
22 changed files with 556 additions and 105 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue