Skip to content

Commit 10949d5

Browse files
committed
fix(deepseek): reject unsupported multimodal prompts
## About Fail fast when DeepSeek chat receives unsupported prompt objects instead of sending invalid request payloads through the OpenAI-compatible transport. This makes DeepSeek support more predictable by treating non-text prompt objects as prompt errors unless the provider actually supports them. ## Changes - Adapt DeepSeek text content as text blocks in the request adapter. - Raise PromptError for image_url, local_file, and remote_file. - Fix DeepSeek prompt errors to report the actual unsupported object kind. - Update DeepSeek adapter specs for unsupported multimodal prompt objects. - Remove the misleading local-file example from the DeepSeek provider docs.
1 parent b825f11 commit 10949d5

4 files changed

Lines changed: 54 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ Changes since `v5.1.0`.
1717

1818
### Fix
1919

20+
* **Reject unsupported DeepSeek multimodal prompt objects early** <br>
21+
Raise `LLM::PromptError` for `image_url`, `local_file`, and
22+
`remote_file` in DeepSeek chat requests instead of sending invalid
23+
OpenAI-compatible payloads that the provider rejects at runtime.
24+
2025
* **Preserve DeepSeek reasoning content across tool turns** <br>
2126
Replay `reasoning_content` when serializing prior assistant messages for
2227
DeepSeek chat completions, so thinking-mode tool calls can continue into

lib/llm/providers/deepseek.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module LLM
1515
#
1616
# llm = LLM.deepseek(key: ENV["KEY"])
1717
# ctx = LLM::Context.new(llm)
18-
# ctx.talk ["Tell me about this photo", ctx.local_file("/images/photo.png")]
18+
# ctx.talk "Hello"
1919
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
2020
class DeepSeek < OpenAI
2121
require_relative "deepseek/request_adapter"

lib/llm/providers/deepseek/request_adapter/completion.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,28 @@ def adapt
3030

3131
def adapt_content(content)
3232
case content
33+
when LLM::Object
34+
adapt_object(content)
3335
when String
34-
content.to_s
36+
[{type: :text, text: content.to_s}]
3537
when LLM::Message
3638
adapt_content(content.content)
3739
when LLM::Function::Return
3840
throw(:abort, {role: "tool", tool_call_id: content.id, content: LLM.json.dump(content.value)})
39-
when LLM::Object
40-
prompt_error!(content)
4141
else
4242
prompt_error!(content)
4343
end
4444
end
4545

46+
def adapt_object(object)
47+
case object.kind
48+
when :image_url, :local_file, :remote_file
49+
prompt_error!(object)
50+
else
51+
prompt_error!(object)
52+
end
53+
end
54+
4655
def adapt_message
4756
case content
4857
when Array
@@ -64,7 +73,7 @@ def adapt_array
6473

6574
def prompt_error!(object)
6675
if LLM::Object === object
67-
raise LLM::PromptError, "The given LLM::Object with kind '#{content.kind}' is not " \
76+
raise LLM::PromptError, "The given LLM::Object with kind '#{object.kind}' is not " \
6877
"supported by the DeepSeek API"
6978
else
7079
raise LLM::PromptError, "The given object (an instance of #{object.class}) " \

spec/deepseek/request_adapter_spec.rb

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# frozen_string_literal: true
22

33
require "setup"
4+
require "tempfile"
45
require "llm/providers/deepseek"
56

67
RSpec.describe "LLM::DeepSeek::RequestAdapter::Completion" do
78
describe "#adapt" do
89
subject(:payload) { LLM::DeepSeek::RequestAdapter::Completion.new(message).adapt }
910

11+
let(:provider) { LLM.deepseek(key: "test") }
12+
1013
context "with assistant content" do
1114
let(:message) do
1215
LLM::Message.new("assistant", "answer", reasoning_content: "thought")
@@ -15,12 +18,43 @@
1518
it "preserves reasoning content" do
1619
expect(payload).to eq(
1720
role: "assistant",
18-
content: "answer",
21+
content: [{type: :text, text: "answer"}],
1922
reasoning_content: "thought"
2023
)
2124
end
2225
end
2326

27+
context "with image content" do
28+
let(:message) do
29+
LLM::Message.new("user", [ctx.image_url("https://example.com/cat.png")])
30+
end
31+
32+
let(:ctx) { LLM::Context.new(provider) }
33+
34+
it "raises a prompt error" do
35+
expect { payload }.to raise_error(LLM::PromptError, /image_url/)
36+
end
37+
end
38+
39+
context "with local file content" do
40+
let(:tempfile) do
41+
Tempfile.create(["example", ".pdf"]).tap {
42+
_1.write("%PDF-1.4\n")
43+
_1.rewind
44+
}
45+
end
46+
47+
let(:message) do
48+
LLM::Message.new("user", [ctx.local_file(tempfile.path)])
49+
end
50+
51+
let(:ctx) { LLM::Context.new(provider) }
52+
53+
it "raises a prompt error" do
54+
expect { payload }.to raise_error(LLM::PromptError, /local_file/)
55+
end
56+
end
57+
2458
context "with assistant tool calls" do
2559
let(:message) do
2660
LLM::Message.new("assistant", nil, {

0 commit comments

Comments
 (0)