litellm 1.0.0.rc1
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 +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/VERSION +1 -0
- data/lib/litellm/client.rb +243 -0
- data/lib/litellm/configuration.rb +99 -0
- data/lib/litellm/errors.rb +98 -0
- data/lib/litellm/streaming.rb +156 -0
- data/lib/litellm/tool_handler.rb +93 -0
- data/lib/litellm/utils/tool_definition.rb +173 -0
- data/lib/litellm/version.rb +5 -0
- data/lib/litellm.rb +59 -0
- metadata +88 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 1dd4997378f275e63183ff9dad3c067f78937094193f9caa7524a9d81e11f8f7
         | 
| 4 | 
            +
              data.tar.gz: b4ce66a0b829c04c0c7a0a797b38f310c980fb25bac1c2aacb5c032b03df1bc1
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 2af4c2ab3c5b56179738e794db8a74e54e8889feab46a0c1f1d026a2adb0bc5b6145955b6c3a96db36881c39bc6c4a1a429e6417a13e86f1893bc750e91e3ffa
         | 
| 7 | 
            +
              data.tar.gz: 28b9ffdd35c4709fec7fcec727a00685727a0ce4ac332966928c47f9e4a89b78f817181c1357ee84b8c4dbf3a0813b28497a947155dfa110fe27b60a88d458ca
         | 
    
        data/CHANGELOG.md
    ADDED
    
    
    
        data/LICENSE.txt
    ADDED
    
    | @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            The MIT License (MIT)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Copyright (c) 2020 Alex
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 6 | 
            +
            of this software and associated documentation files (the "Software"), to deal
         | 
| 7 | 
            +
            in the Software without restriction, including without limitation the rights
         | 
| 8 | 
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 9 | 
            +
            copies of the Software, and to permit persons to whom the Software is
         | 
| 10 | 
            +
            furnished to do so, subject to the following conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be included in
         | 
| 13 | 
            +
            all copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 16 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 17 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 18 | 
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 19 | 
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 20 | 
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 21 | 
            +
            THE SOFTWARE.
         | 
    
        data/VERSION
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            1.0.0.rc1
         | 
| @@ -0,0 +1,243 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module LiteLLM
         | 
| 4 | 
            +
              class Client
         | 
| 5 | 
            +
                Error = Class.new(StandardError)
         | 
| 6 | 
            +
                ConfigurationError = Class.new(Error)
         | 
| 7 | 
            +
                ConnectionError = Class.new(Error)
         | 
| 8 | 
            +
                TimeoutError = Class.new(Error)
         | 
| 9 | 
            +
                APIError = Class.new(Error)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                # @param config [LiteLLM::Config] Configuration object
         | 
| 12 | 
            +
                def initialize(config = LiteLLM.configuration)
         | 
| 13 | 
            +
                  @config = config
         | 
| 14 | 
            +
                  @messages = []
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  validate_configuration!
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                # Make a completion request
         | 
| 20 | 
            +
                # @param messages [Array<Hash>] Array of message objects
         | 
| 21 | 
            +
                # @param model [String, nil] Optional model override
         | 
| 22 | 
            +
                # @param stream [Boolean] Whether to stream the response
         | 
| 23 | 
            +
                # @param tools [Array<ToolDefinition>] Array of tools available for the request
         | 
| 24 | 
            +
                # @return [Hash, Enumerator] Response data or stream
         | 
| 25 | 
            +
                def completion(messages:, model: nil, stream: false, tools: [], system_message: nil, **options,
         | 
| 26 | 
            +
                               &block)
         | 
| 27 | 
            +
                  @messages = messages.dup
         | 
| 28 | 
            +
                  @messages = [{ role: "system", content: system_message }] + @messages if system_message
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  payload = build_payload(
         | 
| 31 | 
            +
                    messages: @messages,
         | 
| 32 | 
            +
                    model: model || @config.model,
         | 
| 33 | 
            +
                    stream: stream,
         | 
| 34 | 
            +
                    **options
         | 
| 35 | 
            +
                  )
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  payload[:tools] = build_tools_schema(tools) unless tools.empty?
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  if stream
         | 
| 40 | 
            +
                    raise ArgumentError, "Block required for streaming requests" unless block_given?
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    stream_completion(payload, tools, model, &block)
         | 
| 43 | 
            +
                  else
         | 
| 44 | 
            +
                    non_stream_completion(payload, tools, model)
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # Generate embeddings for input text
         | 
| 49 | 
            +
                # @param input [String, Array<String>] Input text(s)
         | 
| 50 | 
            +
                # @param model [String, nil] Optional model override
         | 
| 51 | 
            +
                # @param options [Hash] Additional options to pass to the API
         | 
| 52 | 
            +
                # @return [Hash] Embedding response
         | 
| 53 | 
            +
                def embedding(input:, model: nil, dimensions: nil, **options)
         | 
| 54 | 
            +
                  payload = build_payload(
         | 
| 55 | 
            +
                    input: input,
         | 
| 56 | 
            +
                    model: model || @config.embedding_model,
         | 
| 57 | 
            +
                    dimensions: dimensions || @config.embedding_dimensions,
         | 
| 58 | 
            +
                    **options
         | 
| 59 | 
            +
                  )
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  response = make_request("/embeddings", payload)
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  handle_json_response(response).dig("data", 0, "embedding")
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                # Generate images from a prompt
         | 
| 67 | 
            +
                # @param prompt [String] Image generation prompt
         | 
| 68 | 
            +
                # @param options [Hash] Additional options to pass to the API
         | 
| 69 | 
            +
                # @return [Hash] Image generation response
         | 
| 70 | 
            +
                def image_generation(prompt:, model: nil, **options)
         | 
| 71 | 
            +
                  payload = build_payload(
         | 
| 72 | 
            +
                    prompt: prompt,
         | 
| 73 | 
            +
                    model: model || @config.image_model,
         | 
| 74 | 
            +
                    **options
         | 
| 75 | 
            +
                  )
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  response = make_request("/images/generations", payload)
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  handle_json_response(response).dig("data", 0, "url")
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                private
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def stream_completion(payload, tools, model, &block)
         | 
| 85 | 
            +
                  LiteLLM.logger.debug "Starting streaming completion request"
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  make_request("/chat/completions", payload) do |content, tool_call_payload|
         | 
| 88 | 
            +
                    if content
         | 
| 89 | 
            +
                      block.call(content)
         | 
| 90 | 
            +
                    elsif tool_call_payload
         | 
| 91 | 
            +
                      handle_tool_calls(tool_call_payload, tools, model, true, &block)
         | 
| 92 | 
            +
                    end
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  LiteLLM.logger.debug "Completed streaming request"
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                def non_stream_completion(payload, tools, model)
         | 
| 99 | 
            +
                  response = make_request("/chat/completions", payload)
         | 
| 100 | 
            +
                  response = handle_json_response(response, "/chat/completions")
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  tool_calls = response.dig("choices", 0, "message", "tool_calls")
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  if tool_calls&.any?
         | 
| 105 | 
            +
                    LiteLLM.logger.debug "Tool calls detected in non-streaming response"
         | 
| 106 | 
            +
                    handle_tool_calls(response, tools, model, false)
         | 
| 107 | 
            +
                  else
         | 
| 108 | 
            +
                    response.dig("choices", 0, "message", "content")
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                def build_payload(**params)
         | 
| 113 | 
            +
                  params = params
         | 
| 114 | 
            +
                           .transform_keys(&:to_sym)
         | 
| 115 | 
            +
                           .compact
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                  LiteLLM.logger.debug "--------------------------------"
         | 
| 118 | 
            +
                  LiteLLM.logger.debug "LiteLLM Request: #{params.to_json}"
         | 
| 119 | 
            +
                  LiteLLM.logger.debug "--------------------------------"
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  params
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                def build_tools_schema(tools)
         | 
| 125 | 
            +
                  tools.flat_map do |tool|
         | 
| 126 | 
            +
                    unless tool.is_a?(Module) || tool.respond_to?(:to_tool_format)
         | 
| 127 | 
            +
                      raise ArgumentError, "Tool must include ToolDefinition module"
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    tool.to_tool_format
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                def make_request(endpoint, payload, &block)
         | 
| 135 | 
            +
                  request_id = SecureRandom.uuid
         | 
| 136 | 
            +
                  log_request(request_id, endpoint, payload)
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  response = connection.post(endpoint) do |req|
         | 
| 139 | 
            +
                    req.headers["X-Request-ID"] = request_id
         | 
| 140 | 
            +
                    req.body = payload.to_json
         | 
| 141 | 
            +
                    req.options.on_data = Streaming.process_stream(&block) if payload[:stream]
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  log_response(request_id, response)
         | 
| 145 | 
            +
                  response
         | 
| 146 | 
            +
                rescue StandardError => e
         | 
| 147 | 
            +
                  log_error(request_id, e)
         | 
| 148 | 
            +
                  raise
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                def handle_tool_calls(response_data, tools, model, stream, &block)
         | 
| 152 | 
            +
                  LiteLLM.logger.debug "Handling tool calls, stream=#{stream}"
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                  begin
         | 
| 155 | 
            +
                    LiteLLM.logger.debug "Tool call response: #{response_data.inspect}"
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                    # Add tool-related messages to conversation history
         | 
| 158 | 
            +
                    tool_messages = ToolHandler.call(response: response_data, available_tools: tools)
         | 
| 159 | 
            +
                    @messages += tool_messages
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                    LiteLLM.logger.debug "Tool handler generated messages: #{@messages.inspect}"
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                    completion(messages: @messages, model: model, tools: tools, stream: stream, &block)
         | 
| 164 | 
            +
                  rescue StandardError => e
         | 
| 165 | 
            +
                    LiteLLM.logger.error "Error in handle_tool_calls: #{e.message}\n#{e.backtrace.join("\n")}"
         | 
| 166 | 
            +
                    raise
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
                end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                def handle_json_response(response, endpoint = nil)
         | 
| 171 | 
            +
                  LiteLLM.logger.debug <<~LOG
         | 
| 172 | 
            +
                    --------------------------------
         | 
| 173 | 
            +
                    LiteLLM RAW Response: #{response.inspect}
         | 
| 174 | 
            +
                    --------------------------------
         | 
| 175 | 
            +
                  LOG
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                  parsed_response = ErrorHandler.parse_json(response.body)
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                  ErrorHandler.validate_completion_response(parsed_response) if endpoint == "/chat/completions"
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                  parsed_response
         | 
| 182 | 
            +
                end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                def connection
         | 
| 185 | 
            +
                  @connection ||= Faraday.new(url: @config.base_url) do |f|
         | 
| 186 | 
            +
                    f.request :json
         | 
| 187 | 
            +
                    f.response :raise_error
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                    f.headers["X-LITELLM-TIMEOUT"] = @config.timeout.to_s
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                    f.headers["Authorization"] = "Bearer #{@config.api_key}" if @config.api_key
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                    f.headers["X-LITELLM-ENABLE-MESSAGE-REDACTION"] = true if @config.enable_message_redaction
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                    f.options.timeout = @config.timeout
         | 
| 196 | 
            +
                    f.adapter Faraday.default_adapter
         | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
                rescue URI::InvalidURIError => e
         | 
| 199 | 
            +
                  ErrorHandler.handle_error(
         | 
| 200 | 
            +
                    ConfigurationError,
         | 
| 201 | 
            +
                    "Invalid base URL: #{@config.base_url}",
         | 
| 202 | 
            +
                    e,
         | 
| 203 | 
            +
                    debug: @config.debug
         | 
| 204 | 
            +
                  )
         | 
| 205 | 
            +
                end
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                def validate_configuration!
         | 
| 208 | 
            +
                  raise ConfigurationError, "Base URL is required" if @config.base_url.nil?
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                  validate_base_url!
         | 
| 211 | 
            +
                end
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                def validate_base_url!
         | 
| 214 | 
            +
                  uri = URI.parse(@config.base_url)
         | 
| 215 | 
            +
                  unless uri.scheme&.match?(/\Ahttps?\z/)
         | 
| 216 | 
            +
                    raise ConfigurationError, "Base URL must be HTTP or HTTPS"
         | 
| 217 | 
            +
                  end
         | 
| 218 | 
            +
                rescue URI::InvalidURIError => e
         | 
| 219 | 
            +
                  raise ConfigurationError, "Invalid base URL: #{e.message}"
         | 
| 220 | 
            +
                end
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                def log_request(request_id, endpoint, payload)
         | 
| 223 | 
            +
                  LiteLLM.logger.info("[#{request_id}] Request to #{endpoint}")
         | 
| 224 | 
            +
                  return unless LiteLLM.configuration.debug
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                  LiteLLM.logger.debug("[#{request_id}] Payload: #{payload.inspect}")
         | 
| 227 | 
            +
                end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                def log_response(request_id, response)
         | 
| 230 | 
            +
                  LiteLLM.logger.info("[#{request_id}] Response received")
         | 
| 231 | 
            +
                  return unless LiteLLM.configuration.debug
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                  LiteLLM.logger.debug("[#{request_id}] Response: #{response.inspect}")
         | 
| 234 | 
            +
                end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                def log_error(request_id, e)
         | 
| 237 | 
            +
                  LiteLLM.logger.error("[#{request_id}] Error: #{e.message}")
         | 
| 238 | 
            +
                  return unless LiteLLM.configuration.debug
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                  LiteLLM.logger.debug("[#{request_id}] Backtrace: #{e.backtrace.join("\n")}")
         | 
| 241 | 
            +
                end
         | 
| 242 | 
            +
              end
         | 
| 243 | 
            +
            end
         | 
| @@ -0,0 +1,99 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "logger"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module LiteLLM
         | 
| 6 | 
            +
              DEFAULTS = {
         | 
| 7 | 
            +
                base_url: "http://localhost:8000",
         | 
| 8 | 
            +
                timeout: 120,
         | 
| 9 | 
            +
                model: "gpt-4o",
         | 
| 10 | 
            +
                embedding_model: "text-embedding-3-large",
         | 
| 11 | 
            +
                image_model: "dall-e-2",
         | 
| 12 | 
            +
                embedding_dimensions: 1536,
         | 
| 13 | 
            +
                enable_message_redaction: false,
         | 
| 14 | 
            +
                debug: false,
         | 
| 15 | 
            +
                api_key: ENV.fetch("LITE_LLM_API_KEY", nil)
         | 
| 16 | 
            +
              }.freeze
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              class << self
         | 
| 19 | 
            +
                attr_writer :configuration
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def configuration
         | 
| 22 | 
            +
                  @configuration ||= Configuration.new
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def configure
         | 
| 26 | 
            +
                  yield(configuration)
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def logger
         | 
| 30 | 
            +
                  configuration.logger
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              class Configuration
         | 
| 35 | 
            +
                attr_reader :base_url, :timeout, :model, :embedding_model, :image_model, :embedding_dimensions,
         | 
| 36 | 
            +
                            :debug, :logger, :enable_message_redaction, :api_key
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def initialize
         | 
| 39 | 
            +
                  @base_url = DEFAULTS[:base_url]
         | 
| 40 | 
            +
                  @timeout = DEFAULTS[:timeout]
         | 
| 41 | 
            +
                  @model = DEFAULTS[:model]
         | 
| 42 | 
            +
                  @embedding_model = DEFAULTS[:embedding_model]
         | 
| 43 | 
            +
                  @image_model = DEFAULTS[:image_model]
         | 
| 44 | 
            +
                  @embedding_dimensions = DEFAULTS[:embedding_dimensions]
         | 
| 45 | 
            +
                  @enable_message_redaction = DEFAULTS[:enable_message_redaction]
         | 
| 46 | 
            +
                  @debug = DEFAULTS[:debug]
         | 
| 47 | 
            +
                  @api_key = DEFAULTS[:api_key]
         | 
| 48 | 
            +
                  @logger = setup_default_logger
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def base_url=(value)
         | 
| 52 | 
            +
                  @base_url = value unless value.nil?
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def timeout=(value)
         | 
| 56 | 
            +
                  @timeout = value.to_i unless value.nil?
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def model=(value)
         | 
| 60 | 
            +
                  @model = value unless value.nil?
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def embedding_model=(value)
         | 
| 64 | 
            +
                  @embedding_model = value unless value.nil?
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def image_model=(value)
         | 
| 68 | 
            +
                  @image_model = value unless value.nil?
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def enable_message_redaction=(value)
         | 
| 72 | 
            +
                  @enable_message_redaction = value unless value.nil?
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def api_key=(value)
         | 
| 76 | 
            +
                  @api_key = value unless value.nil?
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def debug=(value)
         | 
| 80 | 
            +
                  @debug = if value.nil?
         | 
| 81 | 
            +
                             defined?(Rails) ? Rails.env.development? : false
         | 
| 82 | 
            +
                           else
         | 
| 83 | 
            +
                             value.to_s == "true"
         | 
| 84 | 
            +
                           end
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def logger=(value)
         | 
| 88 | 
            +
                  @logger = value if value
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                private
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def setup_default_logger
         | 
| 94 | 
            +
                  logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
         | 
| 95 | 
            +
                  logger.level = debug ? Logger::DEBUG : Logger::INFO
         | 
| 96 | 
            +
                  logger
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
              end
         | 
| 99 | 
            +
            end
         | 
| @@ -0,0 +1,98 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module LiteLLM
         | 
| 4 | 
            +
              class BaseError < StandardError
         | 
| 5 | 
            +
                attr_reader :original_error
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(message = nil, original_error = nil)
         | 
| 8 | 
            +
                  super(message)
         | 
| 9 | 
            +
                  @original_error = original_error
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def set_debug_backtrace(debug_mode)
         | 
| 13 | 
            +
                  return unless @original_error
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  set_backtrace(
         | 
| 16 | 
            +
                    debug_mode ? @original_error.backtrace : [@original_error.backtrace.first]
         | 
| 17 | 
            +
                  )
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              class ConfigurationError < BaseError; end
         | 
| 22 | 
            +
              class APIError < BaseError; end
         | 
| 23 | 
            +
              class TimeoutError < BaseError; end
         | 
| 24 | 
            +
              class ConnectionError < BaseError; end
         | 
| 25 | 
            +
              class ToolCallError < BaseError; end
         | 
| 26 | 
            +
              class InsufficientQuotaError < APIError; end
         | 
| 27 | 
            +
              class RateLimitError < APIError; end
         | 
| 28 | 
            +
              class AuthenticationError < APIError; end
         | 
| 29 | 
            +
              class ValidationError < APIError; end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              module ErrorHandler
         | 
| 32 | 
            +
                def self.handle_error(error_class, message, original_error = nil)
         | 
| 33 | 
            +
                  error = error_class.new(message, original_error)
         | 
| 34 | 
            +
                  error.set_debug_backtrace(LiteLLM.debug?)
         | 
| 35 | 
            +
                  raise error
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def self.handle_api_error(error)
         | 
| 39 | 
            +
                  error_body = begin
         | 
| 40 | 
            +
                    parse_json(error.response[:body])
         | 
| 41 | 
            +
                  rescue StandardError
         | 
| 42 | 
            +
                    nil
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                  error_message = error_body&.dig("error", "message") || error.message
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  case error.response[:status]
         | 
| 47 | 
            +
                  when 401
         | 
| 48 | 
            +
                    handle_error(AuthenticationError, "Authentication failed: #{error_message}", error)
         | 
| 49 | 
            +
                  when 422
         | 
| 50 | 
            +
                    handle_error(ValidationError, "Validation failed: #{error_message}", error)
         | 
| 51 | 
            +
                  when 429
         | 
| 52 | 
            +
                    if error_body&.dig("error", "type") == "insufficient_quota"
         | 
| 53 | 
            +
                      handle_error(InsufficientQuotaError, "API quota exceeded", error)
         | 
| 54 | 
            +
                    else
         | 
| 55 | 
            +
                      handle_error(RateLimitError, "Rate limit exceeded", error)
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
                  else
         | 
| 58 | 
            +
                    handle_error(APIError, "API request failed (#{error.response[:status]}): #{error_message}",
         | 
| 59 | 
            +
                                 error)
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def self.validate_completion_response(response)
         | 
| 64 | 
            +
                  return if response["choices"]&.first&.dig("message")
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  raise APIError, "Invalid response format from server"
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def self.configuration_error_message(api_key, base_url)
         | 
| 70 | 
            +
                  [
         | 
| 71 | 
            +
                    ("API key not configured" if api_key.nil?),
         | 
| 72 | 
            +
                    ("Base URL not configured" if base_url.nil?)
         | 
| 73 | 
            +
                  ].compact.join(" and ")
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def self.handle_tool_error(message, original_error = nil)
         | 
| 77 | 
            +
                  error = ToolCallError.new(message, original_error)
         | 
| 78 | 
            +
                  error.set_debug_backtrace(LiteLLM.debug?)
         | 
| 79 | 
            +
                  raise error
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                private
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def self.parse_json(body)
         | 
| 85 | 
            +
                  JSON.parse(body)
         | 
| 86 | 
            +
                rescue JSON::ParserError => e
         | 
| 87 | 
            +
                  handle_error(APIError, "Invalid JSON response from server: #{e.message}", e)
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def self.log_debug_response(response)
         | 
| 91 | 
            +
                  <<~LOG
         | 
| 92 | 
            +
                    --------------------------------
         | 
| 93 | 
            +
                    LiteLLM RAW Response: #{response.inspect}
         | 
| 94 | 
            +
                    --------------------------------
         | 
| 95 | 
            +
                  LOG
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
              end
         | 
| 98 | 
            +
            end
         | 
| @@ -0,0 +1,156 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module LiteLLM
         | 
| 4 | 
            +
              module Streaming
         | 
| 5 | 
            +
                class StreamingError < StandardError; end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                class StreamBuffer
         | 
| 8 | 
            +
                  attr_reader :content
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def initialize
         | 
| 11 | 
            +
                    @content = ""
         | 
| 12 | 
            +
                    @tool_calls = {}
         | 
| 13 | 
            +
                    @tool_call_id = nil
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def add_chunk(parsed_chunk)
         | 
| 17 | 
            +
                    delta = parsed_chunk.dig("choices", 0, "delta")
         | 
| 18 | 
            +
                    return process_content(delta) if delta&.key?("content")
         | 
| 19 | 
            +
                    return process_tool_call(delta) if delta&.key?("tool_calls")
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    nil
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def tool_calls?
         | 
| 25 | 
            +
                    @tool_calls.any?
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def tool_call_payload
         | 
| 29 | 
            +
                    return nil unless tool_calls?
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    # Format the response to match standard API response format
         | 
| 32 | 
            +
                    {
         | 
| 33 | 
            +
                      "choices" => [
         | 
| 34 | 
            +
                        {
         | 
| 35 | 
            +
                          "message" => {
         | 
| 36 | 
            +
                            "tool_calls" => format_tool_calls
         | 
| 37 | 
            +
                          }
         | 
| 38 | 
            +
                        }
         | 
| 39 | 
            +
                      ]
         | 
| 40 | 
            +
                    }
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def reset_tool_call_id
         | 
| 44 | 
            +
                    @tool_call_id = nil
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  private
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def format_tool_calls
         | 
| 50 | 
            +
                    @tool_calls.values.map do |call|
         | 
| 51 | 
            +
                      {
         | 
| 52 | 
            +
                        "id" => call[:id],
         | 
| 53 | 
            +
                        "type" => call[:type] || "function",
         | 
| 54 | 
            +
                        "function" => {
         | 
| 55 | 
            +
                          "name" => call[:function_name],
         | 
| 56 | 
            +
                          "arguments" => call[:arguments]
         | 
| 57 | 
            +
                        }
         | 
| 58 | 
            +
                      }
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  def process_content(delta)
         | 
| 63 | 
            +
                    content = delta["content"].to_s
         | 
| 64 | 
            +
                    @content += content
         | 
| 65 | 
            +
                    content
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  def process_tool_call(delta)
         | 
| 69 | 
            +
                    delta["tool_calls"].each do |tool_call|
         | 
| 70 | 
            +
                      extract_tool_call_to_buffer(tool_call)
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
                    nil
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def extract_tool_call_to_buffer(tool_call)
         | 
| 76 | 
            +
                    @tool_call_id = tool_call["id"] if tool_call["id"]
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    return unless @tool_call_id
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    @tool_calls[@tool_call_id] ||= {
         | 
| 81 | 
            +
                      id: @tool_call_id,
         | 
| 82 | 
            +
                      index: tool_call["index"],
         | 
| 83 | 
            +
                      function_name: nil,
         | 
| 84 | 
            +
                      type: tool_call["type"],
         | 
| 85 | 
            +
                      arguments: ""
         | 
| 86 | 
            +
                    }
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    if (type = tool_call["type"])
         | 
| 89 | 
            +
                      @tool_calls[@tool_call_id][:type] = type
         | 
| 90 | 
            +
                    end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                    if (function_name = tool_call.dig("function", "name"))
         | 
| 93 | 
            +
                      @tool_calls[@tool_call_id][:function_name] = function_name
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                    if (new_arguments = tool_call.dig("function", "arguments"))
         | 
| 97 | 
            +
                      @tool_calls[@tool_call_id][:arguments] += new_arguments.to_s
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
                end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                class << self
         | 
| 103 | 
            +
                  def process_stream(&block)
         | 
| 104 | 
            +
                    return proc {} unless block
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                    parser = EventStreamParser::Parser.new
         | 
| 107 | 
            +
                    buffer = StreamBuffer.new
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    proc do |chunk|
         | 
| 110 | 
            +
                      parser.feed(chunk) do |_type, data|
         | 
| 111 | 
            +
                        if data.strip == "[DONE]"
         | 
| 112 | 
            +
                          handle_done(buffer, &block)
         | 
| 113 | 
            +
                          next
         | 
| 114 | 
            +
                        end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                        process_chunk(data, buffer, &block)
         | 
| 117 | 
            +
                      end
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
                  rescue StandardError => e
         | 
| 120 | 
            +
                    log_error("Stream Processing Failed", e)
         | 
| 121 | 
            +
                    raise StreamingError, "Failed to process stream: #{e.message}"
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  private
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  def process_chunk(data, buffer, &block)
         | 
| 127 | 
            +
                    parsed_data = JSON.parse(data)
         | 
| 128 | 
            +
                    log_debug("Received chunk: #{data}")
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    if content = buffer.add_chunk(parsed_data)
         | 
| 131 | 
            +
                      block.call(content, nil)
         | 
| 132 | 
            +
                    end
         | 
| 133 | 
            +
                  rescue JSON::ParserError => e
         | 
| 134 | 
            +
                    log_error("Stream Chunk Parsing Failed", e, data: data)
         | 
| 135 | 
            +
                  end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                  def handle_done(buffer, &block)
         | 
| 138 | 
            +
                    return unless buffer.tool_calls?
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                    tool_calls_response = buffer.tool_call_payload
         | 
| 141 | 
            +
                    log_debug("Tool call payload: #{tool_calls_response.inspect}")
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                    block.call(nil, tool_calls_response)
         | 
| 144 | 
            +
                    buffer.reset_tool_call_id
         | 
| 145 | 
            +
                  end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                  def log_debug(message)
         | 
| 148 | 
            +
                    LiteLLM.logger.debug("[Streaming] #{message}")
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  def log_error(message, error, **context)
         | 
| 152 | 
            +
                    LiteLLM.logger.error("[Streaming] #{message}", error: error, **context)
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
              end
         | 
| 156 | 
            +
            end
         | 
| @@ -0,0 +1,93 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module LiteLLM
         | 
| 4 | 
            +
              module ToolHandler
         | 
| 5 | 
            +
                class ToolCallError < BaseError; end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def self.call(response:, available_tools: [])
         | 
| 8 | 
            +
                  tool_calls = response.dig("choices", 0, "message", "tool_calls")
         | 
| 9 | 
            +
                  return response unless tool_calls&.any?
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  # Build a mapping of function names to tools
         | 
| 12 | 
            +
                  @available_tools = {}
         | 
| 13 | 
            +
                  available_tools.each do |tool|
         | 
| 14 | 
            +
                    tool.class.function_definitions.each do |function_name, definition|
         | 
| 15 | 
            +
                      @available_tools[definition[:name]] = tool
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  LiteLLM.logger.debug "Available tool functions: #{@available_tools.keys.inspect}"
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  messages = [
         | 
| 22 | 
            +
                    response.dig("choices", 0, "message").merge("role" => "assistant")
         | 
| 23 | 
            +
                  ]
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  tool_results = execute_tool_calls(tool_calls)
         | 
| 26 | 
            +
                  messages.concat(build_tool_messages(tool_results))
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  messages
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                private
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def self.execute_tool_calls(tool_calls)
         | 
| 34 | 
            +
                  tool_calls.map do |tool_call|
         | 
| 35 | 
            +
                    function_name = tool_call.dig("function", "name")
         | 
| 36 | 
            +
                    arguments = parse_tool_arguments(tool_call.dig("function", "arguments"))
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    tool = @available_tools[function_name]
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    unless tool
         | 
| 41 | 
            +
                      raise ToolCallError,
         | 
| 42 | 
            +
                            "Tool function '#{function_name}' not available for this request: DEBUG: #{@available_tools.inspect}"
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    output = tool.execute_function(function_name, arguments)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    LiteLLM.logger.info "Tool call executed: #{function_name} with arguments: #{arguments.inspect}, output: #{output.inspect}"
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    {
         | 
| 50 | 
            +
                      tool_call_id: tool_call["id"],
         | 
| 51 | 
            +
                      function_name: function_name,
         | 
| 52 | 
            +
                      output: output
         | 
| 53 | 
            +
                    }
         | 
| 54 | 
            +
                  rescue StandardError => e
         | 
| 55 | 
            +
                    handle_tool_execution_error(tool_call, e)
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def self.parse_tool_arguments(arguments_json)
         | 
| 60 | 
            +
                  return {} if arguments_json.nil? || arguments_json.empty?
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  JSON.parse(arguments_json).transform_keys(&:to_sym)
         | 
| 63 | 
            +
                rescue JSON::ParserError => e
         | 
| 64 | 
            +
                  raise ToolCallError.new("Invalid tool call arguments: #{e.message}", e)
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def self.build_tool_messages(results)
         | 
| 68 | 
            +
                  results.map do |result|
         | 
| 69 | 
            +
                    {
         | 
| 70 | 
            +
                      "role" => "tool",
         | 
| 71 | 
            +
                      "tool_call_id" => result[:tool_call_id],
         | 
| 72 | 
            +
                      "name" => result[:function_name],
         | 
| 73 | 
            +
                      "content" => result[:output].to_s
         | 
| 74 | 
            +
                    }
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def self.handle_tool_execution_error(tool_call, error)
         | 
| 79 | 
            +
                  function_name = tool_call.dig("function", "name")
         | 
| 80 | 
            +
                  arguments = tool_call.dig("function", "arguments")
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  LiteLLM.logger.error "Tool call failed: #{function_name} with arguments: #{arguments.inspect}"
         | 
| 83 | 
            +
                  LiteLLM.logger.error "Error details: #{error.class} - #{error.message}"
         | 
| 84 | 
            +
                  LiteLLM.logger.error error.backtrace.join("\n") if error.backtrace
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  {
         | 
| 87 | 
            +
                    tool_call_id: tool_call["id"],
         | 
| 88 | 
            +
                    function_name: function_name,
         | 
| 89 | 
            +
                    output: "Error executing tool: #{error.message}"
         | 
| 90 | 
            +
                  }
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
              end
         | 
| 93 | 
            +
            end
         | 
| @@ -0,0 +1,173 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "json"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module LiteLLM
         | 
| 6 | 
            +
              module Utils
         | 
| 7 | 
            +
                module ToolDefinition
         | 
| 8 | 
            +
                  def self.included(base)
         | 
| 9 | 
            +
                    base.extend(ClassMethods)
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  module ClassMethods
         | 
| 13 | 
            +
                    def define_function(method_name, description:, &block)
         | 
| 14 | 
            +
                      function_definitions[method_name] = {
         | 
| 15 | 
            +
                        name: "#{normalized_tool_name}__#{method_name}",
         | 
| 16 | 
            +
                        description: description,
         | 
| 17 | 
            +
                        parameters: build_parameters(&block)
         | 
| 18 | 
            +
                      }
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def function_definitions
         | 
| 22 | 
            +
                      @function_definitions ||= {}
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def normalized_tool_name
         | 
| 26 | 
            +
                      @normalized_tool_name ||= name.split("::").last.gsub(/Tool\z/, "")
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    private
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    def build_parameters(&block)
         | 
| 32 | 
            +
                      return nil unless block_given?
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      builder = ParameterBuilder.new
         | 
| 35 | 
            +
                      schema = builder.build(&block)
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      {
         | 
| 38 | 
            +
                        type: "object",
         | 
| 39 | 
            +
                        properties: schema[:properties],
         | 
| 40 | 
            +
                        required: schema[:required],
         | 
| 41 | 
            +
                        additionalProperties: false
         | 
| 42 | 
            +
                      }
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def to_tool_format
         | 
| 47 | 
            +
                    self.class.function_definitions.values.map do |function|
         | 
| 48 | 
            +
                      { type: "function", function: function }
         | 
| 49 | 
            +
                    end
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  def execute_function(function_name, parameters = {})
         | 
| 53 | 
            +
                    method_name = function_name.to_s.split("__").last
         | 
| 54 | 
            +
                    log_prefix = "[#{self.class.name}##{method_name}]"
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    LiteLLM.logger.info("#{log_prefix} Called with parameters: #{parameters.inspect}")
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    unless respond_to?(method_name, true)
         | 
| 59 | 
            +
                      error_message = "Function '#{method_name}' not found in #{self.class.name}"
         | 
| 60 | 
            +
                      LiteLLM.logger.error("#{log_prefix} #{error_message}")
         | 
| 61 | 
            +
                      raise NoMethodError, error_message
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    begin
         | 
| 65 | 
            +
                      symbolized_params = parameters.transform_keys(&:to_sym)
         | 
| 66 | 
            +
                      validate_parameters(method_name, symbolized_params)
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                      result = send(method_name, **symbolized_params)
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                      LiteLLM.logger.info("#{log_prefix} Execution successful, returned: #{result.inspect}")
         | 
| 71 | 
            +
                      result
         | 
| 72 | 
            +
                    rescue ArgumentError => e
         | 
| 73 | 
            +
                      LiteLLM.logger.error("#{log_prefix} Parameter validation failed: #{e.message}")
         | 
| 74 | 
            +
                      raise
         | 
| 75 | 
            +
                    rescue StandardError => e
         | 
| 76 | 
            +
                      LiteLLM.logger.error("#{log_prefix} Execution failed: #{e.class} - #{e.message}")
         | 
| 77 | 
            +
                      LiteLLM.logger.error("#{log_prefix} Backtrace: #{e.backtrace.first(5).join("\n")}")
         | 
| 78 | 
            +
                      raise
         | 
| 79 | 
            +
                    end
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  private
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  def validate_parameters(method_name, params)
         | 
| 85 | 
            +
                    function_def = self.class.function_definitions[method_name.to_sym]
         | 
| 86 | 
            +
                    return unless function_def && function_def[:parameters]
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                    required_params = function_def[:parameters][:required] || []
         | 
| 89 | 
            +
                    missing_params = required_params - params.keys.map(&:to_s)
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    return if missing_params.empty?
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    raise ArgumentError, "Missing required parameters: #{missing_params.join(', ')}"
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  class ParameterBuilder
         | 
| 97 | 
            +
                    VALID_TYPES = %w[object array string number integer boolean null]
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    def initialize
         | 
| 100 | 
            +
                      @properties = {}
         | 
| 101 | 
            +
                      @required = []
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    def build(&block)
         | 
| 105 | 
            +
                      instance_eval(&block)
         | 
| 106 | 
            +
                      { properties: @properties, required: @required }
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    def property(name, type:, description: nil, enum: nil, required: false, default: nil,
         | 
| 110 | 
            +
                                 &block)
         | 
| 111 | 
            +
                      validate_property_params(name, type, enum, required)
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                      prop = {
         | 
| 114 | 
            +
                        type: type,
         | 
| 115 | 
            +
                        description: description,
         | 
| 116 | 
            +
                        enum: enum,
         | 
| 117 | 
            +
                        default: default
         | 
| 118 | 
            +
                      }.compact
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                      if block_given?
         | 
| 121 | 
            +
                        nested_builder = ParameterBuilder.new
         | 
| 122 | 
            +
                        nested_schema = nested_builder.build(&block)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                        case type
         | 
| 125 | 
            +
                        when "object"
         | 
| 126 | 
            +
                          if nested_schema[:properties].empty?
         | 
| 127 | 
            +
                            raise ArgumentError, "Object properties must have at least one property"
         | 
| 128 | 
            +
                          end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                          prop[:properties] = nested_schema[:properties]
         | 
| 131 | 
            +
                          prop[:required] = nested_schema[:required] unless nested_schema[:required].empty?
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                        when "array"
         | 
| 134 | 
            +
                          if nested_schema[:properties].empty?
         | 
| 135 | 
            +
                            raise ArgumentError, "Array items must be defined"
         | 
| 136 | 
            +
                          end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                          # For simplicity, use the first property definition as the items schema
         | 
| 139 | 
            +
                          # This assumes a single item type definition in the block
         | 
| 140 | 
            +
                          prop[:items] = nested_schema[:properties].values.first
         | 
| 141 | 
            +
                        end
         | 
| 142 | 
            +
                      end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                      @properties[name] = prop
         | 
| 145 | 
            +
                      @required << name.to_s if required
         | 
| 146 | 
            +
                    end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                    alias item property
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                    private
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                    def validate_property_params(name, type, enum, required)
         | 
| 153 | 
            +
                      unless name.is_a?(Symbol)
         | 
| 154 | 
            +
                        raise ArgumentError,
         | 
| 155 | 
            +
                              "Name must be a symbol, got: #{name.class}"
         | 
| 156 | 
            +
                      end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                      unless VALID_TYPES.include?(type)
         | 
| 159 | 
            +
                        raise ArgumentError, "Invalid type '#{type}'. Valid types: #{VALID_TYPES.join(', ')}"
         | 
| 160 | 
            +
                      end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                      unless enum.nil? || enum.is_a?(Array)
         | 
| 163 | 
            +
                        raise ArgumentError, "Enum must be nil or an array, got: #{enum.class}"
         | 
| 164 | 
            +
                      end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                      return if required.is_a?(TrueClass) || required.is_a?(FalseClass)
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                      raise ArgumentError, "Required must be boolean, got: #{required.class}"
         | 
| 169 | 
            +
                    end
         | 
| 170 | 
            +
                  end
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
              end
         | 
| 173 | 
            +
            end
         | 
    
        data/lib/litellm.rb
    ADDED
    
    | @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'faraday'
         | 
| 4 | 
            +
            require 'event_stream_parser'
         | 
| 5 | 
            +
            require 'json'
         | 
| 6 | 
            +
            require 'logger'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            require_relative 'litellm/version'
         | 
| 9 | 
            +
            require_relative 'litellm/configuration'
         | 
| 10 | 
            +
            require_relative 'litellm/streaming'
         | 
| 11 | 
            +
            require_relative 'litellm/client'
         | 
| 12 | 
            +
            require_relative 'litellm/errors'
         | 
| 13 | 
            +
            require_relative 'litellm/tool_handler'
         | 
| 14 | 
            +
            require_relative 'litellm/utils/tool_definition'
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            module LiteLLM
         | 
| 17 | 
            +
              class Error < StandardError; end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              module Utils
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              class << self
         | 
| 23 | 
            +
                def configuration
         | 
| 24 | 
            +
                  @configuration ||= Configuration.new
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def configure
         | 
| 28 | 
            +
                  yield(configuration)
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def reset_configuration
         | 
| 32 | 
            +
                  @configuration = Configuration.new
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def client
         | 
| 36 | 
            +
                  @client ||= Client.new
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def completion(*args)
         | 
| 40 | 
            +
                  client.completion(*args)
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def embedding(*args)
         | 
| 44 | 
            +
                  client.embedding(*args)
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def image_generation(*args)
         | 
| 48 | 
            +
                  client.image_generation(*args)
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def logger
         | 
| 52 | 
            +
                  configuration.logger
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def debug?
         | 
| 56 | 
            +
                  configuration.debug
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,88 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: litellm
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 1.0.0.rc1
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Mohamed Nimir
         | 
| 8 | 
            +
            bindir: bin
         | 
| 9 | 
            +
            cert_chain: []
         | 
| 10 | 
            +
            date: 2025-03-09 00:00:00.000000000 Z
         | 
| 11 | 
            +
            dependencies:
         | 
| 12 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 13 | 
            +
              name: event_stream_parser
         | 
| 14 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 15 | 
            +
                requirements:
         | 
| 16 | 
            +
                - - ">="
         | 
| 17 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 18 | 
            +
                    version: 0.3.0
         | 
| 19 | 
            +
                - - "<"
         | 
| 20 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 21 | 
            +
                    version: 2.0.0
         | 
| 22 | 
            +
              type: :runtime
         | 
| 23 | 
            +
              prerelease: false
         | 
| 24 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 25 | 
            +
                requirements:
         | 
| 26 | 
            +
                - - ">="
         | 
| 27 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 28 | 
            +
                    version: 0.3.0
         | 
| 29 | 
            +
                - - "<"
         | 
| 30 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 31 | 
            +
                    version: 2.0.0
         | 
| 32 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 33 | 
            +
              name: faraday
         | 
| 34 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 35 | 
            +
                requirements:
         | 
| 36 | 
            +
                - - ">="
         | 
| 37 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 38 | 
            +
                    version: '1'
         | 
| 39 | 
            +
              type: :runtime
         | 
| 40 | 
            +
              prerelease: false
         | 
| 41 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 42 | 
            +
                requirements:
         | 
| 43 | 
            +
                - - ">="
         | 
| 44 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 45 | 
            +
                    version: '1'
         | 
| 46 | 
            +
            description: A Ruby client for LiteLLM (the LLM proxy/gateway).
         | 
| 47 | 
            +
            email:
         | 
| 48 | 
            +
            - mohamed.nimir@eptikar.com
         | 
| 49 | 
            +
            executables: []
         | 
| 50 | 
            +
            extensions: []
         | 
| 51 | 
            +
            extra_rdoc_files: []
         | 
| 52 | 
            +
            files:
         | 
| 53 | 
            +
            - CHANGELOG.md
         | 
| 54 | 
            +
            - LICENSE.txt
         | 
| 55 | 
            +
            - VERSION
         | 
| 56 | 
            +
            - lib/litellm.rb
         | 
| 57 | 
            +
            - lib/litellm/client.rb
         | 
| 58 | 
            +
            - lib/litellm/configuration.rb
         | 
| 59 | 
            +
            - lib/litellm/errors.rb
         | 
| 60 | 
            +
            - lib/litellm/streaming.rb
         | 
| 61 | 
            +
            - lib/litellm/tool_handler.rb
         | 
| 62 | 
            +
            - lib/litellm/utils/tool_definition.rb
         | 
| 63 | 
            +
            - lib/litellm/version.rb
         | 
| 64 | 
            +
            homepage: https://github.com/eptikar/litellm-ruby
         | 
| 65 | 
            +
            licenses:
         | 
| 66 | 
            +
            - MIT
         | 
| 67 | 
            +
            metadata:
         | 
| 68 | 
            +
              source_code_uri: https://github.com/eptikar/ruby-litellm
         | 
| 69 | 
            +
              changelog_uri: https://github.com/eptikar/ruby-litellm/blob/main/CHANGELOG.md
         | 
| 70 | 
            +
              bug_tracker_uri: https://github.com/eptikar/ruby-litellm/issues
         | 
| 71 | 
            +
            rdoc_options: []
         | 
| 72 | 
            +
            require_paths:
         | 
| 73 | 
            +
            - lib
         | 
| 74 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 75 | 
            +
              requirements:
         | 
| 76 | 
            +
              - - ">="
         | 
| 77 | 
            +
                - !ruby/object:Gem::Version
         | 
| 78 | 
            +
                  version: 2.6.0
         | 
| 79 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 80 | 
            +
              requirements:
         | 
| 81 | 
            +
              - - ">="
         | 
| 82 | 
            +
                - !ruby/object:Gem::Version
         | 
| 83 | 
            +
                  version: '0'
         | 
| 84 | 
            +
            requirements: []
         | 
| 85 | 
            +
            rubygems_version: 3.6.2
         | 
| 86 | 
            +
            specification_version: 4
         | 
| 87 | 
            +
            summary: LiteLLM Ruby Client.
         | 
| 88 | 
            +
            test_files: []
         |