ruby_llm 1.6.1 → 1.6.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1fd977cc46769d8d235fe13a31e030a133618a335c8cd012ab8de2b7d439297
4
- data.tar.gz: ce8d22464541ac19246e6018fde5c7b414ba0022bc5995f8900052f3d0ac5b42
3
+ metadata.gz: 1b210b6577ab40b05d222354a9cb4c9dd115d7237e4b88be3cd63ee42aa2e122
4
+ data.tar.gz: 58d0df110e469fef90f5c5591bed4dd3a2d4369afa982217793c11fecb00e320
5
5
  SHA512:
6
- metadata.gz: aec038a65867c5fa13b93a11aa5356183b8f105bb7f48792e6300a9259a809206bc07da0b54c63a0a0de726cc3698ec2cd79c511be43e6fd8f47273adecc47a9
7
- data.tar.gz: 5386da468b9f413d7fbf27192a693c2427a1e88a9082a7375f5641f01854a7cbd2235c1c8108ea870c97c97ea79c712bdd5d33e2ac09a22e25a74f9510a1599f
6
+ metadata.gz: 3979e1be85c20e8961eb54bcc1d3cf0e8f49dc10fbb5bbbf57e292a098b4c09b053101473c59aef2186c5583276ea5c909b461ba34ac4f8c8eb0f0a5e69cc76d
7
+ data.tar.gz: 452f86034599f6d8b2d200c3357137f0ec93087b410c4eef5e5393976414b8a986871fa532c6149fb69b081d66f1a823891ab27bd186fb9a31a1ca232707a13d
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  **One *beautiful* Ruby API for GPT, Claude, Gemini, and more.** Easily build chatbots, AI agents, RAG applications, and content generators. Features chat (text, images, audio, PDFs), image generation, embeddings, tools (function calling), structured output, Rails integration, and streaming. Works with OpenAI, Anthropic, Google Gemini, AWS Bedrock, DeepSeek, Mistral, Ollama (local models), OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API.
7
7
 
8
8
  <div class="badge-container">
9
- <a href="https://badge.fury.io/rb/ruby_llm"><img src="https://badge.fury.io/rb/ruby_llm.svg?a=3" alt="Gem Version" /></a>
9
+ <a href="https://badge.fury.io/rb/ruby_llm"><img src="https://badge.fury.io/rb/ruby_llm.svg?a=4" alt="Gem Version" /></a>
10
10
  <a href="https://github.com/testdouble/standard"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Ruby Style Guide" /></a>
11
11
  <a href="https://rubygems.org/gems/ruby_llm"><img alt="Gem Downloads" src="https://img.shields.io/gem/dt/ruby_llm"></a>
12
12
  <a href="https://codecov.io/gh/crmne/ruby_llm"><img src="https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg" alt="codecov" /></a>
@@ -234,7 +234,7 @@ module RubyLLM
234
234
  end
235
235
 
236
236
  def persist_new_message
237
- @message = messages.create!(role: :assistant, content: String.new)
237
+ @message = messages.create!(role: :assistant, content: '')
238
238
  end
239
239
 
240
240
  def persist_message_completion(message)
data/lib/ruby_llm/chat.rb CHANGED
@@ -51,10 +51,6 @@ module RubyLLM
51
51
  end
52
52
 
53
53
  def with_tool(tool)
54
- unless @model.supports_functions?
55
- raise UnsupportedFunctionsError, "Model #{@model.id} doesn't support function calling"
56
- end
57
-
58
54
  tool_instance = tool.is_a?(Class) ? tool.new : tool
59
55
  @tools[tool_instance.name.to_sym] = tool_instance
60
56
  self
@@ -94,11 +90,7 @@ module RubyLLM
94
90
  self
95
91
  end
96
92
 
97
- def with_schema(schema, force: false)
98
- unless force || @model.structured_output?
99
- raise UnsupportedStructuredOutputError, "Model #{@model.id} doesn't support structured output"
100
- end
101
-
93
+ def with_schema(schema)
102
94
  schema_instance = schema.is_a?(Class) ? schema.new : schema
103
95
 
104
96
  # Accept both RubyLLM::Schema instances and plain JSON schemas
@@ -44,7 +44,6 @@ module RubyLLM
44
44
  :logger,
45
45
  :log_file,
46
46
  :log_level,
47
- :log_assume_model_exists,
48
47
  :log_stream_debug
49
48
 
50
49
  def initialize
@@ -64,7 +63,6 @@ module RubyLLM
64
63
  # Logging configuration
65
64
  @log_file = $stdout
66
65
  @log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
67
- @log_assume_model_exists = true
68
66
  @log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
69
67
  end
70
68
 
@@ -43,7 +43,7 @@ module RubyLLM
43
43
  def process_attachments(attachments)
44
44
  if attachments.is_a?(Hash)
45
45
  # Ignores types (like :image, :audio, :text, :pdf) since we have robust MIME type detection
46
- attachments.each_value(&method(:process_attachments_array_or_string))
46
+ attachments.each_value { |attachment| process_attachments_array_or_string(attachment) }
47
47
  else
48
48
  process_attachments_array_or_string attachments
49
49
  end
@@ -23,9 +23,7 @@ module RubyLLM
23
23
  class ConfigurationError < StandardError; end
24
24
  class InvalidRoleError < StandardError; end
25
25
  class ModelNotFoundError < StandardError; end
26
- class UnsupportedFunctionsError < StandardError; end
27
26
  class UnsupportedAttachmentError < StandardError; end
28
- class UnsupportedStructuredOutputError < StandardError; end
29
27
 
30
28
  # Error classes for different HTTP status codes
31
29
  class BadRequestError < Error; end
@@ -16285,7 +16285,6 @@
16285
16285
  "supported_parameters": [
16286
16286
  "frequency_penalty",
16287
16287
  "logit_bias",
16288
- "logprobs",
16289
16288
  "max_tokens",
16290
16289
  "min_p",
16291
16290
  "presence_penalty",
@@ -16297,7 +16296,6 @@
16297
16296
  "tool_choice",
16298
16297
  "tools",
16299
16298
  "top_k",
16300
- "top_logprobs",
16301
16299
  "top_p"
16302
16300
  ]
16303
16301
  }
@@ -17474,7 +17472,6 @@
17474
17472
  "supported_parameters": [
17475
17473
  "frequency_penalty",
17476
17474
  "logit_bias",
17477
- "logprobs",
17478
17475
  "max_tokens",
17479
17476
  "min_p",
17480
17477
  "presence_penalty",
@@ -17484,7 +17481,6 @@
17484
17481
  "stop",
17485
17482
  "temperature",
17486
17483
  "top_k",
17487
- "top_logprobs",
17488
17484
  "top_p"
17489
17485
  ]
17490
17486
  }
@@ -19472,6 +19468,72 @@
19472
19468
  ]
19473
19469
  }
19474
19470
  },
19471
+ {
19472
+ "id": "mistralai/mistral-medium-3.1",
19473
+ "name": "Mistral: Mistral Medium 3.1",
19474
+ "provider": "openrouter",
19475
+ "family": "mistralai",
19476
+ "created_at": "2025-08-13 16:33:59 +0200",
19477
+ "context_window": 131072,
19478
+ "max_output_tokens": null,
19479
+ "knowledge_cutoff": null,
19480
+ "modalities": {
19481
+ "input": [
19482
+ "text",
19483
+ "image"
19484
+ ],
19485
+ "output": [
19486
+ "text"
19487
+ ]
19488
+ },
19489
+ "capabilities": [
19490
+ "streaming",
19491
+ "function_calling",
19492
+ "structured_output"
19493
+ ],
19494
+ "pricing": {
19495
+ "text_tokens": {
19496
+ "standard": {
19497
+ "input_per_million": 0.39999999999999997,
19498
+ "output_per_million": 2.0
19499
+ }
19500
+ }
19501
+ },
19502
+ "metadata": {
19503
+ "description": "Mistral Medium 3.1 is an updated version of Mistral Medium 3, which is a high-performance enterprise-grade language model designed to deliver frontier-level capabilities at significantly reduced operational cost. It balances state-of-the-art reasoning and multimodal performance with 8× lower cost compared to traditional large models, making it suitable for scalable deployments across professional and industrial use cases.\n\nThe model excels in domains such as coding, STEM reasoning, and enterprise adaptation. It supports hybrid, on-prem, and in-VPC deployments and is optimized for integration into custom workflows. Mistral Medium 3.1 offers competitive accuracy relative to larger models like Claude Sonnet 3.5/3.7, Llama 4 Maverick, and Command R+, while maintaining broad compatibility across cloud environments.",
19504
+ "architecture": {
19505
+ "modality": "text+image->text",
19506
+ "input_modalities": [
19507
+ "text",
19508
+ "image"
19509
+ ],
19510
+ "output_modalities": [
19511
+ "text"
19512
+ ],
19513
+ "tokenizer": "Mistral",
19514
+ "instruct_type": null
19515
+ },
19516
+ "top_provider": {
19517
+ "context_length": 131072,
19518
+ "max_completion_tokens": null,
19519
+ "is_moderated": false
19520
+ },
19521
+ "per_request_limits": null,
19522
+ "supported_parameters": [
19523
+ "frequency_penalty",
19524
+ "max_tokens",
19525
+ "presence_penalty",
19526
+ "response_format",
19527
+ "seed",
19528
+ "stop",
19529
+ "structured_outputs",
19530
+ "temperature",
19531
+ "tool_choice",
19532
+ "tools",
19533
+ "top_p"
19534
+ ]
19535
+ }
19536
+ },
19475
19537
  {
19476
19538
  "id": "mistralai/mistral-nemo",
19477
19539
  "name": "Mistral: Mistral Nemo",
@@ -29554,6 +29616,7 @@
29554
29616
  "response_format",
29555
29617
  "seed",
29556
29618
  "stop",
29619
+ "structured_outputs",
29557
29620
  "temperature",
29558
29621
  "tool_choice",
29559
29622
  "tools",
@@ -29700,7 +29763,7 @@
29700
29763
  "name": "Sonar",
29701
29764
  "provider": "perplexity",
29702
29765
  "family": "sonar",
29703
- "created_at": "2025-08-13 11:35:41 +0200",
29766
+ "created_at": "2025-08-14 00:27:27 +0200",
29704
29767
  "context_window": 128000,
29705
29768
  "max_output_tokens": 4096,
29706
29769
  "knowledge_cutoff": null,
@@ -29732,7 +29795,7 @@
29732
29795
  "name": "Sonar Deep Research",
29733
29796
  "provider": "perplexity",
29734
29797
  "family": "sonar_deep_research",
29735
- "created_at": "2025-08-13 11:35:41 +0200",
29798
+ "created_at": "2025-08-14 00:27:27 +0200",
29736
29799
  "context_window": 128000,
29737
29800
  "max_output_tokens": 4096,
29738
29801
  "knowledge_cutoff": null,
@@ -29767,7 +29830,7 @@
29767
29830
  "name": "Sonar Pro",
29768
29831
  "provider": "perplexity",
29769
29832
  "family": "sonar_pro",
29770
- "created_at": "2025-08-13 11:35:41 +0200",
29833
+ "created_at": "2025-08-14 00:27:27 +0200",
29771
29834
  "context_window": 200000,
29772
29835
  "max_output_tokens": 8192,
29773
29836
  "knowledge_cutoff": null,
@@ -29799,7 +29862,7 @@
29799
29862
  "name": "Sonar Reasoning",
29800
29863
  "provider": "perplexity",
29801
29864
  "family": "sonar_reasoning",
29802
- "created_at": "2025-08-13 11:35:41 +0200",
29865
+ "created_at": "2025-08-14 00:27:27 +0200",
29803
29866
  "context_window": 128000,
29804
29867
  "max_output_tokens": 4096,
29805
29868
  "knowledge_cutoff": null,
@@ -29831,7 +29894,7 @@
29831
29894
  "name": "Sonar Reasoning Pro",
29832
29895
  "provider": "perplexity",
29833
29896
  "family": "sonar_reasoning_pro",
29834
- "created_at": "2025-08-13 11:35:41 +0200",
29897
+ "created_at": "2025-08-14 00:27:27 +0200",
29835
29898
  "context_window": 128000,
29836
29899
  "max_output_tokens": 8192,
29837
29900
  "knowledge_cutoff": null,
@@ -64,16 +64,12 @@ module RubyLLM
64
64
 
65
65
  model = Model::Info.new(
66
66
  id: model_id,
67
- name: model_id.gsub('-', ' ').capitalize,
67
+ name: model_id.tr('-', ' ').capitalize,
68
68
  provider: provider_instance.slug,
69
69
  capabilities: %w[function_calling streaming],
70
70
  modalities: { input: %w[text image], output: %w[text] },
71
71
  metadata: { warning: 'Assuming model exists, capabilities may not be accurate' }
72
72
  )
73
- if RubyLLM.config.log_assume_model_exists
74
- RubyLLM.logger.warn "Assuming model '#{model_id}' exists for provider '#{provider}'. " \
75
- 'Capabilities may not be accurately reflected.'
76
- end
77
73
  else
78
74
  model = Models.find model_id, provider
79
75
  provider_class = Provider.providers[model.provider.to_sym] || raise(Error,
@@ -47,7 +47,7 @@ module RubyLLM
47
47
  end
48
48
 
49
49
  def handle_stream(&block)
50
- buffer = String.new
50
+ buffer = +''
51
51
  proc do |chunk, _bytes, env|
52
52
  if env && env.status != 200
53
53
  handle_failed_response(chunk, buffer, env)
@@ -80,7 +80,7 @@ module RubyLLM
80
80
  content: extract_content(data),
81
81
  tool_calls: tool_calls,
82
82
  input_tokens: data.dig('usageMetadata', 'promptTokenCount'),
83
- output_tokens: data.dig('usageMetadata', 'candidatesTokenCount'),
83
+ output_tokens: calculate_output_tokens(data),
84
84
  model_id: data['modelVersion'] || response.env.url.path.split('/')[3].split(':')[0],
85
85
  raw: response
86
86
  )
@@ -133,6 +133,12 @@ module RubyLLM
133
133
  parts = candidate.dig('content', 'parts')
134
134
  parts&.any? { |p| p['functionCall'] }
135
135
  end
136
+
137
+ def calculate_output_tokens(data)
138
+ candidates = data.dig('usageMetadata', 'candidatesTokenCount') || 0
139
+ thoughts = data.dig('usageMetadata', 'thoughtsTokenCount') || 0
140
+ candidates + thoughts
141
+ end
136
142
  end
137
143
  end
138
144
  end
@@ -42,7 +42,10 @@ module RubyLLM
42
42
  end
43
43
 
44
44
  def extract_output_tokens(data)
45
- data.dig('usageMetadata', 'candidatesTokenCount')
45
+ candidates = data.dig('usageMetadata', 'candidatesTokenCount') || 0
46
+ thoughts = data.dig('usageMetadata', 'thoughtsTokenCount') || 0
47
+ total = candidates + thoughts
48
+ total.positive? ? total : nil
46
49
  end
47
50
 
48
51
  def parse_streaming_error(data)
@@ -198,11 +198,11 @@ module RubyLLM
198
198
  .gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1\2\3')
199
199
  .gsub(/^(?:Gpt|Chatgpt|Tts|Dall E) /) { |m| special_prefix_format(m.strip) }
200
200
  .gsub(/^O([13]) /, 'O\1-')
201
- .gsub(/^O[13] Mini/, '\0'.gsub(' ', '-'))
201
+ .gsub(/^O[13] Mini/, '\0'.tr(' ', '-'))
202
202
  .gsub(/\d\.\d /, '\0'.sub(' ', '-'))
203
203
  .gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime|Transcribe|Tts)/, '4o-')
204
204
  .gsub(/\bHd\b/, 'HD')
205
- .gsub(/(?:Omni|Text) Moderation/, '\0'.gsub(' ', '-'))
205
+ .gsub(/(?:Omni|Text) Moderation/, '\0'.tr(' ', '-'))
206
206
  .gsub('Text Embedding', 'text-embedding-')
207
207
  end
208
208
 
@@ -8,7 +8,7 @@ module RubyLLM
8
8
  attr_reader :content, :model_id, :tool_calls
9
9
 
10
10
  def initialize
11
- @content = String.new
11
+ @content = +''
12
12
  @tool_calls = {}
13
13
  @input_tokens = 0
14
14
  @output_tokens = 0
@@ -66,7 +66,7 @@ module RubyLLM
66
66
  new_tool_calls.each_value do |tool_call|
67
67
  if tool_call.id
68
68
  tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
69
- tool_call_arguments = tool_call.arguments.empty? ? String.new : tool_call.arguments
69
+ tool_call_arguments = tool_call.arguments.empty? ? +'' : tool_call.arguments
70
70
  @tool_calls[tool_call.id] = ToolCall.new(
71
71
  id: tool_call_id,
72
72
  name: tool_call.name,
@@ -43,7 +43,7 @@ module RubyLLM
43
43
  private
44
44
 
45
45
  def to_json_stream(&)
46
- buffer = String.new
46
+ buffer = +''
47
47
  parser = EventStreamParser::Parser.new
48
48
 
49
49
  create_stream_processor(parser, buffer, &)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '1.6.1'
4
+ VERSION = '1.6.2'
5
5
  end
@@ -65,7 +65,7 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
65
65
 
66
66
  base_name = Regexp.last_match(1)
67
67
  # Normalize to Anthropic naming convention
68
- anthropic_name = base_name.gsub('.', '-')
68
+ anthropic_name = base_name.tr('.', '-')
69
69
 
70
70
  # Skip if we already have an alias for this
71
71
  next if aliases[anthropic_name]
@@ -91,7 +91,7 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
91
91
  # OpenRouter uses "google/" prefix and sometimes different naming
92
92
  openrouter_variants = [
93
93
  "google/#{model}",
94
- "google/#{model.gsub('gemini-', 'gemini-').gsub('.', '-')}",
94
+ "google/#{model.gsub('gemini-', 'gemini-').tr('.', '-')}",
95
95
  "google/#{model.gsub('gemini-', 'gemini-')}"
96
96
  ]
97
97
 
@@ -86,7 +86,7 @@ def generate_models_markdown
86
86
  end
87
87
 
88
88
  def generate_provider_sections
89
- RubyLLM::Provider.providers.map do |provider, provider_class|
89
+ RubyLLM::Provider.providers.filter_map do |provider, provider_class|
90
90
  models = RubyLLM.models.by_provider(provider)
91
91
  next if models.none?
92
92
 
@@ -95,7 +95,7 @@ def generate_provider_sections
95
95
 
96
96
  #{models_table(models)}
97
97
  PROVIDER
98
- end.compact.join("\n\n")
98
+ end.join("\n\n")
99
99
  end
100
100
 
101
101
  def generate_capability_sections
@@ -107,7 +107,7 @@ def generate_capability_sections
107
107
  'Batch Processing' => RubyLLM.models.select { |m| m.capabilities.include?('batch') }
108
108
  }
109
109
 
110
- capabilities.map do |capability, models|
110
+ capabilities.filter_map do |capability, models|
111
111
  next if models.none?
112
112
 
113
113
  <<~CAPABILITY
@@ -115,7 +115,7 @@ def generate_capability_sections
115
115
 
116
116
  #{models_table(models)}
117
117
  CAPABILITY
118
- end.compact.join("\n\n")
118
+ end.join("\n\n")
119
119
  end
120
120
 
121
121
  def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino