DeepSeek Harness read_image 400: model vs route

TroubleshootingPublished 2026-10-30Author: DeepSeek Plugin Market
DeepSeek HarnessDSHread_image400inputModalitiesopenai-responsesvision model
One read_image makes a DeepSeek Harness session return 400 even for text. The check is the model's image capability, not the protocol: separate model and route.

Many people's first instinct is "openai-responses does not support images", but the community's criterion is more precise: core degrades only when the model explicitly declares "I do not support images", and a vision model declares that it does — so the image block is sent as-is to an endpoint that cannot carry it, and then stays in history forever. In one sentence: capability is declared per model, carrying is decided per route, and this failure is exactly where the two disagree (#11311).

DeepSeek Harness: Step 1: check the model's declared input array

Whether DSH degrades depends only on whether inputModalities in the model info includes image; if it does, no degradation, and only if it does not is the block replaced with a text placeholder.

  1. Open that provider's model config and find the input or inputModalities field.
  2. If it is [text, image], DSH treats the model as image-capable and sends image blocks as-is.
  3. If it is [text], or the field is missing and judged to lack image, core replaces images with text placeholders.
  4. The logic lives in packages/llm/llm/src/index.ts: the condition requires inputModalities !== undefined and no image before calling projectImagesForTextModel.
  5. inputModalities comes from the pi-ai model's input array, mapped in by the adapter.

The community verified it in reverse with a recording adapter: with ['text','image'] declared the adapter received image blocks, while with ['text'] core replaced them with placeholder text. The degradation logic is fine; it is simply never triggered.

DSH plugin: Step 2: separate "model capability" from "route carrying"

The same model on the same provider may accept images on one protocol and be completely unable on another, and inputModalities cannot express that difference.

  1. Run three comparisons against the gateway: POST /v1/chat/completions with plain text, with a public image URL, and with a base64 data URL.
  2. Community results: all three returned 200, and the image requests reported image_tokens: 176.
  3. Then try POST /v1/responses: plain text failed three times with 500 bad_response_body.
  4. Then /v1/responses with an image block: 400, with the image part marked type:'unknown'.
  5. Conclusion: this provider's Responses endpoint is currently unusable for images, while the Chat Completions endpoint works fully.

This step divides responsibility: the gateway side is an environmental fact, and DSH's problem is that it knows a protocol path has no legal image form, still stuffs image blocks into the request as-is, and has no self-healing.

DeepSeek Harness: Step 3: confirm whether the session is already polluted

Watching whether the 400's error position shifts right as history grows is the fastest way to tell "one bad block in history".

  1. Read the error body: Invalid JSON data: Failed to deserialize the JSON body into the target type: input: data did not match any variant of untagged enum ResponseInput at line 1 column 422733.
  2. Note the column number. The community's three failures were at 422557, 422664, and 422733, moving monotonically right as content was appended.
  3. This means the body is single-line JSON over 422 KB, and the failure point is fixed in the region of the image block.
  4. Check the timeline: before this, the session had completed more than 30 requests successfully; the first 400 appeared exactly on the step where read_image's image block first entered history.
  5. Plain-text messages afterwards also return 400, because the bad block is still there and every request replays it.

DeepSeek Harness: Three workarounds: change protocol, change declaration, attach a plugin

Ordered by blast radius, smallest first: change the protocol, then consider the plugin, and only then degrade the model declaration.

  1. Change the protocol: switch that provider's api from openai-responses to openai-completions, where the chat endpoint handles both text and local base64 images. This is the easiest and smallest-blast-radius option.
  2. Attach the route plugin: dsh plugin add github:argszero/dsh-image-route-guard#v0.1.0, with this config:
yaml
- id: image-route-guard
  config:
    routes:
      - provider: agnes
        model: agnes-3.0-flash
  1. The plugin projects image blocks into core's own placeholder text only on the routes you name, byte-identical to textOnlyImageText, and it also walks tool_result (exactly the shape read_image returns).
  2. Unnamed routes, calls without images, and requests it cannot parse all pass through unchanged, silently.
  3. Degrade the model declaration: change input to [text] so requests carry placeholder text — but it is global, and the same model loses images on routes that can carry them too.

DSH Plugin: Why "delete the image declaration" is not the best answer

It strips image capability from the same model id on every route, while you wanted to fix just one route.

  1. Capability declarations live on the model entry, and every route pointing at that entry shares one copy.
  2. Carrying capability lives on the protocol endpoint, and two endpoints of the same provider can be completely different.
  3. Fixing a local problem with a global switch trades a bigger loss for a smaller repair.
  4. The community plugin exists precisely for this gap: it takes effect per route and leaves other routes alone.
  5. The plugin author admits its boundary too: it removes bad blocks from requests but does not teach provider paths to degrade, nor can it decide which protocols can carry images.

DeepSeek Harness: Fix direction: the check must be able to see the route

The real fix is to bring "can this route carry images" into the model info so the same degradation decision runs per route.

  1. The protocol is the adapter's own fact, and llm-pi-ai's catalog already maps openai-responses to the corresponding compat gate.
  2. The adapter can carry a "this route carries images" flag into the model info.
  3. The same projection would then run only on routes declared incapable, with no plugin needed.
  4. Session self-healing is the other half: catch body validation failures, clear image blocks from history and retry, or explicitly say "this session cannot be recovered".
  5. Until then the plugin is an honest stopgap; once core changes, it can be removed.

Cautions

Change one layer at a time and verify the session afterward.

  1. Do not conclude from the protocol alone. Different endpoints on the same protocol can behave completely differently; measure it.
  2. Do not ignore history. After fixing the config the old session still returns 400; start a new session or clean that part of history.
  3. Do not treat a gateway problem as a DSH defect. The Responses endpoint being unusable for images is an environmental fact; DSH's problem is the missing degradation and self-healing.
  4. Write the full route in the plugin. Specifying only provider covers every model under it; provider + model is more precise.
  5. Keep the column number. It is the only quantitative clue for whether the bad block is still there.

If your model declaration was already [text], this article does not apply — go check context overflow or image size instead.

If the old session still returns 400 after changing the protocol, check once more whether the column is still moving right — movement means the bad block is still in history and you need a new session.See Fix "url field must be base64 encoded image" DSH LM Studio error for the related case.

FAQ

After read_image in DeepSeek Harness the whole session returns 400, even for plain text — why?

DeepSeek Harness returns 400 on every later turn because that image block is now written into session history and is replayed on every request. As long as the target route has no legal image form, the gateway rejects the whole request, including pure-text turns, and the session has no self-healing path.

Is this DeepSeek Harness 400 caused by the openai-responses protocol not supporting images?

No, the 400 comes from the model's declared image capability rather than from the protocol that DeepSeek Harness sends over. llm/src/index.ts degrades to text only when modelInfo.inputModalities explicitly lacks image; a vision model declares image, so degradation never runs and the image block is sent as-is.

Does changing the model input to [text] fix it in DeepSeek Harness, and what does it cost?

Yes, changing the input to [text] works around it in DeepSeek Harness, but the cost is global: the same model id also stops receiving images on other routes that can carry them. Capability is declared per model while carrying is decided per route, and a declaration cannot express that difference.

How is dsh-image-route-guard configured in DeepSeek Harness, and how does it differ from changing the model declaration?

dsh-image-route-guard projects image blocks into text placeholders only on routes you name in DeepSeek Harness, leaving other routes to send images as usual. Install with dsh plugin add github:argszero/dsh-image-route-guard#v0.1.0 and write routes: [{ provider, model }] in its config.

How do I confirm in DeepSeek Harness that the route cannot carry images rather than the model being incapable?

To confirm the route cannot carry images in DeepSeek Harness, test the gateway directly: send the same image through chat/completions with a URL and with base64, then through responses. In the community case the first two returned 200 with image_tokens: 176, while the last returned 500 for plain text and marked the image block type:'unknown'.

Related Terms

inputModalities
inputModalities is the capability field declaring which input types a model accepts, derived from a pi-ai model's input array. Core degrades image blocks to text placeholders only when it explicitly lacks image.— https://github.com/deepseek-ai/deepseek-harness/discussions/11311
projectImagesForTextModel
projectImagesForTextModel is the primitive that replaces image blocks in a message with text placeholders, using textOnlyImageText to produce forms like [image: name size bytes]. The degradation logic works; the problem is that it is never triggered.— https://github.com/deepseek-ai/deepseek-harness/discussions/11311
json_parse_error
json_parse_error is the error code a gateway returns when deserializing the request body, in this case indicating that input matches no variant of ResponseInput. It usually means the body contains a block shape the endpoint does not recognize.— https://github.com/deepseek-ai/deepseek-harness/discussions/11311
route carrying capability
Route carrying capability is whether a specific protocol endpoint of a provider can accept a given content block, independent of the model capability declaration. The same model may accept images on one protocol and not on another.— https://github.com/deepseek-ai/deepseek-harness/discussions/11311

Sources