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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +110 -2
- data/lib/net/llm/anthropic.rb +110 -0
- data/lib/net/llm/ollama.rb +126 -0
- data/lib/net/llm/version.rb +1 -1
- data/lib/net/llm.rb +38 -0
- metadata +46 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3fd1840f770f19440379094b5b58827b6a39981a6d32a62caae2bb4e16fdd903
|
4
|
+
data.tar.gz: 93a759a0b3772207c355b82bd049ebf7529c75113b8ae347119814cb3c94f822
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
data/lib/net/llm/version.rb
CHANGED
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.
|
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
|