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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +16 -0
- data/lib/net/llm/anthropic.rb +33 -42
- data/lib/net/llm/ollama.rb +38 -60
- data/lib/net/llm/openai.rb +50 -0
- data/lib/net/llm/version.rb +1 -1
- data/lib/net/llm.rb +7 -62
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de6c591968c95eafb5af04842bc5fc8f4f0235369a6f7010843ab9adbe2199cd
|
4
|
+
data.tar.gz: f8dbd38c119eacc38814a5eb173f1a2158564bce75dd5c6d111d3900e17f8136
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/net/llm/anthropic.rb
CHANGED
@@ -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
|
-
|
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(
|
19
|
+
stream_request(url, payload, &block)
|
19
20
|
else
|
20
|
-
post_request(
|
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
|
39
|
-
|
40
|
-
|
39
|
+
def headers
|
40
|
+
{
|
41
|
+
"x-api-key" => api_key,
|
42
|
+
"anthropic-version" => "2023-06-01"
|
43
|
+
}
|
44
|
+
end
|
41
45
|
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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(
|
54
|
-
http
|
55
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
62
|
+
buffer = ""
|
63
|
+
response.read_body do |chunk|
|
64
|
+
buffer += chunk
|
73
65
|
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
79
|
-
|
70
|
+
json = JSON.parse(event[:data])
|
71
|
+
block.call(json)
|
80
72
|
|
81
|
-
|
82
|
-
end
|
73
|
+
break if json["type"] == "message_stop"
|
83
74
|
end
|
84
75
|
end
|
85
76
|
end
|
data/lib/net/llm/ollama.rb
CHANGED
@@ -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
|
-
|
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(
|
19
|
+
stream_request(url, payload, &block)
|
19
20
|
else
|
20
|
-
post_request(
|
21
|
+
post_request(url, payload)
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
24
25
|
def generate(prompt, &block)
|
25
|
-
|
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(
|
30
|
+
stream_request(url, payload, &block)
|
30
31
|
else
|
31
|
-
post_request(
|
32
|
+
post_request(url, payload)
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
36
|
def embeddings(input)
|
36
|
-
|
37
|
+
url = build_url("/api/embed")
|
37
38
|
payload = { model: model, input: input }
|
38
|
-
post_request(
|
39
|
+
post_request(url, payload)
|
39
40
|
end
|
40
41
|
|
41
42
|
def tags
|
42
|
-
|
43
|
-
|
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
|
-
|
49
|
+
url = build_url("/api/show")
|
48
50
|
payload = { name: name }
|
49
|
-
post_request(
|
51
|
+
post_request(url, payload)
|
50
52
|
end
|
51
53
|
|
52
54
|
private
|
53
55
|
|
54
|
-
def
|
56
|
+
def build_url(path)
|
55
57
|
base = host.start_with?("http://", "https://") ? host : "http://#{host}"
|
56
|
-
|
58
|
+
"#{base}#{path}"
|
57
59
|
end
|
58
60
|
|
59
|
-
def
|
60
|
-
|
61
|
-
|
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
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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(
|
84
|
-
http
|
85
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
78
|
+
buffer = ""
|
79
|
+
response.read_body do |chunk|
|
80
|
+
buffer += chunk
|
102
81
|
|
103
|
-
|
104
|
-
|
82
|
+
while (message = extract_message(buffer))
|
83
|
+
next if message.empty?
|
105
84
|
|
106
|
-
|
107
|
-
|
85
|
+
json = JSON.parse(message)
|
86
|
+
block.call(json)
|
108
87
|
|
109
|
-
|
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
|
data/lib/net/llm/version.rb
CHANGED
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/
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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.
|
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:
|
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:
|
41
|
+
name: uri
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '0
|
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
|
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
|