net-llm 0.1.0 → 0.2.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: 3fd1840f770f19440379094b5b58827b6a39981a6d32a62caae2bb4e16fdd903
4
+ data.tar.gz: 93a759a0b3772207c355b82bd049ebf7529c75113b8ae347119814cb3c94f822
5
5
  SHA512:
6
- metadata.gz: fb6e7aebda0fbb74e06892f2eacc6812c428922743e5130ef5fde3e5ef235bbb9156a73234fdd57be04afba3ef9d62d438345dfd044a8db03798ef17b41344e4
7
- data.tar.gz: 61d7e73ea24484b1c7115ae19f192579370f66e925fd80c1337a6e94ef54af16b05d465d3a83a8d5a6fcff45eaac01d38d73ceaed8ff0973db6ccaa3787b264d
6
+ metadata.gz: f822a5bd79ceb1dba8bed5465e85acc747b44de49ff3c28b18894e774e2a275e81164cea5d4b7612aca25a4d9106e34e73e8b470b66fce17e684d4dd8c807117
7
+ data.tar.gz: 1f87089448aa9ee4b8a45bff6ce2e7a87af2fac261ca4d091ea4c2525b83b4a0282217d97b9e1df14a1adcfc8a0128e63c9da17ce2123b811a707a345bde12af
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-10-08
4
+
5
+ - Add Ollama provider with streaming support
6
+ - `/api/chat` endpoint with streaming
7
+ - `/api/generate` endpoint with streaming
8
+ - `/api/embed` endpoint for embeddings
9
+ - `/api/tags` endpoint to list models
10
+ - `/api/show` endpoint for model info
11
+ - Add Anthropic (Claude) provider with streaming support
12
+ - `/v1/messages` endpoint with streaming
13
+ - Support for system prompts
14
+ - Support for tools/function calling
15
+ - Extend OpenAI provider
16
+ - Add `/v1/models` endpoint
17
+ - Add `/v1/embeddings` endpoint
18
+
3
19
  ## [0.1.0] - 2025-10-07
4
20
 
5
21
  - Initial release
data/README.md CHANGED
@@ -69,16 +69,124 @@ 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
+
72
174
  ## API Coverage
73
175
 
74
176
  ### OpenAI
75
177
  - `/v1/chat/completions` (with tools support)
178
+ - `/v1/models`
179
+ - `/v1/embeddings`
76
180
 
77
181
  ### Ollama
78
- Coming soon
182
+ - `/api/chat` (with streaming)
183
+ - `/api/generate` (with streaming)
184
+ - `/api/embed`
185
+ - `/api/tags`
186
+ - `/api/show`
79
187
 
80
188
  ### Anthropic (Claude)
81
- Coming soon
189
+ - `/v1/messages` (with streaming and tools)
82
190
 
83
191
  ## Development
84
192
 
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Llm
5
+ class Anthropic
6
+ attr_reader :api_key, :model
7
+
8
+ def initialize(api_key:, model: "claude-3-5-sonnet-20241022")
9
+ @api_key = api_key
10
+ @model = model
11
+ end
12
+
13
+ def messages(messages, system: nil, max_tokens: 1024, tools: nil, &block)
14
+ uri = URI("https://api.anthropic.com/v1/messages")
15
+ payload = build_payload(messages, system, max_tokens, tools, block_given?)
16
+
17
+ if block_given?
18
+ stream_request(uri, payload, &block)
19
+ else
20
+ post_request(uri, payload)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def build_payload(messages, system, max_tokens, tools, stream)
27
+ payload = {
28
+ model: model,
29
+ max_tokens: max_tokens,
30
+ messages: messages,
31
+ stream: stream
32
+ }
33
+ payload[:system] = system if system
34
+ payload[:tools] = tools if tools
35
+ payload
36
+ end
37
+
38
+ def post_request(uri, payload)
39
+ http = Net::HTTP.new(uri.hostname, uri.port)
40
+ http.use_ssl = true
41
+
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
47
+
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)
51
+ end
52
+
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
69
+
70
+ buffer = ""
71
+ response.read_body do |chunk|
72
+ buffer += chunk
73
+
74
+ while (event = extract_sse_event(buffer))
75
+ next if event[:data].nil? || event[:data].empty?
76
+ next if event[:data] == "[DONE]"
77
+
78
+ json = JSON.parse(event[:data])
79
+ block.call(json)
80
+
81
+ break if json["type"] == "message_stop"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def extract_sse_event(buffer)
89
+ event_end = buffer.index("\n\n")
90
+ return nil unless event_end
91
+
92
+ event_data = buffer[0...event_end]
93
+ buffer.replace(buffer[(event_end + 2)..-1] || "")
94
+
95
+ event = {}
96
+ event_data.split("\n").each do |line|
97
+ if line.start_with?("event: ")
98
+ event[:event] = line[7..-1]
99
+ elsif line.start_with?("data: ")
100
+ event[:data] = line[6..-1]
101
+ elsif line == "data:"
102
+ event[:data] = ""
103
+ end
104
+ end
105
+
106
+ event
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Llm
5
+ class Ollama
6
+ attr_reader :host, :model
7
+
8
+ def initialize(host: "localhost:11434", model: "llama2")
9
+ @host = host
10
+ @model = model
11
+ end
12
+
13
+ def chat(messages, &block)
14
+ uri = build_uri("/api/chat")
15
+ payload = { model: model, messages: messages, stream: block_given? }
16
+
17
+ if block_given?
18
+ stream_request(uri, payload, &block)
19
+ else
20
+ post_request(uri, payload)
21
+ end
22
+ end
23
+
24
+ def generate(prompt, &block)
25
+ uri = build_uri("/api/generate")
26
+ payload = { model: model, prompt: prompt, stream: block_given? }
27
+
28
+ if block_given?
29
+ stream_request(uri, payload, &block)
30
+ else
31
+ post_request(uri, payload)
32
+ end
33
+ end
34
+
35
+ def embeddings(input)
36
+ uri = build_uri("/api/embed")
37
+ payload = { model: model, input: input }
38
+ post_request(uri, payload)
39
+ end
40
+
41
+ def tags
42
+ uri = build_uri("/api/tags")
43
+ get_request(uri)
44
+ end
45
+
46
+ def show(name)
47
+ uri = build_uri("/api/show")
48
+ payload = { name: name }
49
+ post_request(uri, payload)
50
+ end
51
+
52
+ private
53
+
54
+ def build_uri(path)
55
+ base = host.start_with?("http://", "https://") ? host : "http://#{host}"
56
+ URI("#{base}#{path}")
57
+ end
58
+
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)
68
+ end
69
+
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)
81
+ end
82
+
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
98
+
99
+ buffer = ""
100
+ response.read_body do |chunk|
101
+ buffer += chunk
102
+
103
+ while (message = extract_message(buffer))
104
+ next if message.empty?
105
+
106
+ json = JSON.parse(message)
107
+ block.call(json)
108
+
109
+ break if json["done"]
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def extract_message(buffer)
117
+ message_end = buffer.index("\n")
118
+ return nil unless message_end
119
+
120
+ message = buffer[0...message_end]
121
+ buffer.replace(buffer[(message_end + 1)..-1] || "")
122
+ message
123
+ end
124
+ end
125
+ end
126
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Net
4
4
  module Llm
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
data/lib/net/llm.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "llm/version"
4
+ require_relative "llm/ollama"
5
+ require_relative "llm/anthropic"
6
+ require "net/http"
7
+ require "json"
8
+ require "uri"
4
9
 
5
10
  module Net
6
11
  module Llm
@@ -33,6 +38,39 @@ module Net
33
38
  raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
34
39
  JSON.parse(response.body)
35
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
36
74
  end
37
75
  end
38
76
  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.2.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,8 @@ 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
25
69
  - lib/net/llm/version.rb
26
70
  - sig/net/llm.rbs
27
71
  homepage: https://github.com/xlgmokha/net-llm