Skip to content

fix(aux): structured-output 400s no longer kill fallback candidates or cost a doomed first request on DeepSeek (#83390, #105191, salvage #92908) - #113966

Merged
teknium1 merged 3 commits into
mainfrom
fix/baux-A-deepseek-response-format
Sep 17, 2026
Merged

teknium1 merged 3 commits into
mainfrom
fix/baux-A-deepseek-response-format

Conversation

@teknium1

@teknium1 teknium1 commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Auxiliary tasks that attach response_format (session titles via agent/title_generator.py, plugin structured calls via agent/plugin_llm.py) no longer fail when a fallback provider rejects response_format, and providers known to lack json_schema no longer pay a guaranteed-fail first request.

  • _call_fallback_candidate_sync/_async get the primary path's rung: one retry without response_format on 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).
  • New sibling agent/auxiliary_structured_output.py: a provider profile may declare unsupported_response_formats (DeepSeek: ("json_schema",), its API implements only json_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 to json_object) is the same end state the retry already produced; json_object needs a JSON-mentioning prompt and some relays return empty content under it.
  • ProviderProfile.unsupported_response_formats documented in the model-provider plugin guide; tests/agent/conftest.py resets 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 real call_llm / async_call_llm / generate_title with temp HERMES_HOMEs.
before: fallback_chain → DeepSeek: BadRequestError … This response_format type is unavailable now re-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 on provider: deepseek, memo after one rejection elsewhere). Control: a json_schema-capable stand-in and a real OpenAI gpt-5-mini call still receive response_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_config translation (8f2d61e, f709bd8).

Dropped hunks

Review follow-up (241411a):

  • MAJOR memo keyed by (host, type) blinded every model on an aggregator host after one model's json_schema 400, and the by-shape rule (response_format + json_schema in the body) also fed the memo from schema-VALIDATION 400s of json_schema-capable providers — fixed: memo is now (endpoint, model, type), every remember_structured_output_rejection caller (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 (openrouter openai/gpt-5-mini lost the field after another model's rejection; validation 400 memoised) → both False on base, green on the head; test_rejection_memo_is_per_model_and_ignores_schema_validation_errors red on base by source swap.
  • COMPOSITION _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 the remember column, so the fallback-only copy was deleted here instead of shipping two seams for one behaviour.
  • MINOR caller list named the kanban decomposer / goal judge — fixed: only agent/title_generator.py and agent/plugin_llm.py attach response_format on aux calls (body first line + test module docstring).
  • MINOR profile gate fired only for provider == deepseek — fixed (4 LOC): a custom route whose base_url host is api.deepseek.com resolves to the DeepSeek profile via the existing base_url→provider inference; asserted in test_known_unsupported_route_skips_response_format_before_first_request.

Infographic

structured-output

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 3e0481e — refactor(aux): drop the fallback-only structured-output rung

debug info

CI timings

CI timings · View report · View job

Wall time 6m12s vs 5m48s (+6.9%). 7 job(s) slower, 5 faster, 1 unchanged.

  • Docs Site / docs-site-checks: -93.0s
  • OS-specific tests / macOS-only tests: +20.0s
  • Python tests / Run tests: -13.0s
  • Python lints / ruff enforcement (blocking): -4.0s
  • OS-specific tests / Windows-only tests: +4.0s

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/plugins Plugin system and bundled plugins provider/deepseek DeepSeek API P3 Low — cosmetic, nice to have labels Sep 17, 2026
teknium1 and others added 3 commits September 17, 2026 09:00
…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
teknium1 force-pushed the fix/baux-A-deepseek-response-format branch from 241411a to 3e0481e Compare September 17, 2026 16:02
@teknium1
teknium1 merged commit 5cc8177 into main Sep 17, 2026
34 checks passed
@teknium1
teknium1 deleted the fix/baux-A-deepseek-response-format branch September 17, 2026 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment