Skip to content

Commit 70d9d64

Browse files
committed
Initial Mistral Implementation
1 parent fdf1406 commit 70d9d64

File tree

3 files changed

+23
-168
lines changed

3 files changed

+23
-168
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ RubyLLM.configure do |config|
103103
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
104104
config.gemini_api_key = ENV['GEMINI_API_KEY']
105105
config.deepseek_api_key = ENV['DEEPSEEK_API_KEY']
106-
config.mistral_api_key = ENV['MISTRAL_API_KEY']
106+
config.mistral_api_key = ENV['MISTRAL_API_KEY']
107+
# Optional
107108
end
108109
```
109110

lib/ruby_llm/providers/mistral/chat.rb

Lines changed: 10 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,9 @@ def render_payload(messages, tools:, temperature:, model:, stream: nil)
2222
Array(tools)
2323
end
2424

25-
# Ensure messages are properly processed to handle nil values
26-
processed_messages = messages.map { |m| render_message(m) }
27-
28-
# Use the debugging helper for vision models to diagnose image issues
29-
if supports_vision?(model)
30-
debug_image_handling(messages, processed_messages)
31-
end
32-
33-
# Debug any nil values in content arrays for vision models
34-
if ENV["DEBUG"] && supports_vision?(model)
35-
processed_messages.each_with_index do |msg, idx|
36-
if msg[:content].is_a?(Array) && msg[:content].any?(&:nil?)
37-
RubyLLM.logger.debug "WARNING: Nil values detected in content array for message #{idx}"
38-
RubyLLM.logger.debug "Original content before processing: #{messages[idx].content.inspect}"
39-
RubyLLM.logger.debug "Processed content: #{msg[:content].inspect}"
40-
end
41-
end
42-
end
43-
4425
payload = {
4526
model: model,
46-
messages: processed_messages,
27+
messages: messages.map { |m| render_message(m) },
4728
temperature: temperature,
4829
stream: stream,
4930
tools: tools_array&.any? ? tools_array.map { |t| render_tool(t) } : nil,
@@ -63,13 +44,7 @@ def render_message(message)
6344
result[:role] = message.role
6445

6546
# Handle content (text or multimodal)
66-
if message.content.nil?
67-
# If content is nil, provide a simple empty message
68-
result[:content] = ""
69-
elsif message.content.is_a?(Array)
70-
# Log detailed information for debugging
71-
RubyLLM.logger.debug "Processing array content in message: #{message.content.inspect}" if ENV["DEBUG"]
72-
47+
if message.content.is_a?(Array)
7348
# Filter out any nil values and ensure the array is not empty
7449
filtered_content = message.content.compact
7550
if filtered_content.empty?
@@ -112,37 +87,17 @@ def format_multimodal_content(content)
11287
# As a workaround, we filter out nil values below, but this doesn't solve the root issue
11388
# which requires changes to the core library's Content handling.
11489

115-
# Filter out nil values and perform additional validation
90+
# Filter out nil values
11691
filtered_content = content.compact
11792

118-
if filtered_content.empty?
119-
RubyLLM.logger.warn "WARNING: All items in the content array were nil or empty"
120-
# Return a simple text message to avoid API errors
121-
return [{ type: "text", text: "Please provide content for this message." }]
122-
end
93+
RubyLLM.logger.debug "Filtered content: #{filtered_content.inspect}"
12394

124-
# Filter out any malformed image items (those without required data)
125-
validated_content = filtered_content.map do |item|
95+
filtered_content.map do |item|
12696
if item.is_a?(Hash) && item[:type] == "image"
127-
if item[:url].nil? && (item[:data].nil? || item[:data].empty?)
128-
RubyLLM.logger.warn "WARNING: Skipping malformed image item: #{item.inspect}"
129-
nil
130-
else
131-
format_image_content(item)
132-
end
97+
format_image_content(item)
13398
else
13499
item
135100
end
136-
end.compact
137-
138-
RubyLLM.logger.debug "Validated multimodal content: #{validated_content.inspect}"
139-
140-
# If all content was filtered out, provide a fallback
141-
if validated_content.empty?
142-
RubyLLM.logger.warn "WARNING: All content was filtered out due to validation"
143-
[{ type: "text", text: "Please provide valid content for this message." }]
144-
else
145-
validated_content
146101
end
147102
end
148103

@@ -163,45 +118,24 @@ def format_image_content(image)
163118
# NOTE: This method is currently not getting properly formatted images from the Content class
164119
# in vision-related tests. Debug logs show this method isn't being called with valid image data.
165120

166-
# Basic validation - return nil for invalid image data
167-
return nil unless image.is_a?(Hash)
168-
169-
# Log detailed information about image formatting attempts in debug mode
170-
RubyLLM.logger.debug "Formatting image: #{image.inspect}" if ENV["DEBUG"]
171-
172-
result = nil
173-
174-
if image[:url] && !image[:url].empty?
175-
result = {
121+
if image[:url]
122+
{
176123
type: "image",
177124
source: {
178125
type: "url",
179126
url: image[:url],
180127
},
181128
}
182-
elsif image[:data] && !image[:data].empty?
183-
result = {
129+
elsif image[:data]
130+
{
184131
type: "image",
185132
source: {
186133
type: "base64",
187134
media_type: image[:media_type] || "image/jpeg",
188135
data: image[:data],
189136
},
190137
}
191-
elsif image[:source] && image[:source].is_a?(Hash)
192-
# The image may already be in the correct format for Mistral
193-
# Just do some basic validation
194-
if (image[:source][:type] == "url" && image[:source][:url]) ||
195-
(image[:source][:type] == "base64" && image[:source][:data])
196-
result = image
197-
end
198-
end
199-
200-
if result.nil?
201-
RubyLLM.logger.warn "WARNING: Unable to format image: #{image.inspect}"
202138
end
203-
204-
result
205139
end
206140

207141
def render_tool_call(tool_call)
@@ -280,53 +214,6 @@ def parse_tool_arguments(args)
280214
args
281215
end
282216
end
283-
284-
# Helper method to check if a model supports vision capabilities
285-
def supports_vision?(model_id)
286-
Mistral.capabilities.supports_vision?(model_id)
287-
end
288-
289-
# Debugging helper to assist with troubleshooting image handling issues
290-
# This is a comprehensive method that logs detailed information about the content
291-
# being processed to help identify issues with image handling.
292-
def debug_image_handling(messages, formatted_messages)
293-
return unless ENV["DEBUG"]
294-
295-
RubyLLM.logger.debug "==== IMAGE HANDLING DIAGNOSTICS ===="
296-
297-
messages.each_with_index do |msg, i|
298-
next unless msg.content.is_a?(Array)
299-
300-
RubyLLM.logger.debug "Message #{i}: Original content array:"
301-
msg.content.each_with_index do |item, j|
302-
if item.nil?
303-
RubyLLM.logger.debug " Item #{j}: NIL VALUE"
304-
elsif item.is_a?(Hash) && item[:type] == "image"
305-
has_url = item[:url] && !item[:url].empty?
306-
has_data = item[:data] && !item[:data].empty?
307-
has_source = item[:source] && item[:source].is_a?(Hash)
308-
309-
details = []
310-
details << "url=#{item[:url].to_s[0..30]}..." if has_url
311-
details << "data_size=#{item[:data].to_s.size rescue "N/A"}" if has_data
312-
details << "source=#{item[:source].inspect}" if has_source
313-
314-
RubyLLM.logger.debug " Item #{j}: IMAGE - #{details.join(", ")}"
315-
else
316-
RubyLLM.logger.debug " Item #{j}: #{item.class} - #{item.inspect[0..100]}..."
317-
end
318-
end
319-
320-
if formatted_messages && formatted_messages[i] && formatted_messages[i][:content].is_a?(Array)
321-
RubyLLM.logger.debug "Message #{i}: Formatted content array:"
322-
formatted_messages[i][:content].each_with_index do |item, j|
323-
RubyLLM.logger.debug " Item #{j}: #{item.inspect[0..100]}..."
324-
end
325-
end
326-
end
327-
328-
RubyLLM.logger.debug "==== END DIAGNOSTICS ===="
329-
end
330217
end
331218
end
332219
end

spec/spec_helper.rb

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,28 @@
33
require "simplecov"
44
require "simplecov-cobertura"
55
require "codecov"
6-
require "vcr"
6+
require "ruby_llm"
77

88
SimpleCov.start do
99
enable_coverage :branch
1010

1111
formatter SimpleCov::Formatter::MultiFormatter.new(
1212
[
1313
SimpleCov::Formatter::SimpleFormatter,
14-
(SimpleCov::Formatter::Codecov if ENV["CODECOV_TOKEN"]),
14+
SimpleCov::Formatter::Codecov,
1515
SimpleCov::Formatter::CoberturaFormatter,
16-
].compact
16+
]
1717
)
1818
end
1919

2020
require "active_record"
2121
require "bundler/setup"
2222
require "fileutils"
23-
require "ruby_llm"
24-
require "webmock/rspec"
25-
26-
# VCR Configuration
27-
VCR.configure do |config|
28-
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
29-
config.hook_into :webmock
30-
config.configure_rspec_metadata!
31-
32-
# Don't record new HTTP interactions when running in CI
33-
config.default_cassette_options = {
34-
record: ENV["CI"] ? :none : :new_episodes,
35-
}
36-
37-
# Create new cassette directory if it doesn't exist
38-
FileUtils.mkdir_p(config.cassette_library_dir)
39-
40-
# Allow HTTP connections when necessary - this will fail PRs by design if they don't have cassettes
41-
config.allow_http_connections_when_no_cassette = true
42-
43-
# Filter out API keys from the recorded cassettes
44-
config.filter_sensitive_data("<OPENAI_API_KEY>") { ENV.fetch("OPENAI_API_KEY", nil) }
45-
config.filter_sensitive_data("<ANTHROPIC_API_KEY>") { ENV.fetch("ANTHROPIC_API_KEY", nil) }
46-
config.filter_sensitive_data("<GEMINI_API_KEY>") { ENV.fetch("GEMINI_API_KEY", nil) }
47-
config.filter_sensitive_data("<DEEPSEEK_API_KEY>") { ENV.fetch("DEEPSEEK_API_KEY", nil) }
48-
49-
# Filter sensitive response headers
50-
config.filter_sensitive_data("<OPENAI_ORGANIZATION>") do |interaction|
51-
interaction.response.headers["Openai-Organization"]&.first
52-
end
53-
config.filter_sensitive_data("<X_REQUEST_ID>") { |interaction| interaction.response.headers["X-Request-Id"]&.first }
54-
config.filter_sensitive_data("<REQUEST_ID>") { |interaction| interaction.response.headers["Request-Id"]&.first }
55-
config.filter_sensitive_data("<CF_RAY>") { |interaction| interaction.response.headers["Cf-Ray"]&.first }
56-
57-
# Filter cookies
58-
config.before_record do |interaction|
59-
if interaction.response.headers["Set-Cookie"]
60-
interaction.response.headers["Set-Cookie"] = interaction.response.headers["Set-Cookie"].map { "<COOKIE>" }
61-
end
62-
end
63-
end
6423

6524
RSpec.configure do |config|
6625
# Enable flags like --only-failures and --next-failure
6726
config.example_status_persistence_file_path = ".rspec_status"
27+
config.example_status_persistence_file_path = ".rspec_status"
6828

6929
# Disable RSpec exposing methods globally on `Module` and `main`
7030
config.disable_monkey_patching!
@@ -74,16 +34,23 @@
7434
end
7535

7636
config.around do |example|
37+
cassette_name = example.full_description.parameterize(separator: "_").delete_prefix("rubyllm_")
7738
cassette_name = example.full_description.parameterize(separator: "_").delete_prefix("rubyllm_")
7839
VCR.use_cassette(cassette_name) do
7940
example.run
8041
end
8142
end
8243
end
8344

45+
RSpec.shared_context "with configured RubyLLM" do
8446
RSpec.shared_context "with configured RubyLLM" do
8547
before do
8648
RubyLLM.configure do |config|
49+
config.openai_api_key = ENV.fetch("OPENAI_API_KEY", "dummy-openai-key")
50+
config.anthropic_api_key = ENV.fetch("ANTHROPIC_API_KEY", "dummy-anthropic-key")
51+
config.gemini_api_key = ENV.fetch("GEMINI_API_KEY", "dummy-gemini-key")
52+
config.deepseek_api_key = ENV.fetch("DEEPSEEK_API_KEY", "dummy-deepseek-key")
53+
config.mistral_api_key = ENV.fetch("MISTRAL_API_KEY", "dummy-mistral-key")
8754
config.openai_api_key = ENV.fetch("OPENAI_API_KEY", "dummy-openai-key")
8855
config.anthropic_api_key = ENV.fetch("ANTHROPIC_API_KEY", "dummy-anthropic-key")
8956
config.gemini_api_key = ENV.fetch("GEMINI_API_KEY", "dummy-gemini-key")

0 commit comments

Comments
 (0)