Skip to content

Commit 0df2751

Browse files
committed
Base mistral implementation
1 parent 347e630 commit 0df2751

File tree

15 files changed

+724
-4
lines changed

15 files changed

+724
-4
lines changed

lib/ruby_llm.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
'api' => 'API',
1818
'deepseek' => 'DeepSeek',
1919
'bedrock' => 'Bedrock',
20-
'openrouter' => 'OpenRouter'
20+
'openrouter' => 'OpenRouter',
21+
'mistral' => 'Mistral'
2122
)
2223
loader.ignore("#{__dir__}/tasks")
2324
loader.ignore("#{__dir__}/ruby_llm/railtie")
@@ -82,6 +83,7 @@ def logger
8283
RubyLLM::Provider.register :bedrock, RubyLLM::Providers::Bedrock
8384
RubyLLM::Provider.register :openrouter, RubyLLM::Providers::OpenRouter
8485
RubyLLM::Provider.register :ollama, RubyLLM::Providers::Ollama
86+
RubyLLM::Provider.register :mistral, RubyLLM::Providers::Mistral
8587

8688
if defined?(Rails::Railtie)
8789
require 'ruby_llm/railtie'

lib/ruby_llm/aliases.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,14 @@
7676
"o3-mini": {
7777
"openai": "o3-mini",
7878
"openrouter": "openai/o3-mini"
79+
},
80+
"mistral-medium": {
81+
"mistral": "mistral-medium-latest"
82+
},
83+
"mistral-large": {
84+
"mistral": "mistral-large-latest"
85+
},
86+
"mistral-small": {
87+
"mistral": "mistral-small-latest"
7988
}
8089
}

lib/ruby_llm/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Configuration
2222
:bedrock_session_token,
2323
:openrouter_api_key,
2424
:ollama_api_base,
25+
:mistral_api_key,
2526
# Default models
2627
:default_model,
2728
:default_embedding_model,

lib/ruby_llm/models.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11409,6 +11409,24 @@
1140911409
}
1141011410
}
1141111411
},
11412+
{
11413+
"id": "mistralai/mistral-embed",
11414+
"created_at": "2024-03-15T00:00:00Z",
11415+
"display_name": "Mistral Embed",
11416+
"provider": "mistral",
11417+
"context_window": 32768,
11418+
"max_tokens": 32768,
11419+
"type": "embedding",
11420+
"family": "mistral_embed",
11421+
"supports_vision": false,
11422+
"supports_functions": false,
11423+
"supports_json_mode": false,
11424+
"input_price_per_million": 0.01,
11425+
"output_price_per_million": 0.01,
11426+
"metadata": {
11427+
"description": "Mistral Embed - specialized model for generating text embeddings"
11428+
}
11429+
},
1141211430
{
1141311431
"id": "moonshotai/kimi-vl-a3b-thinking:free",
1141411432
"created_at": "2025-04-10T19:07:21+02:00",

lib/ruby_llm/providers/mistral.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
module RubyLLM
2+
module Providers
3+
# Mistral API integration. Handles chat completion, embeddings,
4+
# and Mistral's streaming format. Supports Mistral models.
5+
module Mistral
6+
extend Provider
7+
extend Mistral::Chat
8+
extend Mistral::Embeddings
9+
extend Mistral::Models
10+
extend Mistral::Streaming
11+
extend Mistral::Tools
12+
extend Mistral::Capabilities
13+
extend Mistral::Media
14+
15+
def self.extended(base)
16+
base.extend(Provider)
17+
base.extend(Mistral::Chat)
18+
base.extend(Mistral::Embeddings)
19+
base.extend(Mistral::Models)
20+
base.extend(Mistral::Streaming)
21+
base.extend(Mistral::Tools)
22+
base.extend(Mistral::Capabilities)
23+
base.extend(Mistral::Media)
24+
end
25+
26+
module_function
27+
28+
def api_base
29+
'https://api.mistral.ai/v1'
30+
end
31+
32+
def headers
33+
{
34+
'Authorization' => "Bearer #{RubyLLM.config.mistral_api_key}"
35+
}
36+
end
37+
38+
def capabilities
39+
Mistral::Capabilities
40+
end
41+
42+
def slug
43+
'mistral'
44+
end
45+
46+
def configuration_requirements
47+
%i[mistral_api_key]
48+
end
49+
end
50+
end
51+
end
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
module RubyLLM
2+
module Providers
3+
module Mistral
4+
# Determines capabilities and pricing for Mistral models
5+
module Capabilities
6+
module_function
7+
8+
# Returns the context window size for the given model ID
9+
# @param model_id [String] the model identifier
10+
# @return [Integer] the context window size in tokens
11+
def context_window_for(model_id)
12+
case model_id
13+
when /mistral-large-2411/ then 131_000
14+
when /mistral-small-latest/ then 32_000
15+
when /mistral-medium-latest/ then 32_000
16+
when /ministral-3b-latest/ then 131_000
17+
when /ministral-8b-latest/ then 131_000
18+
when /codestral-2501/ then 256_000
19+
when /pixtral-large-latest/ then 131_000
20+
when /mistral-saba-latest/ then 32_000
21+
when /mistral-embed/ then 8_000
22+
when /mistral-moderation-latest/ then 8_000
23+
when /codestral-mamba-latest/ then 1_000_000 # Using a large number for "Infinite-length"
24+
else 32_000 # Default to smallest common size
25+
end
26+
end
27+
28+
# Returns the maximum output tokens for the given model ID
29+
# @param model_id [String] the model identifier
30+
# @return [Integer] the maximum output tokens
31+
def max_tokens_for(model_id)
32+
# Generally, max output tokens is slightly less than context window
33+
(context_window_for(model_id) * 0.9).to_i
34+
end
35+
36+
# Returns the input price per million tokens for the given model ID
37+
# @param model_id [String] the model identifier
38+
# @return [Float] the price per million tokens for input
39+
def input_price_for(model_id)
40+
PRICES.dig(model_family(model_id), :input) || default_input_price
41+
end
42+
43+
# Returns the output price per million tokens for the given model ID
44+
# @param model_id [String] the model identifier
45+
# @return [Float] the price per million tokens for output
46+
def output_price_for(model_id)
47+
PRICES.dig(model_family(model_id), :output) || default_output_price
48+
end
49+
50+
# Determines if the model supports vision capabilities
51+
# @param model_id [String] the model identifier
52+
# @return [Boolean] true if the model supports vision
53+
def supports_vision?(model_id)
54+
# Check for all vision-capable models in Mistral's lineup
55+
#
56+
# NOTE: While this correctly identifies models that support vision capabilities,
57+
# there are currently issues with the image handling in the test suite.
58+
# The 'pixtral-12b-latest can understand images' test fails because image data
59+
# isn't properly passed to the API. This requires fixes in the core Content handling code.
60+
#
61+
# Known vision-capable models in Mistral's lineup:
62+
# - pixtral-12b-latest
63+
# - pixtral-large-latest
64+
model_id.match?(/pixtral|mistral-large-vision|other-vision-models/)
65+
end
66+
67+
# Determines if the model supports function calling
68+
# @param model_id [String] the model identifier
69+
# @return [Boolean] true if the model supports functions
70+
def supports_functions?(model_id)
71+
!model_id.match?(/embed|moderation/)
72+
end
73+
74+
# Determines if the model supports audio input/output
75+
# @param model_id [String] the model identifier
76+
# @return [Boolean] true if the model supports audio
77+
def supports_audio?(_model_id)
78+
false
79+
end
80+
81+
# Determines if the model supports JSON mode
82+
# @param model_id [String] the model identifier
83+
# @return [Boolean] true if the model supports JSON mode
84+
def supports_json_mode?(model_id)
85+
!model_id.match?(/embed|moderation/)
86+
end
87+
88+
# Formats the model ID into a human-readable display name
89+
# @param model_id [String] the model identifier
90+
# @return [String] the formatted display name
91+
def format_display_name(model_id)
92+
model_id.then { |id| humanize(id) }
93+
.then { |name| apply_special_formatting(name) }
94+
end
95+
96+
# Determines the type of model
97+
# @param model_id [String] the model identifier
98+
# @return [String] the model type (chat, embedding, moderation)
99+
def model_type(model_id)
100+
case model_id
101+
when /embed/ then "embedding"
102+
when /moderation/ then "moderation"
103+
else "chat"
104+
end
105+
end
106+
107+
# Determines if the model supports structured output
108+
# @param model_id [String] the model identifier
109+
# @return [Boolean] true if the model supports structured output
110+
def supports_structured_output?(model_id)
111+
!model_id.match?(/embed|moderation/)
112+
end
113+
114+
# Determines the model family for pricing and capability lookup
115+
# @param model_id [String] the model identifier
116+
# @return [Symbol] the model family identifier
117+
def model_family(model_id)
118+
case model_id
119+
when /mistral-large/ then "large"
120+
when /mistral-small/ then "small"
121+
when /codestral/ then "codestral"
122+
when /pixtral/ then "pixtral"
123+
when /mistral-nemo/ then "nemo"
124+
when /embed/ then "embedding"
125+
else "other"
126+
end
127+
end
128+
129+
# Pricing information for Mistral models (per million tokens)
130+
PRICES = {
131+
"large" => { input: 2.00, output: 6.00 },
132+
"small" => { input: 0.20, output: 0.60 },
133+
"codestral" => { input: 0.30, output: 0.90 },
134+
"pixtral" => { input: 0.15, output: 0.15 },
135+
"nemo" => { input: 0.15, output: 0.15 },
136+
"embedding" => { price: 0.1 },
137+
}.freeze
138+
139+
# Default input price when model-specific pricing is not available
140+
# @return [Float] the default price per million tokens
141+
def default_input_price
142+
0.20 # Default to small model pricing
143+
end
144+
145+
# Default output price when model-specific pricing is not available
146+
# @return [Float] the default price per million tokens
147+
def default_output_price
148+
0.60 # Default to small model pricing
149+
end
150+
151+
# Converts a model ID to a human-readable format
152+
# @param id [String] the model identifier
153+
# @return [String] the humanized model name
154+
def humanize(id)
155+
id.tr("-", " ")
156+
.split
157+
.map(&:capitalize)
158+
.join(" ")
159+
end
160+
161+
# Applies special formatting rules to model names
162+
# @param name [String] the humanized model name
163+
# @return [String] the specially formatted model name
164+
def apply_special_formatting(name)
165+
name
166+
.gsub("Mistral ", "Mistral-")
167+
.gsub("Ministral ", "Ministral-")
168+
.gsub("Codestral ", "Codestral-")
169+
.gsub("Pixtral ", "Pixtral-")
170+
.gsub("Mathstral ", "Mathstral-")
171+
.gsub("Embed ", "Embed-")
172+
end
173+
end
174+
end
175+
end
176+
end

0 commit comments

Comments
 (0)