mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(fireworks_ai): fold instructions and developer items into one leading system message on the Responses path
This commit is contained in:
parent
82e6b84f5a
commit
deaadc21d3
2 changed files with 100 additions and 14 deletions
|
|
@ -5,6 +5,7 @@ from urllib.parse import unquote
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from openai.types.responses import EasyInputMessageParam, ResponseInputItemParam
|
from openai.types.responses import EasyInputMessageParam, ResponseInputItemParam
|
||||||
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
from litellm.llms.fireworks_ai.common_utils import (
|
from litellm.llms.fireworks_ai.common_utils import (
|
||||||
resolve_fireworks_api_key,
|
resolve_fireworks_api_key,
|
||||||
|
|
@ -31,16 +32,32 @@ def _session_params(litellm_params: GenericLiteLLMParams) -> Mapping[str, object
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _developer_item_as_system(item: ResponseInputItemParam) -> ResponseInputItemParam:
|
_instructions_adapter: Final = TypeAdapter[str | None](str | None)
|
||||||
if "role" not in item or item["role"] != "developer":
|
|
||||||
return item
|
|
||||||
return EasyInputMessageParam(role="system", content=item["content"], type="message")
|
|
||||||
|
|
||||||
|
|
||||||
def _developer_items_as_system(input: str | ResponseInputParam) -> str | ResponseInputParam:
|
def _instruction_text(item: ResponseInputItemParam) -> str | None:
|
||||||
if isinstance(input, str):
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
return input
|
return input
|
||||||
return [_developer_item_as_system(item) for item in input]
|
leading: Final = EasyInputMessageParam(role="system", content="\n\n".join(instruction_texts), 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)
|
||||||
|
)
|
||||||
|
return [leading, *rest]
|
||||||
|
|
||||||
|
|
||||||
class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||||
|
|
@ -68,9 +85,6 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||||
base: Final = (api_base or get_secret_str("FIREWORKS_API_BASE") or FIREWORKS_AI_DEFAULT_API_BASE).rstrip("/")
|
base: Final = (api_base or get_secret_str("FIREWORKS_API_BASE") or FIREWORKS_AI_DEFAULT_API_BASE).rstrip("/")
|
||||||
return f"{base}/responses"
|
return f"{base}/responses"
|
||||||
|
|
||||||
def _validate_input_param(self, input: str | ResponseInputParam) -> str | ResponseInputParam:
|
|
||||||
return _developer_items_as_system(super()._validate_input_param(input))
|
|
||||||
|
|
||||||
def transform_responses_api_request(
|
def transform_responses_api_request(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
|
|
@ -79,10 +93,16 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||||
litellm_params: GenericLiteLLMParams,
|
litellm_params: GenericLiteLLMParams,
|
||||||
headers: dict, # mutable-ok: overrides the base class signature
|
headers: dict, # mutable-ok: overrides the base class signature
|
||||||
) -> dict: # mutable-ok: overrides the base class signature
|
) -> dict: # mutable-ok: overrides the base class signature
|
||||||
|
instructions: Final = _instructions_adapter.validate_python(
|
||||||
|
response_api_optional_request_params.get("instructions")
|
||||||
|
)
|
||||||
|
folded_params: Final = { # mutable-ok: the base class takes the optional params as a dict
|
||||||
|
key: value for key, value in response_api_optional_request_params.items() if key != "instructions"
|
||||||
|
}
|
||||||
return super().transform_responses_api_request(
|
return super().transform_responses_api_request(
|
||||||
model=resolve_fireworks_resource_name(model),
|
model=resolve_fireworks_resource_name(model),
|
||||||
input=input,
|
input=_with_single_leading_system_item(self._validate_input_param(input), instructions),
|
||||||
response_api_optional_request_params=response_api_optional_request_params,
|
response_api_optional_request_params=folded_params,
|
||||||
litellm_params=litellm_params,
|
litellm_params=litellm_params,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ def test_responses_call_forwards_previous_response_id_and_store() -> None:
|
||||||
assert body["input"][0]["call_id"] == "call_abc123"
|
assert body["input"][0]["call_id"] == "call_abc123"
|
||||||
|
|
||||||
|
|
||||||
def test_responses_call_sends_developer_items_as_system_messages() -> None:
|
def test_responses_call_hoists_developer_items_into_one_leading_system_message() -> None:
|
||||||
client: Final = _mock_http_client(_fireworks_response("accounts/fireworks/models/kimi-k3"))
|
client: Final = _mock_http_client(_fireworks_response("accounts/fireworks/models/kimi-k3"))
|
||||||
with patch(HTTPX_CLIENT_FACTORY, return_value=client):
|
with patch(HTTPX_CLIENT_FACTORY, return_value=client):
|
||||||
litellm.responses(
|
litellm.responses(
|
||||||
|
|
@ -176,12 +176,78 @@ def test_responses_call_sends_developer_items_as_system_messages() -> None:
|
||||||
)
|
)
|
||||||
_, _, body = _sent_request(client)
|
_, _, body = _sent_request(client)
|
||||||
assert tuple(body["input"]) == (
|
assert tuple(body["input"]) == (
|
||||||
{"role": "user", "content": "Hi there"},
|
|
||||||
{"role": "system", "content": "Answer with exactly one word.", "type": "message"},
|
{"role": "system", "content": "Answer with exactly one word.", "type": "message"},
|
||||||
|
{"role": "user", "content": "Hi there"},
|
||||||
{"role": "user", "content": [{"type": "input_text", "text": "What is the capital of France?"}]},
|
{"role": "user", "content": [{"type": "input_text", "text": "What is the capital of France?"}]},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_responses_call_folds_instructions_and_developer_item_into_one_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="You are a coding agent running in the Codex CLI.",
|
||||||
|
input=[ # mutable-ok: the Responses API takes input as a JSON list
|
||||||
|
{
|
||||||
|
"role": "developer",
|
||||||
|
"content": [{"type": "input_text", "text": "<permissions instructions>read-only</permissions instructions>"}],
|
||||||
|
},
|
||||||
|
{"role": "user", "content": [{"type": "input_text", "text": "What is the capital of France?"}]},
|
||||||
|
{"id": "rs_1", "type": "reasoning", "summary": [{"type": "summary_text", "text": "A trivial question."}]},
|
||||||
|
{
|
||||||
|
"id": "msg_1",
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "completed",
|
||||||
|
"content": [{"type": "output_text", "text": "Paris is the capital of France.", "annotations": []}],
|
||||||
|
},
|
||||||
|
{"role": "user", "content": [{"type": "input_text", "text": "And of Spain?"}]},
|
||||||
|
],
|
||||||
|
store=False,
|
||||||
|
api_key="fw-test-key",
|
||||||
|
)
|
||||||
|
_, _, body = _sent_request(client)
|
||||||
|
assert "instructions" not in body
|
||||||
|
assert tuple(body["input"]) == (
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": (
|
||||||
|
"You are a coding agent running in the Codex CLI.\n\n"
|
||||||
|
"<permissions instructions>read-only</permissions instructions>"
|
||||||
|
),
|
||||||
|
"type": "message",
|
||||||
|
},
|
||||||
|
{"role": "user", "content": [{"type": "input_text", "text": "What is the capital of France?"}]},
|
||||||
|
{"id": "rs_1", "type": "reasoning", "summary": [{"type": "summary_text", "text": "A trivial question."}]},
|
||||||
|
{
|
||||||
|
"id": "msg_1",
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "completed",
|
||||||
|
"content": [{"type": "output_text", "text": "Paris is the capital of France.", "annotations": []}],
|
||||||
|
},
|
||||||
|
{"role": "user", "content": [{"type": "input_text", "text": "And of Spain?"}]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
litellm.responses(
|
||||||
|
model="fireworks_ai/accounts/fireworks/models/kimi-k3",
|
||||||
|
instructions="Answer with exactly one word.",
|
||||||
|
input="What is the capital of France?",
|
||||||
|
api_key="fw-test-key",
|
||||||
|
)
|
||||||
|
_, _, body = _sent_request(client)
|
||||||
|
assert "instructions" not in body
|
||||||
|
assert tuple(body["input"]) == (
|
||||||
|
{"role": "system", "content": "Answer with exactly one word.", "type": "message"},
|
||||||
|
{"role": "user", "content": "What is the capital of France?"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_responses_call_maps_pydantic_developer_items_and_replays_pydantic_output_items() -> None:
|
def test_responses_call_maps_pydantic_developer_items_and_replays_pydantic_output_items() -> None:
|
||||||
client: Final = _mock_http_client(_fireworks_response("accounts/fireworks/models/kimi-k3"))
|
client: Final = _mock_http_client(_fireworks_response("accounts/fireworks/models/kimi-k3"))
|
||||||
pydantic_input: Final = cast(
|
pydantic_input: Final = cast(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue