Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ollama as a supported provider #10

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ RubyLLM.configure do |config|
end
```

Alternatively, you can point to an Ollama instance; here it is configured to the most common address when Ollama is installed locally:
```
RubyLLM.configure do |config|
config.ollama_api_base_url = 'http://localhost:11434'
end

# needs to be called to populate Ollama models before using any
RubyLLM.models.refresh!
```

## Your First Chat

Let's start with a simple chat interaction:
Expand Down Expand Up @@ -64,6 +74,10 @@ claude_chat.ask "Tell me about Ruby programming language"
# Use Gemini
gemini_chat = RubyLLM.chat(model: 'gemini-2.0-flash')
gemini_chat.ask "What are the best Ruby gems for machine learning?"

# Use an Ollama model:
ollama_chat = RubyLLM.chat(model: 'gemma3:latest')
ollama_chat.ask "What is Alphabet?"
```

## Exploring Available Models
Expand Down Expand Up @@ -161,4 +175,4 @@ Now that you've got the basics down, you're ready to explore more advanced featu

- [Chatting with AI]({% link guides/chat.md %}) - Learn more about chat capabilities
- [Using Tools]({% link guides/tools.md %}) - Let AI use your Ruby code
- [Rails Integration]({% link guides/rails.md %}) - Persist chats in your Rails apps
- [Rails Integration]({% link guides/rails.md %}) - Persist chats in your Rails apps
4 changes: 3 additions & 1 deletion lib/ruby_llm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
'llm' => 'LLM',
'openai' => 'OpenAI',
'api' => 'API',
'deepseek' => 'DeepSeek'
'deepseek' => 'DeepSeek',
'ollama' => 'Ollama'
)
loader.setup

Expand Down Expand Up @@ -68,6 +69,7 @@ def logger
RubyLLM::Provider.register :anthropic, RubyLLM::Providers::Anthropic
RubyLLM::Provider.register :gemini, RubyLLM::Providers::Gemini
RubyLLM::Provider.register :deepseek, RubyLLM::Providers::DeepSeek
RubyLLM::Provider.register :ollama, RubyLLM::Providers::Ollama

if defined?(Rails::Railtie)
require 'ruby_llm/railtie'
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_llm/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Configuration
:anthropic_api_key,
:gemini_api_key,
:deepseek_api_key,
:ollama_api_base_url,
:default_model,
:default_embedding_model,
:default_image_model,
Expand Down
20 changes: 16 additions & 4 deletions lib/ruby_llm/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def complete(messages, tools:, temperature:, model:, &block)
end

def list_models
return [] unless enabled?

response = connection.get(models_url) do |req|
req.headers.merge! headers
end
Expand Down Expand Up @@ -123,11 +125,21 @@ def to_json_stream(&block) # rubocop:disable Metrics/MethodLength
RubyLLM.logger.debug "Accumulating error chunk: #{chunk}"
end
else
parser.feed(chunk) do |_type, data|
unless data == '[DONE]'
parsed_data = JSON.parse(data)
block.call(parsed_data)
content_type = env.response_headers['content-type']
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super sure about this; as far as I could see, Ollama uses newline delimited JSON lines rather than standard event streams.

Of the API providers, I only have a Gemini key and it does work after this commit (both streaming and synch) so this might be correct.


case content_type
when 'text/event-stream'
parser.feed(chunk) do |_type, data|
unless data == '[DONE]'
parsed_data = JSON.parse(data)
block.call(parsed_data)
end
end
when 'application/x-ndjson'
parsed_data = JSON.parse(chunk)
block.call(parsed_data)
else
raise NotImplementedError, "unsupported content-type in streaming response: #{content_type}"
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ruby_llm/providers/anthropic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ module Anthropic

module_function

def enabled?
!!RubyLLM.config.anthropic_api_key
end

def api_base
'https://api.anthropic.com'
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ruby_llm/providers/deepseek.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ module DeepSeek

module_function

def enabled?
!!RubyLLM.config.deepseek_api_key
end

def api_base
'https://api.deepseek.com'
end
Expand Down
4 changes: 4 additions & 0 deletions lib/ruby_llm/providers/gemini.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ module Gemini

module_function

def enabled?
!!RubyLLM.config.gemini_api_key
end

def api_base
'https://generativelanguage.googleapis.com/v1beta'
end
Expand Down
2 changes: 2 additions & 0 deletions lib/ruby_llm/providers/gemini/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def models_url
end

def list_models
return [] unless enabled?

response = connection.get("models?key=#{RubyLLM.config.gemini_api_key}") do |req|
req.headers.merge! headers
end
Expand Down
36 changes: 36 additions & 0 deletions lib/ruby_llm/providers/ollama.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module RubyLLM
module Providers
# Native Ollama API implementation
module Ollama
extend Provider
extend Ollama::Chat
extend Ollama::Embeddings
extend Ollama::Models
extend Ollama::Streaming

module_function

def enabled?
!!RubyLLM.config.ollama_api_base_url
end

def api_base
RubyLLM.config.ollama_api_base_url
end

def headers
{}
end

def capabilities
Ollama::Capabilities
end

def slug
'ollama'
end
end
end
end
138 changes: 138 additions & 0 deletions lib/ruby_llm/providers/ollama/capabilities.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# frozen_string_literal: true

module RubyLLM
module Providers
module Ollama
# Determines capabilities for Ollama
module Capabilities # rubocop:disable Metrics/ModuleLength
module_function

# Returns the context window size (input token limit) for the given model
# @param model_id [String] the model identifier
# @return [Integer] the context window size in tokens
def context_window_for(model_id)
# FIXME: revise
4_192 # Sensible (and conservative) default for unknown models
end

# Returns the maximum output tokens for the given model
# @param model_id [String] the model identifier
# @return [Integer] the maximum output tokens
def max_tokens_for(model_id)
# FIXME: revise
32_768
end

# Returns the input price per million tokens for the given model
# @param model_id [String] the model identifier
# @return [Float] the price per million tokens in USD
def input_price_for(model_id)
0.0
end

# Returns the output price per million tokens for the given model
# @param model_id [String] the model identifier
# @return [Float] the price per million tokens in USD
def output_price_for(model_id)
0.0
end

# Determines if the model supports vision (image/video) inputs
# @param model_id [String] the model identifier
# @return [Boolean] true if the model supports vision inputs
def supports_vision?(model_id)
# FIXME: revise
false
end

# Determines if the model supports function calling
# @param model_id [String] the model identifier
# @return [Boolean] true if the model supports function calling
def supports_functions?(model_id)
# FIXME: revise
false
end

# Determines if the model supports JSON mode
# @param model_id [String] the model identifier
# @return [Boolean] true if the model supports JSON mode
def supports_json_mode?(model_id)
# FIXME: revise
false
end

# Formats the model ID into a human-readable display name
# @param model_id [String] the model identifier
# @return [String] the formatted display name
def format_display_name(model_id)
model_id
.delete_prefix('models/')
.split('-')
.map(&:capitalize)
.join(' ')
.gsub(/(\d+\.\d+)/, ' \1') # Add space before version numbers
.gsub(/\s+/, ' ') # Clean up multiple spaces
.strip
end

# Determines if the model supports context caching
# @param model_id [String] the model identifier
# @return [Boolean] true if the model supports caching
def supports_caching?(model_id)
# FIXME: revise
true
end

# Determines if the model supports tuning
# @param model_id [String] the model identifier
# @return [Boolean] true if the model supports tuning
def supports_tuning?(model_id)
# FIXME: revise
false
end

# Determines if the model supports audio inputs
# @param model_id [String] the model identifier
# @return [Boolean] true if the model supports audio inputs
def supports_audio?(model_id)
# FIXME: revise
false
end

# Returns the type of model (chat, embedding, image)
# @param model_id [String] the model identifier
# @return [String] the model type
def model_type(model_id)
# FIXME: revise
'chat'
end

# Returns the model family identifier
# @param model_id [String] the model identifier
# @return [String] the model family identifier
def model_family(model_id) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
'other'
end

# Returns the context length for the model
# @param model_id [String] the model identifier
# @return [Integer] the context length in tokens
def context_length(model_id)
context_window_for(model_id)
end

# Default input price for unknown models
# @return [Float] the default input price per million tokens
def default_input_price
0.0
end

# Default output price for unknown models
# @return [Float] the default output price per million tokens
def default_output_price
0.0
end
end
end
end
end
Loading