Comparing changes
v0.1.0
→
v0.2.0
4 commits
10 files changed
Commits
Changed files (10)
lib
elelem
ollama
spec
elelem
ollama
lib/elelem/ollama/client.rb
@@ -6,12 +6,13 @@ module Elelem
def initialize(
model:,
host: "localhost:11434",
- think: false,
- keep_alive: "5m",
+ think: nil,
+ keep_alive: nil,
options: {},
params: {},
read_timeout: 3600,
- open_timeout: 10
+ open_timeout: 10,
+ http: nil
)
@uri = normalize_uri(host)
@model = model
@@ -21,6 +22,7 @@ module Elelem
@params = params
@read_timeout = read_timeout
@open_timeout = open_timeout
+ @http = http
end
def fetch(messages, tools = [], &block)
@@ -103,24 +105,17 @@ module Elelem
end
def stream(body)
- request = Net::HTTP::Post.new(@uri)
- request["content-type"] = "application/json"
- request.body = JSON.generate(body)
+ http.post(@uri, body: body) do |response|
+ raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
- http = Net::HTTP.new(@uri.host, @uri.port)
- http.use_ssl = @uri.scheme == "https"
- http.read_timeout = @read_timeout
- http.open_timeout = @open_timeout
-
- http.start do |conn|
- conn.request(request) do |response|
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
-
- read_ndjson_stream(response) { |event| yield event }
- end
+ read_ndjson_stream(response) { |event| yield event }
end
end
+ def http
+ @http ||= HttpAdapter.new(read_timeout: @read_timeout, open_timeout: @open_timeout)
+ end
+
def read_ndjson_stream(response)
buffer = String.new
@@ -145,6 +140,28 @@ module Elelem
}
end
end
+
+ class HttpAdapter
+ def initialize(read_timeout:, open_timeout:)
+ @read_timeout = read_timeout
+ @open_timeout = open_timeout
+ end
+
+ def post(uri, body:)
+ request = Net::HTTP::Post.new(uri)
+ request["content-type"] = "application/json"
+ request.body = JSON.generate(body)
+
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = uri.scheme == "https"
+ http.read_timeout = @read_timeout
+ http.open_timeout = @open_timeout
+
+ http.start do |conn|
+ conn.request(request) { |response| yield response }
+ end
+ end
+ end
end
end
end
lib/elelem/ollama/version.rb
@@ -2,6 +2,6 @@
module Elelem
module Ollama
- VERSION = "0.1.0"
+ VERSION = "0.2.0"
end
end
lib/elelem/ollama.rb
@@ -8,9 +8,13 @@ require "uri"
require_relative "ollama/version"
require_relative "ollama/client"
-Elelem::Providers.register(:ollama) do
- Elelem::Ollama::Client.new(
- model: ENV.fetch("OLLAMA_MODEL", "gpt-oss:latest"),
- host: ENV.fetch("OLLAMA_HOST", "localhost:11434")
- )
+Elelem.configure do |config|
+ config.provider(:ollama) do
+ Elelem::Ollama::Client.new(
+ model: ENV.fetch("OLLAMA_MODEL", "gpt-oss:latest"),
+ host: ENV.fetch("OLLAMA_HOST", "localhost:11434"),
+ think: ENV["OLLAMA_THINK"],
+ keep_alive: ENV["OLLAMA_KEEP_ALIVE"]
+ )
+ end
end
spec/elelem/ollama/client_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::Ollama::Client do
+ subject(:client) { described_class.new(model: "gpt-oss:latest", http:, **params) }
+
+ let(:params) { {} }
+ let(:messages) { [{ role: "user", content: "hi" }] }
+ let(:body) { http.body }
+
+ let(:response) do
+ ::Net::HTTPOK.new("1.1", "200", "OK").tap do |it|
+ allow(it).to receive(:read_body).and_yield(%({"done":true,"message":{}}\n))
+ end
+ end
+
+ let(:http) do
+ Class.new do
+ attr_reader :body
+
+ def initialize(response)
+ @response = response
+ end
+
+ def post(_url, body:)
+ @body = body
+ yield @response
+ end
+ end.new(response)
+ end
+
+ describe "#fetch" do
+ it "sends only model, messages and stream by default" do
+ client.fetch(messages) { }
+
+ expect(body).to eq(model: "gpt-oss:latest", messages:, stream: true)
+ end
+
+ it "sends tools when present" do
+ tools = [{ type: "function", function: { name: "read" } }]
+
+ client.fetch(messages, tools) { }
+
+ expect(body[:tools]).to eq(tools)
+ end
+
+ context "with tuning keywords" do
+ let(:params) { { think: "high", keep_alive: "30m", options: { num_ctx: 32_768 } } }
+
+ it "sends them in the request body" do
+ client.fetch(messages) { }
+
+ expect(body).to include(think: "high", keep_alive: "30m", options: { num_ctx: 32_768 })
+ end
+ end
+
+ context "with an empty options hash" do
+ let(:params) { { options: {} } }
+
+ it "omits options" do
+ client.fetch(messages) { }
+
+ expect(body).not_to have_key(:options)
+ end
+ end
+
+ context "with passthrough params" do
+ let(:params) { { params: { format: "json", truncate: false, top_logprobs: 3 } } }
+
+ it "merges them into the request body" do
+ client.fetch(messages) { }
+
+ expect(body).to include(format: "json", truncate: false, top_logprobs: 3)
+ end
+ end
+
+ context "with a passthrough param that collides with a keyword" do
+ let(:params) { { think: "low", params: { think: "high" } } }
+
+ it "prefers the passthrough value" do
+ client.fetch(messages) { }
+
+ expect(body[:think]).to eq("high")
+ end
+ end
+ end
+end
spec/spec_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative "../lib/elelem/ollama"
+
+RSpec.configure do |config|
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
.rspec
@@ -0,0 +1,1 @@
+--require spec_helper
elelem-ollama.gemspec
@@ -27,8 +27,8 @@ Gem::Specification.new do |spec|
spec.executables = ["elelem-ollama"]
spec.require_paths = ["lib"]
- spec.add_dependency "elelem", "~> 0.10"
- spec.add_dependency "json", "~> 2.21"
+ spec.add_dependency "elelem", "~> 0.11"
+ spec.add_dependency "json", "~> 3.0"
spec.add_dependency "net-http", "~> 0.9"
spec.add_dependency "uri", "~> 1.1"
end
Gemfile
@@ -6,3 +6,4 @@ gemspec name: "elelem-ollama"
gem "irb"
gem "rake", "~> 13.0"
+gem "rspec", "~> 3.0"
Gemfile.lock
@@ -1,41 +1,30 @@
PATH
remote: .
specs:
- elelem-ollama (0.1.0)
- elelem (~> 0.10)
- json (~> 2.21)
+ elelem-ollama (0.2.0)
+ elelem (~> 0.11)
+ json (~> 3.0)
net-http (~> 0.9)
uri (~> 1.1)
GEM
remote: https://rubygems.org/
specs:
- base64 (0.3.0)
bigdecimal (4.1.2)
- date (3.5.1)
- digest (3.2.1)
- elelem (0.10.0)
- base64 (~> 0.1)
- date (~> 3.0)
- digest (~> 3.0)
+ diff-lcs (1.6.2)
+ elelem (0.13.0)
erb (~> 6.0)
- fileutils (~> 1.0)
- json (~> 2.0)
- json_schemer (~> 2.0)
- logger (~> 1.0)
- net-hippie (~> 1.0)
- open3 (~> 0.1)
- optparse (~> 0.1)
- pathname (~> 0.1)
- reline (~> 0.6)
- securerandom (~> 0.1)
+ io-console (~> 0.9)
+ json (~> 3.0)
+ json_schemer (~> 2.5)
+ logger (~> 1.7)
+ open3 (~> 0.2)
+ optparse (~> 0.8)
+ pathname (~> 0.5)
+ reline (~> 0.7)
shellwords (~> 0.2)
- stringio (~> 3.0)
- tempfile (~> 0.3)
uri (~> 1.0)
- webrick (~> 1.9)
erb (6.0.7)
- fileutils (1.8.0)
hana (1.3.7)
io-console (0.9.2)
irb (1.18.0)
@@ -43,27 +32,16 @@ GEM
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
- json (2.21.2)
+ json (3.0.0)
json_schemer (2.5.0)
bigdecimal
hana (~> 1.3)
regexp_parser (~> 2.0)
simpleidn (~> 0.2)
logger (1.7.0)
- monitor (0.2.0)
- net-hippie (1.5.1)
- base64 (~> 0.1)
- json (~> 2.0)
- logger (~> 1.0)
- monitor (~> 0.1)
- net-http (~> 0.1)
- openssl (~> 4.0)
- resolv (~> 0.1)
- timeout (~> 0.1)
net-http (0.9.1)
uri (>= 0.11.1)
open3 (0.2.1)
- openssl (4.0.2)
optparse (0.8.1)
pathname (0.5.0)
pp (0.6.4)
@@ -83,16 +61,23 @@ GEM
regexp_parser (2.12.0)
reline (0.7.0)
io-console (~> 0.5)
- resolv (0.7.2)
- securerandom (0.4.1)
+ rspec (3.13.2)
+ rspec-core (~> 3.13.0)
+ rspec-expectations (~> 3.13.0)
+ rspec-mocks (~> 3.13.0)
+ rspec-core (3.13.6)
+ rspec-support (~> 3.13.0)
+ rspec-expectations (3.13.5)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-mocks (3.13.8)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-support (3.13.7)
shellwords (0.2.2)
simpleidn (0.3.0)
- stringio (3.2.0)
- tempfile (0.3.1)
- timeout (0.6.1)
tsort (0.2.0)
uri (1.1.1)
- webrick (1.9.2)
PLATFORMS
ruby
@@ -102,28 +87,23 @@ DEPENDENCIES
elelem-ollama!
irb
rake (~> 13.0)
+ rspec (~> 3.0)
CHECKSUMS
- base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
bundler (4.0.20) sha256=7978a8ac648767f5e635bc522445b79e80a52b907a39a36c2d8085ed6bc762ae
- date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
- digest (3.2.1) sha256=ab3312b4e272d7d5dc41c564c86a25861a1f34ac5153374199a0b74861395947
- elelem (0.10.0) sha256=de67f3a28351640da471e6e80e7a1d79e83e293cbc502a9e98c3c609cad2e237
- elelem-ollama (0.1.0)
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
+ elelem (0.13.0)
+ elelem-ollama (0.2.0)
erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
- fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02
hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
- json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
+ json (3.0.0) sha256=1ff82a28c05c5cc7b646f3e3a3ac710e4ae2aa933690cba12f1f650b0ecdeb99
json_schemer (2.5.0) sha256=2f01fb4cce721a4e08dd068fc2030cffd0702a7f333f1ea2be6e8991f00ae396
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
- monitor (0.2.0) sha256=18698584f161ca6611d73130663990ca7dc439d9d88095226e683035d66f1f0f
- net-hippie (1.5.1) sha256=76ecfc7d8df7866c3ed4f2e25fe963d87537451a1fb0d318b58eca161127fbcc
net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
open3 (0.2.1) sha256=8e2d7d2113526351201438c1aa35c8139f0141c9e8913baa007c898973bf3952
- openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2
optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
pathname (0.5.0) sha256=d5a331784f6e1f2fefb31c2ff0b8855aabfb661d807284ead9fa47b883d81623
pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
@@ -134,16 +114,15 @@ CHECKSUMS
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
- resolv (0.7.2) sha256=626d044d975ab2daac759bf898416f1b51e2cb8dcd6727c2b5b5b28b97ead2e1
- securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
shellwords (0.2.2) sha256=b8695a791de2f71472de5abdc3f4332f6535a4177f55d8f99e7e44266cd32f94
simpleidn (0.3.0) sha256=12ca730bed2f3db04d11e9bfd1bca3e11fb37f55b21eb2e9793fb5814bf54d03
- stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
- tempfile (0.3.1) sha256=0bb53ab646744e505eb3102147e22ae130a626a15563e882428c1ec973fed76a
- timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
- webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
BUNDLED WITH
4.0.20
Rakefile
@@ -1,4 +1,8 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
-task default: %i[]
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+task default: %i[spec]