ruby_llm_community 0.0.1 → 0.0.3

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +22 -0
  3. data/README.md +172 -0
  4. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
  5. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
  8. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
  9. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
  10. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
  11. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  12. data/lib/generators/ruby_llm/install_generator.rb +121 -0
  13. data/lib/ruby_llm/active_record/acts_as.rb +382 -0
  14. data/lib/ruby_llm/aliases.json +217 -0
  15. data/lib/ruby_llm/aliases.rb +56 -0
  16. data/lib/ruby_llm/attachment.rb +164 -0
  17. data/lib/ruby_llm/chat.rb +226 -0
  18. data/lib/ruby_llm/chunk.rb +6 -0
  19. data/lib/ruby_llm/configuration.rb +73 -0
  20. data/lib/ruby_llm/connection.rb +126 -0
  21. data/lib/ruby_llm/content.rb +52 -0
  22. data/lib/ruby_llm/context.rb +29 -0
  23. data/lib/ruby_llm/embedding.rb +30 -0
  24. data/lib/ruby_llm/error.rb +84 -0
  25. data/lib/ruby_llm/image.rb +53 -0
  26. data/lib/ruby_llm/message.rb +81 -0
  27. data/lib/ruby_llm/mime_type.rb +67 -0
  28. data/lib/ruby_llm/model/info.rb +101 -0
  29. data/lib/ruby_llm/model/modalities.rb +22 -0
  30. data/lib/ruby_llm/model/pricing.rb +51 -0
  31. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  32. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  33. data/lib/ruby_llm/model.rb +7 -0
  34. data/lib/ruby_llm/models.json +29924 -0
  35. data/lib/ruby_llm/models.rb +214 -0
  36. data/lib/ruby_llm/models_schema.json +168 -0
  37. data/lib/ruby_llm/provider.rb +221 -0
  38. data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
  39. data/lib/ruby_llm/providers/anthropic/chat.rb +120 -0
  40. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  41. data/lib/ruby_llm/providers/anthropic/media.rb +116 -0
  42. data/lib/ruby_llm/providers/anthropic/models.rb +56 -0
  43. data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
  44. data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
  45. data/lib/ruby_llm/providers/anthropic.rb +37 -0
  46. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  47. data/lib/ruby_llm/providers/bedrock/chat.rb +76 -0
  48. data/lib/ruby_llm/providers/bedrock/media.rb +73 -0
  49. data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
  50. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  51. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
  52. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
  53. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
  54. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +92 -0
  55. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
  56. data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
  57. data/lib/ruby_llm/providers/bedrock.rb +83 -0
  58. data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
  59. data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
  60. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  61. data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
  62. data/lib/ruby_llm/providers/gemini/chat.rb +146 -0
  63. data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
  64. data/lib/ruby_llm/providers/gemini/images.rb +48 -0
  65. data/lib/ruby_llm/providers/gemini/media.rb +55 -0
  66. data/lib/ruby_llm/providers/gemini/models.rb +41 -0
  67. data/lib/ruby_llm/providers/gemini/streaming.rb +66 -0
  68. data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
  69. data/lib/ruby_llm/providers/gemini.rb +36 -0
  70. data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
  71. data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
  72. data/lib/ruby_llm/providers/gpustack.rb +33 -0
  73. data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
  74. data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
  75. data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
  76. data/lib/ruby_llm/providers/mistral/models.rb +49 -0
  77. data/lib/ruby_llm/providers/mistral.rb +32 -0
  78. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  79. data/lib/ruby_llm/providers/ollama/media.rb +50 -0
  80. data/lib/ruby_llm/providers/ollama.rb +29 -0
  81. data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
  82. data/lib/ruby_llm/providers/openai/chat.rb +87 -0
  83. data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
  84. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  85. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  86. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  87. data/lib/ruby_llm/providers/openai/response.rb +116 -0
  88. data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
  89. data/lib/ruby_llm/providers/openai/streaming.rb +191 -0
  90. data/lib/ruby_llm/providers/openai/tools.rb +100 -0
  91. data/lib/ruby_llm/providers/openai.rb +44 -0
  92. data/lib/ruby_llm/providers/openai_base.rb +44 -0
  93. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  94. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  95. data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
  96. data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
  97. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  98. data/lib/ruby_llm/providers/perplexity.rb +52 -0
  99. data/lib/ruby_llm/railtie.rb +17 -0
  100. data/lib/ruby_llm/stream_accumulator.rb +103 -0
  101. data/lib/ruby_llm/streaming.rb +162 -0
  102. data/lib/ruby_llm/tool.rb +100 -0
  103. data/lib/ruby_llm/tool_call.rb +31 -0
  104. data/lib/ruby_llm/utils.rb +49 -0
  105. data/lib/ruby_llm/version.rb +5 -0
  106. data/lib/ruby_llm.rb +98 -0
  107. data/lib/tasks/aliases.rake +235 -0
  108. data/lib/tasks/models_docs.rake +224 -0
  109. data/lib/tasks/models_update.rake +108 -0
  110. data/lib/tasks/release.rake +32 -0
  111. data/lib/tasks/vcr.rake +99 -0
  112. metadata +128 -7
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Gemini
6
+ # Streaming methods for the Gemini API implementation
7
+ module Streaming
8
+ def stream_url
9
+ "models/#{@model}:streamGenerateContent?alt=sse"
10
+ end
11
+
12
+ def build_chunk(data)
13
+ Chunk.new(
14
+ role: :assistant,
15
+ model_id: extract_model_id(data),
16
+ content: extract_content(data),
17
+ input_tokens: extract_input_tokens(data),
18
+ output_tokens: extract_output_tokens(data),
19
+ cached_tokens: extract_cached_tokens(data),
20
+ tool_calls: extract_tool_calls(data)
21
+ )
22
+ end
23
+
24
+ private
25
+
26
+ def extract_model_id(data)
27
+ data['modelVersion']
28
+ end
29
+
30
+ def extract_content(data)
31
+ return nil unless data['candidates']&.any?
32
+
33
+ candidate = data['candidates'][0]
34
+ parts = candidate.dig('content', 'parts')
35
+ return nil unless parts
36
+
37
+ text_parts = parts.select { |p| p['text'] }
38
+ text_parts.map { |p| p['text'] }.join if text_parts.any?
39
+ end
40
+
41
+ def extract_input_tokens(data)
42
+ data.dig('usageMetadata', 'promptTokenCount')
43
+ end
44
+
45
+ def extract_output_tokens(data)
46
+ candidates = data.dig('usageMetadata', 'candidatesTokenCount') || 0
47
+ thoughts = data.dig('usageMetadata', 'thoughtsTokenCount') || 0
48
+ total = candidates + thoughts
49
+ total.positive? ? total : nil
50
+ end
51
+
52
+ def extract_cached_tokens(data)
53
+ data.dig('usageMetadata', 'cachedContentTokenCount')
54
+ end
55
+
56
+ def parse_streaming_error(data)
57
+ error_data = JSON.parse(data)
58
+ [error_data['error']['code'], error_data['error']['message']]
59
+ rescue JSON::ParserError => e
60
+ RubyLLM.logger.debug "Failed to parse streaming error: #{e.message}"
61
+ [500, "Failed to parse error: #{data}"]
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Gemini
6
+ # Tools methods for the Gemini API implementation
7
+ module Tools
8
+ # Format tools for Gemini API
9
+ def format_tools(tools)
10
+ return [] if tools.empty?
11
+
12
+ [{
13
+ functionDeclarations: tools.values.map { |tool| function_declaration_for(tool) }
14
+ }]
15
+ end
16
+
17
+ # Extract tool calls from response data
18
+ def extract_tool_calls(data)
19
+ return nil unless data
20
+
21
+ candidate = data.is_a?(Hash) ? data.dig('candidates', 0) : nil
22
+ return nil unless candidate
23
+
24
+ parts = candidate.dig('content', 'parts')
25
+ return nil unless parts.is_a?(Array)
26
+
27
+ function_call_part = parts.find { |p| p['functionCall'] }
28
+ return nil unless function_call_part
29
+
30
+ function_data = function_call_part['functionCall']
31
+ return nil unless function_data
32
+
33
+ id = SecureRandom.uuid
34
+
35
+ {
36
+ id => ToolCall.new(
37
+ id: id,
38
+ name: function_data['name'],
39
+ arguments: function_data['args']
40
+ )
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ # Format a single tool for Gemini API
47
+ def function_declaration_for(tool)
48
+ {
49
+ name: tool.name,
50
+ description: tool.description,
51
+ parameters: tool.parameters.any? ? format_parameters(tool.parameters) : nil
52
+ }.compact
53
+ end
54
+
55
+ # Format tool parameters for Gemini API
56
+ def format_parameters(parameters)
57
+ {
58
+ type: 'OBJECT',
59
+ properties: parameters.transform_values do |param|
60
+ {
61
+ type: param_type_for_gemini(param.type),
62
+ description: param.description
63
+ }.compact
64
+ end,
65
+ required: parameters.select { |_, p| p.required }.keys.map(&:to_s)
66
+ }
67
+ end
68
+
69
+ # Convert RubyLLM param types to Gemini API types
70
+ def param_type_for_gemini(type)
71
+ case type.to_s.downcase
72
+ when 'integer', 'number', 'float' then 'NUMBER'
73
+ when 'boolean' then 'BOOLEAN'
74
+ when 'array' then 'ARRAY'
75
+ when 'object' then 'OBJECT'
76
+ else 'STRING'
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Native Gemini API implementation
6
+ class Gemini < Provider
7
+ include Gemini::Chat
8
+ include Gemini::Embeddings
9
+ include Gemini::Images
10
+ include Gemini::Models
11
+ include Gemini::Streaming
12
+ include Gemini::Tools
13
+ include Gemini::Media
14
+
15
+ def api_base
16
+ 'https://generativelanguage.googleapis.com/v1beta'
17
+ end
18
+
19
+ def headers
20
+ {
21
+ 'x-goog-api-key' => @config.gemini_api_key
22
+ }
23
+ end
24
+
25
+ class << self
26
+ def capabilities
27
+ Gemini::Capabilities
28
+ end
29
+
30
+ def configuration_requirements
31
+ %i[gemini_api_key]
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class 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
+ class 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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # GPUStack API integration based on Ollama.
6
+ class GPUStack < OpenAIBase
7
+ include GPUStack::Chat
8
+ include GPUStack::Models
9
+
10
+ def api_base
11
+ @config.gpustack_api_base
12
+ end
13
+
14
+ def headers
15
+ return {} unless @config.gpustack_api_key
16
+
17
+ {
18
+ 'Authorization' => "Bearer #{@config.gpustack_api_key}"
19
+ }
20
+ end
21
+
22
+ class << self
23
+ def local?
24
+ true
25
+ end
26
+
27
+ def configuration_requirements
28
+ %i[gpustack_api_base]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ # Determines capabilities for Mistral models
7
+ module Capabilities
8
+ module_function
9
+
10
+ def supports_streaming?(model_id)
11
+ # All chat models support streaming, but not embedding/moderation/OCR/transcription
12
+ !model_id.match?(/embed|moderation|ocr|transcriptions/)
13
+ end
14
+
15
+ def supports_tools?(model_id)
16
+ # Most chat models support tools except embedding/moderation/OCR/voxtral/transcription
17
+ !model_id.match?(/embed|moderation|ocr|voxtral|transcriptions|mistral-(tiny|small)-(2312|2402)/)
18
+ end
19
+
20
+ def supports_vision?(model_id)
21
+ # Models with vision capabilities
22
+ model_id.match?(/pixtral|mistral-small-(2503|2506)|mistral-medium/)
23
+ end
24
+
25
+ def supports_json_mode?(model_id)
26
+ # Most chat models support JSON mode (structured output)
27
+ !model_id.match?(/embed|moderation|ocr|voxtral|transcriptions/) && supports_tools?(model_id)
28
+ end
29
+
30
+ def format_display_name(model_id)
31
+ case model_id
32
+ when /mistral-large/ then 'Mistral Large'
33
+ when /mistral-medium/ then 'Mistral Medium'
34
+ when /mistral-small/ then 'Mistral Small'
35
+ when /ministral-3b/ then 'Ministral 3B'
36
+ when /ministral-8b/ then 'Ministral 8B'
37
+ when /codestral/ then 'Codestral'
38
+ when /pixtral-large/ then 'Pixtral Large'
39
+ when /pixtral-12b/ then 'Pixtral 12B'
40
+ when /mistral-embed/ then 'Mistral Embed'
41
+ when /mistral-moderation/ then 'Mistral Moderation'
42
+ else model_id.split('-').map(&:capitalize).join(' ')
43
+ end
44
+ end
45
+
46
+ def model_family(model_id)
47
+ case model_id
48
+ when /mistral-large/ then 'mistral-large'
49
+ when /mistral-medium/ then 'mistral-medium'
50
+ when /mistral-small/ then 'mistral-small'
51
+ when /ministral/ then 'ministral'
52
+ when /codestral/ then 'codestral'
53
+ when /pixtral/ then 'pixtral'
54
+ when /mistral-embed/ then 'mistral-embed'
55
+ when /mistral-moderation/ then 'mistral-moderation'
56
+ else 'mistral'
57
+ end
58
+ end
59
+
60
+ def context_window_for(_model_id)
61
+ 32_768 # Default for most Mistral models
62
+ end
63
+
64
+ def max_tokens_for(_model_id)
65
+ 8192 # Default for most Mistral models
66
+ end
67
+
68
+ def modalities_for(model_id)
69
+ case model_id
70
+ when /pixtral/
71
+ {
72
+ input: %w[text image],
73
+ output: ['text']
74
+ }
75
+ when /embed/
76
+ {
77
+ input: ['text'],
78
+ output: ['embeddings']
79
+ }
80
+ else
81
+ {
82
+ input: ['text'],
83
+ output: ['text']
84
+ }
85
+ end
86
+ end
87
+
88
+ def capabilities_for(model_id) # rubocop:disable Metrics/PerceivedComplexity
89
+ case model_id
90
+ when /moderation/ then ['moderation']
91
+ when /voxtral.*transcribe/ then ['transcription']
92
+ when /ocr/ then ['vision']
93
+ else
94
+ capabilities = []
95
+ capabilities << 'streaming' if supports_streaming?(model_id)
96
+ capabilities << 'function_calling' if supports_tools?(model_id)
97
+ capabilities << 'structured_output' if supports_json_mode?(model_id)
98
+ capabilities << 'vision' if supports_vision?(model_id)
99
+
100
+ # Model-specific capabilities
101
+ capabilities << 'reasoning' if model_id.match?(/magistral/)
102
+ capabilities << 'batch' unless model_id.match?(/voxtral|ocr|embed|moderation/)
103
+ capabilities << 'fine_tuning' if model_id.match?(/mistral-(small|medium|large)|devstral/)
104
+ capabilities << 'distillation' if model_id.match?(/ministral/)
105
+ capabilities << 'predicted_outputs' if model_id.match?(/codestral/)
106
+
107
+ capabilities.uniq
108
+ end
109
+ end
110
+
111
+ def pricing_for(_model_id)
112
+ {
113
+ input: 0.0,
114
+ output: 0.0
115
+ }
116
+ end
117
+
118
+ def release_date_for(model_id)
119
+ case model_id
120
+ # 2023 releases
121
+ when 'open-mistral-7b', 'mistral-tiny' then '2023-09-27'
122
+ when 'mistral-medium-2312', 'mistral-small-2312', 'mistral-small',
123
+ 'open-mixtral-8x7b', 'mistral-tiny-2312' then '2023-12-11'
124
+
125
+ # 2024 releases
126
+ when 'mistral-embed' then '2024-01-11'
127
+ when 'mistral-large-2402', 'mistral-small-2402' then '2024-02-26'
128
+ when 'open-mixtral-8x22b', 'open-mixtral-8x22b-2404' then '2024-04-17'
129
+ when 'codestral-2405' then '2024-05-22'
130
+ when 'codestral-mamba-2407', 'codestral-mamba-latest', 'open-codestral-mamba' then '2024-07-16'
131
+ when 'open-mistral-nemo', 'open-mistral-nemo-2407', 'mistral-tiny-2407',
132
+ 'mistral-tiny-latest' then '2024-07-18'
133
+ when 'mistral-large-2407' then '2024-07-24'
134
+ when 'pixtral-12b-2409', 'pixtral-12b-latest', 'pixtral-12b' then '2024-09-17'
135
+ when 'mistral-small-2409' then '2024-09-18'
136
+ when 'ministral-3b-2410', 'ministral-3b-latest', 'ministral-8b-2410',
137
+ 'ministral-8b-latest' then '2024-10-16'
138
+ when 'pixtral-large-2411', 'pixtral-large-latest', 'mistral-large-pixtral-2411' then '2024-11-12'
139
+ when 'mistral-large-2411', 'mistral-large-latest', 'mistral-large' then '2024-11-20'
140
+ when 'codestral-2411-rc5', 'mistral-moderation-2411', 'mistral-moderation-latest' then '2024-11-26'
141
+ when 'codestral-2412' then '2024-12-17'
142
+
143
+ # 2025 releases
144
+ when 'mistral-small-2501' then '2025-01-13'
145
+ when 'codestral-2501' then '2025-01-14'
146
+ when 'mistral-saba-2502', 'mistral-saba-latest' then '2025-02-18'
147
+ when 'mistral-small-2503' then '2025-03-03'
148
+ when 'mistral-ocr-2503' then '2025-03-21'
149
+ when 'mistral-medium', 'mistral-medium-latest', 'mistral-medium-2505' then '2025-05-06'
150
+ when 'codestral-embed', 'codestral-embed-2505' then '2025-05-21'
151
+ when 'mistral-ocr-2505', 'mistral-ocr-latest' then '2025-05-23'
152
+ when 'devstral-small-2505' then '2025-05-28'
153
+ when 'mistral-small-2506', 'mistral-small-latest', 'magistral-medium-2506',
154
+ 'magistral-medium-latest' then '2025-06-10'
155
+ when 'devstral-small-2507', 'devstral-small-latest', 'devstral-medium-2507',
156
+ 'devstral-medium-latest' then '2025-07-09'
157
+ when 'codestral-2508', 'codestral-latest' then '2025-08-30'
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class 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, cache_prompts: {}) # rubocop:disable Metrics/ParameterLists
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
+ class 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
+ class 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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Mistral API integration.
6
+ class Mistral < OpenAIBase
7
+ include Mistral::Chat
8
+ include Mistral::Models
9
+ include Mistral::Embeddings
10
+
11
+ def api_base
12
+ 'https://api.mistral.ai/v1'
13
+ end
14
+
15
+ def headers
16
+ {
17
+ 'Authorization' => "Bearer #{@config.mistral_api_key}"
18
+ }
19
+ end
20
+
21
+ class << self
22
+ def capabilities
23
+ Mistral::Capabilities
24
+ end
25
+
26
+ def configuration_requirements
27
+ %i[mistral_api_key]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Chat methods of the Ollama API integration
7
+ module Chat
8
+ module_function
9
+
10
+ def format_messages(messages)
11
+ messages.map do |msg|
12
+ {
13
+ role: format_role(msg.role),
14
+ content: Ollama::Media.format_content(msg.content),
15
+ tool_calls: format_tool_calls(msg.tool_calls),
16
+ tool_call_id: msg.tool_call_id
17
+ }.compact
18
+ end
19
+ end
20
+
21
+ def format_role(role)
22
+ # Ollama doesn't use the new OpenAI convention for system prompts
23
+ role.to_s
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Handles formatting of media content (images, audio) for OpenAI APIs
7
+ module Media
8
+ extend OpenAI::Media
9
+
10
+ module_function
11
+
12
+ def format_content(content)
13
+ # Convert Hash/Array back to JSON string for API
14
+ return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
15
+ return content unless content.is_a?(Content)
16
+
17
+ parts = []
18
+ parts << format_text(content.text) if content.text
19
+
20
+ content.attachments.each do |attachment|
21
+ case attachment.type
22
+ when :image
23
+ parts << Ollama::Media.format_image(attachment)
24
+ when :pdf
25
+ parts << format_pdf(attachment)
26
+ when :audio
27
+ parts << format_audio(attachment)
28
+ when :text
29
+ parts << format_text_file(attachment)
30
+ else
31
+ raise UnsupportedAttachmentError, attachment.mime_type
32
+ end
33
+ end
34
+
35
+ parts
36
+ end
37
+
38
+ def format_image(image)
39
+ {
40
+ type: 'image_url',
41
+ image_url: {
42
+ url: "data:#{image.mime_type};base64,#{image.encoded}",
43
+ detail: 'auto'
44
+ }
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end