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.
- checksums.yaml +4 -4
- data/LICENSE +22 -0
- data/README.md +172 -0
- data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install_generator.rb +121 -0
- data/lib/ruby_llm/active_record/acts_as.rb +382 -0
- data/lib/ruby_llm/aliases.json +217 -0
- data/lib/ruby_llm/aliases.rb +56 -0
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +226 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +73 -0
- data/lib/ruby_llm/connection.rb +126 -0
- data/lib/ruby_llm/content.rb +52 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +30 -0
- data/lib/ruby_llm/error.rb +84 -0
- data/lib/ruby_llm/image.rb +53 -0
- data/lib/ruby_llm/message.rb +81 -0
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +101 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +51 -0
- data/lib/ruby_llm/model/pricing_category.rb +48 -0
- data/lib/ruby_llm/model/pricing_tier.rb +34 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +29924 -0
- data/lib/ruby_llm/models.rb +214 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/provider.rb +221 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +120 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +116 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +56 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
- data/lib/ruby_llm/providers/anthropic.rb +37 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +76 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +73 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +92 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
- data/lib/ruby_llm/providers/bedrock.rb +83 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +146 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
- data/lib/ruby_llm/providers/gemini/images.rb +48 -0
- data/lib/ruby_llm/providers/gemini/media.rb +55 -0
- data/lib/ruby_llm/providers/gemini/models.rb +41 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +66 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
- data/lib/ruby_llm/providers/gemini.rb +36 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
- data/lib/ruby_llm/providers/gpustack.rb +33 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
- data/lib/ruby_llm/providers/mistral/models.rb +49 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +50 -0
- data/lib/ruby_llm/providers/ollama.rb +29 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
- data/lib/ruby_llm/providers/openai/chat.rb +87 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +81 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/response.rb +116 -0
- data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +191 -0
- data/lib/ruby_llm/providers/openai/tools.rb +100 -0
- data/lib/ruby_llm/providers/openai.rb +44 -0
- data/lib/ruby_llm/providers/openai_base.rb +44 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +52 -0
- data/lib/ruby_llm/railtie.rb +17 -0
- data/lib/ruby_llm/stream_accumulator.rb +103 -0
- data/lib/ruby_llm/streaming.rb +162 -0
- data/lib/ruby_llm/tool.rb +100 -0
- data/lib/ruby_llm/tool_call.rb +31 -0
- data/lib/ruby_llm/utils.rb +49 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +98 -0
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/models_docs.rake +224 -0
- data/lib/tasks/models_update.rake +108 -0
- data/lib/tasks/release.rake +32 -0
- data/lib/tasks/vcr.rake +99 -0
- 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
|