net-llm 0.2.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fd1840f770f19440379094b5b58827b6a39981a6d32a62caae2bb4e16fdd903
4
- data.tar.gz: 93a759a0b3772207c355b82bd049ebf7529c75113b8ae347119814cb3c94f822
3
+ metadata.gz: de6c591968c95eafb5af04842bc5fc8f4f0235369a6f7010843ab9adbe2199cd
4
+ data.tar.gz: f8dbd38c119eacc38814a5eb173f1a2158564bce75dd5c6d111d3900e17f8136
5
5
  SHA512:
6
- metadata.gz: f822a5bd79ceb1dba8bed5465e85acc747b44de49ff3c28b18894e774e2a275e81164cea5d4b7612aca25a4d9106e34e73e8b470b66fce17e684d4dd8c807117
7
- data.tar.gz: 1f87089448aa9ee4b8a45bff6ce2e7a87af2fac261ca4d091ea4c2525b83b4a0282217d97b9e1df14a1adcfc8a0128e63c9da17ce2123b811a707a345bde12af
6
+ metadata.gz: dd8e9c389c9ed5f0614b386cf56a6359275d156197b4fe284671fc0f062d839181b938f91b898b472cf71f2d9090f5aedb9b4b725616ba97acf9f072f1a5a2a3
7
+ data.tar.gz: 3ac65c38e85ff03fd29e1a9dd227d674455773a35290b6871ac18d05f844afc6142a0daebcb2c88a75ec858d57b0fc7c9eddb5b07a2e4a1c10917cb6156be0bd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-10-08
4
+ ### Fixed
5
+ - Added missing net-hippie runtime dependency to gemspec
6
+
7
+ ## [0.3.0] - 2025-10-08
8
+ ### Changed
9
+ - Refactored all providers to use net-hippie HTTP client
10
+ - Reduced code duplication across providers
11
+ - Improved error handling consistency
12
+
13
+ ### Added
14
+ - Added net-hippie dependency for cleaner HTTP interactions
15
+
3
16
  ## [0.2.0] - 2025-10-08
4
17
 
5
18
  - Add Ollama provider with streaming support
data/README.md CHANGED
@@ -171,6 +171,22 @@ tools = [
171
171
  response = client.messages(messages, tools: tools)
172
172
  ```
173
173
 
174
+ ## Error Handling
175
+
176
+ All non-streaming API methods return error information as a hash when requests fail:
177
+
178
+ ```ruby
179
+ response = client.chat(messages, tools)
180
+
181
+ if response["code"]
182
+ puts "Error #{response["code"]}: #{response["body"]}"
183
+ else
184
+ puts response.dig('choices', 0, 'message', 'content')
185
+ end
186
+ ```
187
+
188
+ Streaming methods still raise exceptions on HTTP errors.
189
+
174
190
  ## API Coverage
175
191
 
176
192
  ### OpenAI
@@ -3,21 +3,22 @@
3
3
  module Net
4
4
  module Llm
5
5
  class Anthropic
6
- attr_reader :api_key, :model
6
+ attr_reader :api_key, :model, :http
7
7
 
8
- def initialize(api_key:, model: "claude-3-5-sonnet-20241022")
8
+ def initialize(api_key:, model: "claude-3-5-sonnet-20241022", http: Net::Llm.http)
9
9
  @api_key = api_key
10
10
  @model = model
11
+ @http = http
11
12
  end
12
13
 
13
14
  def messages(messages, system: nil, max_tokens: 1024, tools: nil, &block)
14
- uri = URI("https://api.anthropic.com/v1/messages")
15
+ url = "https://api.anthropic.com/v1/messages"
15
16
  payload = build_payload(messages, system, max_tokens, tools, block_given?)
16
17
 
17
18
  if block_given?
18
- stream_request(uri, payload, &block)
19
+ stream_request(url, payload, &block)
19
20
  else
20
- post_request(uri, payload)
21
+ post_request(url, payload)
21
22
  end
22
23
  end
23
24
 
@@ -35,51 +36,41 @@ module Net
35
36
  payload
36
37
  end
37
38
 
38
- def post_request(uri, payload)
39
- http = Net::HTTP.new(uri.hostname, uri.port)
40
- http.use_ssl = true
39
+ def headers
40
+ {
41
+ "x-api-key" => api_key,
42
+ "anthropic-version" => "2023-06-01"
43
+ }
44
+ end
41
45
 
42
- request = Net::HTTP::Post.new(uri)
43
- request["x-api-key"] = api_key
44
- request["anthropic-version"] = "2023-06-01"
45
- request["Content-Type"] = "application/json"
46
- request.body = payload.to_json
46
+ def post_request(url, payload)
47
+ handle_response(http.post(url, headers: headers, body: payload))
48
+ end
47
49
 
48
- response = http.start { |h| h.request(request) }
49
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
50
- JSON.parse(response.body)
50
+ def handle_response(response)
51
+ if response.is_a?(Net::HTTPSuccess)
52
+ JSON.parse(response.body)
53
+ else
54
+ { "code" => response.code, "body" => response.body }
55
+ end
51
56
  end
52
57
 
53
- def stream_request(uri, payload, &block)
54
- http = Net::HTTP.new(uri.hostname, uri.port)
55
- http.use_ssl = true
56
- http.read_timeout = 3600
57
-
58
- request = Net::HTTP::Post.new(uri)
59
- request["x-api-key"] = api_key
60
- request["anthropic-version"] = "2023-06-01"
61
- request["Content-Type"] = "application/json"
62
- request.body = payload.to_json
63
-
64
- http.start do |h|
65
- h.request(request) do |response|
66
- unless response.is_a?(Net::HTTPSuccess)
67
- raise "HTTP #{response.code}: #{response.body}"
68
- end
58
+ def stream_request(url, payload, &block)
59
+ http.post(url, headers: headers, body: payload) do |response|
60
+ raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
69
61
 
70
- buffer = ""
71
- response.read_body do |chunk|
72
- buffer += chunk
62
+ buffer = ""
63
+ response.read_body do |chunk|
64
+ buffer += chunk
73
65
 
74
- while (event = extract_sse_event(buffer))
75
- next if event[:data].nil? || event[:data].empty?
76
- next if event[:data] == "[DONE]"
66
+ while (event = extract_sse_event(buffer))
67
+ next if event[:data].nil? || event[:data].empty?
68
+ next if event[:data] == "[DONE]"
77
69
 
78
- json = JSON.parse(event[:data])
79
- block.call(json)
70
+ json = JSON.parse(event[:data])
71
+ block.call(json)
80
72
 
81
- break if json["type"] == "message_stop"
82
- end
73
+ break if json["type"] == "message_stop"
83
74
  end
84
75
  end
85
76
  end
@@ -3,111 +3,89 @@
3
3
  module Net
4
4
  module Llm
5
5
  class Ollama
6
- attr_reader :host, :model
6
+ attr_reader :host, :model, :http
7
7
 
8
- def initialize(host: "localhost:11434", model: "llama2")
8
+ def initialize(host: "localhost:11434", model: "llama2", http: Net::Llm.http)
9
9
  @host = host
10
10
  @model = model
11
+ @http = http
11
12
  end
12
13
 
13
14
  def chat(messages, &block)
14
- uri = build_uri("/api/chat")
15
+ url = build_url("/api/chat")
15
16
  payload = { model: model, messages: messages, stream: block_given? }
16
17
 
17
18
  if block_given?
18
- stream_request(uri, payload, &block)
19
+ stream_request(url, payload, &block)
19
20
  else
20
- post_request(uri, payload)
21
+ post_request(url, payload)
21
22
  end
22
23
  end
23
24
 
24
25
  def generate(prompt, &block)
25
- uri = build_uri("/api/generate")
26
+ url = build_url("/api/generate")
26
27
  payload = { model: model, prompt: prompt, stream: block_given? }
27
28
 
28
29
  if block_given?
29
- stream_request(uri, payload, &block)
30
+ stream_request(url, payload, &block)
30
31
  else
31
- post_request(uri, payload)
32
+ post_request(url, payload)
32
33
  end
33
34
  end
34
35
 
35
36
  def embeddings(input)
36
- uri = build_uri("/api/embed")
37
+ url = build_url("/api/embed")
37
38
  payload = { model: model, input: input }
38
- post_request(uri, payload)
39
+ post_request(url, payload)
39
40
  end
40
41
 
41
42
  def tags
42
- uri = build_uri("/api/tags")
43
- get_request(uri)
43
+ url = build_url("/api/tags")
44
+ response = http.get(url)
45
+ handle_response(response)
44
46
  end
45
47
 
46
48
  def show(name)
47
- uri = build_uri("/api/show")
49
+ url = build_url("/api/show")
48
50
  payload = { name: name }
49
- post_request(uri, payload)
51
+ post_request(url, payload)
50
52
  end
51
53
 
52
54
  private
53
55
 
54
- def build_uri(path)
56
+ def build_url(path)
55
57
  base = host.start_with?("http://", "https://") ? host : "http://#{host}"
56
- URI("#{base}#{path}")
58
+ "#{base}#{path}"
57
59
  end
58
60
 
59
- def get_request(uri)
60
- http = Net::HTTP.new(uri.hostname, uri.port)
61
- http.use_ssl = uri.scheme == "https"
62
- request = Net::HTTP::Get.new(uri)
63
- request["Accept"] = "application/json"
64
-
65
- response = http.start { |h| h.request(request) }
66
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
67
- JSON.parse(response.body)
61
+ def post_request(url, payload)
62
+ response = http.post(url, body: payload)
63
+ handle_response(response)
68
64
  end
69
65
 
70
- def post_request(uri, payload)
71
- http = Net::HTTP.new(uri.hostname, uri.port)
72
- http.use_ssl = uri.scheme == "https"
73
- request = Net::HTTP::Post.new(uri)
74
- request["Accept"] = "application/json"
75
- request["Content-Type"] = "application/json"
76
- request.body = payload.to_json
77
-
78
- response = http.start { |h| h.request(request) }
79
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
80
- JSON.parse(response.body)
66
+ def handle_response(response)
67
+ if response.is_a?(Net::HTTPSuccess)
68
+ JSON.parse(response.body)
69
+ else
70
+ { "code" => response.code, "body" => response.body }
71
+ end
81
72
  end
82
73
 
83
- def stream_request(uri, payload, &block)
84
- http = Net::HTTP.new(uri.hostname, uri.port)
85
- http.use_ssl = uri.scheme == "https"
86
- http.read_timeout = 3600
87
-
88
- request = Net::HTTP::Post.new(uri)
89
- request["Accept"] = "application/json"
90
- request["Content-Type"] = "application/json"
91
- request.body = payload.to_json
92
-
93
- http.start do |h|
94
- h.request(request) do |response|
95
- unless response.is_a?(Net::HTTPSuccess)
96
- raise "HTTP #{response.code}: #{response.body}"
97
- end
74
+ def stream_request(url, payload, &block)
75
+ http.post(url, body: payload) do |response|
76
+ raise "HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
98
77
 
99
- buffer = ""
100
- response.read_body do |chunk|
101
- buffer += chunk
78
+ buffer = ""
79
+ response.read_body do |chunk|
80
+ buffer += chunk
102
81
 
103
- while (message = extract_message(buffer))
104
- next if message.empty?
82
+ while (message = extract_message(buffer))
83
+ next if message.empty?
105
84
 
106
- json = JSON.parse(message)
107
- block.call(json)
85
+ json = JSON.parse(message)
86
+ block.call(json)
108
87
 
109
- break if json["done"]
110
- end
88
+ break if json["done"]
111
89
  end
112
90
  end
113
91
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Llm
5
+ class OpenAI
6
+ attr_reader :api_key, :base_url, :model, :http
7
+
8
+ def initialize(api_key:, base_url: "https://api.openai.com/v1", model: "gpt-4o-mini", http: Net::Llm.http)
9
+ @api_key = api_key
10
+ @base_url = base_url
11
+ @model = model
12
+ @http = http
13
+ end
14
+
15
+ def chat(messages, tools)
16
+ handle_response(http.post(
17
+ "#{base_url}/chat/completions",
18
+ headers: headers,
19
+ body: { model: model, messages: messages, tools: tools, tool_choice: "auto" }
20
+ ))
21
+ end
22
+
23
+ def models
24
+ handle_response(http.get("#{base_url}/models", headers: headers))
25
+ end
26
+
27
+ def embeddings(input, model: "text-embedding-ada-002")
28
+ handle_response(http.post(
29
+ "#{base_url}/embeddings",
30
+ headers: headers,
31
+ body: { model: model, input: input },
32
+ ))
33
+ end
34
+
35
+ private
36
+
37
+ def headers
38
+ { "Authorization" => Net::Hippie.bearer_auth(api_key) }
39
+ end
40
+
41
+ def handle_response(response)
42
+ if response.is_a?(Net::HTTPSuccess)
43
+ JSON.parse(response.body)
44
+ else
45
+ { "code" => response.code, "body" => response.body }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Net
4
4
  module Llm
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
data/lib/net/llm.rb CHANGED
@@ -1,76 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "llm/version"
4
+ require_relative "llm/openai"
4
5
  require_relative "llm/ollama"
5
6
  require_relative "llm/anthropic"
6
- require "net/http"
7
+ require "net/hippie"
7
8
  require "json"
8
- require "uri"
9
9
 
10
10
  module Net
11
11
  module Llm
12
12
  class Error < StandardError; end
13
- DEFAULT_TIMEOUT = 60 * 2
14
13
 
15
- class OpenAI
16
- attr_reader :api_key, :base_url, :model
17
-
18
- def initialize(api_key:, base_url: "https://api.openai.com/v1", model: "gpt-4o-mini")
19
- @api_key = api_key
20
- @base_url = base_url
21
- @model = model
22
- end
23
-
24
- def chat(messages, tools, timeout: DEFAULT_TIMEOUT)
25
- uri = URI("#{base_url}/chat/completions")
26
- request = Net::HTTP::Post.new(uri)
27
- request["Authorization"] = "Bearer #{api_key}"
28
- request["Content-Type"] = "application/json"
29
- request.body = { model: model, messages: messages, tools: tools, tool_choice: "auto" }.to_json
30
-
31
- http = Net::HTTP.new(uri.hostname, uri.port)
32
- http.use_ssl = true
33
- http.open_timeout = timeout
34
- http.read_timeout = timeout
35
- http.write_timeout = timeout if http.respond_to?(:write_timeout=)
36
-
37
- response = http.start { |h| h.request(request) }
38
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
39
- JSON.parse(response.body)
40
- end
41
-
42
- def models(timeout: DEFAULT_TIMEOUT)
43
- uri = URI("#{base_url}/models")
44
- request = Net::HTTP::Get.new(uri)
45
- request["Authorization"] = "Bearer #{api_key}"
46
-
47
- http = Net::HTTP.new(uri.hostname, uri.port)
48
- http.use_ssl = true
49
- http.open_timeout = timeout
50
- http.read_timeout = timeout
51
-
52
- response = http.start { |h| h.request(request) }
53
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
54
- JSON.parse(response.body)
55
- end
56
-
57
- def embeddings(input, model: "text-embedding-ada-002", timeout: DEFAULT_TIMEOUT)
58
- uri = URI("#{base_url}/embeddings")
59
- request = Net::HTTP::Post.new(uri)
60
- request["Authorization"] = "Bearer #{api_key}"
61
- request["Content-Type"] = "application/json"
62
- request.body = { model: model, input: input }.to_json
63
-
64
- http = Net::HTTP.new(uri.hostname, uri.port)
65
- http.use_ssl = true
66
- http.open_timeout = timeout
67
- http.read_timeout = timeout
68
- http.write_timeout = timeout if http.respond_to?(:write_timeout=)
69
-
70
- response = http.start { |h| h.request(request) }
71
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
72
- JSON.parse(response.body)
73
- end
14
+ def self.http
15
+ @http ||= Net::Hippie::Client.new(
16
+ read_timeout: 3600,
17
+ open_timeout: 10
18
+ )
74
19
  end
75
20
  end
76
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -24,7 +24,7 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: uri
27
+ name: net-hippie
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
@@ -38,19 +38,19 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.0'
40
40
  - !ruby/object:Gem::Dependency
41
- name: net-http
41
+ name: uri
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0.6'
46
+ version: '1.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '0.6'
53
+ version: '1.0'
54
54
  description: A minimal Ruby gem providing interfaces to connect to OpenAI, Ollama,
55
55
  and Anthropic (Claude) LLM APIs
56
56
  email:
@@ -66,6 +66,7 @@ files:
66
66
  - lib/net/llm.rb
67
67
  - lib/net/llm/anthropic.rb
68
68
  - lib/net/llm/ollama.rb
69
+ - lib/net/llm/openai.rb
69
70
  - lib/net/llm/version.rb
70
71
  - sig/net/llm.rbs
71
72
  homepage: https://github.com/xlgmokha/net-llm