net-llm 0.1.0 → 0.3.0

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: 28baf2a76420a22be944c1a46aab05d8dfceda575bbb9f341260e592a9e7711f
4
- data.tar.gz: f746ecda882d6f08336753fd45319aa28d133c36f43fea9334e83564735d11cd
3
+ metadata.gz: 5fc7c8401eba4ab7b254b1247280c97f61db7fd7ed9b562c90bae0bc4066fbbc
4
+ data.tar.gz: a74469b7086118a424b20f688a1ed899a28cb8652bbc5683c65d5bfba8d748ea
5
5
  SHA512:
6
- metadata.gz: fb6e7aebda0fbb74e06892f2eacc6812c428922743e5130ef5fde3e5ef235bbb9156a73234fdd57be04afba3ef9d62d438345dfd044a8db03798ef17b41344e4
7
- data.tar.gz: 61d7e73ea24484b1c7115ae19f192579370f66e925fd80c1337a6e94ef54af16b05d465d3a83a8d5a6fcff45eaac01d38d73ceaed8ff0973db6ccaa3787b264d
6
+ metadata.gz: fcbb8722b6d6c2d9414ce49ded2a4abff7da0051e924483f6900676d6602ae64f5c1437f152b025b4a84a7c1700a2c9874254521f9b39206ab757955e18e042f
7
+ data.tar.gz: bea9a981fead23269fdc12f0012030187dfbd7da1bc444a8d23eee75fe4eba1dae68a9f1570e54352a8fe3c84bd3abd1d6563b88612342d48ffd425fbb6b95d4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2025-10-08
4
+ ### Changed
5
+ - Refactored all providers to use net-hippie HTTP client
6
+ - Reduced code duplication across providers
7
+ - Improved error handling consistency
8
+
9
+ ### Added
10
+ - Added net-hippie dependency for cleaner HTTP interactions
11
+
12
+ ## [0.2.0] - 2025-10-08
13
+
14
+ - Add Ollama provider with streaming support
15
+ - `/api/chat` endpoint with streaming
16
+ - `/api/generate` endpoint with streaming
17
+ - `/api/embed` endpoint for embeddings
18
+ - `/api/tags` endpoint to list models
19
+ - `/api/show` endpoint for model info
20
+ - Add Anthropic (Claude) provider with streaming support
21
+ - `/v1/messages` endpoint with streaming
22
+ - Support for system prompts
23
+ - Support for tools/function calling
24
+ - Extend OpenAI provider
25
+ - Add `/v1/models` endpoint
26
+ - Add `/v1/embeddings` endpoint
27
+
3
28
  ## [0.1.0] - 2025-10-07
4
29
 
5
30
  - Initial release
data/README.md CHANGED
@@ -69,16 +69,140 @@ tools = [
69
69
  response = client.chat(messages, tools)
70
70
  ```
71
71
 
72
+ ### Ollama
73
+
74
+ ```ruby
75
+ require 'net/llm'
76
+
77
+ client = Net::Llm::Ollama.new(
78
+ host: 'localhost:11434',
79
+ model: 'gpt-oss:latest'
80
+ )
81
+
82
+ messages = [
83
+ { role: 'user', content: 'Hello!' }
84
+ ]
85
+
86
+ response = client.chat(messages)
87
+ puts response['message']['content']
88
+ ```
89
+
90
+ #### Streaming
91
+
92
+ ```ruby
93
+ client.chat(messages) do |chunk|
94
+ print chunk.dig('message', 'content')
95
+ end
96
+ ```
97
+
98
+ #### Generate
99
+
100
+ ```ruby
101
+ response = client.generate('Write a haiku')
102
+ puts response['response']
103
+
104
+ client.generate('Write a haiku') do |chunk|
105
+ print chunk['response']
106
+ end
107
+ ```
108
+
109
+ #### Other Endpoints
110
+
111
+ ```ruby
112
+ client.embeddings('Hello world')
113
+ client.tags
114
+ client.show('llama2')
115
+ ```
116
+
117
+ ### Anthropic (Claude)
118
+
119
+ ```ruby
120
+ require 'net/llm'
121
+
122
+ client = Net::Llm::Anthropic.new(
123
+ api_key: ENV['ANTHROPIC_API_KEY'],
124
+ model: 'claude-3-5-sonnet-20241022'
125
+ )
126
+
127
+ messages = [
128
+ { role: 'user', content: 'Hello!' }
129
+ ]
130
+
131
+ response = client.messages(messages)
132
+ puts response.dig('content', 0, 'text')
133
+ ```
134
+
135
+ #### With System Prompt
136
+
137
+ ```ruby
138
+ response = client.messages(
139
+ messages,
140
+ system: 'You are a helpful assistant'
141
+ )
142
+ ```
143
+
144
+ #### Streaming
145
+
146
+ ```ruby
147
+ client.messages(messages) do |event|
148
+ if event['type'] == 'content_block_delta'
149
+ print event.dig('delta', 'text')
150
+ end
151
+ end
152
+ ```
153
+
154
+ #### With Tools
155
+
156
+ ```ruby
157
+ tools = [
158
+ {
159
+ name: 'get_weather',
160
+ description: 'Get current weather',
161
+ input_schema: {
162
+ type: 'object',
163
+ properties: {
164
+ location: { type: 'string' }
165
+ },
166
+ required: ['location']
167
+ }
168
+ }
169
+ ]
170
+
171
+ response = client.messages(messages, tools: tools)
172
+ ```
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
+
72
190
  ## API Coverage
73
191
 
74
192
  ### OpenAI
75
193
  - `/v1/chat/completions` (with tools support)
194
+ - `/v1/models`
195
+ - `/v1/embeddings`
76
196
 
77
197
  ### Ollama
78
- Coming soon
198
+ - `/api/chat` (with streaming)
199
+ - `/api/generate` (with streaming)
200
+ - `/api/embed`
201
+ - `/api/tags`
202
+ - `/api/show`
79
203
 
80
204
  ### Anthropic (Claude)
81
- Coming soon
205
+ - `/v1/messages` (with streaming and tools)
82
206
 
83
207
  ## Development
84
208
 
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Llm
5
+ class Anthropic
6
+ attr_reader :api_key, :model, :http
7
+
8
+ def initialize(api_key:, model: "claude-3-5-sonnet-20241022", http: Net::Llm.http)
9
+ @api_key = api_key
10
+ @model = model
11
+ @http = http
12
+ end
13
+
14
+ def messages(messages, system: nil, max_tokens: 1024, tools: nil, &block)
15
+ url = "https://api.anthropic.com/v1/messages"
16
+ payload = build_payload(messages, system, max_tokens, tools, block_given?)
17
+
18
+ if block_given?
19
+ stream_request(url, payload, &block)
20
+ else
21
+ post_request(url, payload)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def build_payload(messages, system, max_tokens, tools, stream)
28
+ payload = {
29
+ model: model,
30
+ max_tokens: max_tokens,
31
+ messages: messages,
32
+ stream: stream
33
+ }
34
+ payload[:system] = system if system
35
+ payload[:tools] = tools if tools
36
+ payload
37
+ end
38
+
39
+ def headers
40
+ {
41
+ "x-api-key" => api_key,
42
+ "anthropic-version" => "2023-06-01"
43
+ }
44
+ end
45
+
46
+ def post_request(url, payload)
47
+ handle_response(http.post(url, headers: headers, body: payload))
48
+ end
49
+
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
56
+ end
57
+
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)
61
+
62
+ buffer = ""
63
+ response.read_body do |chunk|
64
+ buffer += chunk
65
+
66
+ while (event = extract_sse_event(buffer))
67
+ next if event[:data].nil? || event[:data].empty?
68
+ next if event[:data] == "[DONE]"
69
+
70
+ json = JSON.parse(event[:data])
71
+ block.call(json)
72
+
73
+ break if json["type"] == "message_stop"
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def extract_sse_event(buffer)
80
+ event_end = buffer.index("\n\n")
81
+ return nil unless event_end
82
+
83
+ event_data = buffer[0...event_end]
84
+ buffer.replace(buffer[(event_end + 2)..-1] || "")
85
+
86
+ event = {}
87
+ event_data.split("\n").each do |line|
88
+ if line.start_with?("event: ")
89
+ event[:event] = line[7..-1]
90
+ elsif line.start_with?("data: ")
91
+ event[:data] = line[6..-1]
92
+ elsif line == "data:"
93
+ event[:data] = ""
94
+ end
95
+ end
96
+
97
+ event
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Llm
5
+ class Ollama
6
+ attr_reader :host, :model, :http
7
+
8
+ def initialize(host: "localhost:11434", model: "llama2", http: Net::Llm.http)
9
+ @host = host
10
+ @model = model
11
+ @http = http
12
+ end
13
+
14
+ def chat(messages, &block)
15
+ url = build_url("/api/chat")
16
+ payload = { model: model, messages: messages, stream: block_given? }
17
+
18
+ if block_given?
19
+ stream_request(url, payload, &block)
20
+ else
21
+ post_request(url, payload)
22
+ end
23
+ end
24
+
25
+ def generate(prompt, &block)
26
+ url = build_url("/api/generate")
27
+ payload = { model: model, prompt: prompt, stream: block_given? }
28
+
29
+ if block_given?
30
+ stream_request(url, payload, &block)
31
+ else
32
+ post_request(url, payload)
33
+ end
34
+ end
35
+
36
+ def embeddings(input)
37
+ url = build_url("/api/embed")
38
+ payload = { model: model, input: input }
39
+ post_request(url, payload)
40
+ end
41
+
42
+ def tags
43
+ url = build_url("/api/tags")
44
+ response = http.get(url)
45
+ handle_response(response)
46
+ end
47
+
48
+ def show(name)
49
+ url = build_url("/api/show")
50
+ payload = { name: name }
51
+ post_request(url, payload)
52
+ end
53
+
54
+ private
55
+
56
+ def build_url(path)
57
+ base = host.start_with?("http://", "https://") ? host : "http://#{host}"
58
+ "#{base}#{path}"
59
+ end
60
+
61
+ def post_request(url, payload)
62
+ response = http.post(url, body: payload)
63
+ handle_response(response)
64
+ end
65
+
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
72
+ end
73
+
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)
77
+
78
+ buffer = ""
79
+ response.read_body do |chunk|
80
+ buffer += chunk
81
+
82
+ while (message = extract_message(buffer))
83
+ next if message.empty?
84
+
85
+ json = JSON.parse(message)
86
+ block.call(json)
87
+
88
+ break if json["done"]
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def extract_message(buffer)
95
+ message_end = buffer.index("\n")
96
+ return nil unless message_end
97
+
98
+ message = buffer[0...message_end]
99
+ buffer.replace(buffer[(message_end + 1)..-1] || "")
100
+ message
101
+ end
102
+ end
103
+ end
104
+ 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.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/net/llm.rb CHANGED
@@ -1,38 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "llm/version"
4
+ require_relative "llm/openai"
5
+ require_relative "llm/ollama"
6
+ require_relative "llm/anthropic"
7
+ require "net/hippie"
8
+ require "json"
4
9
 
5
10
  module Net
6
11
  module Llm
7
12
  class Error < StandardError; end
8
- DEFAULT_TIMEOUT = 60 * 2
9
13
 
10
- class OpenAI
11
- attr_reader :api_key, :base_url, :model
12
-
13
- def initialize(api_key:, base_url: "https://api.openai.com/v1", model: "gpt-4o-mini")
14
- @api_key = api_key
15
- @base_url = base_url
16
- @model = model
17
- end
18
-
19
- def chat(messages, tools, timeout: DEFAULT_TIMEOUT)
20
- uri = URI("#{base_url}/chat/completions")
21
- request = Net::HTTP::Post.new(uri)
22
- request["Authorization"] = "Bearer #{api_key}"
23
- request["Content-Type"] = "application/json"
24
- request.body = { model: model, messages: messages, tools: tools, tool_choice: "auto" }.to_json
25
-
26
- http = Net::HTTP.new(uri.hostname, uri.port)
27
- http.use_ssl = true
28
- http.open_timeout = timeout
29
- http.read_timeout = timeout
30
- http.write_timeout = timeout if http.respond_to?(:write_timeout=)
31
-
32
- response = http.start { |h| h.request(request) }
33
- raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
34
- JSON.parse(response.body)
35
- end
14
+ def self.http
15
+ @http ||= Net::Hippie::Client.new(
16
+ read_timeout: 3600,
17
+ open_timeout: 10
18
+ )
36
19
  end
37
20
  end
38
21
  end
metadata CHANGED
@@ -1,14 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: uri
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: net-http
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.6'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.6'
12
54
  description: A minimal Ruby gem providing interfaces to connect to OpenAI, Ollama,
13
55
  and Anthropic (Claude) LLM APIs
14
56
  email:
@@ -22,6 +64,9 @@ files:
22
64
  - README.md
23
65
  - Rakefile
24
66
  - lib/net/llm.rb
67
+ - lib/net/llm/anthropic.rb
68
+ - lib/net/llm/ollama.rb
69
+ - lib/net/llm/openai.rb
25
70
  - lib/net/llm/version.rb
26
71
  - sig/net/llm.rbs
27
72
  homepage: https://github.com/xlgmokha/net-llm