diff --git a/litellm/llms/fireworks_ai/responses/transformation.py b/litellm/llms/fireworks_ai/responses/transformation.py index 61ef0731900..660a07181ff 100644 --- a/litellm/llms/fireworks_ai/responses/transformation.py +++ b/litellm/llms/fireworks_ai/responses/transformation.py @@ -4,7 +4,12 @@ from typing import TYPE_CHECKING, Final from urllib.parse import unquote import httpx -from openai.types.responses import EasyInputMessageParam, ResponseInputItemParam +from openai.types.responses import ( + EasyInputMessageParam, + ResponseInputContentParam, + ResponseInputItemParam, + ResponseInputTextParam, +) from pydantic import TypeAdapter from litellm.llms.fireworks_ai.common_utils import ( @@ -35,27 +40,42 @@ def _session_params(litellm_params: GenericLiteLLMParams) -> Mapping[str, object _instructions_adapter: Final = TypeAdapter[str | None](str | None) -def _instruction_text(item: ResponseInputItemParam) -> str | None: +def _instruction_parts(item: ResponseInputItemParam) -> tuple[ResponseInputContentParam, ...] | None: if "role" not in item or (item["role"] != "system" and item["role"] != "developer"): return None content: Final = item["content"] if isinstance(content, str): - return content - return "\n\n".join(part["text"] for part in content if part["type"] == "input_text") + return (ResponseInputTextParam(type="input_text", text=content),) + return tuple(content) + + +def _leading_system_content( + instructions: str | None, parts: tuple[ResponseInputContentParam, ...] +) -> str | list[ResponseInputContentParam]: + text: Final = "\n\n".join( + chunk for chunk in (instructions, *(part["text"] for part in parts if part["type"] == "input_text")) if chunk + ) + non_text: Final = tuple(part for part in parts if part["type"] != "input_text") + if not non_text: + return text + return [ResponseInputTextParam(type="input_text", text=text), *non_text] if text else list(non_text) def _with_single_leading_system_item( input: str | ResponseInputParam, instructions: str | None ) -> str | ResponseInputParam: items: Final = () if isinstance(input, str) else tuple(input) - instruction_texts: Final = tuple(text for text in (instructions, *map(_instruction_text, items)) if text) - if not instruction_texts: + instruction_parts: Final = tuple( + part for item_parts in map(_instruction_parts, items) if item_parts is not None for part in item_parts + ) + content: Final = _leading_system_content(instructions, instruction_parts) + if not content: return input - leading: Final = EasyInputMessageParam(role="system", content="\n\n".join(instruction_texts), type="message") + leading: Final = EasyInputMessageParam(role="system", content=content, type="message") rest: Final = ( (EasyInputMessageParam(role="user", content=input),) if isinstance(input, str) - else tuple(item for item in items if _instruction_text(item) is None) + else tuple(item for item in items if _instruction_parts(item) is None) ) return [leading, *rest] diff --git a/tests/test_litellm/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py b/tests/test_litellm/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py index 3462e041a19..9d947b25d69 100644 --- a/tests/test_litellm/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py +++ b/tests/test_litellm/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py @@ -231,6 +231,40 @@ def test_responses_call_folds_instructions_and_developer_item_into_one_leading_s ) +def test_responses_call_keeps_non_text_developer_parts_on_the_leading_system_message() -> None: + client: Final = _mock_http_client(_fireworks_response("accounts/fireworks/models/qwen3p8-2p4t-a95b")) + with patch(HTTPX_CLIENT_FACTORY, return_value=client): + litellm.responses( + model="fireworks_ai/accounts/fireworks/models/qwen3p8-2p4t-a95b", + instructions="Answer with one word.", + input=[ # mutable-ok: the Responses API takes input as a JSON list + { + "role": "developer", + "content": [ + {"type": "input_text", "text": "Match the style of this reference image."}, + {"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo=", "detail": "auto"}, + ], + }, + {"role": "user", "content": "What is the capital of France?"}, + ], + store=False, + api_key="fw-test-key", + ) + _, _, body = _sent_request(client) + assert "instructions" not in body + assert tuple(body["input"]) == ( + { + "role": "system", + "content": [ + {"type": "input_text", "text": "Answer with one word.\n\nMatch the style of this reference image."}, + {"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo=", "detail": "auto"}, + ], + "type": "message", + }, + {"role": "user", "content": "What is the capital of France?"}, + ) + + def test_responses_call_turns_string_input_with_instructions_into_system_then_user_messages() -> None: client: Final = _mock_http_client(_fireworks_response("accounts/fireworks/models/kimi-k3")) with patch(HTTPX_CLIENT_FACTORY, return_value=client):