mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(proxy): seed litellm_call_id into request data before parsing can fail
The failure hook received data without the resolved id when body parsing or add_litellm_data_to_request raised, so proxy-only spend logging minted a fresh id that did not match the error log or the x-litellm-call-id header. The id is now part of the request data from the start and merged over the parsed body, which also removes the post-hoc in-place assignment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
99aa9f76c8
commit
b96a804004
6 changed files with 92 additions and 20 deletions
|
|
@ -94,12 +94,12 @@ async def image_generation(
|
|||
version,
|
||||
)
|
||||
|
||||
data = {}
|
||||
litellm_call_id: Final = resolve_litellm_call_id(request.headers.get("x-litellm-call-id"))
|
||||
data = {"litellm_call_id": litellm_call_id}
|
||||
try:
|
||||
# Use orjson to parse JSON data, orjson speeds up requests significantly
|
||||
body: Final = await request.body()
|
||||
data = orjson.loads(body)
|
||||
data = orjson.loads(body) | data
|
||||
|
||||
# Include original request and headers in the data
|
||||
data = await add_litellm_data_to_request(
|
||||
|
|
@ -110,7 +110,6 @@ async def image_generation(
|
|||
version=version,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
data["litellm_call_id"] = litellm_call_id
|
||||
|
||||
if isinstance(model, str):
|
||||
reject_url_valued_destination("model", model)
|
||||
|
|
|
|||
|
|
@ -199,15 +199,15 @@ async def chat_completion_pass_through_endpoint(
|
|||
version,
|
||||
)
|
||||
|
||||
data = {}
|
||||
litellm_call_id: Final = resolve_litellm_call_id(request.headers.get("x-litellm-call-id"))
|
||||
data = {"litellm_call_id": litellm_call_id}
|
||||
try:
|
||||
body: Final = await request.body()
|
||||
body_str: Final = body.decode()
|
||||
try:
|
||||
data = ast.literal_eval(body_str)
|
||||
data = ast.literal_eval(body_str) | data
|
||||
except Exception:
|
||||
data = json.loads(body_str)
|
||||
data = json.loads(body_str) | data
|
||||
|
||||
data["adapter_id"] = adapter_id
|
||||
|
||||
|
|
@ -228,7 +228,6 @@ async def chat_completion_pass_through_endpoint(
|
|||
version=version,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
data["litellm_call_id"] = litellm_call_id
|
||||
|
||||
# override with user settings, these are params passed via cli
|
||||
if user_temperature:
|
||||
|
|
|
|||
|
|
@ -11471,12 +11471,12 @@ async def moderations(
|
|||
```
|
||||
"""
|
||||
global proxy_logging_obj
|
||||
data: dict = {}
|
||||
litellm_call_id: Final = resolve_litellm_call_id(request.headers.get("x-litellm-call-id"))
|
||||
data: dict = {"litellm_call_id": litellm_call_id}
|
||||
try:
|
||||
# Use orjson to parse JSON data, orjson speeds up requests significantly
|
||||
body: Final = await request.body()
|
||||
data = orjson.loads(body)
|
||||
data = orjson.loads(body) | data
|
||||
|
||||
# Include original request and headers in the data
|
||||
data = await add_litellm_data_to_request(
|
||||
|
|
@ -11487,7 +11487,6 @@ async def moderations(
|
|||
version=version,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
data["litellm_call_id"] = litellm_call_id
|
||||
|
||||
data["model"] = (
|
||||
general_settings.get("moderation_model", None) # server default
|
||||
|
|
@ -11595,12 +11594,12 @@ async def audio_speech(
|
|||
https://platform.openai.com/docs/api-reference/audio/createSpeech
|
||||
"""
|
||||
global proxy_logging_obj
|
||||
data: dict = {}
|
||||
litellm_call_id: Final = resolve_litellm_call_id(request.headers.get("x-litellm-call-id"))
|
||||
data: dict = {"litellm_call_id": litellm_call_id}
|
||||
try:
|
||||
# Use orjson to parse JSON data, orjson speeds up requests significantly
|
||||
body: Final = await request.body()
|
||||
data = orjson.loads(body)
|
||||
data = orjson.loads(body) | data
|
||||
|
||||
# Include original request and headers in the data
|
||||
data = await add_litellm_data_to_request(
|
||||
|
|
@ -11611,7 +11610,6 @@ async def audio_speech(
|
|||
version=version,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
data["litellm_call_id"] = litellm_call_id
|
||||
|
||||
if data.get("user", None) is None and user_api_key_dict.user_id is not None:
|
||||
data["user"] = user_api_key_dict.user_id
|
||||
|
|
@ -11730,12 +11728,12 @@ async def audio_transcriptions(
|
|||
https://platform.openai.com/docs/api-reference/audio/createTranscription?lang=curl
|
||||
"""
|
||||
global proxy_logging_obj
|
||||
data: dict = {}
|
||||
litellm_call_id: Final = resolve_litellm_call_id(request.headers.get("x-litellm-call-id"))
|
||||
data: dict = {"litellm_call_id": litellm_call_id}
|
||||
try:
|
||||
# Use orjson to parse JSON data, orjson speeds up requests significantly
|
||||
form_data: Final = await get_form_data(request)
|
||||
data = {key: value for key, value in form_data.items() if key != "file"}
|
||||
data = {key: value for key, value in form_data.items() if key != "file"} | data
|
||||
|
||||
# Include original request and headers in the data
|
||||
data = await add_litellm_data_to_request(
|
||||
|
|
@ -11746,7 +11744,6 @@ async def audio_transcriptions(
|
|||
version=version,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
data["litellm_call_id"] = litellm_call_id
|
||||
|
||||
if data.get("user", None) is None and user_api_key_dict.user_id is not None:
|
||||
data["user"] = user_api_key_dict.user_id
|
||||
|
|
|
|||
|
|
@ -58,11 +58,11 @@ async def rerank(
|
|||
version,
|
||||
)
|
||||
|
||||
data = {}
|
||||
litellm_call_id: Final = resolve_litellm_call_id(request.headers.get("x-litellm-call-id"))
|
||||
data = {"litellm_call_id": litellm_call_id}
|
||||
try:
|
||||
body: Final = await request.body()
|
||||
data = orjson.loads(body)
|
||||
data = orjson.loads(body) | data
|
||||
|
||||
# Include original request and headers in the data
|
||||
data = await add_litellm_data_to_request(
|
||||
|
|
@ -73,7 +73,6 @@ async def rerank(
|
|||
version=version,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
data["litellm_call_id"] = litellm_call_id
|
||||
|
||||
### CALL HOOKS ### - modify incoming data / reject request before calling the model
|
||||
data = await proxy_logging_obj.pre_call_hook(user_api_key_dict=user_api_key_dict, data=data, call_type="rerank")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
import copy
|
||||
import logging
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Iterator, Mapping
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict
|
||||
|
||||
|
|
@ -279,3 +279,52 @@ async def test_failure_log_carries_the_callers_litellm_call_id(
|
|||
record = next(r for r in caplog.records if "Exception occured" in r.getMessage())
|
||||
assert record.litellm_call_id == call_id
|
||||
assert call_id in record.getMessage()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failure_before_the_provider_call_bills_the_callers_litellm_call_id(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""LIT-7836: when the request is rejected while it is still being prepared, the
|
||||
failure hook must see the same litellm_call_id the response header answers with,
|
||||
otherwise the spend row is stored under a freshly minted id nobody can look up."""
|
||||
call_id = "images-early-7836"
|
||||
hook_request_data: list[Mapping[str, object]] = []
|
||||
|
||||
async def rejecting_add_litellm_data_to_request(**_: object) -> object:
|
||||
raise HTTPException(status_code=400, detail={"error": "tag not allowed"})
|
||||
|
||||
async def fake_post_call_failure_hook(*, request_data: Mapping[str, object], **_: object) -> None:
|
||||
hook_request_data.append(request_data)
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.add_litellm_data_to_request", rejecting_add_litellm_data_to_request)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_config", {})
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.proxy_logging_obj",
|
||||
SimpleNamespace(post_call_failure_hook=fake_post_call_failure_hook),
|
||||
)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.version", "test-version")
|
||||
|
||||
body = orjson.dumps({"model": "dall-e-3", "prompt": "a lighthouse at dusk", "litellm_call_id": "from-the-body"})
|
||||
|
||||
async def receive() -> dict[str, object]:
|
||||
return {"type": "http.request", "body": body, "more_body": False}
|
||||
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/v1/images/generations",
|
||||
"headers": [(b"x-litellm-call-id", call_id.encode())],
|
||||
},
|
||||
receive,
|
||||
)
|
||||
|
||||
with pytest.raises(ProxyException) as raised:
|
||||
await endpoints.image_generation(request=request, fastapi_response=Response(), user_api_key_dict=UserAPIKeyAuth())
|
||||
|
||||
assert raised.value.headers["x-litellm-call-id"] == call_id
|
||||
assert [data["litellm_call_id"] for data in hook_request_data] == [call_id]
|
||||
|
|
|
|||
|
|
@ -13046,6 +13046,35 @@ async def test_moderations_failure_log_carries_the_callers_litellm_call_id(caplo
|
|||
assert call_id in record.getMessage()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_moderations_unparseable_body_bills_the_callers_litellm_call_id():
|
||||
"""LIT-7836: a body that fails to parse must still hand the failure hook the
|
||||
litellm_call_id the response header answers with, so the spend row is findable."""
|
||||
from litellm.proxy._types import ProxyException
|
||||
|
||||
call_id = "moderations-early-7836"
|
||||
|
||||
request = MagicMock()
|
||||
request.headers = {"x-litellm-call-id": call_id}
|
||||
request.body = AsyncMock(return_value=b'{"input": ')
|
||||
fake_logging = MagicMock()
|
||||
fake_logging.post_call_failure_hook = AsyncMock()
|
||||
|
||||
with (
|
||||
patch.object(proxy_server_module, "proxy_logging_obj", new=fake_logging), # test-quality-ok: module global, no injection point
|
||||
pytest.raises(ProxyException) as raised,
|
||||
):
|
||||
await proxy_server_module.moderations(
|
||||
request=request,
|
||||
fastapi_response=MagicMock(),
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", spend=0.0),
|
||||
)
|
||||
|
||||
assert raised.value.headers["x-litellm-call-id"] == call_id
|
||||
hook_request_data = fake_logging.post_call_failure_hook.await_args.kwargs["request_data"]
|
||||
assert hook_request_data["litellm_call_id"] == call_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_moderations_already_shaped_failure_answers_with_the_callers_litellm_call_id():
|
||||
"""LIT-7836: a ProxyException raised inside /v1/moderations is re-raised unwrapped but still
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue