ruby_llm 1.4.0 → 1.5.1

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.
@@ -0,0 +1,168 @@
1
+ {
2
+ "title": "RubyLLM Models Schema",
3
+ "description": "Schema for validating the structure of models.json",
4
+ "type": "array",
5
+ "items": {
6
+ "type": "object",
7
+ "required": ["id", "name", "provider", "context_window", "max_output_tokens"],
8
+ "properties": {
9
+ "id": {
10
+ "type": "string",
11
+ "description": "Unique identifier for the model"
12
+ },
13
+ "name": {
14
+ "type": "string",
15
+ "description": "Display name of the model"
16
+ },
17
+ "provider": {
18
+ "type": "string",
19
+ "description": "Provider of the model (e.g., openai, anthropic, mistral)"
20
+ },
21
+ "family": {
22
+ "type": ["string", "null"],
23
+ "description": "Model family (e.g., gpt-4, claude-3)"
24
+ },
25
+ "created_at": {
26
+ "type": ["null", {"type": "string", "format": "date-time"}],
27
+ "description": "Creation date of the model"
28
+ },
29
+ "context_window": {
30
+ "type": ["null", {"type": "integer", "minimum": 0}],
31
+ "description": "Maximum context window size"
32
+ },
33
+ "max_output_tokens": {
34
+ "type": ["null", {"type": "integer", "minimum": 0}],
35
+ "description": "Maximum output tokens"
36
+ },
37
+ "knowledge_cutoff": {
38
+ "type": ["null", {"type": "string", "format": "date"}],
39
+ "description": "Knowledge cutoff date"
40
+ },
41
+ "modalities": {
42
+ "type": "object",
43
+ "required": ["input", "output"],
44
+ "properties": {
45
+ "input": {
46
+ "type": "array",
47
+ "items": {
48
+ "type": "string",
49
+ "enum": ["text", "image", "audio", "pdf", "video", "file"]
50
+ },
51
+ "uniqueItems": true,
52
+ "description": "Supported input modalities"
53
+ },
54
+ "output": {
55
+ "type": "array",
56
+ "items": {
57
+ "type": "string",
58
+ "enum": ["text", "image", "audio", "embeddings", "moderation"]
59
+ },
60
+ "uniqueItems": true,
61
+ "description": "Supported output modalities"
62
+ }
63
+ }
64
+ },
65
+ "capabilities": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "string",
69
+ "enum": [
70
+ "streaming", "function_calling", "structured_output", "predicted_outputs",
71
+ "distillation", "fine_tuning", "batch", "realtime", "image_generation",
72
+ "speech_generation", "transcription", "translation", "citations", "reasoning",
73
+ "caching", "moderation", "json_mode", "vision"
74
+ ]
75
+ },
76
+ "uniqueItems": true,
77
+ "description": "Model capabilities"
78
+ },
79
+ "pricing": {
80
+ "type": "object",
81
+ "properties": {
82
+ "text_tokens": {
83
+ "type": "object",
84
+ "required": ["standard"],
85
+ "properties": {
86
+ "standard": {
87
+ "type": "object",
88
+ "properties": {
89
+ "input_per_million": {"type": "number", "minimum": 0},
90
+ "cached_input_per_million": {"type": "number", "minimum": 0},
91
+ "output_per_million": {"type": "number", "minimum": 0},
92
+ "reasoning_output_per_million": {"type": "number", "minimum": 0}
93
+ }
94
+ },
95
+ "batch": {
96
+ "type": "object",
97
+ "properties": {
98
+ "input_per_million": {"type": "number", "minimum": 0},
99
+ "output_per_million": {"type": "number", "minimum": 0}
100
+ }
101
+ }
102
+ }
103
+ },
104
+ "images": {
105
+ "type": "object",
106
+ "properties": {
107
+ "standard": {
108
+ "type": "object",
109
+ "properties": {
110
+ "input": {"type": "number", "minimum": 0},
111
+ "output": {"type": "number", "minimum": 0}
112
+ }
113
+ },
114
+ "batch": {
115
+ "type": "object",
116
+ "properties": {
117
+ "input": {"type": "number", "minimum": 0},
118
+ "output": {"type": "number", "minimum": 0}
119
+ }
120
+ }
121
+ }
122
+ },
123
+ "audio_tokens": {
124
+ "type": "object",
125
+ "properties": {
126
+ "standard": {
127
+ "type": "object",
128
+ "properties": {
129
+ "input_per_million": {"type": "number", "minimum": 0},
130
+ "output_per_million": {"type": "number", "minimum": 0}
131
+ }
132
+ },
133
+ "batch": {
134
+ "type": "object",
135
+ "properties": {
136
+ "input_per_million": {"type": "number", "minimum": 0},
137
+ "output_per_million": {"type": "number", "minimum": 0}
138
+ }
139
+ }
140
+ }
141
+ },
142
+ "embeddings": {
143
+ "type": "object",
144
+ "properties": {
145
+ "standard": {
146
+ "type": "object",
147
+ "properties": {
148
+ "input_per_million": {"type": "number", "minimum": 0}
149
+ }
150
+ },
151
+ "batch": {
152
+ "type": "object",
153
+ "properties": {
154
+ "input_per_million": {"type": "number", "minimum": 0}
155
+ }
156
+ }
157
+ }
158
+ }
159
+ },
160
+ "description": "Pricing information for the model"
161
+ },
162
+ "metadata": {
163
+ "type": "object",
164
+ "description": "Additional metadata about the model"
165
+ }
166
+ }
167
+ }
168
+ }
@@ -280,6 +280,9 @@ module RubyLLM
280
280
  # Embedding output
281
281
  modalities[:output] << 'embeddings' if model_id.match?(/embedding|gemini-embedding/)
282
282
 
283
+ # Image output for imagen models
284
+ modalities[:output] = ['image'] if model_id.match?(/imagen/)
285
+
283
286
  modalities
284
287
  end
285
288
 
@@ -0,0 +1,163 @@
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
+ # 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
+ 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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Mistral API integration.
6
+ module Mistral
7
+ extend OpenAI
8
+ extend Mistral::Chat
9
+ extend Mistral::Models
10
+ extend Mistral::Embeddings
11
+
12
+ module_function
13
+
14
+ def api_base(_config)
15
+ 'https://api.mistral.ai/v1'
16
+ end
17
+
18
+ def headers(config)
19
+ {
20
+ 'Authorization' => "Bearer #{config.mistral_api_key}"
21
+ }
22
+ end
23
+
24
+ def capabilities
25
+ Mistral::Capabilities
26
+ end
27
+
28
+ def slug
29
+ 'mistral'
30
+ end
31
+
32
+ def configuration_requirements
33
+ %i[mistral_api_key]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Perplexity
6
+ # Determines capabilities and pricing for Perplexity models
7
+ module Capabilities
8
+ module_function
9
+
10
+ # Returns the context window size for the given model ID
11
+ # @param model_id [String] the model identifier
12
+ # @return [Integer] the context window size in tokens
13
+ def context_window_for(model_id)
14
+ case model_id
15
+ when /sonar-pro/ then 200_000
16
+ else 128_000
17
+ end
18
+ end
19
+
20
+ # Returns the maximum number of tokens that can be generated
21
+ # @param model_id [String] the model identifier
22
+ # @return [Integer] the maximum number of tokens
23
+ def max_tokens_for(model_id)
24
+ case model_id
25
+ when /sonar-(?:pro|reasoning-pro)/ then 8_192
26
+ else 4_096
27
+ end
28
+ end
29
+
30
+ # Returns the price per million tokens for input
31
+ # @param model_id [String] the model identifier
32
+ # @return [Float] the price per million tokens in USD
33
+ def input_price_for(model_id)
34
+ PRICES.dig(model_family(model_id), :input) || 1.0
35
+ end
36
+
37
+ # Returns the price per million tokens for output
38
+ # @param model_id [String] the model identifier
39
+ # @return [Float] the price per million tokens in USD
40
+ def output_price_for(model_id)
41
+ PRICES.dig(model_family(model_id), :output) || 1.0
42
+ end
43
+
44
+ # Determines if the model supports vision capabilities
45
+ # @param model_id [String] the model identifier
46
+ # @return [Boolean] true if the model supports vision
47
+ def supports_vision?(model_id)
48
+ case model_id
49
+ when /sonar-reasoning-pro/, /sonar-reasoning/, /sonar-pro/, /sonar/ then true
50
+ else false
51
+ end
52
+ end
53
+
54
+ # Determines if the model supports function calling
55
+ # @param model_id [String] the model identifier
56
+ # @return [Boolean] true if the model supports functions
57
+ def supports_functions?(_model_id)
58
+ false
59
+ end
60
+
61
+ # Determines if the model supports JSON mode
62
+ def supports_json_mode?(_model_id)
63
+ true
64
+ end
65
+
66
+ # Formats the model ID into a human-readable display name
67
+ # @param model_id [String] the model identifier
68
+ # @return [String] the formatted display name
69
+ def format_display_name(model_id)
70
+ case model_id
71
+ when 'sonar' then 'Sonar'
72
+ when 'sonar-pro' then 'Sonar Pro'
73
+ when 'sonar-reasoning' then 'Sonar Reasoning'
74
+ when 'sonar-reasoning-pro' then 'Sonar Reasoning Pro'
75
+ when 'sonar-deep-research' then 'Sonar Deep Research'
76
+ else
77
+ model_id.split('-')
78
+ .map(&:capitalize)
79
+ .join(' ')
80
+ end
81
+ end
82
+
83
+ # Returns the model type
84
+ # @param model_id [String] the model identifier
85
+ # @return [String] the model type (e.g., 'chat')
86
+ def model_type(_model_id)
87
+ 'chat'
88
+ end
89
+
90
+ # Returns the model family
91
+ # @param model_id [String] the model identifier
92
+ # @return [Symbol] the model family
93
+ def model_family(model_id)
94
+ case model_id
95
+ when 'sonar' then :sonar
96
+ when 'sonar-pro' then :sonar_pro
97
+ when 'sonar-reasoning' then :sonar_reasoning
98
+ when 'sonar-reasoning-pro' then :sonar_reasoning_pro
99
+ when 'sonar-deep-research' then :sonar_deep_research
100
+ else :unknown
101
+ end
102
+ end
103
+
104
+ def modalities_for(_model_id)
105
+ {
106
+ input: ['text'],
107
+ output: ['text']
108
+ }
109
+ end
110
+
111
+ def capabilities_for(model_id)
112
+ capabilities = %w[streaming json_mode]
113
+ capabilities << 'vision' if supports_vision?(model_id)
114
+ capabilities
115
+ end
116
+
117
+ def pricing_for(model_id)
118
+ family = model_family(model_id)
119
+ prices = PRICES.fetch(family, { input: 1.0, output: 1.0 })
120
+
121
+ standard_pricing = {
122
+ input_per_million: prices[:input],
123
+ output_per_million: prices[:output]
124
+ }
125
+
126
+ # Add special pricing if available
127
+ standard_pricing[:citation_per_million] = prices[:citation] if prices[:citation]
128
+ standard_pricing[:reasoning_per_million] = prices[:reasoning] if prices[:reasoning]
129
+ standard_pricing[:search_per_thousand] = prices[:search_queries] if prices[:search_queries]
130
+
131
+ {
132
+ text_tokens: {
133
+ standard: standard_pricing
134
+ }
135
+ }
136
+ end
137
+
138
+ # Pricing information for Perplexity models (USD per 1M tokens)
139
+ PRICES = {
140
+ sonar: {
141
+ input: 1.0,
142
+ output: 1.0
143
+ },
144
+ sonar_pro: {
145
+ input: 3.0,
146
+ output: 15.0
147
+ },
148
+ sonar_reasoning: {
149
+ input: 1.0,
150
+ output: 5.0
151
+ },
152
+ sonar_reasoning_pro: {
153
+ input: 2.0,
154
+ output: 8.0
155
+ },
156
+ sonar_deep_research: {
157
+ input: 2.0,
158
+ output: 8.0,
159
+ citation: 2.0,
160
+ reasoning: 3.0,
161
+ search_queries: 5.0
162
+ }
163
+ }.freeze
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Perplexity
6
+ # Chat formatting for Perplexity provider
7
+ module Chat
8
+ module_function
9
+
10
+ def format_role(role)
11
+ # Perplexity doesn't use the new OpenAI convention for system prompts
12
+ role.to_s
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end