mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
fix(llm): stop DeepSeek enrichment failing on json_schema rejection
Enriching a model with `llm_provider=deepseek` failed outright with
HTTP 400 "This response_format type is unavailable now". Probing the
endpoint shows why:
response_format absent -> 200
{"type": "json_object"} -> 200
{"type": "json_schema",...} -> 400
`chat_completion_json` preferred `json_schema` for a real reason -- LM
Studio and other local OpenAI-compatible servers reject `json_object`
but accept `json_schema` -- and guarded the fallback with a substring
test for `'response_format.type'` (the wording of those servers'
rejection). DeepSeek's message is "This response_format type is
unavailable now", which does not contain that substring, so the guard
re-raised and the retry never ran.
Make the format a per-provider chain instead of a single guess:
- `_JSON_OBJECT_ONLY_PROVIDERS` lists providers known to reject
json_schema (currently just deepseek). They ask for `json_object`
first, so the common case costs one request and no wasted retry.
- Everyone else keeps `json_schema` first, then downgrades through
`json_object` and finally prompt-only mode.
- A downgrade now happens on any error mentioning `response_format`,
which covers wording variants without swallowing unrelated failures:
auth errors, unknown models, and rate limits still surface unchanged
because their messages never name the parameter.
`json_object` is sufficient here: the skill prompt already specifies the
exact JSON shape, and `_try_salvage_json` repairs imperfect output.
Verified against the real configured endpoint with the real
`enrich_hf_metadata` prompt, prompt renderer, and ModelScope model card
for jj3550945163/Krea-2-LORA: a 9,815-character prompt returns
parseable JSON (base_model "Flux.1 Krea", description, tags, notes).
Three regression tests cover the DeepSeek ordering, the
json_schema -> json_object downgrade, and the no-retry-on-unrelated-400
path. Full backend suite: 2856 passed.
This commit is contained in:
@@ -76,6 +76,25 @@ class MockSession:
|
||||
pass
|
||||
|
||||
|
||||
class RecordingSession:
|
||||
"""Mock session that records each request payload and replays responses."""
|
||||
|
||||
def __init__(self, responses):
|
||||
self._responses = list(responses)
|
||||
self.payloads = []
|
||||
|
||||
def post(self, url, json=None, headers=None):
|
||||
self.payloads.append(json)
|
||||
index = min(len(self.payloads) - 1, len(self._responses) - 1)
|
||||
return self._responses[index]
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llm_service():
|
||||
"""Create an LLMService with mock settings."""
|
||||
@@ -298,6 +317,107 @@ class TestLLMServiceChatCompletionJson:
|
||||
assert result == {"key": "value"}
|
||||
assert call_index == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_completion_json_prefers_json_object_for_deepseek(self):
|
||||
"""DeepSeek rejects json_schema, so json_object is used first.
|
||||
|
||||
Regression: DeepSeek answers json_schema with
|
||||
"This response_format type is unavailable now", which the old
|
||||
substring check did not recognise, so enrichment failed outright.
|
||||
"""
|
||||
settings = MockSettings(
|
||||
llm_enabled=True,
|
||||
llm_provider="deepseek",
|
||||
llm_api_key="sk-test-key",
|
||||
llm_api_base="https://api.deepseek.com/v1",
|
||||
llm_model="deepseek-v4-flash",
|
||||
)
|
||||
service = LLMService(settings)
|
||||
|
||||
session = RecordingSession(
|
||||
[
|
||||
MockResponse(
|
||||
200,
|
||||
json_data={
|
||||
"choices": [{"message": {"content": '{"key": "value"}'}}],
|
||||
"usage": {},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with mock.patch("aiohttp.ClientSession", return_value=session):
|
||||
result = await service.chat_completion_json(
|
||||
system_prompt="You are helpful.",
|
||||
user_prompt="Return JSON.",
|
||||
)
|
||||
|
||||
assert result == {"key": "value"}
|
||||
assert len(session.payloads) == 1
|
||||
assert session.payloads[0]["response_format"] == {"type": "json_object"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_completion_json_downgrades_from_json_schema(
|
||||
self, llm_service,
|
||||
):
|
||||
"""json_schema → json_object when the provider rejects json_schema."""
|
||||
session = RecordingSession(
|
||||
[
|
||||
MockResponse(
|
||||
400,
|
||||
text_data=(
|
||||
'{"error":{"message":"This response_format type is '
|
||||
'unavailable now","type":"invalid_request_error"}}'
|
||||
),
|
||||
),
|
||||
MockResponse(
|
||||
200,
|
||||
json_data={
|
||||
"choices": [{"message": {"content": '{"key": "value"}'}}],
|
||||
"usage": {},
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
with mock.patch("aiohttp.ClientSession", return_value=session):
|
||||
result = await llm_service.chat_completion_json(
|
||||
system_prompt="You are helpful.",
|
||||
user_prompt="Return JSON.",
|
||||
)
|
||||
|
||||
assert result == {"key": "value"}
|
||||
assert [p.get("response_format") for p in session.payloads] == [
|
||||
{
|
||||
"type": "json_schema",
|
||||
"json_schema": {"name": "metadata", "schema": {"type": "object"}},
|
||||
},
|
||||
{"type": "json_object"},
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_completion_json_does_not_retry_unrelated_errors(
|
||||
self, llm_service,
|
||||
):
|
||||
"""Unrelated 400s are surfaced unchanged, without format downgrades."""
|
||||
session = RecordingSession(
|
||||
[
|
||||
MockResponse(
|
||||
400,
|
||||
text_data='{"error":{"message":"Model not found"}}',
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with mock.patch("aiohttp.ClientSession", return_value=session):
|
||||
with pytest.raises(LLMResponseError, match="HTTP 400"):
|
||||
await llm_service.chat_completion_json(
|
||||
system_prompt="You are helpful.",
|
||||
user_prompt="Return JSON.",
|
||||
)
|
||||
|
||||
assert len(session.payloads) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_completion_json_raises_on_non_json(self, llm_service):
|
||||
# Non-JSON content raises LLMResponseError (salvage also fails)
|
||||
|
||||
Reference in New Issue
Block a user