diff --git a/.rubocop.yml b/.rubocop.yml index 0c7d6f45..e895ef9a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,5 @@ plugins: + - rubocop-performance - rubocop-rake - rubocop-rspec @@ -21,7 +22,18 @@ Metrics/MethodLength: Enabled: false Metrics/ModuleLength: Enabled: false +Performance/CollectionLiteralInLoop: + Exclude: + - spec/**/* +Performance/RedundantBlockCall: + Enabled: false # TODO: temporarily disabled to avoid potential breaking change +Performance/StringInclude: + Exclude: + - lib/ruby_llm/providers/**/capabilities.rb +Performance/UnfreezeString: + Exclude: + - spec/**/* RSpec/ExampleLength: Enabled: false RSpec/MultipleExpectations: - Enabled: false \ No newline at end of file + Enabled: false diff --git a/Gemfile b/Gemfile index 1b915c3c..9175a69c 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,7 @@ group :development do # rubocop:disable Metrics/BlockLength gem 'reline' gem 'rspec', '~> 3.12' gem 'rubocop', '>= 1.0' + gem 'rubocop-performance' gem 'rubocop-rake', '>= 0.6' gem 'rubocop-rspec' gem 'ruby_llm-schema', '~> 0.1.0' diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 85fe5396..ee919672 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -234,7 +234,7 @@ def setup_persistence_callbacks end def persist_new_message - @message = messages.create!(role: :assistant, content: String.new) + @message = messages.create!(role: :assistant, content: '') end def persist_message_completion(message) diff --git a/lib/ruby_llm/content.rb b/lib/ruby_llm/content.rb index 392d8ffd..6358d4ef 100644 --- a/lib/ruby_llm/content.rb +++ b/lib/ruby_llm/content.rb @@ -43,7 +43,7 @@ def process_attachments_array_or_string(attachments) def process_attachments(attachments) if attachments.is_a?(Hash) # Ignores types (like :image, :audio, :text, :pdf) since we have robust MIME type detection - attachments.each_value(&method(:process_attachments_array_or_string)) + attachments.each_value { |attachment| process_attachments_array_or_string(attachment) } else process_attachments_array_or_string attachments end diff --git a/lib/ruby_llm/models.rb b/lib/ruby_llm/models.rb index 6a6bd10e..d7914a4c 100644 --- a/lib/ruby_llm/models.rb +++ b/lib/ruby_llm/models.rb @@ -64,7 +64,7 @@ def resolve(model_id, provider: nil, assume_exists: false, config: nil) # ruboco model = Model::Info.new( id: model_id, - name: model_id.gsub('-', ' ').capitalize, + name: model_id.tr('-', ' ').capitalize, provider: provider_instance.slug, capabilities: %w[function_calling streaming], modalities: { input: %w[text image], output: %w[text] }, diff --git a/lib/ruby_llm/providers/bedrock/streaming/base.rb b/lib/ruby_llm/providers/bedrock/streaming/base.rb index 222673f3..34772978 100644 --- a/lib/ruby_llm/providers/bedrock/streaming/base.rb +++ b/lib/ruby_llm/providers/bedrock/streaming/base.rb @@ -47,7 +47,7 @@ def stream_response(connection, payload, additional_headers = {}, &block) end def handle_stream(&block) - buffer = String.new + buffer = +'' proc do |chunk, _bytes, env| if env && env.status != 200 handle_failed_response(chunk, buffer, env) diff --git a/lib/ruby_llm/providers/openai/capabilities.rb b/lib/ruby_llm/providers/openai/capabilities.rb index 9f4efac4..4fa23ae6 100644 --- a/lib/ruby_llm/providers/openai/capabilities.rb +++ b/lib/ruby_llm/providers/openai/capabilities.rb @@ -198,11 +198,11 @@ def apply_special_formatting(name) .gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1\2\3') .gsub(/^(?:Gpt|Chatgpt|Tts|Dall E) /) { |m| special_prefix_format(m.strip) } .gsub(/^O([13]) /, 'O\1-') - .gsub(/^O[13] Mini/, '\0'.gsub(' ', '-')) + .gsub(/^O[13] Mini/, '\0'.tr(' ', '-')) .gsub(/\d\.\d /, '\0'.sub(' ', '-')) .gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime|Transcribe|Tts)/, '4o-') .gsub(/\bHd\b/, 'HD') - .gsub(/(?:Omni|Text) Moderation/, '\0'.gsub(' ', '-')) + .gsub(/(?:Omni|Text) Moderation/, '\0'.tr(' ', '-')) .gsub('Text Embedding', 'text-embedding-') end diff --git a/lib/ruby_llm/stream_accumulator.rb b/lib/ruby_llm/stream_accumulator.rb index 3764838c..6d2715a5 100644 --- a/lib/ruby_llm/stream_accumulator.rb +++ b/lib/ruby_llm/stream_accumulator.rb @@ -8,7 +8,7 @@ class StreamAccumulator attr_reader :content, :model_id, :tool_calls def initialize - @content = String.new + @content = +'' @tool_calls = {} @input_tokens = 0 @output_tokens = 0 @@ -66,7 +66,7 @@ def accumulate_tool_calls(new_tool_calls) new_tool_calls.each_value do |tool_call| if tool_call.id tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id - tool_call_arguments = tool_call.arguments.empty? ? String.new : tool_call.arguments + tool_call_arguments = tool_call.arguments.empty? ? +'' : tool_call.arguments @tool_calls[tool_call.id] = ToolCall.new( id: tool_call_id, name: tool_call.name, diff --git a/lib/ruby_llm/streaming.rb b/lib/ruby_llm/streaming.rb index 29566241..dae2f2fc 100644 --- a/lib/ruby_llm/streaming.rb +++ b/lib/ruby_llm/streaming.rb @@ -43,7 +43,7 @@ def handle_stream(&block) private def to_json_stream(&) - buffer = String.new + buffer = +'' parser = EventStreamParser::Parser.new create_stream_processor(parser, buffer, &) diff --git a/lib/tasks/aliases.rake b/lib/tasks/aliases.rake index bd55690c..c58a7085 100644 --- a/lib/tasks/aliases.rake +++ b/lib/tasks/aliases.rake @@ -65,7 +65,7 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength base_name = Regexp.last_match(1) # Normalize to Anthropic naming convention - anthropic_name = base_name.gsub('.', '-') + anthropic_name = base_name.tr('.', '-') # Skip if we already have an alias for this next if aliases[anthropic_name] @@ -91,7 +91,7 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength # OpenRouter uses "google/" prefix and sometimes different naming openrouter_variants = [ "google/#{model}", - "google/#{model.gsub('gemini-', 'gemini-').gsub('.', '-')}", + "google/#{model.gsub('gemini-', 'gemini-').tr('.', '-')}", "google/#{model.gsub('gemini-', 'gemini-')}" ] diff --git a/lib/tasks/models_docs.rake b/lib/tasks/models_docs.rake index a31bb267..2e849c16 100644 --- a/lib/tasks/models_docs.rake +++ b/lib/tasks/models_docs.rake @@ -86,7 +86,7 @@ def generate_models_markdown end def generate_provider_sections - RubyLLM::Provider.providers.map do |provider, provider_class| + RubyLLM::Provider.providers.filter_map do |provider, provider_class| models = RubyLLM.models.by_provider(provider) next if models.none? @@ -95,7 +95,7 @@ def generate_provider_sections #{models_table(models)} PROVIDER - end.compact.join("\n\n") + end.join("\n\n") end def generate_capability_sections @@ -107,7 +107,7 @@ def generate_capability_sections 'Batch Processing' => RubyLLM.models.select { |m| m.capabilities.include?('batch') } } - capabilities.map do |capability, models| + capabilities.filter_map do |capability, models| next if models.none? <<~CAPABILITY @@ -115,7 +115,7 @@ def generate_capability_sections #{models_table(models)} CAPABILITY - end.compact.join("\n\n") + end.join("\n\n") end def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity diff --git a/spec/ruby_llm/chat_error_spec.rb b/spec/ruby_llm/chat_error_spec.rb index afe58c73..31d82067 100644 --- a/spec/ruby_llm/chat_error_spec.rb +++ b/spec/ruby_llm/chat_error_spec.rb @@ -14,7 +14,7 @@ RSpec::Matchers.define :look_like_json do match do |actual| - actual.strip.start_with?('{') || actual.strip.start_with?('[') + actual.strip.start_with?('{', '[') end failure_message do |actual| diff --git a/spec/ruby_llm/chat_tools_spec.rb b/spec/ruby_llm/chat_tools_spec.rb index b1552106..b8138a8d 100644 --- a/spec/ruby_llm/chat_tools_spec.rb +++ b/spec/ruby_llm/chat_tools_spec.rb @@ -300,7 +300,7 @@ def execute(query:) # Monkey-patch to count complete calls described_class.define_method(:complete) do |&block| call_count += 1 - original_complete.bind(self).call(&block) + original_complete.bind_call(self, &block) end chat = RubyLLM.chat.with_tool(HaltingTool)