Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion litellm/proxy/response_api_endpoints/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,15 @@ async def cursor_chat_completions(
# Convert 'messages' to 'input' for Responses API compatibility
# Cursor sends 'messages' but Responses API expects 'input'
if "messages" in data and "input" not in data:
data["input"] = data.pop("messages")
(
input_items,
instructions,
) = responses_api_bridge.transformation_handler.convert_chat_completion_messages_to_responses_api(
data.pop("messages")
)
Comment on lines +343 to +345

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Assistant text is dropped

When an assistant message contains both nonempty content and tool calls, the converter emits only the function-call items and skips the content branch, causing the assistant's text to disappear from subsequent conversation history.

data["input"] = input_items
if instructions and not data.get("instructions"):
data["instructions"] = instructions
Comment on lines +347 to +348

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 System instructions are discarded

When a request contains both a system message and a nonempty instructions field, conversion removes the system message from input but this guard prevents adding its text to instructions, causing that part of the prompt to be silently omitted.


processor = ProxyBaseLLMRequestProcessing(data=data)

Expand Down
141 changes: 141 additions & 0 deletions tests/test_litellm/proxy/response_api_endpoints/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,3 +711,144 @@ def test_unresolvable_connection_model_still_drops_cross_provider(self):
call_kwargs: dict = {}
handler._inject_credentials(call_kwargs, model="vertex_ai/gemini-2.0-flash")
assert "custom_llm_provider" not in call_kwargs


class TestCursorChatCompletionsInputTransformation:
"""
/cursor/chat/completions receives Chat Completions shaped messages; they must be
translated into Responses API input items before hitting the responses code path
"""

def _post(self, payload: dict) -> dict:
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.response_api_endpoints import endpoints as cursor_endpoints

captured: dict = {}

class FakeProcessor:
def __init__(self, data):
captured["data"] = data
self.data = data

async def base_process_llm_request(self, **kwargs):
from litellm.types.llms.openai import (
ResponseAPIUsage,
ResponsesAPIResponse,
)

return ResponsesAPIResponse(
id="resp_1",
created_at=0,
model="gpt-4o",
object="response",
output=[
{
"type": "message",
"id": "msg_1",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "ok",
"annotations": [],
}
],
}
],
parallel_tool_calls=False,
tool_choice="auto",
tools=[],
status="completed",
usage=ResponseAPIUsage(
input_tokens=0, output_tokens=0, total_tokens=0
),
)

app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
token="hashed", user_id="u"
)
try:
with patch.object(
cursor_endpoints, "ProxyBaseLLMRequestProcessing", FakeProcessor
):
TestClient(app).post(
"/cursor/chat/completions",
json=payload,
headers={"Authorization": "Bearer sk-1234"},
)
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)

return captured["data"]

def test_image_url_block_is_converted_to_input_image(self):
data = self._post(
{
"model": "gpt-4o",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "what is this?"},
{
"type": "image_url",
"image_url": {
"url": "https://example.com/cat.png",
"detail": "low",
},
},
],
}
],
}
)

assert "messages" not in data
assert data["input"] == [
{
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "what is this?"},
{
"type": "input_image",
"image_url": "https://example.com/cat.png",
"detail": "low",
},
],
}
]

def test_system_message_becomes_instructions(self):
data = self._post(
{
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "be terse"},
{"role": "user", "content": "hi"},
],
}
)

assert data["instructions"] == "be terse"
assert data["input"] == [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hi"}],
}
]

def test_responses_api_input_is_left_untouched(self):
payload_input = [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hi"}],
}
]
data = self._post({"model": "gpt-4o", "input": payload_input})

assert data["input"] == payload_input
Loading