spring-projects / spring-projects/spring-ai

DeepSeekChatModel silently drops UserMessage image media for vision-capable deepseek-flash

Open
#6,978 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
9.5k
Forks
2.9k
Avg merge
1d 7h
Merged PRs (30d)
6

Description

Bug description

DeepSeekChatModel silently drops images attached to UserMessage.media when building a Chat Completions request. The prompt contains the image, but the generated user message contains only the text, so a vision-capable model never receives the image.

DeepSeek's current Vision documentation explicitly supports image input for deepseek-flash, using OpenAI-compatible content arrays with text and image_url blocks.

Environment
  • Spring AI: 2.0.2-SNAPSHOT
  • Locally resolved DeepSeek artifact: spring-ai-deepseek-2.0.2-20260911.153205-13.jar
  • Java: 21
  • Spring Boot dependency management: 4.1.0
  • Model: deepseek-flash
  • Provider endpoint: https://api.deepseek.com
  • No vector store, memory advisor, or tools are needed to reproduce.
Steps to reproduce
  1. Construct a UserMessage containing text and one PNG Media with an image URI.
  2. Build a prompt with DeepSeekChatOptions.model("deepseek-flash").
  3. Inspect the result of DeepSeekChatModel.createRequest(prompt, false).

Actual serialized messages:

[{"content":"这个图片讲了什么","role":"user"}]

The input has media.size() == 1, but there is no image_url in the generated request. This happens during request conversion, before any HTTP request or image download.

Expected behavior

Images in user messages should be mapped to the provider's supported multimodal format, for example:

[
  {
    "role": "user",
    "content": [
      {"type": "text", "text": "这个图片讲了什么"},
      {"type": "image_url", "image_url": {"url": "https://example.com/diagnostic.png"}}
    ]
  }
]

If native image input is not supported yet, an explicit unsupported-media error would be preferable to silently sending a text-only request.

Minimal Complete Reproducible Example

The following standalone Java probe was run against the dependencies above. It uses reflection only to inspect the package-private request builder; it does not send an HTTP request and requires no real API key. The example.com URL is a placeholder and is never downloaded.

Dependencies: org.springframework.ai:spring-ai-deepseek:2.0.2-SNAPSHOT and org.springframework.ai:spring-ai-openai:2.0.2-SNAPSHOT, with Spring Boot 4.1.0 dependency management.

import java.net.URI;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.content.Media;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.util.MimeTypeUtils;

public class MediaProbe {
    static Object request(Object model, UserMessage message, ChatOptions options)
            throws Exception {
        var method = model.getClass().getDeclaredMethod(
                "createRequest", Prompt.class, boolean.class);
        method.setAccessible(true);
        return method.invoke(model, new Prompt(message, options), false);
    }

    public static void main(String[] args) throws Exception {
        var user = UserMessage.builder()
                .text("这个图片讲了什么")
                .media(new Media(MimeTypeUtils.IMAGE_PNG,
                        URI.create("https://example.com/diagnostic.png")))
                .build();

        var deepOptions = DeepSeekChatOptions.builder()
                .model("deepseek-flash").build();
        var deep = DeepSeekChatModel.builder()
                .deepSeekApi(DeepSeekApi.builder()
                        .apiKey("diagnostic-placeholder").build())
                .options(deepOptions).build();
        var deepRequest = (DeepSeekApi.ChatCompletionRequest)
                request(deep, user, deepOptions);

        System.out.println("Input media count: " + user.getMedia().size());
        System.out.println("DeepSeek serialized messages: "
                + new tools.jackson.databind.ObjectMapper()
                        .writeValueAsString(deepRequest.messages()));

        var openOptions = OpenAiChatOptions.builder()
                .model("deepseek-flash")
                .baseUrl("https://api.deepseek.com")
                .apiKey("diagnostic-placeholder").build();
        var open = OpenAiChatModel.builder().options(openOptions).build();
        var openRequest =
                (com.openai.models.chat.completions.ChatCompletionCreateParams)
                        request(open, user, openOptions);
        var parts = openRequest.messages().getFirst()
                .asUser().content().asArrayOfContentParts();

        System.out.println("OpenAI content parts: " + parts);
        if (parts.size() != 2 || !parts.get(1).isImageUrl()) {
            throw new AssertionError("Image part missing in OpenAI control");
        }
    }
}

Observed: DeepSeek produces only the text string; the OpenAI adapter preserves both the text and the image URL for the identical UserMessage. This comparison verifies request conversion, not an end-to-end provider call.

Root cause / possible implementation area
  • DeepSeekChatModel.createRequest: the USER/SYSTEM branch reads only message.getText() and never reads UserMessage.getMedia(). The same text-only branch is present in the current main source inspected on September 13, 2026.
  • DeepSeekApi.ChatCompletionMessage: content is typed as String, so supporting image blocks also requires a request representation that can express a content array while preserving text response handling.
  • Both synchronous and streaming paths use createRequest; the executable probe above covers the synchronous request conversion.
Related work and release planning

I searched existing issues and PRs for DeepSeek vision/multimodal/image support and did not find a direct implementation. #6729 concerns ordered content parts and explicitly leaves DeepSeek text-only, so it appears to be related infrastructure rather than a fix for this issue.

Is native DeepSeek image input planned for a particular release or milestone? Should applications use the OpenAI-compatible adapter in the meantime, including any necessary handling of DeepSeek-specific thinking/reasoning options?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java, then inspect DeepSeekApi.java to understand the current content representation. Run the standalone Java probe to compare DeepSeek and OpenAI request conversion. Done means the DeepSeek request preserves both the text and image URL instead of silently producing text-only content.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
ai, api
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
62/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.