mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-17 04:34:25 +00:00
fix(deepseek): downgrade json_schema response_format to json_object
DeepSeek's /chat/completions rejects response_format={"type":"json_schema"}
with "This response_format type is unavailable now" — no DeepSeek model
supports native Structured Outputs, only JSON mode ({"type":"json_object"}).
DeepSeekChatConfig now downgrades json_schema to json_object and moves the
schema into the prompt via response_schema_prompt (the same helper the Gemini
path uses when it can't enforce a schema natively), so structured-output
requests succeed instead of 400ing.
The downgrade is unconditional rather than gated on supports_response_schema:
the model-cost map marks DeepSeek supports_response_schema=True (#20885) and
downstream consumers filter their model lists on that flag, so it is left
unchanged.
Fixes #7580
Fixes #7646
This commit is contained in:
parent
9e7ba65759
commit
5670c8da6b
2 changed files with 132 additions and 0 deletions
|
|
@ -8,6 +8,7 @@ import litellm
|
|||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
handle_messages_with_content_list_to_str_conversion,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import response_schema_prompt
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
from litellm.utils import supports_reasoning
|
||||
|
|
@ -135,6 +136,59 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
and (optional_params.get("thinking") or {}).get("type") == "enabled"
|
||||
)
|
||||
|
||||
def _downgrade_json_schema_to_json_object(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[AllMessageValues],
|
||||
optional_params: dict,
|
||||
) -> Tuple[List[AllMessageValues], dict]:
|
||||
"""
|
||||
DeepSeek's /chat/completions rejects `response_format={"type": "json_schema"}`
|
||||
with "This response_format type is unavailable now" — no DeepSeek model
|
||||
supports native Structured Outputs, only JSON mode (`{"type": "json_object"}`).
|
||||
See recurring reports #7580 and #7646.
|
||||
|
||||
DeepSeek models are marked `supports_response_schema=True` in the model-cost
|
||||
map (#20885) and downstream consumers filter their model lists on that flag, so
|
||||
flipping it is not an option. Instead, honour the flag: downgrade json_schema to
|
||||
json_object and move the schema into the prompt via `response_schema_prompt`
|
||||
(the same helper the Gemini path uses when it can't enforce a schema natively).
|
||||
The injected message also satisfies DeepSeek's requirement that the word "json"
|
||||
appear in the input when json_object mode is used.
|
||||
|
||||
The downgrade is unconditional (not gated on `supports_response_schema`) because
|
||||
no DeepSeek model supports native json_schema.
|
||||
"""
|
||||
response_format = optional_params.get("response_format")
|
||||
if (
|
||||
not isinstance(response_format, dict)
|
||||
or response_format.get("type") != "json_schema"
|
||||
):
|
||||
return messages, optional_params
|
||||
|
||||
json_schema: Optional[dict] = None
|
||||
nested_schema = response_format.get("json_schema")
|
||||
if isinstance(nested_schema, dict):
|
||||
json_schema = nested_schema.get("schema")
|
||||
elif "response_schema" in response_format:
|
||||
json_schema = response_format["response_schema"]
|
||||
if not json_schema:
|
||||
return messages, optional_params
|
||||
|
||||
messages = messages + [
|
||||
{
|
||||
"role": "user",
|
||||
"content": response_schema_prompt(
|
||||
model=model, response_schema=json_schema
|
||||
),
|
||||
}
|
||||
]
|
||||
optional_params = {
|
||||
**optional_params,
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
return messages, optional_params
|
||||
|
||||
@staticmethod
|
||||
def _drop_unsupported_tools(optional_params: dict) -> dict:
|
||||
"""
|
||||
|
|
@ -222,6 +276,9 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
like deepseek-v3.2 that support thinking as opt-in but not always-on.
|
||||
"""
|
||||
optional_params = self._drop_unsupported_tools(optional_params)
|
||||
messages, optional_params = self._downgrade_json_schema_to_json_object(
|
||||
model=model, messages=messages, optional_params=optional_params
|
||||
)
|
||||
if self._thinking_mode_active(model=model, optional_params=optional_params):
|
||||
messages = self._fill_reasoning_content(messages)
|
||||
return super().transform_request(
|
||||
|
|
@ -245,6 +302,9 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
fix for multi-turn thinking-mode conversations.
|
||||
"""
|
||||
optional_params = self._drop_unsupported_tools(optional_params)
|
||||
messages, optional_params = self._downgrade_json_schema_to_json_object(
|
||||
model=model, messages=messages, optional_params=optional_params
|
||||
)
|
||||
if self._thinking_mode_active(model=model, optional_params=optional_params):
|
||||
messages = self._fill_reasoning_content(messages)
|
||||
return await super().async_transform_request(
|
||||
|
|
|
|||
|
|
@ -101,3 +101,75 @@ async def test_async_transform_request_strips_unsupported_tools_from_body():
|
|||
|
||||
assert [tool["type"] for tool in body["tools"]] == ["function"]
|
||||
assert body["tools"][0]["function"]["name"] == "shell"
|
||||
|
||||
|
||||
_JSON_SCHEMA_RESPONSE_FORMAT = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "answer",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"answer": {"type": "string"}},
|
||||
"required": ["answer"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
"strict": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_downgrade_json_schema_to_json_object():
|
||||
config = DeepSeekChatConfig()
|
||||
messages, optional_params = config._downgrade_json_schema_to_json_object(
|
||||
model="deepseek-chat",
|
||||
messages=[{"role": "user", "content": "Is the sky blue?"}],
|
||||
optional_params={"response_format": dict(_JSON_SCHEMA_RESPONSE_FORMAT)},
|
||||
)
|
||||
|
||||
assert optional_params["response_format"] == {"type": "json_object"}
|
||||
injected = messages[-1]
|
||||
assert injected["role"] == "user"
|
||||
# DeepSeek json_object mode requires the word "json" in the prompt
|
||||
assert "json" in injected["content"].lower()
|
||||
assert "answer" in injected["content"]
|
||||
|
||||
|
||||
def test_downgrade_is_noop_for_json_object():
|
||||
config = DeepSeekChatConfig()
|
||||
original_messages = [{"role": "user", "content": "Is the sky blue?"}]
|
||||
messages, optional_params = config._downgrade_json_schema_to_json_object(
|
||||
model="deepseek-chat",
|
||||
messages=original_messages,
|
||||
optional_params={"response_format": {"type": "json_object"}},
|
||||
)
|
||||
|
||||
assert optional_params["response_format"] == {"type": "json_object"}
|
||||
assert messages == original_messages
|
||||
|
||||
|
||||
def test_transform_request_downgrades_json_schema_response_format():
|
||||
config = DeepSeekChatConfig()
|
||||
body = config.transform_request(
|
||||
model="deepseek-chat",
|
||||
messages=[{"role": "user", "content": "Is the sky blue?"}],
|
||||
optional_params={"response_format": dict(_JSON_SCHEMA_RESPONSE_FORMAT)},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert body["response_format"] == {"type": "json_object"}
|
||||
assert any("answer" in m.get("content", "") for m in body["messages"])
|
||||
|
||||
|
||||
async def test_async_transform_request_downgrades_json_schema_response_format():
|
||||
config = DeepSeekChatConfig()
|
||||
body = await config.async_transform_request(
|
||||
model="deepseek-chat",
|
||||
messages=[{"role": "user", "content": "Is the sky blue?"}],
|
||||
optional_params={"response_format": dict(_JSON_SCHEMA_RESPONSE_FORMAT)},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert body["response_format"] == {"type": "json_object"}
|
||||
assert any("answer" in m.get("content", "") for m in body["messages"])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue