ruby_llm 0.1.0.pre30 → 0.1.0.pre31
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/.github/workflows/{gem-push.yml → cicd.yml} +32 -4
- data/.rspec_status +27 -0
- data/lib/ruby_llm/active_record/acts_as.rb +5 -5
- data/lib/ruby_llm/chat.rb +2 -2
- data/lib/ruby_llm/configuration.rb +3 -1
- data/lib/ruby_llm/content.rb +79 -0
- data/lib/ruby_llm/embedding.rb +9 -3
- data/lib/ruby_llm/message.rb +9 -1
- data/lib/ruby_llm/models.json +14 -14
- data/lib/ruby_llm/provider.rb +39 -14
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +81 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +86 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +37 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +97 -0
- data/lib/ruby_llm/providers/anthropic.rb +8 -234
- data/lib/ruby_llm/providers/deepseek/capabilites.rb +101 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -2
- data/lib/ruby_llm/providers/gemini/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/gemini/models.rb +20 -0
- data/lib/ruby_llm/providers/gemini.rb +5 -10
- data/lib/ruby_llm/providers/openai/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/openai/chat.rb +68 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +39 -0
- data/lib/ruby_llm/providers/openai/models.rb +40 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +31 -0
- data/lib/ruby_llm/providers/openai/tools.rb +69 -0
- data/lib/ruby_llm/providers/openai.rb +15 -197
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +4 -2
- data/ruby_llm.gemspec +2 -0
- metadata +48 -8
- data/.github/workflows/test.yml +0 -35
- data/lib/ruby_llm/model_capabilities/anthropic.rb +0 -79
- data/lib/ruby_llm/model_capabilities/deepseek.rb +0 -132
- data/lib/ruby_llm/model_capabilities/gemini.rb +0 -190
- data/lib/ruby_llm/model_capabilities/openai.rb +0 -189
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: a3c91f92537c2e154f458422c8447345cf1d230da68fb982df3b45b102fe985d
         | 
| 4 | 
            +
              data.tar.gz: fd24e6187a456c0ec7ade007cf39b0766e41da58bb8184a0567939ea1c048e75
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 6cba0bb735838fb5700d504e8665842815001bbab5c8a1a829131eaefef6e10b7633fde20be63bf4174eaac045298590489a777572a5bbea9141a32392bc7a36
         | 
| 7 | 
            +
              data.tar.gz: a2707917f85c6fdd31c8f61f6ae9d3414ac385d17bf017335572cf61be03bd6dcd344fd277e207fcbc0954e3cc445cbdeb40b17d432de0800191a31b0dab7383
         | 
| @@ -1,18 +1,46 @@ | |
| 1 | 
            -
            name:  | 
| 1 | 
            +
            name: CI
         | 
| 2 2 |  | 
| 3 3 | 
             
            on:
         | 
| 4 4 | 
             
              push:
         | 
| 5 5 | 
             
                branches: [ "main" ]
         | 
| 6 6 | 
             
              pull_request:
         | 
| 7 7 | 
             
                branches: [ "main" ]
         | 
| 8 | 
            +
              workflow_call:
         | 
| 8 9 |  | 
| 9 10 | 
             
            jobs:
         | 
| 10 11 | 
             
              test:
         | 
| 11 | 
            -
                 | 
| 12 | 
            +
                runs-on: ubuntu-latest
         | 
| 13 | 
            +
                strategy:
         | 
| 14 | 
            +
                  matrix:
         | 
| 15 | 
            +
                    ruby-version: ['3.1']
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                steps:
         | 
| 18 | 
            +
                - uses: actions/checkout@v4
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                - name: Set up Ruby
         | 
| 21 | 
            +
                  uses: ruby/setup-ruby@v1
         | 
| 22 | 
            +
                  with:
         | 
| 23 | 
            +
                    ruby-version: ${{ matrix.ruby-version }}
         | 
| 24 | 
            +
                    bundler-cache: true
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                - name: Install dependencies
         | 
| 27 | 
            +
                  run: bundle install
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                - name: Check code format
         | 
| 30 | 
            +
                  run: bundle exec rubocop
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                - name: Run tests
         | 
| 33 | 
            +
                  env:
         | 
| 34 | 
            +
                    OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
         | 
| 35 | 
            +
                    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
         | 
| 36 | 
            +
                    GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
         | 
| 37 | 
            +
                    DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
         | 
| 38 | 
            +
                  run: bundle exec rspec
         | 
| 12 39 |  | 
| 13 | 
            -
               | 
| 14 | 
            -
                needs: test # This ensures tests must pass before building/publishing
         | 
| 40 | 
            +
              publish:
         | 
| 15 41 | 
             
                name: Build + Publish
         | 
| 42 | 
            +
                needs: test
         | 
| 43 | 
            +
                if: github.event_name == 'push' && github.ref == 'refs/heads/main'
         | 
| 16 44 | 
             
                runs-on: ubuntu-latest
         | 
| 17 45 | 
             
                permissions:
         | 
| 18 46 | 
             
                  contents: read
         | 
    
        data/.rspec_status
    ADDED
    
    | @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            example_id                                     | status | run_time        |
         | 
| 2 | 
            +
            ---------------------------------------------- | ------ | --------------- |
         | 
| 3 | 
            +
            ./spec/integration/chat_spec.rb[1:1:1:1]       | passed | 0.5826 seconds  |
         | 
| 4 | 
            +
            ./spec/integration/chat_spec.rb[1:1:1:2]       | passed | 3.43 seconds    |
         | 
| 5 | 
            +
            ./spec/integration/chat_spec.rb[1:1:2:1]       | passed | 0.49383 seconds |
         | 
| 6 | 
            +
            ./spec/integration/chat_spec.rb[1:1:2:2]       | passed | 1.36 seconds    |
         | 
| 7 | 
            +
            ./spec/integration/chat_spec.rb[1:1:3:1]       | passed | 0.74049 seconds |
         | 
| 8 | 
            +
            ./spec/integration/chat_spec.rb[1:1:3:2]       | passed | 4.11 seconds    |
         | 
| 9 | 
            +
            ./spec/integration/embeddings_spec.rb[1:1:1:1] | passed | 0.2892 seconds  |
         | 
| 10 | 
            +
            ./spec/integration/embeddings_spec.rb[1:1:1:2] | passed | 0.31644 seconds |
         | 
| 11 | 
            +
            ./spec/integration/embeddings_spec.rb[1:1:2:1] | passed | 0.89277 seconds |
         | 
| 12 | 
            +
            ./spec/integration/embeddings_spec.rb[1:1:2:2] | passed | 1.55 seconds    |
         | 
| 13 | 
            +
            ./spec/integration/error_handling_spec.rb[1:1] | passed | 0.21297 seconds |
         | 
| 14 | 
            +
            ./spec/integration/rails_spec.rb[1:1]          | passed | 4.05 seconds    |
         | 
| 15 | 
            +
            ./spec/integration/rails_spec.rb[1:2]          | passed | 1.82 seconds    |
         | 
| 16 | 
            +
            ./spec/integration/streaming_spec.rb[1:1:1:1]  | passed | 0.58445 seconds |
         | 
| 17 | 
            +
            ./spec/integration/streaming_spec.rb[1:1:1:2]  | passed | 6.04 seconds    |
         | 
| 18 | 
            +
            ./spec/integration/streaming_spec.rb[1:1:2:1]  | passed | 0.47171 seconds |
         | 
| 19 | 
            +
            ./spec/integration/streaming_spec.rb[1:1:2:2]  | passed | 2.39 seconds    |
         | 
| 20 | 
            +
            ./spec/integration/streaming_spec.rb[1:1:3:1]  | passed | 0.72016 seconds |
         | 
| 21 | 
            +
            ./spec/integration/streaming_spec.rb[1:1:3:2]  | passed | 3.59 seconds    |
         | 
| 22 | 
            +
            ./spec/integration/tools_spec.rb[1:1:1:1]      | passed | 3.1 seconds     |
         | 
| 23 | 
            +
            ./spec/integration/tools_spec.rb[1:1:1:2]      | passed | 7.04 seconds    |
         | 
| 24 | 
            +
            ./spec/integration/tools_spec.rb[1:1:2:1]      | passed | 1.42 seconds    |
         | 
| 25 | 
            +
            ./spec/integration/tools_spec.rb[1:1:2:2]      | passed | 2.24 seconds    |
         | 
| 26 | 
            +
            ./spec/integration/tools_spec.rb[1:1:3:1]      | passed | 2.16 seconds    |
         | 
| 27 | 
            +
            ./spec/integration/tools_spec.rb[1:1:3:2]      | passed | 5.26 seconds    |
         | 
| @@ -73,22 +73,22 @@ module RubyLLM | |
| 73 73 | 
             
                  end
         | 
| 74 74 |  | 
| 75 75 | 
             
                  def to_llm
         | 
| 76 | 
            -
                    chat  | 
| 76 | 
            +
                    @chat ||= RubyLLM.chat(model: model_id)
         | 
| 77 77 |  | 
| 78 78 | 
             
                    # Load existing messages into chat
         | 
| 79 79 | 
             
                    messages.each do |msg|
         | 
| 80 | 
            -
                      chat.add_message(msg.to_llm)
         | 
| 80 | 
            +
                      @chat.add_message(msg.to_llm)
         | 
| 81 81 | 
             
                    end
         | 
| 82 82 |  | 
| 83 83 | 
             
                    # Set up message persistence
         | 
| 84 | 
            -
                    chat.on_new_message { persist_new_message }
         | 
| 85 | 
            -
             | 
| 84 | 
            +
                    @chat.on_new_message { persist_new_message }
         | 
| 85 | 
            +
                         .on_end_message { |msg| persist_message_completion(msg) }
         | 
| 86 86 | 
             
                  end
         | 
| 87 87 |  | 
| 88 88 | 
             
                  def ask(message, &block)
         | 
| 89 89 | 
             
                    message = { role: :user, content: message }
         | 
| 90 90 | 
             
                    messages.create!(**message)
         | 
| 91 | 
            -
                     | 
| 91 | 
            +
                    to_llm.complete(&block)
         | 
| 92 92 | 
             
                  end
         | 
| 93 93 |  | 
| 94 94 | 
             
                  alias say ask
         | 
    
        data/lib/ruby_llm/chat.rb
    CHANGED
    
    
| @@ -16,10 +16,12 @@ module RubyLLM | |
| 16 16 | 
             
                              :deepseek_api_key,
         | 
| 17 17 | 
             
                              :default_model,
         | 
| 18 18 | 
             
                              :default_embedding_model,
         | 
| 19 | 
            -
                              :request_timeout
         | 
| 19 | 
            +
                              :request_timeout,
         | 
| 20 | 
            +
                              :max_retries
         | 
| 20 21 |  | 
| 21 22 | 
             
                def initialize
         | 
| 22 23 | 
             
                  @request_timeout = 120
         | 
| 24 | 
            +
                  @max_retries = 3
         | 
| 23 25 | 
             
                  @default_model = 'gpt-4o-mini'
         | 
| 24 26 | 
             
                  @default_embedding_model = 'text-embedding-3-small'
         | 
| 25 27 | 
             
                end
         | 
| @@ -0,0 +1,79 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RubyLLM
         | 
| 4 | 
            +
              # Represents the content received from the LLM
         | 
| 5 | 
            +
              class Content
         | 
| 6 | 
            +
                def initialize(text = nil, attachments = {})
         | 
| 7 | 
            +
                  @parts = []
         | 
| 8 | 
            +
                  @parts << { type: 'text', text: text } unless text.nil? || text.empty?
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  Array(attachments[:image]).each do |source|
         | 
| 11 | 
            +
                    @parts << attach_image(source)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  Array(attachments[:audio]).each do |source|
         | 
| 15 | 
            +
                    @parts << attach_audio(source)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def to_a
         | 
| 20 | 
            +
                  return if @parts.empty?
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  @parts
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def format
         | 
| 26 | 
            +
                  return @parts.first[:text] if @parts.size == 1 && @parts.first[:type] == 'text'
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  to_a
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                private
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def attach_image(source) # rubocop:disable Metrics/MethodLength
         | 
| 34 | 
            +
                  source = File.expand_path(source) unless source.start_with?('http')
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  return { type: 'image_url', image_url: { url: source } } if source.start_with?('http')
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  data = Base64.strict_encode64(File.read(source))
         | 
| 39 | 
            +
                  mime_type = mime_type_for(source)
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  {
         | 
| 42 | 
            +
                    type: 'image',
         | 
| 43 | 
            +
                    source: {
         | 
| 44 | 
            +
                      type: 'base64',
         | 
| 45 | 
            +
                      media_type: mime_type,
         | 
| 46 | 
            +
                      data: data
         | 
| 47 | 
            +
                    }
         | 
| 48 | 
            +
                  }
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def attach_audio(source)
         | 
| 52 | 
            +
                  source = File.expand_path(source) unless source.start_with?('http')
         | 
| 53 | 
            +
                  data = encode_file(source)
         | 
| 54 | 
            +
                  format = File.extname(source).delete('.') || 'wav'
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  {
         | 
| 57 | 
            +
                    type: 'input_audio',
         | 
| 58 | 
            +
                    input_audio: {
         | 
| 59 | 
            +
                      data: data,
         | 
| 60 | 
            +
                      format: format
         | 
| 61 | 
            +
                    }
         | 
| 62 | 
            +
                  }
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def encode_file(source)
         | 
| 66 | 
            +
                  if source.start_with?('http')
         | 
| 67 | 
            +
                    response = Faraday.get(source)
         | 
| 68 | 
            +
                    Base64.strict_encode64(response.body)
         | 
| 69 | 
            +
                  else
         | 
| 70 | 
            +
                    Base64.strict_encode64(File.read(source))
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def mime_type_for(path)
         | 
| 75 | 
            +
                  ext = File.extname(path).delete('.')
         | 
| 76 | 
            +
                  "image/#{ext}"
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
            end
         | 
    
        data/lib/ruby_llm/embedding.rb
    CHANGED
    
    | @@ -3,10 +3,16 @@ | |
| 3 3 | 
             
            module RubyLLM
         | 
| 4 4 | 
             
              # Core embedding interface. Provides a clean way to generate embeddings
         | 
| 5 5 | 
             
              # from text using various provider models.
         | 
| 6 | 
            -
               | 
| 7 | 
            -
                 | 
| 6 | 
            +
              class Embedding
         | 
| 7 | 
            +
                attr_reader :vectors, :model, :input_tokens
         | 
| 8 8 |  | 
| 9 | 
            -
                def  | 
| 9 | 
            +
                def initialize(vectors:, model:, input_tokens: 0)
         | 
| 10 | 
            +
                  @vectors = vectors
         | 
| 11 | 
            +
                  @model = model
         | 
| 12 | 
            +
                  @input_tokens = input_tokens
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def self.embed(text, model: nil)
         | 
| 10 16 | 
             
                  model_id = model || RubyLLM.config.default_embedding_model
         | 
| 11 17 | 
             
                  Models.find(model_id)
         | 
| 12 18 |  | 
    
        data/lib/ruby_llm/message.rb
    CHANGED
    
    | @@ -11,7 +11,7 @@ module RubyLLM | |
| 11 11 |  | 
| 12 12 | 
             
                def initialize(options = {})
         | 
| 13 13 | 
             
                  @role = options[:role].to_sym
         | 
| 14 | 
            -
                  @content = options[:content]
         | 
| 14 | 
            +
                  @content = normalize_content(options[:content])
         | 
| 15 15 | 
             
                  @tool_calls = options[:tool_calls]
         | 
| 16 16 | 
             
                  @input_tokens = options[:input_tokens]
         | 
| 17 17 | 
             
                  @output_tokens = options[:output_tokens]
         | 
| @@ -47,6 +47,14 @@ module RubyLLM | |
| 47 47 |  | 
| 48 48 | 
             
                private
         | 
| 49 49 |  | 
| 50 | 
            +
                def normalize_content(content)
         | 
| 51 | 
            +
                  case content
         | 
| 52 | 
            +
                  when Content then content.format
         | 
| 53 | 
            +
                  when String then Content.new(content).format
         | 
| 54 | 
            +
                  else content
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 50 58 | 
             
                def ensure_valid_role
         | 
| 51 59 | 
             
                  raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
         | 
| 52 60 | 
             
                end
         | 
    
        data/lib/ruby_llm/models.json
    CHANGED
    
    | @@ -727,7 +727,7 @@ | |
| 727 727 | 
             
                "family": "gemini20_flash",
         | 
| 728 728 | 
             
                "supports_vision": true,
         | 
| 729 729 | 
             
                "supports_functions": true,
         | 
| 730 | 
            -
                "supports_json_mode":  | 
| 730 | 
            +
                "supports_json_mode": true,
         | 
| 731 731 | 
             
                "input_price_per_million": 0.1,
         | 
| 732 732 | 
             
                "output_price_per_million": 0.4,
         | 
| 733 733 | 
             
                "metadata": {
         | 
| @@ -746,7 +746,7 @@ | |
| 746 746 | 
             
                "family": "gemini20_flash",
         | 
| 747 747 | 
             
                "supports_vision": true,
         | 
| 748 748 | 
             
                "supports_functions": true,
         | 
| 749 | 
            -
                "supports_json_mode":  | 
| 749 | 
            +
                "supports_json_mode": true,
         | 
| 750 750 | 
             
                "input_price_per_million": 0.1,
         | 
| 751 751 | 
             
                "output_price_per_million": 0.4,
         | 
| 752 752 | 
             
                "metadata": {
         | 
| @@ -765,7 +765,7 @@ | |
| 765 765 | 
             
                "family": "gemini20_flash",
         | 
| 766 766 | 
             
                "supports_vision": true,
         | 
| 767 767 | 
             
                "supports_functions": true,
         | 
| 768 | 
            -
                "supports_json_mode":  | 
| 768 | 
            +
                "supports_json_mode": true,
         | 
| 769 769 | 
             
                "input_price_per_million": 0.1,
         | 
| 770 770 | 
             
                "output_price_per_million": 0.4,
         | 
| 771 771 | 
             
                "metadata": {
         | 
| @@ -784,7 +784,7 @@ | |
| 784 784 | 
             
                "family": "gemini20_flash",
         | 
| 785 785 | 
             
                "supports_vision": true,
         | 
| 786 786 | 
             
                "supports_functions": true,
         | 
| 787 | 
            -
                "supports_json_mode":  | 
| 787 | 
            +
                "supports_json_mode": true,
         | 
| 788 788 | 
             
                "input_price_per_million": 0.1,
         | 
| 789 789 | 
             
                "output_price_per_million": 0.4,
         | 
| 790 790 | 
             
                "metadata": {
         | 
| @@ -803,7 +803,7 @@ | |
| 803 803 | 
             
                "family": "gemini20_flash",
         | 
| 804 804 | 
             
                "supports_vision": true,
         | 
| 805 805 | 
             
                "supports_functions": true,
         | 
| 806 | 
            -
                "supports_json_mode":  | 
| 806 | 
            +
                "supports_json_mode": true,
         | 
| 807 807 | 
             
                "input_price_per_million": 0.1,
         | 
| 808 808 | 
             
                "output_price_per_million": 0.4,
         | 
| 809 809 | 
             
                "metadata": {
         | 
| @@ -822,7 +822,7 @@ | |
| 822 822 | 
             
                "family": "gemini20_flash",
         | 
| 823 823 | 
             
                "supports_vision": true,
         | 
| 824 824 | 
             
                "supports_functions": true,
         | 
| 825 | 
            -
                "supports_json_mode":  | 
| 825 | 
            +
                "supports_json_mode": true,
         | 
| 826 826 | 
             
                "input_price_per_million": 0.1,
         | 
| 827 827 | 
             
                "output_price_per_million": 0.4,
         | 
| 828 828 | 
             
                "metadata": {
         | 
| @@ -841,7 +841,7 @@ | |
| 841 841 | 
             
                "family": "gemini20_flash_lite",
         | 
| 842 842 | 
             
                "supports_vision": true,
         | 
| 843 843 | 
             
                "supports_functions": false,
         | 
| 844 | 
            -
                "supports_json_mode":  | 
| 844 | 
            +
                "supports_json_mode": true,
         | 
| 845 845 | 
             
                "input_price_per_million": 0.075,
         | 
| 846 846 | 
             
                "output_price_per_million": 0.3,
         | 
| 847 847 | 
             
                "metadata": {
         | 
| @@ -860,7 +860,7 @@ | |
| 860 860 | 
             
                "family": "gemini20_flash_lite",
         | 
| 861 861 | 
             
                "supports_vision": true,
         | 
| 862 862 | 
             
                "supports_functions": false,
         | 
| 863 | 
            -
                "supports_json_mode":  | 
| 863 | 
            +
                "supports_json_mode": true,
         | 
| 864 864 | 
             
                "input_price_per_million": 0.075,
         | 
| 865 865 | 
             
                "output_price_per_million": 0.3,
         | 
| 866 866 | 
             
                "metadata": {
         | 
| @@ -879,7 +879,7 @@ | |
| 879 879 | 
             
                "family": "gemini20_flash",
         | 
| 880 880 | 
             
                "supports_vision": true,
         | 
| 881 881 | 
             
                "supports_functions": true,
         | 
| 882 | 
            -
                "supports_json_mode":  | 
| 882 | 
            +
                "supports_json_mode": true,
         | 
| 883 883 | 
             
                "input_price_per_million": 0.1,
         | 
| 884 884 | 
             
                "output_price_per_million": 0.4,
         | 
| 885 885 | 
             
                "metadata": {
         | 
| @@ -898,7 +898,7 @@ | |
| 898 898 | 
             
                "family": "gemini20_flash",
         | 
| 899 899 | 
             
                "supports_vision": true,
         | 
| 900 900 | 
             
                "supports_functions": true,
         | 
| 901 | 
            -
                "supports_json_mode":  | 
| 901 | 
            +
                "supports_json_mode": true,
         | 
| 902 902 | 
             
                "input_price_per_million": 0.1,
         | 
| 903 903 | 
             
                "output_price_per_million": 0.4,
         | 
| 904 904 | 
             
                "metadata": {
         | 
| @@ -917,7 +917,7 @@ | |
| 917 917 | 
             
                "family": "gemini20_flash",
         | 
| 918 918 | 
             
                "supports_vision": true,
         | 
| 919 919 | 
             
                "supports_functions": true,
         | 
| 920 | 
            -
                "supports_json_mode":  | 
| 920 | 
            +
                "supports_json_mode": true,
         | 
| 921 921 | 
             
                "input_price_per_million": 0.1,
         | 
| 922 922 | 
             
                "output_price_per_million": 0.4,
         | 
| 923 923 | 
             
                "metadata": {
         | 
| @@ -936,7 +936,7 @@ | |
| 936 936 | 
             
                "family": "gemini20_flash",
         | 
| 937 937 | 
             
                "supports_vision": true,
         | 
| 938 938 | 
             
                "supports_functions": true,
         | 
| 939 | 
            -
                "supports_json_mode":  | 
| 939 | 
            +
                "supports_json_mode": true,
         | 
| 940 940 | 
             
                "input_price_per_million": 0.1,
         | 
| 941 941 | 
             
                "output_price_per_million": 0.4,
         | 
| 942 942 | 
             
                "metadata": {
         | 
| @@ -955,7 +955,7 @@ | |
| 955 955 | 
             
                "family": "other",
         | 
| 956 956 | 
             
                "supports_vision": true,
         | 
| 957 957 | 
             
                "supports_functions": true,
         | 
| 958 | 
            -
                "supports_json_mode":  | 
| 958 | 
            +
                "supports_json_mode": true,
         | 
| 959 959 | 
             
                "input_price_per_million": 0.075,
         | 
| 960 960 | 
             
                "output_price_per_million": 0.3,
         | 
| 961 961 | 
             
                "metadata": {
         | 
| @@ -974,7 +974,7 @@ | |
| 974 974 | 
             
                "family": "other",
         | 
| 975 975 | 
             
                "supports_vision": true,
         | 
| 976 976 | 
             
                "supports_functions": true,
         | 
| 977 | 
            -
                "supports_json_mode":  | 
| 977 | 
            +
                "supports_json_mode": true,
         | 
| 978 978 | 
             
                "input_price_per_million": 0.075,
         | 
| 979 979 | 
             
                "output_price_per_million": 0.3,
         | 
| 980 980 | 
             
                "metadata": {
         | 
    
        data/lib/ruby_llm/provider.rb
    CHANGED
    
    | @@ -5,15 +5,11 @@ module RubyLLM | |
| 5 5 | 
             
              # Handles the complexities of API communication, streaming responses,
         | 
| 6 6 | 
             
              # and error handling so individual providers can focus on their unique features.
         | 
| 7 7 | 
             
              module Provider
         | 
| 8 | 
            -
                def self.included(base)
         | 
| 9 | 
            -
                  base.include(InstanceMethods)
         | 
| 10 | 
            -
                end
         | 
| 11 | 
            -
             | 
| 12 8 | 
             
                # Common functionality for all LLM providers. Implements the core provider
         | 
| 13 9 | 
             
                # interface so specific providers only need to implement a few key methods.
         | 
| 14 | 
            -
                module  | 
| 10 | 
            +
                module Methods
         | 
| 15 11 | 
             
                  def complete(messages, tools:, temperature:, model:, &block)
         | 
| 16 | 
            -
                    payload =  | 
| 12 | 
            +
                    payload = render_payload messages, tools: tools, temperature: temperature, model: model, stream: block_given?
         | 
| 17 13 |  | 
| 18 14 | 
             
                    if block_given?
         | 
| 19 15 | 
             
                      stream_response payload, &block
         | 
| @@ -31,7 +27,7 @@ module RubyLLM | |
| 31 27 | 
             
                  end
         | 
| 32 28 |  | 
| 33 29 | 
             
                  def embed(text, model:)
         | 
| 34 | 
            -
                    payload =  | 
| 30 | 
            +
                    payload = render_embedding_payload text, model: model
         | 
| 35 31 | 
             
                    response = post embedding_url, payload
         | 
| 36 32 | 
             
                    parse_embedding_response response
         | 
| 37 33 | 
             
                  end
         | 
| @@ -63,9 +59,29 @@ module RubyLLM | |
| 63 59 | 
             
                    end
         | 
| 64 60 | 
             
                  end
         | 
| 65 61 |  | 
| 66 | 
            -
                  def connection
         | 
| 62 | 
            +
                  def connection # rubocop:disable Metrics/MethodLength
         | 
| 67 63 | 
             
                    @connection ||= Faraday.new(api_base) do |f|
         | 
| 68 64 | 
             
                      f.options.timeout = RubyLLM.config.request_timeout
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                      # Add retry middleware before request/response handling
         | 
| 67 | 
            +
                      f.request :retry, {
         | 
| 68 | 
            +
                        max: RubyLLM.config.max_retries,
         | 
| 69 | 
            +
                        interval: 0.05,
         | 
| 70 | 
            +
                        interval_randomness: 0.5,
         | 
| 71 | 
            +
                        backoff_factor: 2,
         | 
| 72 | 
            +
                        exceptions: [
         | 
| 73 | 
            +
                          Errno::ETIMEDOUT,
         | 
| 74 | 
            +
                          Timeout::Error,
         | 
| 75 | 
            +
                          Faraday::TimeoutError,
         | 
| 76 | 
            +
                          Faraday::ConnectionFailed,
         | 
| 77 | 
            +
                          Faraday::RetriableResponse,
         | 
| 78 | 
            +
                          RubyLLM::RateLimitError,
         | 
| 79 | 
            +
                          RubyLLM::ServerError,
         | 
| 80 | 
            +
                          RubyLLM::ServiceUnavailableError
         | 
| 81 | 
            +
                        ],
         | 
| 82 | 
            +
                        retry_statuses: [429, 500, 502, 503, 504]
         | 
| 83 | 
            +
                      }
         | 
| 84 | 
            +
             | 
| 69 85 | 
             
                      f.request :json
         | 
| 70 86 | 
             
                      f.response :json
         | 
| 71 87 | 
             
                      f.adapter Faraday.default_adapter
         | 
| @@ -111,9 +127,16 @@ module RubyLLM | |
| 111 127 | 
             
                  maybe_json
         | 
| 112 128 | 
             
                end
         | 
| 113 129 |  | 
| 130 | 
            +
                def parse_error(response)
         | 
| 131 | 
            +
                  return if response.body.empty?
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  body = try_parse_json(response.body)
         | 
| 134 | 
            +
                  body.is_a?(Hash) ? body.dig('error', 'message') : body
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
             | 
| 114 137 | 
             
                def capabilities
         | 
| 115 138 | 
             
                  provider_name = self.class.name.split('::').last
         | 
| 116 | 
            -
                   | 
| 139 | 
            +
                  provider_name::Capabilities
         | 
| 117 140 | 
             
                end
         | 
| 118 141 |  | 
| 119 142 | 
             
                def slug
         | 
| @@ -121,15 +144,17 @@ module RubyLLM | |
| 121 144 | 
             
                end
         | 
| 122 145 |  | 
| 123 146 | 
             
                class << self
         | 
| 124 | 
            -
                  def  | 
| 125 | 
            -
                     | 
| 147 | 
            +
                  def extended(base)
         | 
| 148 | 
            +
                    base.extend(Methods)
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  def register(name, provider_module)
         | 
| 152 | 
            +
                    providers[name.to_sym] = provider_module
         | 
| 126 153 | 
             
                  end
         | 
| 127 154 |  | 
| 128 155 | 
             
                  def for(model)
         | 
| 129 156 | 
             
                    model_info = Models.find(model)
         | 
| 130 | 
            -
                     | 
| 131 | 
            -
             | 
| 132 | 
            -
                    provider_class.new
         | 
| 157 | 
            +
                    providers[model_info.provider.to_sym]
         | 
| 133 158 | 
             
                  end
         | 
| 134 159 |  | 
| 135 160 | 
             
                  def providers
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RubyLLM
         | 
| 4 | 
            +
              module Providers
         | 
| 5 | 
            +
                module Anthropic
         | 
| 6 | 
            +
                  # Determines capabilities and pricing for Anthropic models
         | 
| 7 | 
            +
                  module Capabilities
         | 
| 8 | 
            +
                    module_function
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    def determine_context_window(model_id)
         | 
| 11 | 
            +
                      case model_id
         | 
| 12 | 
            +
                      when /claude-3/ then 200_000
         | 
| 13 | 
            +
                      else 100_000
         | 
| 14 | 
            +
                      end
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    def determine_max_tokens(model_id)
         | 
| 18 | 
            +
                      case model_id
         | 
| 19 | 
            +
                      when /claude-3-5/ then 8_192
         | 
| 20 | 
            +
                      else 4_096
         | 
| 21 | 
            +
                      end
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    def get_input_price(model_id)
         | 
| 25 | 
            +
                      PRICES.dig(model_family(model_id), :input) || default_input_price
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    def get_output_price(model_id)
         | 
| 29 | 
            +
                      PRICES.dig(model_family(model_id), :output) || default_output_price
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    def supports_vision?(model_id)
         | 
| 33 | 
            +
                      return false if model_id.match?(/claude-3-5-haiku/)
         | 
| 34 | 
            +
                      return false if model_id.match?(/claude-[12]/)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                      true
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    def supports_functions?(model_id)
         | 
| 40 | 
            +
                      model_id.include?('claude-3')
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    def supports_json_mode?(model_id)
         | 
| 44 | 
            +
                      model_id.include?('claude-3')
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    def model_family(model_id)
         | 
| 48 | 
            +
                      case model_id
         | 
| 49 | 
            +
                      when /claude-3-5-sonnet/  then :claude35_sonnet
         | 
| 50 | 
            +
                      when /claude-3-5-haiku/   then :claude35_haiku
         | 
| 51 | 
            +
                      when /claude-3-opus/      then :claude3_opus
         | 
| 52 | 
            +
                      when /claude-3-sonnet/    then :claude3_sonnet
         | 
| 53 | 
            +
                      when /claude-3-haiku/     then :claude3_haiku
         | 
| 54 | 
            +
                      else :claude2
         | 
| 55 | 
            +
                      end
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    def model_type(_)
         | 
| 59 | 
            +
                      'chat'
         | 
| 60 | 
            +
                    end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    PRICES = {
         | 
| 63 | 
            +
                      claude35_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
         | 
| 64 | 
            +
                      claude35_haiku: { input: 0.80, output: 4.0 }, # $0.80/$4.00 per million tokens
         | 
| 65 | 
            +
                      claude3_opus: { input: 15.0, output: 75.0 }, # $15.00/$75.00 per million tokens
         | 
| 66 | 
            +
                      claude3_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
         | 
| 67 | 
            +
                      claude3_haiku: { input: 0.25, output: 1.25 }, # $0.25/$1.25 per million tokens
         | 
| 68 | 
            +
                      claude2: { input: 3.0,  output: 15.0 } # Default pricing for Claude 2.x models
         | 
| 69 | 
            +
                    }.freeze
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    def default_input_price
         | 
| 72 | 
            +
                      3.0
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    def default_output_price
         | 
| 76 | 
            +
                      15.0
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| @@ -0,0 +1,86 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RubyLLM
         | 
| 4 | 
            +
              module Providers
         | 
| 5 | 
            +
                module Anthropic
         | 
| 6 | 
            +
                  # Chat methods of the OpenAI API integration
         | 
| 7 | 
            +
                  module Chat
         | 
| 8 | 
            +
                    private
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    def completion_url
         | 
| 11 | 
            +
                      '/v1/messages'
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    def render_payload(messages, tools:, temperature:, model:, stream: false)
         | 
| 15 | 
            +
                      {
         | 
| 16 | 
            +
                        model: model,
         | 
| 17 | 
            +
                        messages: messages.map { |msg| format_message(msg) },
         | 
| 18 | 
            +
                        temperature: temperature,
         | 
| 19 | 
            +
                        stream: stream,
         | 
| 20 | 
            +
                        max_tokens: RubyLLM.models.find(model).max_tokens
         | 
| 21 | 
            +
                      }.tap do |payload|
         | 
| 22 | 
            +
                        payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
         | 
| 23 | 
            +
                      end
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    def parse_completion_response(response)
         | 
| 27 | 
            +
                      data = response.body
         | 
| 28 | 
            +
                      content_blocks = data['content'] || []
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      text_content = extract_text_content(content_blocks)
         | 
| 31 | 
            +
                      tool_use = find_tool_use(content_blocks)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                      build_message(data, text_content, tool_use)
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    def extract_text_content(blocks)
         | 
| 37 | 
            +
                      text_blocks = blocks.select { |c| c['type'] == 'text' }
         | 
| 38 | 
            +
                      text_blocks.map { |c| c['text'] }.join('')
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    def build_message(data, content, tool_use)
         | 
| 42 | 
            +
                      Message.new(
         | 
| 43 | 
            +
                        role: :assistant,
         | 
| 44 | 
            +
                        content: content,
         | 
| 45 | 
            +
                        tool_calls: parse_tool_calls(tool_use),
         | 
| 46 | 
            +
                        input_tokens: data.dig('usage', 'input_tokens'),
         | 
| 47 | 
            +
                        output_tokens: data.dig('usage', 'output_tokens'),
         | 
| 48 | 
            +
                        model_id: data['model']
         | 
| 49 | 
            +
                      )
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    def format_message(msg)
         | 
| 53 | 
            +
                      if msg.tool_call?
         | 
| 54 | 
            +
                        format_tool_call(msg)
         | 
| 55 | 
            +
                      elsif msg.tool_result?
         | 
| 56 | 
            +
                        format_tool_result(msg)
         | 
| 57 | 
            +
                      else
         | 
| 58 | 
            +
                        format_basic_message(msg)
         | 
| 59 | 
            +
                      end
         | 
| 60 | 
            +
                    end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    def format_basic_message(msg)
         | 
| 63 | 
            +
                      {
         | 
| 64 | 
            +
                        role: convert_role(msg.role),
         | 
| 65 | 
            +
                        content: msg.content
         | 
| 66 | 
            +
                      }
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    def convert_role(role)
         | 
| 70 | 
            +
                      case role
         | 
| 71 | 
            +
                      when :tool then 'user'
         | 
| 72 | 
            +
                      when :user then 'user'
         | 
| 73 | 
            +
                      else 'assistant'
         | 
| 74 | 
            +
                      end
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                    def format_text_block(content)
         | 
| 78 | 
            +
                      {
         | 
| 79 | 
            +
                        type: 'text',
         | 
| 80 | 
            +
                        text: content
         | 
| 81 | 
            +
                      }
         | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
            end
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RubyLLM
         | 
| 4 | 
            +
              module Providers
         | 
| 5 | 
            +
                module Anthropic
         | 
| 6 | 
            +
                  # Embeddings methods of the Anthropic API integration
         | 
| 7 | 
            +
                  module Embeddings
         | 
| 8 | 
            +
                    private
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    def embed
         | 
| 11 | 
            +
                      raise Error "Anthropic doesn't support embeddings"
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    alias render_embedding_payload embed
         | 
| 15 | 
            +
                    alias embedding_url embed
         | 
| 16 | 
            +
                    alias parse_embedding_response embed
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
            end
         |