This commit is contained in:
Fazeel Usmani 2026-10-28 14:47:17 +09:00 • committed by GitHub
commit ae121b8719
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 122 additions and 10 deletions

View file

@ -73,6 +73,13 @@ class LiteLLMCompletionTransformationHandler:
**kwargs,
)
if previous_response_client_ids:
raise litellm.BadRequestError(
message="previous_response_id on the chat completions bridge is only supported by litellm.aresponses()",
model=model,
llm_provider=custom_llm_provider or "",
)
completion_args: Final = {}
completion_args.update(kwargs)
completion_args.update(litellm_completion_request)

View file

@ -1320,6 +1320,15 @@ class LiteLLMCompletionResponsesConfig:
_messages: Final = litellm_completion_request.get("messages") or []
route: Final = CompletionRoute(model=model, custom_llm_provider=provider)
session_history: Final = chat_completion_session.get("messages") or []
if previous_response_id and not session_history and not input_replaces_history:
raise litellm.BadRequestError(
message=(
"previous_response_id was not found. The chat completions bridge rebuilds earlier turns from "
"spend logs, so they must be written to a database with store_prompts_in_spend_logs enabled"
),
model=model,
llm_provider=provider or "",
)
stored_messages: Final = LiteLLMCompletionResponsesConfig._with_compaction_blocks_the_route_takes(
_instruction_messages(session_history) if input_replaces_history else session_history,
route,

View file

@ -22,7 +22,7 @@ import httpx
import pytest
import litellm
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.responses.litellm_completion_transformation.handler import (
LiteLLMCompletionTransformationHandler,
)
@ -209,15 +209,19 @@ async def test_bridged_follow_up_turn_keeps_the_addressed_response_id_off_the_pr
client: Final = AsyncHTTPHandler()
client.client = httpx.AsyncClient(transport=httpx.MockTransport(provider))
response = await litellm.aresponses(
model="azure_ai/claude-sonnet-4-6",
api_base="https://fake-foundry-resource.services.ai.azure.com",
api_key="fake-api-key",
input="Double it",
previous_response_id="resp_turn_one",
client=client,
**{ADDRESSED_RESPONSE_ID_FIELD: "resp_turn_one"},
)
with patch(
"litellm.proxy.proxy_server.prisma_client",
SimpleNamespace(db=_StoredTurns((_stored_mcp_turn("resp_turn_one"),))),
):
response = await litellm.aresponses(
model="azure_ai/claude-sonnet-4-6",
api_base="https://fake-foundry-resource.services.ai.azure.com",
api_key="fake-api-key",
input="Double it",
previous_response_id="resp_turn_one",
client=client,
**{ADDRESSED_RESPONSE_ID_FIELD: "resp_turn_one"},
)
assert provider.request_body is not None, "the bridged turn never reached the provider"
assert ADDRESSED_RESPONSE_ID_FIELD not in provider.request_body, (
@ -310,3 +314,94 @@ async def test_bridged_next_turn_replays_a_turn_stored_under_the_id_the_client_h
("assistant", json.dumps([{"type": "text", "text": "Noted."}])),
("user", json.dumps([{"type": "text", "text": "Double it"}])),
]
def _turn_stored_without_prompts(request_id: str) -> Mapping[str, object]:
return {
"request_id": request_id,
"call_type": "aresponses",
"session_id": "turn-one-session",
"proxy_server_request": "{}",
"response": "{}",
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
"prisma_client",
[
pytest.param(None, id="no_database"),
pytest.param(
SimpleNamespace(db=_StoredTurns((_turn_stored_without_prompts("resp_turn_one"),))),
id="prompts_not_stored",
),
],
)
async def test_bridged_follow_up_to_a_response_it_cannot_replay_is_rejected(prisma_client: SimpleNamespace | None):
provider: Final = _RecordingAnthropicHandler(_ANTHROPIC_MESSAGE_PAYLOAD)
client: Final = AsyncHTTPHandler()
client.client = httpx.AsyncClient(transport=httpx.MockTransport(provider))
with (
patch("litellm.proxy.proxy_server.prisma_client", prisma_client),
pytest.raises(litellm.BadRequestError, match="previous_response_id was not found"),
):
await litellm.aresponses(
model="azure_ai/claude-sonnet-4-6",
api_base="https://fake-foundry-resource.services.ai.azure.com",
api_key="fake-api-key",
input="Double it",
previous_response_id="resp_turn_one",
client=client,
)
assert provider.request_body is None
def test_sync_bridged_follow_up_is_rejected_instead_of_dropping_the_history():
provider: Final = _RecordingAnthropicHandler(_ANTHROPIC_MESSAGE_PAYLOAD)
client: Final = HTTPHandler()
client.client = httpx.Client(transport=httpx.MockTransport(provider))
with pytest.raises(litellm.BadRequestError, match=r"only supported by litellm\.aresponses\(\)"):
litellm.responses(
model="azure_ai/claude-sonnet-4-6",
api_base="https://fake-foundry-resource.services.ai.azure.com",
api_key="fake-api-key",
input="Double it",
previous_response_id="resp_turn_one",
client=client,
)
assert provider.request_body is None
@pytest.mark.asyncio
async def test_bridged_compaction_turn_without_stored_history_sends_only_its_own_input():
provider: Final = _RecordingAnthropicHandler(_ANTHROPIC_MESSAGE_PAYLOAD)
client: Final = AsyncHTTPHandler()
client.client = httpx.AsyncClient(transport=httpx.MockTransport(provider))
summary: Final = {"type": "compaction", "content": "My favorite number is 7."}
with patch("litellm.proxy.proxy_server.prisma_client", None):
await litellm.aresponses(
model="azure_ai/claude-sonnet-4-6",
api_base="https://fake-foundry-resource.services.ai.azure.com",
api_key="fake-api-key",
input=[
{"type": "compaction", "id": "cmp_turn_one", "encrypted_content": json.dumps(summary)},
{"role": "user", "content": "Double it"},
],
previous_response_id="resp_turn_one",
client=client,
)
assert provider.request_body is not None, "the compaction turn never reached the provider"
assert [
(message["role"], json.dumps(message["content"])) for message in provider.request_body["messages"]
] == [
(
"user",
json.dumps([{"type": "text", "text": "My favorite number is 7."}, {"type": "text", "text": "Double it"}]),
),
]

View file

@ -6853,6 +6853,7 @@ def test_compaction_replay_invents_no_call_for_an_output_whose_call_was_never_st
litellm_completion_request=chat_request,
instructions=None,
session_lookup=no_stored_turns,
input_replaces_history=True,
)
)