fix(aux): structured-output 400s no longer kill fallback candidates or cost a doomed first request on DeepSeek (#83390, #105191, salvage #92908) - #113966
Merged
Conversation
૮ >ﻌ< ა ci reviewran on 3e0481e — refactor(aux): drop the fallback-only structured-output rung debug infoCI timingsCI timings · View report · View jobWall time 6m12s vs 5m48s (+6.9%). 7 job(s) slower, 5 faster, 1 unchanged.
|
…ates or costs a doomed first request Two open atoms of #83390 (DeepSeek "This response_format type is unavailable now"): * `_call_fallback_candidate_sync/_async` only special-cased auth errors, so when the primary aux provider failed (timeout, rate limit, payment) and the fallback landed on a provider that rejects `json_schema`, the 400 re-raised and the whole task died — the primary-path rung from #89589 never applied there. Both fallback paths now retry once without `response_format`. * Every structured aux call (titles, kanban decomposer, goal judge, plugin structured calls) paid a guaranteed-fail request on providers that lack `json_schema` before the retry. A provider profile can now declare `unsupported_response_formats` (DeepSeek: json_schema, per https://api-docs.deepseek.com/guides/json_mode) and the recovery ladder remembers any route that rejected a type once (host:port scoped), so `_build_call_kwargs` — shared by the primary and fallback paths — omits the field before the first request. Dropping rather than downgrading to json_object matches the end state the retry already produced; json_object needs a JSON-mentioning prompt and some relays return empty content under it. New logic lives in agent/auxiliary_structured_output.py; the facade only gains the fallback rung next to the predicate it uses. tests/agent/conftest.py resets the process-level memo per test. Fixes #83390, #105191. Closes duplicates #84976, #88830, #102849, #113064. Co-authored-by: Legion-is-life <Legion-is-life@users.noreply.github.com>
…o capability errors
The process-level memo of routes that rejected a response_format type was
keyed by (endpoint host, type) only. On an aggregator host (openrouter.ai,
the Nous Portal, api.openai.com) one chat-only model's json_schema 400 then
stripped the field from every other model on that host for the rest of the
process, silently degrading schema enforcement for models that support it.
The memo was also fed by any error _is_structured_output_rejection matched,
including the by-shape rule (response_format + json_schema in the body),
which also matches schema-VALIDATION 400s from json_schema-capable providers
("Invalid schema for response_format 'json_schema': additionalProperties
must be false") - those say nothing about the next schema.
- key the memo by (endpoint, model, type); _build_call_kwargs passes the
model to the lookup and every remember_* caller (both fallback candidate
paths and the primary ladder rung) now hands over the rejecting error
- memoise only capability rejections (unavailable / not supported / unknown
or unsupported parameter / extra inputs not permitted); the by-shape match
still drives the one-shot retry but never feeds the memo
- a custom route whose base_url host is api.deepseek.com consults the
DeepSeek profile (via the existing base_url->provider inference), so the
profile gate also fires when the provider name was overridden to custom
- test module docstring: only title_generator and plugin_llm attach
response_format on aux calls
…ved by the rung table Since #113958 every fallback candidate runs the shared parameter rung table (send_with_parameter_rungs), whose structured-output rung strips the field and records the rejection through its remember column. The fallback-side copy could never fire any more; delete it rather than keep two seams for one behaviour.
teknium1
force-pushed
the
fix/baux-A-deepseek-response-format
branch
from
September 17, 2026 16:02
241411a to
3e0481e
Compare
This was referenced Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Auxiliary tasks that attach
response_format(session titles viaagent/title_generator.py, plugin structured calls viaagent/plugin_llm.py) no longer fail when a fallback provider rejectsresponse_format, and providers known to lackjson_schemano longer pay a guaranteed-fail first request._call_fallback_candidate_sync/_asyncget the primary path's rung: one retry withoutresponse_formaton a structured-output 400 (before: any non-auth error re-raised and killed the task after the primary provider had already failed — Ollama timeout → DeepSeek fallback → 400, per the Auxiliary title_generation fails on DeepSeek: HTTP 400 "This response_format type is unavailable now" #83390 thread).agent/auxiliary_structured_output.py: a provider profile may declareunsupported_response_formats(DeepSeek:("json_schema",), its API implements onlyjson_object), and the recovery ladder remembers every (route host:port, model, type) triple a capability rejection produced;_build_call_kwargs— shared by primary and fallback requests, sync and async — omits the field up front. Dropping (not downgrading tojson_object) is the same end state the retry already produced;json_objectneeds a JSON-mentioning prompt and some relays return empty content under it.ProviderProfile.unsupported_response_formatsdocumented in the model-provider plugin guide;tests/agent/conftest.pyresets the process-level memo per test.Live repro: live against a stand-in replaying DeepSeek's documented 400 (
{"error": {"message": "This response_format type is unavailable now", "type": "invalid_request_error"}}, https://api-docs.deepseek.com/guides/json_mode), driving the realcall_llm/async_call_llm/generate_titlewith tempHERMES_HOMEs.before: fallback_chain → DeepSeek:
BadRequestError … This response_format type is unavailable nowre-raised (sync + async),generate_title→None; pinned DeepSeek route: 6 wire requests for 3 calls (3×400 + 3 retries).after: fallback →
fallback candidate fallback_chain[0](custom) rejected the structured-output format field; retrying once without it→ title returned (sync + async); pinned route: the field is omitted up front (1 request per call; profile flag onprovider: deepseek, memo after one rejection elsewhere). Control: a json_schema-capable stand-in and a real OpenAI gpt-5-mini call still receiveresponse_format.type=json_schema(3/3, 1/1). New tests: 3 red on base by source swap, green on the head; 383 focused tests pass.Root cause: the structured-output rejection rung existed only on the primary ladder, and nothing consulted provider capability before attaching the field.
Fixes #83390
Fixes #105191
Fixes #84976
Fixes #88830
Fixes #102849
Fixes #113064
Supersedes #92908 (@Legion-is-life, co-authored — same fallback rung, slimmer shape), #113111 (@fangliquanflq), #110633 (@29206394, its by-shape detection already landed on main), #107963 (@dacheah), #88953 (@gerryqi), #85115 (@Johannnnn506); #87870 (@f-trycua) is already covered on main by the Anthropic
output_configtranslation (8f2d61e, f709bd8).Dropped hunks
extra_bodyinto the auth-refresh retry — not needed; a 401 on the same credential right after a 400 is not a real sequence, so the retry after the strip simply raises on any further error.json_schema → json_objectdowngrade +supports_structured_outputmodel override — replaced by dropping the field (see thread:json_objectempty-content and "prompt must contain json" preconditions).Review follow-up (241411a):
json_schema400, and the by-shape rule (response_format+json_schemain the body) also fed the memo from schema-VALIDATION 400s ofjson_schema-capable providers — fixed: memo is now (endpoint, model, type), everyremember_structured_output_rejectioncaller (both fallback candidate paths, the primary ladder rung) hands over the rejecting error and only capability messages (unavailable / not supported / unknown or unsupported parameter / extra inputs not permitted, never "Invalid schema …") are memoised; the by-shape match still drives the one-shot retry. Reproduced the reviewer's probe on ab4d3a8 (openrouteropenai/gpt-5-minilost the field after another model's rejection; validation 400 memoised) → bothFalseon base, green on the head;test_rejection_memo_is_per_model_and_ignores_schema_validation_errorsred on base by source swap._fallback_structured_output_retry_kwargs+ its block in_call_fallback_candidate_sync/_async— fixed on the rebase (3e0481e): fix(aux): title generation recovers from stacked parameter rejections on primary and fallback routes (#78273, #72351, #109774, salvage #72515) #113958 landed first, its rung table now wraps every fallback candidate and carries theremembercolumn, so the fallback-only copy was deleted here instead of shipping two seams for one behaviour.agent/title_generator.pyandagent/plugin_llm.pyattachresponse_formaton aux calls (body first line + test module docstring).provider == deepseek— fixed (4 LOC): acustomroute whose base_url host isapi.deepseek.comresolves to the DeepSeek profile via the existing base_url→provider inference; asserted intest_known_unsupported_route_skips_response_format_before_first_request.Infographic