Skip to content

Read images with deepseek-v4-flash-vision-exp, and move DeepSeek onto the Responses protocol - #185

Merged
hiyouga merged 11 commits into
devfrom
feat/deepseek-vision-responses
Aug 21, 2026
Merged

Read images with deepseek-v4-flash-vision-exp, and move DeepSeek onto the Responses protocol#185
hiyouga merged 11 commits into
devfrom
feat/deepseek-vision-responses

Conversation

@hiyouga

@hiyouga hiyouga commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

deepseek-v4-flash-vision-exp reads images, DeepSeek's dedicated client moves off Chat
Completions onto the Responses API, and a replay-ordering bug that both Responses clients
carried is fixed.

What the live API does

Probed against api.deepseek.com on 2026-08-21 with a picture whose only content is the word
BANANA, never named in the prompt:

Probe Result
Image in a user message (data URL and HTTP URL) reads BANANA on both protocols
Image in a tool result (function_call_output with input_image) reads BANANA
Same turn, tool result without the image (control) answers NO_IMAGE, so the image really is what it read
Image sent to a non-vision model Chat Completions: 400 This model does not support image. Responses: no error, the image becomes placeholder text and the model answers "Unsupported"
reasoning.effort: "none" 0 reasoning tokens — this is what disables thinking
extra_body {"thinking": {"type": "disabled"}} ignored on /responses: still 148 reasoning tokens
tool_choice: "required" or a named tool works only with thinking off; otherwise 400 Thinking mode does not support this tool_choice
temperature 0.0 and 2.0 accepted (documented as having no effect in thinking mode)
service_tier: "priority" silently ignored
api.siliconflow.cn/v1/responses 404 page not found
openrouter.ai/api/v1/responses with deepseek/deepseek-v4-flash works

Replay probes on the captured tool-call turn: the reasoning item can come back as
reasoning_text content, as a summary, empty, with encrypted_content, or not at all — every
shape is accepted with behavior intact, so the client rebuilds the minimal one from the
thinking text.

The ordering bug

test_tool_use failed intermittently against DeepSeek with
400 No tool output found for tool call <id>. It happens whenever the assistant speaks before
calling a tool: the transform appended the assistant message after the function_call it
preceded, and DeepSeek merges a call into the adjacent assistant message. Isolated probe:

  • user → reasoning → function_call → assistant text → function_call_output → 400
  • user → reasoning → assistant text → function_call → function_call_output → works

Both DeepSeekV4Client and OpenaiResponsesClient now flush collected message text before
appending a standalone input item. The generic client carried the same latent bug.

Decisions taken (confirmed in the conversation)

  • SiliconFlow has no /responses, so its two DeepSeek entries move to the generic
    openai-chat client rather than keeping a second DeepSeek client alive.
  • thinking_level is pre-mapped to what DeepSeek settles on (none/low/high/high/high/max)
    instead of passing medium/xhigh through.
  • temperature and tool_choice keep the current strict behavior: UnsupportedParameterError
    for a temperature other than 1.0 and for anything but auto/none.
  • Images on a non-vision model raise instead of being forwarded, because the Responses API
    degrades silently rather than failing.
  • Unit tests: the DeepSeek cases in the four protocol suites move to the Responses wire
    shape; the empty-response and tool-call-argument suites also gained a case for the generic
    openai-responses client, which had none.

Also in this change

README.md gained a table naming the wire protocol behind every client_type: google-genai
for the Gemini family, ant-messages for Claude, openai-responses for GPT, DeepSeek and
MiniMax, openai-chat for GLM and Kimi, and the OpenAI Embeddings endpoint for
openai-embedding.

Verification

  • E2E: pytest -k deepseek 35 passed (image URL, base64 image and tool-result image among
    them), jest -t deepseek 77 passed.
  • pytest -k openai-responses 9 passed across gateways, covering the shared ordering fix.
  • Offline suites: 196 passed (Python) and 184 passed (TypeScript); ruff check /
    ruff format --check, npm run lint and npm run build clean.
  • Registry entries still construct through AutoLLMClient (test_list_supported_models).

Not covered

deepseek-v4-flash-vision-exp is an experimental model with no entry in the vendor's changelog;
if it is withdrawn, the registry entry and the official E2E slot go with it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77

hiyouga and others added 5 commits August 21, 2026 03:25
DeepSeek documents deepseek-v4-flash-vision-exp and its image input only outside
the pages the snapshot already carried, so llmsdk_docs/deepseek_v4/ gains
docs/vision.md (the three input methods per protocol, detail levels, size and
count limits, and the 384-token upper bound per image) and docs/responses-api.md
(stream event names, parameter and input-item coverage, usage fields). The
quickstart lists the third model and the 1M/384K context figures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
DeepSeekV4Client now calls /responses instead of /chat/completions: it reads the
reasoning_text, output_text and function_call_arguments deltas, rebuilds a
replayed chain of thought as a reasoning item whose content is reasoning_text,
and takes usage from input_tokens/output_tokens with their detail blocks.

thinking_level maps to reasoning.effort pre-collapsed to what DeepSeek settles on
(none/low/high/high/high/max); effort "none" is what disables thinking there,
since the Chat Completions thinking toggle is ignored on this endpoint. The
temperature, tool_choice, fast_mode and prompt_caching restrictions are unchanged.

An image_url item is sent as an input_image part and a tool result's images ride
in the function_call_output, so a tool can hand the model a picture it produced.
A model without vision refuses the item instead: Chat Completions answers 400,
but the Responses API silently swaps the image for placeholder text.

Both this client and OpenaiResponsesClient now flush the message text collected
so far before appending a reasoning, function_call or function_call_output item.
An assistant turn that spoke before calling a tool used to replay as call, then
text, then output, which DeepSeek answered with "No tool output found for tool
call <id>" on the next turn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
The four protocol suites fed DeepSeek chat-completion chunks through a fake
.chat.completions endpoint, which the Responses client no longer calls. The
DeepSeek case moves to the Responses shape in each: the stream-case lists carry a
protocol field, the fakes install either endpoint, and the empty-response and
tool-call-argument suites gained a case for the generic openai-responses client
beside it. The thinking-level table now expects effort "none" for NONE, which is
how thinking is disabled on this endpoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
…-chat

The vision model joins the registry on the official API with Text, Image input, a
1M context window and the deepseek-v4-flash price (CNY 1.5 / 4.5 per million
tokens off-peak, 0.05 on a cache hit; peak rates are double). It replaces
deepseek-v4-flash as the official DeepSeek entry in both E2E model lists with
image understanding enabled, which brings the URL, base64 and tool-result image
tests with it; the three protocol-mode entries stay on deepseek-v4-flash.

deepseek-ai/DeepSeek-V4-Flash and deepseek-ai/DeepSeek-V4-Pro move to the generic
openai-chat client, since api.siliconflow.cn serves no /responses endpoint. The
OpenRouter entries keep the deepseek-v4 client, which its /responses does serve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
Copilot AI lite review requested due to automatic review settings August 21, 2026 10:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

hiyouga and others added 6 commits August 21, 2026 03:28
The README table names what each client_type actually speaks: google-genai for
the Gemini family, ant-messages for Claude, openai-responses for GPT, DeepSeek
and MiniMax, openai-chat for GLM and Kimi, and the OpenAI Embeddings endpoint for
openai-embedding. The generic protocol clients name their own protocol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
…ocol section

The Responses transforms decide once, at the top of the item loop, that anything
which is not message content flushes the text collected so far; the branch chain
below is the one it had before. The continue-based split said the same thing in
more places.

In the README the three generic protocol clients become a list, the supported-model
listing gets a Python and a TypeScript snippet, the client-type-to-protocol table
moves below both, and openai-embedding is code-formatted like the other protocols.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
…h tests

GPT5_6Client and MiniMaxM3Client deferred the assistant message to the end of the
item loop the same way the other two Responses clients did, so an assistant turn
that spoke before calling a tool replayed as call, then text. Both now flush the
collected text before appending a standalone input item.

The audit of the remaining clients found nothing else: Claude and the Anthropic
protocol client build one ordered content block list, Gemini one ordered parts
list, and Chat Completions carries no interleaving at all, since the text lands in
content and the call in tool_calls of the same message.

test_message_order.py and message-order.test.ts run a thinking-text-tool_call turn
through all ten clients with a fake key and assert the emitted order per protocol.
Against the pre-fix tree the four Responses cases fail and the other six pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
…down

An "unused" event is a client's own marker for a wire event that carries nothing
universal. No client yields one today, but nothing guaranteed it: streaming_response
now drops one that escapes and raises on it under AGENTHUB_DEBUG, so a client that
forgets its filter fails in CI instead of shipping empty events to a caller.

The EventType definition says what the four kinds mean, and the development skill
records both client rules it had only in code: unused stays inside the client, and
the message transform keeps the order of the content items.

The unknown-event suites now run every client over the ignorable events its protocol
carries with the debug guard on, and a deliberately leaky client covers the drop and
the raise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBwEdjegVGtfY2yJKwhZ77
@hiyouga
hiyouga merged commit 6edf8e9 into dev Aug 21, 2026
2 of 4 checks passed
@hiyouga
hiyouga deleted the feat/deepseek-vision-responses branch August 21, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants