ruby_llm 1.3.2beta1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +39 -18
  3. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
  4. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  6. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
  7. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
  8. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
  9. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
  10. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  11. data/lib/generators/ruby_llm/install_generator.rb +121 -0
  12. data/lib/ruby_llm/active_record/acts_as.rb +23 -5
  13. data/lib/ruby_llm/aliases.json +6 -21
  14. data/lib/ruby_llm/chat.rb +46 -3
  15. data/lib/ruby_llm/configuration.rb +4 -0
  16. data/lib/ruby_llm/error.rb +1 -0
  17. data/lib/ruby_llm/message.rb +3 -1
  18. data/lib/ruby_llm/models.json +8461 -6427
  19. data/lib/ruby_llm/provider.rb +12 -6
  20. data/lib/ruby_llm/providers/anthropic/chat.rb +13 -12
  21. data/lib/ruby_llm/providers/anthropic/media.rb +2 -0
  22. data/lib/ruby_llm/providers/anthropic/tools.rb +23 -13
  23. data/lib/ruby_llm/providers/bedrock/chat.rb +4 -5
  24. data/lib/ruby_llm/providers/bedrock/media.rb +2 -0
  25. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +2 -2
  26. data/lib/ruby_llm/providers/gemini/chat.rb +37 -2
  27. data/lib/ruby_llm/providers/gemini/media.rb +2 -0
  28. data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
  29. data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
  30. data/lib/ruby_llm/providers/gpustack.rb +36 -0
  31. data/lib/ruby_llm/providers/mistral/capabilities.rb +151 -0
  32. data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
  33. data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
  34. data/lib/ruby_llm/providers/mistral/models.rb +49 -0
  35. data/lib/ruby_llm/providers/mistral.rb +37 -0
  36. data/lib/ruby_llm/providers/ollama/media.rb +2 -0
  37. data/lib/ruby_llm/providers/openai/chat.rb +17 -2
  38. data/lib/ruby_llm/providers/openai/media.rb +2 -0
  39. data/lib/ruby_llm/providers/openai/streaming.rb +14 -0
  40. data/lib/ruby_llm/providers/perplexity/capabilities.rb +167 -0
  41. data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
  42. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  43. data/lib/ruby_llm/providers/perplexity.rb +57 -0
  44. data/lib/ruby_llm/railtie.rb +5 -0
  45. data/lib/ruby_llm/stream_accumulator.rb +3 -2
  46. data/lib/ruby_llm/streaming.rb +25 -7
  47. data/lib/ruby_llm/utils.rb +10 -0
  48. data/lib/ruby_llm/version.rb +1 -1
  49. data/lib/ruby_llm.rb +11 -4
  50. data/lib/tasks/models_docs.rake +7 -7
  51. data/lib/tasks/models_update.rake +2 -0
  52. metadata +22 -1
@@ -10,14 +10,20 @@ module RubyLLM
10
10
  module Methods
11
11
  extend Streaming
12
12
 
13
- def complete(messages, tools:, temperature:, model:, connection:, &)
13
+ def complete(messages, tools:, temperature:, model:, connection:, params: {}, schema: nil, &) # rubocop:disable Metrics/ParameterLists
14
14
  normalized_temperature = maybe_normalize_temperature(temperature, model)
15
15
 
16
- payload = render_payload(messages,
17
- tools: tools,
18
- temperature: normalized_temperature,
19
- model: model,
20
- stream: block_given?)
16
+ payload = Utils.deep_merge(
17
+ params,
18
+ render_payload(
19
+ messages,
20
+ tools: tools,
21
+ temperature: normalized_temperature,
22
+ model: model,
23
+ stream: block_given?,
24
+ schema: schema
25
+ )
26
+ )
21
27
 
22
28
  if block_given?
23
29
  stream_response connection, payload, &
@@ -11,12 +11,12 @@ module RubyLLM
11
11
  '/v1/messages'
12
12
  end
13
13
 
14
- def render_payload(messages, tools:, temperature:, model:, stream: false)
14
+ def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
15
15
  system_messages, chat_messages = separate_messages(messages)
16
16
  system_content = build_system_content(system_messages)
17
17
 
18
- build_base_payload(chat_messages, temperature, model, stream).tap do |payload|
19
- add_optional_fields(payload, system_content:, tools:)
18
+ build_base_payload(chat_messages, model, stream).tap do |payload|
19
+ add_optional_fields(payload, system_content:, tools:, temperature:)
20
20
  end
21
21
  end
22
22
 
@@ -32,22 +32,22 @@ module RubyLLM
32
32
  )
33
33
  end
34
34
 
35
- system_messages.map { |msg| format_message(msg)[:content] }.join("\n\n")
35
+ system_messages.map(&:content).join("\n\n")
36
36
  end
37
37
 
38
- def build_base_payload(chat_messages, temperature, model, stream)
38
+ def build_base_payload(chat_messages, model, stream)
39
39
  {
40
40
  model: model,
41
41
  messages: chat_messages.map { |msg| format_message(msg) },
42
- temperature: temperature,
43
42
  stream: stream,
44
43
  max_tokens: RubyLLM.models.find(model)&.max_tokens || 4096
45
44
  }
46
45
  end
47
46
 
48
- def add_optional_fields(payload, system_content:, tools:)
47
+ def add_optional_fields(payload, system_content:, tools:, temperature:)
49
48
  payload[:tools] = tools.values.map { |t| Tools.function_for(t) } if tools.any?
50
49
  payload[:system] = system_content unless system_content.empty?
50
+ payload[:temperature] = temperature unless temperature.nil?
51
51
  end
52
52
 
53
53
  def parse_completion_response(response)
@@ -55,9 +55,9 @@ module RubyLLM
55
55
  content_blocks = data['content'] || []
56
56
 
57
57
  text_content = extract_text_content(content_blocks)
58
- tool_use = Tools.find_tool_use(content_blocks)
58
+ tool_use_blocks = Tools.find_tool_uses(content_blocks)
59
59
 
60
- build_message(data, text_content, tool_use)
60
+ build_message(data, text_content, tool_use_blocks, response)
61
61
  end
62
62
 
63
63
  def extract_text_content(blocks)
@@ -65,14 +65,15 @@ module RubyLLM
65
65
  text_blocks.map { |c| c['text'] }.join
66
66
  end
67
67
 
68
- def build_message(data, content, tool_use)
68
+ def build_message(data, content, tool_use_blocks, response)
69
69
  Message.new(
70
70
  role: :assistant,
71
71
  content: content,
72
- tool_calls: Tools.parse_tool_calls(tool_use),
72
+ tool_calls: Tools.parse_tool_calls(tool_use_blocks),
73
73
  input_tokens: data.dig('usage', 'input_tokens'),
74
74
  output_tokens: data.dig('usage', 'output_tokens'),
75
- model_id: data['model']
75
+ model_id: data['model'],
76
+ raw: response
76
77
  )
77
78
  end
78
79
 
@@ -8,6 +8,8 @@ module RubyLLM
8
8
  module_function
9
9
 
10
10
  def format_content(content)
11
+ # Convert Hash/Array back to JSON string for API
12
+ return [format_text(content.to_json)] if content.is_a?(Hash) || content.is_a?(Array)
11
13
  return [format_text(content)] unless content.is_a?(Content)
12
14
 
13
15
  parts = []
@@ -7,16 +7,18 @@ module RubyLLM
7
7
  module Tools
8
8
  module_function
9
9
 
10
- def find_tool_use(blocks)
11
- blocks.find { |c| c['type'] == 'tool_use' }
10
+ def find_tool_uses(blocks)
11
+ blocks.select { |c| c['type'] == 'tool_use' }
12
12
  end
13
13
 
14
14
  def format_tool_call(msg)
15
- tool_call = msg.tool_calls.values.first
16
-
17
15
  content = []
16
+
18
17
  content << Media.format_text(msg.content) unless msg.content.nil? || msg.content.empty?
19
- content << format_tool_use_block(tool_call)
18
+
19
+ msg.tool_calls.each_value do |tool_call|
20
+ content << format_tool_use_block(tool_call)
21
+ end
20
22
 
21
23
  {
22
24
  role: 'assistant',
@@ -68,16 +70,24 @@ module RubyLLM
68
70
  end
69
71
  end
70
72
 
71
- def parse_tool_calls(content_block)
72
- return nil unless content_block && content_block['type'] == 'tool_use'
73
+ def parse_tool_calls(content_blocks)
74
+ return nil if content_blocks.nil?
73
75
 
74
- {
75
- content_block['id'] => ToolCall.new(
76
- id: content_block['id'],
77
- name: content_block['name'],
78
- arguments: content_block['input']
76
+ # Handle single content block (backward compatibility)
77
+ content_blocks = [content_blocks] unless content_blocks.is_a?(Array)
78
+
79
+ tool_calls = {}
80
+ content_blocks.each do |block|
81
+ next unless block && block['type'] == 'tool_use'
82
+
83
+ tool_calls[block['id']] = ToolCall.new(
84
+ id: block['id'],
85
+ name: block['name'],
86
+ arguments: block['input']
79
87
  )
80
- }
88
+ end
89
+
90
+ tool_calls.empty? ? nil : tool_calls
81
91
  end
82
92
 
83
93
  def clean_parameters(parameters)
@@ -39,23 +39,22 @@ module RubyLLM
39
39
  "model/#{@model_id}/invoke"
40
40
  end
41
41
 
42
- def render_payload(messages, tools:, temperature:, model:, stream: false) # rubocop:disable Lint/UnusedMethodArgument
42
+ def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/ParameterLists
43
43
  # Hold model_id in instance variable for use in completion_url and stream_url
44
44
  @model_id = model
45
45
 
46
46
  system_messages, chat_messages = Anthropic::Chat.separate_messages(messages)
47
47
  system_content = Anthropic::Chat.build_system_content(system_messages)
48
48
 
49
- build_base_payload(chat_messages, temperature, model).tap do |payload|
50
- Anthropic::Chat.add_optional_fields(payload, system_content:, tools:)
49
+ build_base_payload(chat_messages, model).tap do |payload|
50
+ Anthropic::Chat.add_optional_fields(payload, system_content:, tools:, temperature:)
51
51
  end
52
52
  end
53
53
 
54
- def build_base_payload(chat_messages, temperature, model)
54
+ def build_base_payload(chat_messages, model)
55
55
  {
56
56
  anthropic_version: 'bedrock-2023-05-31',
57
57
  messages: chat_messages.map { |msg| format_message(msg) },
58
- temperature: temperature,
59
58
  max_tokens: RubyLLM.models.find(model)&.max_tokens || 4096
60
59
  }
61
60
  end
@@ -11,6 +11,8 @@ module RubyLLM
11
11
  module_function
12
12
 
13
13
  def format_content(content)
14
+ # Convert Hash/Array back to JSON string for API
15
+ return [Anthropic::Media.format_text(content.to_json)] if content.is_a?(Hash) || content.is_a?(Array)
14
16
  return [Anthropic::Media.format_text(content)] unless content.is_a?(Content)
15
17
 
16
18
  parts = []
@@ -34,7 +34,7 @@ module RubyLLM
34
34
  payload:)
35
35
  accumulator = StreamAccumulator.new
36
36
 
37
- connection.post stream_url, payload do |req|
37
+ response = connection.post stream_url, payload do |req|
38
38
  req.headers.merge! build_headers(signature.headers, streaming: block_given?)
39
39
  req.options.on_data = handle_stream do |chunk|
40
40
  accumulator.add chunk
@@ -42,7 +42,7 @@ module RubyLLM
42
42
  end
43
43
  end
44
44
 
45
- accumulator.to_message
45
+ accumulator.to_message(response)
46
46
  end
47
47
 
48
48
  def handle_stream(&block)
@@ -11,7 +11,7 @@ module RubyLLM
11
11
  "models/#{@model}:generateContent"
12
12
  end
13
13
 
14
- def render_payload(messages, tools:, temperature:, model:, stream: false) # rubocop:disable Lint/UnusedMethodArgument
14
+ def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
15
15
  @model = model # Store model for completion_url/stream_url
16
16
  payload = {
17
17
  contents: format_messages(messages),
@@ -19,6 +19,12 @@ module RubyLLM
19
19
  temperature: temperature
20
20
  }
21
21
  }
22
+
23
+ if schema
24
+ payload[:generationConfig][:responseMimeType] = 'application/json'
25
+ payload[:generationConfig][:responseSchema] = convert_schema_to_gemini(schema)
26
+ end
27
+
22
28
  payload[:tools] = format_tools(tools) if tools.any?
23
29
  payload
24
30
  end
@@ -75,10 +81,39 @@ module RubyLLM
75
81
  tool_calls: tool_calls,
76
82
  input_tokens: data.dig('usageMetadata', 'promptTokenCount'),
77
83
  output_tokens: data.dig('usageMetadata', 'candidatesTokenCount'),
78
- model_id: data['modelVersion'] || response.env.url.path.split('/')[3].split(':')[0]
84
+ model_id: data['modelVersion'] || response.env.url.path.split('/')[3].split(':')[0],
85
+ raw: response
79
86
  )
80
87
  end
81
88
 
89
+ def convert_schema_to_gemini(schema) # rubocop:disable Metrics/PerceivedComplexity
90
+ return nil unless schema
91
+
92
+ case schema[:type]
93
+ when 'object'
94
+ {
95
+ type: 'OBJECT',
96
+ properties: schema[:properties]&.transform_values { |prop| convert_schema_to_gemini(prop) } || {},
97
+ required: schema[:required] || []
98
+ }
99
+ when 'array'
100
+ {
101
+ type: 'ARRAY',
102
+ items: schema[:items] ? convert_schema_to_gemini(schema[:items]) : { type: 'STRING' }
103
+ }
104
+ when 'string'
105
+ result = { type: 'STRING' }
106
+ result[:enum] = schema[:enum] if schema[:enum]
107
+ result
108
+ when 'number', 'integer'
109
+ { type: 'NUMBER' }
110
+ when 'boolean'
111
+ { type: 'BOOLEAN' }
112
+ else
113
+ { type: 'STRING' }
114
+ end
115
+ end
116
+
82
117
  def extract_content(data)
83
118
  candidate = data.dig('candidates', 0)
84
119
  return '' unless candidate
@@ -8,6 +8,8 @@ module RubyLLM
8
8
  module_function
9
9
 
10
10
  def format_content(content)
11
+ # Convert Hash/Array back to JSON string for API
12
+ return [format_text(content.to_json)] if content.is_a?(Hash) || content.is_a?(Array)
11
13
  return [format_text(content)] unless content.is_a?(Content)
12
14
 
13
15
  parts = []
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module GPUStack
6
+ # Chat methods of the GPUStack API integration
7
+ module Chat
8
+ module_function
9
+
10
+ def format_role(role)
11
+ # GPUStack doesn't use the new OpenAI convention for system prompts
12
+ role.to_s
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module GPUStack
6
+ # Models methods of the GPUStack API integration
7
+ module Models
8
+ module_function
9
+
10
+ def models_url
11
+ 'models'
12
+ end
13
+
14
+ def parse_list_models_response(response, slug, _capabilities)
15
+ items = response.body['items'] || []
16
+ items.map do |model|
17
+ Model::Info.new(
18
+ id: model['name'],
19
+ created_at: model['created_at'] ? Time.parse(model['created_at']) : nil,
20
+ display_name: "#{model['source']}/#{model['name']}",
21
+ provider: slug,
22
+ type: determine_model_type(model),
23
+ metadata: {
24
+ description: model['description'],
25
+ source: model['source'],
26
+ huggingface_repo_id: model['huggingface_repo_id'],
27
+ ollama_library_model_name: model['ollama_library_model_name'],
28
+ backend: model['backend'],
29
+ meta: model['meta'],
30
+ categories: model['categories']
31
+ },
32
+ context_window: model.dig('meta', 'n_ctx'),
33
+ # Using context window as max tokens since it's not explicitly provided
34
+ max_tokens: model.dig('meta', 'n_ctx'),
35
+ supports_vision: model.dig('meta', 'support_vision') || false,
36
+ supports_functions: model.dig('meta', 'support_tool_calls') || false,
37
+ supports_json_mode: true, # Assuming all models support JSON mode
38
+ input_price_per_million: 0.0, # Price information not available in new format
39
+ output_price_per_million: 0.0 # Price information not available in new format
40
+ )
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def determine_model_type(model)
47
+ return 'embedding' if model['categories']&.include?('embedding')
48
+ return 'chat' if model['categories']&.include?('llm')
49
+
50
+ 'other'
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # GPUStack API integration based on Ollama.
6
+ module GPUStack
7
+ extend OpenAI
8
+ extend GPUStack::Chat
9
+ extend GPUStack::Models
10
+
11
+ module_function
12
+
13
+ def api_base(config)
14
+ config.gpustack_api_base
15
+ end
16
+
17
+ def headers(config)
18
+ {
19
+ 'Authorization' => "Bearer #{config.gpustack_api_key}"
20
+ }
21
+ end
22
+
23
+ def slug
24
+ 'gpustack'
25
+ end
26
+
27
+ def local?
28
+ true
29
+ end
30
+
31
+ def configuration_requirements
32
+ %i[gpustack_api_base gpustack_api_key]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Mistral
6
+ # Determines capabilities for Mistral models
7
+ module Capabilities
8
+ module_function
9
+
10
+ def supports_streaming?(_model_id)
11
+ true
12
+ end
13
+
14
+ def supports_tools?(_model_id)
15
+ true
16
+ end
17
+
18
+ def supports_vision?(model_id)
19
+ model_id.include?('pixtral')
20
+ end
21
+
22
+ def supports_json_mode?(_model_id)
23
+ true
24
+ end
25
+
26
+ def format_display_name(model_id)
27
+ case model_id
28
+ when /mistral-large/ then 'Mistral Large'
29
+ when /mistral-medium/ then 'Mistral Medium'
30
+ when /mistral-small/ then 'Mistral Small'
31
+ when /ministral-3b/ then 'Ministral 3B'
32
+ when /ministral-8b/ then 'Ministral 8B'
33
+ when /codestral/ then 'Codestral'
34
+ when /pixtral-large/ then 'Pixtral Large'
35
+ when /pixtral-12b/ then 'Pixtral 12B'
36
+ when /mistral-embed/ then 'Mistral Embed'
37
+ when /mistral-moderation/ then 'Mistral Moderation'
38
+ else model_id.split('-').map(&:capitalize).join(' ')
39
+ end
40
+ end
41
+
42
+ def model_family(model_id)
43
+ case model_id
44
+ when /mistral-large/ then 'mistral-large'
45
+ when /mistral-medium/ then 'mistral-medium'
46
+ when /mistral-small/ then 'mistral-small'
47
+ when /ministral/ then 'ministral'
48
+ when /codestral/ then 'codestral'
49
+ when /pixtral/ then 'pixtral'
50
+ when /mistral-embed/ then 'mistral-embed'
51
+ when /mistral-moderation/ then 'mistral-moderation'
52
+ else 'mistral'
53
+ end
54
+ end
55
+
56
+ def context_window_for(_model_id)
57
+ 32_768 # Default for most Mistral models
58
+ end
59
+
60
+ def max_tokens_for(_model_id)
61
+ 8192 # Default for most Mistral models
62
+ end
63
+
64
+ def modalities_for(model_id)
65
+ case model_id
66
+ when /pixtral/
67
+ {
68
+ input: %w[text image],
69
+ output: ['text']
70
+ }
71
+ when /embed/
72
+ {
73
+ input: ['text'],
74
+ output: ['embedding']
75
+ }
76
+ else
77
+ {
78
+ input: ['text'],
79
+ output: ['text']
80
+ }
81
+ end
82
+ end
83
+
84
+ def capabilities_for(model_id)
85
+ case model_id
86
+ when /embed/ then { embeddings: true }
87
+ when /moderation/ then { moderation: true }
88
+ else
89
+ {
90
+ chat: true,
91
+ streaming: supports_streaming?(model_id),
92
+ tools: supports_tools?(model_id),
93
+ vision: supports_vision?(model_id),
94
+ json_mode: supports_json_mode?(model_id)
95
+ }
96
+ end
97
+ end
98
+
99
+ def pricing_for(_model_id)
100
+ {
101
+ input: 0.0,
102
+ output: 0.0
103
+ }
104
+ end
105
+
106
+ def release_date_for(model_id)
107
+ case model_id
108
+ # 2023 releases
109
+ when 'open-mistral-7b', 'mistral-tiny' then '2023-09-27'
110
+ when 'mistral-medium-2312', 'mistral-small-2312', 'mistral-small',
111
+ 'open-mixtral-8x7b', 'mistral-tiny-2312' then '2023-12-11'
112
+
113
+ # 2024 releases
114
+ when 'mistral-embed' then '2024-01-11'
115
+ when 'mistral-large-2402', 'mistral-small-2402' then '2024-02-26'
116
+ when 'open-mixtral-8x22b', 'open-mixtral-8x22b-2404' then '2024-04-17'
117
+ when 'codestral-2405' then '2024-05-22'
118
+ when 'codestral-mamba-2407', 'codestral-mamba-latest', 'open-codestral-mamba' then '2024-07-16'
119
+ when 'open-mistral-nemo', 'open-mistral-nemo-2407', 'mistral-tiny-2407',
120
+ 'mistral-tiny-latest' then '2024-07-18'
121
+ when 'mistral-large-2407' then '2024-07-24'
122
+ when 'pixtral-12b-2409', 'pixtral-12b-latest', 'pixtral-12b' then '2024-09-17'
123
+ when 'mistral-small-2409' then '2024-09-18'
124
+ when 'ministral-3b-2410', 'ministral-3b-latest', 'ministral-8b-2410',
125
+ 'ministral-8b-latest' then '2024-10-16'
126
+ when 'pixtral-large-2411', 'pixtral-large-latest', 'mistral-large-pixtral-2411' then '2024-11-12'
127
+ when 'mistral-large-2411', 'mistral-large-latest', 'mistral-large' then '2024-11-20'
128
+ when 'codestral-2411-rc5', 'mistral-moderation-2411', 'mistral-moderation-latest' then '2024-11-26'
129
+ when 'codestral-2412' then '2024-12-17'
130
+
131
+ # 2025 releases
132
+ when 'mistral-small-2501' then '2025-01-13'
133
+ when 'codestral-2501' then '2025-01-14'
134
+ when 'mistral-saba-2502', 'mistral-saba-latest' then '2025-02-18'
135
+ when 'mistral-small-2503' then '2025-03-03'
136
+ when 'mistral-ocr-2503' then '2025-03-21'
137
+ when 'mistral-medium', 'mistral-medium-latest', 'mistral-medium-2505' then '2025-05-06'
138
+ when 'codestral-embed', 'codestral-embed-2505' then '2025-05-21'
139
+ when 'mistral-ocr-2505', 'mistral-ocr-latest' then '2025-05-23'
140
+ when 'devstral-small-2505' then '2025-05-28'
141
+ when 'mistral-small-2506', 'mistral-small-latest', 'magistral-medium-2506',
142
+ 'magistral-medium-latest' then '2025-06-10'
143
+ when 'devstral-small-2507', 'devstral-small-latest', 'devstral-medium-2507',
144
+ 'devstral-medium-latest' then '2025-07-09'
145
+ when 'codestral-2508', 'codestral-latest' then '2025-08-30'
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Mistral
6
+ # Chat methods for Mistral API
7
+ module Chat
8
+ module_function
9
+
10
+ def format_role(role)
11
+ # Mistral doesn't use the new OpenAI convention for system prompts
12
+ role.to_s
13
+ end
14
+
15
+ # rubocop:disable Metrics/ParameterLists
16
+ def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil)
17
+ payload = super
18
+ # Mistral doesn't support stream_options
19
+ payload.delete(:stream_options)
20
+ payload
21
+ end
22
+ # rubocop:enable Metrics/ParameterLists
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Mistral
6
+ # Embeddings methods for Mistral API
7
+ module Embeddings
8
+ module_function
9
+
10
+ def embedding_url(...)
11
+ 'embeddings'
12
+ end
13
+
14
+ def render_embedding_payload(text, model:, dimensions:) # rubocop:disable Lint/UnusedMethodArgument
15
+ # Mistral doesn't support dimensions parameter
16
+ {
17
+ model: model,
18
+ input: text
19
+ }
20
+ end
21
+
22
+ def parse_embedding_response(response, model:, text:)
23
+ data = response.body
24
+ input_tokens = data.dig('usage', 'prompt_tokens') || 0
25
+ vectors = data['data'].map { |d| d['embedding'] }
26
+
27
+ # If we only got one embedding AND the input was a single string (not an array),
28
+ # return it as a single vector
29
+ vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
30
+
31
+ Embedding.new(vectors:, model:, input_tokens:)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Mistral
6
+ # Model information for Mistral
7
+ module Models
8
+ module_function
9
+
10
+ def models_url
11
+ 'models'
12
+ end
13
+
14
+ def headers(config)
15
+ {
16
+ 'Authorization' => "Bearer #{config.mistral_api_key}"
17
+ }
18
+ end
19
+
20
+ def parse_list_models_response(response, slug, capabilities)
21
+ Array(response.body['data']).map do |model_data|
22
+ model_id = model_data['id']
23
+
24
+ # Use fixed release date for Mistral models
25
+ release_date = capabilities.release_date_for(model_id)
26
+ created_at = release_date ? Time.parse(release_date) : nil
27
+
28
+ Model::Info.new(
29
+ id: model_id,
30
+ name: capabilities.format_display_name(model_id),
31
+ provider: slug,
32
+ family: capabilities.model_family(model_id),
33
+ created_at: created_at,
34
+ context_window: capabilities.context_window_for(model_id),
35
+ max_output_tokens: capabilities.max_tokens_for(model_id),
36
+ modalities: capabilities.modalities_for(model_id),
37
+ capabilities: capabilities.capabilities_for(model_id),
38
+ pricing: capabilities.pricing_for(model_id),
39
+ metadata: {
40
+ object: model_data['object'],
41
+ owned_by: model_data['owned_by']
42
+ }
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end