raimei-nim 0.1.0 → 0.1.2

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: d78531725801b2a89ac2a2e74343c8e07050f1ff68e0afcf3477a91d2696dba2
4
- data.tar.gz: 3e54451e5a36baeaab8af0aa43b4eba36615fddc164cbdec09922d49a9596fda
3
+ metadata.gz: abc44189bf4b4c7c97e31627d738bcadfb4a74b216f434e86e03a3e0b7b59b62
4
+ data.tar.gz: 7a042805a56694d8d2a2a917c9094cd2c79ac5986b0afc0e694265f8b5e376d5
5
5
  SHA512:
6
- metadata.gz: 232bc484fadcec0d57ace1baf1b12917549cc79f9452e2475dcc3d6ac04091c90c5196f81acc9a33edaa2aa7d3493d3468f8a1e9223f7b7f01838c6598466113
7
- data.tar.gz: d52ab3f98d35710d1ffd6a98a01f0637e1bd0f106257d087126b9ecfb39c6691c5f7a9aa0936f88eb07b7fd1df2ba685d917c3c84e7d2cfa0856c49ccec40daf
6
+ metadata.gz: 2319cd31659f2412eecf42b7875891de1d6511dda190ec25b3bba269c1d5560926fc5d4b939a6bc458a45a6b5ee2814b69b5f2064e9f3091ca276cf8af7c0928
7
+ data.tar.gz: 32d33a87c2263b84713805cb866d1ac99cbb1f2f121f169a333caf9abd6345268da6abc83b770117a05b61ad06d94fcb266930872350683a130c5e9336450430
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in raimei-nim.gemspec
6
+ gemspec
7
+
8
+ gem "irb"
9
+ gem "rake", "~> 13.0"
10
+
11
+ gem "minitest", "~> 5.16"
12
+
13
+ gem "rubocop", "~> 1.21"
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Raimei::Nim
2
2
 
3
+ > ⚠️ Experimental: APIs may change.
4
+
5
+
3
6
  TODO: Delete this and the text below, and describe your gem
4
7
 
5
8
  Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/raimei/nim`. To experiment with that code, run `bin/console` for an interactive prompt.
data/exe/raimei-chat ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "optparse"
6
+ require "raimei/nim"
7
+
8
+ opts = { url: ENV["URL"], model: ENV["MODEL"] || "llama3", api_key: ENV["API_KEY"], stream: true }
9
+ OptionParser.new do |o|
10
+ o.banner = "Usage: raimei-chat [--url URL] [--model MODEL] [--api-key KEY] [--no-stream] [prompt...]\n" \
11
+ "Reads from ARGV or stdin."
12
+ o.on("--url URL") { |v| opts[:url] = v }
13
+ o.on("--model MODEL") { |v| opts[:model] = v }
14
+ o.on("--api-key KEY") { |v| opts[:api_key] = v }
15
+ o.on("--no-stream") { opts[:stream] = false }
16
+ end.parse!
17
+
18
+ abort "Set --url or URL env" unless opts[:url]
19
+ prompt = if !STDIN.tty? && ARGV.empty? then STDIN.read else ARGV.join(" ") end
20
+ abort "Provide a prompt (args or stdin)" if prompt.to_s.strip.empty?
21
+
22
+ client = Raimei::NIM::Client.new(url: opts[:url], api_key: opts[:api_key])
23
+ if opts[:stream]
24
+ client.chat(model: opts[:model], messages: [{ role: "user", content: prompt }], stream: true).each { |d| print d }
25
+ puts
26
+ else
27
+ puts client.chat(model: opts[:model], messages: [{ role: "user", content: prompt }], stream: false)
28
+ end
29
+
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Raimei
4
4
  module Nim
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
data/lib/raimei/nim.rb CHANGED
@@ -1,31 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "net/http"
5
- require "uri"
3
+ require "json"; require "net/http"; require "uri"
6
4
 
7
5
  module Raimei
8
6
  module NIM
9
7
  class Client
10
- def initialize(url:, api_key: nil, timeout: 60)
11
- @uri = URI(url) # e.g., http://localhost:11434/v1/chat/completions
12
- @api_key = api_key
13
- @timeout = timeout
8
+ def initialize(url:, api_key: nil, timeout: 60, retries: 2)
9
+ @uri = URI(url); @api_key = api_key; @timeout = timeout; @retries = retries
14
10
  end
15
11
 
16
12
  # messages: [{role:"user", content:"Hi"}]
17
- def chat(model:, messages:, **opts)
18
- body = { model: model, messages: messages, stream: false }.merge(opts)
13
+ # stream: true -> Enumerator (or yield chunks if block given)
14
+ # stream: false -> String
15
+ def chat(model:, messages:, stream: true, **opts, &blk)
16
+ body = { model: model, messages: messages, stream: stream }.merge(opts)
19
17
  req = Net::HTTP::Post.new(@uri)
20
18
  req["Content-Type"] = "application/json"
19
+ req["Accept"] = stream ? "text/event-stream" : "application/json"
21
20
  req["Authorization"] = "Bearer #{@api_key}" if @api_key
22
21
  req.body = JSON.dump(body)
23
22
 
24
- Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == "https", read_timeout: @timeout) do |http|
23
+ with_retries do
24
+ if stream
25
+ if block_given?
26
+ _sse(req) { |delta| blk.call(delta) }
27
+ else
28
+ Enumerator.new { |y| _sse(req) { |delta| y << delta } }
29
+ end
30
+ else
31
+ _json(req)
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def with_retries
39
+ attempts = 0
40
+ begin
41
+ return yield
42
+ rescue => e
43
+ attempts += 1
44
+ raise e if attempts > @retries
45
+ sleep(0.5 * attempts) # tiny backoff
46
+ retry
47
+ end
48
+ end
49
+
50
+ def _json(req)
51
+ Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme=="https", read_timeout:@timeout) do |http|
25
52
  res = http.request(req)
26
53
  raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
27
- json = JSON.parse(res.body)
28
- json.dig("choices", 0, "message", "content").to_s
54
+ j = JSON.parse(res.body)
55
+ j.dig("choices",0,"message","content").to_s
56
+ end
57
+ end
58
+
59
+ def _sse(req)
60
+ Net::HTTP.start(@uri.host, @uri.port, use_ssl:@uri.scheme=="https", read_timeout:@timeout) do |http|
61
+ http.request(req) do |res|
62
+ res.read_body do |chunk|
63
+ chunk.each_line do |line|
64
+ next unless line.start_with?("data:")
65
+ data = line.sub("data:","").strip
66
+ break if data == "[DONE]"
67
+ json = JSON.parse(data) rescue nil
68
+ next unless json
69
+ yield(json.dig("choices",0,"delta","content") || "")
70
+ end
71
+ end
72
+ end
29
73
  end
30
74
  end
31
75
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raimei-nim
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hal Fulton
@@ -9,17 +9,21 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: NVIDIA NIM / OpenAI-compatible client for Ruby (Raimei).
12
+ description: Streaming SSE client with timeouts/retries; works with NIM/TRT-LLM, vLLM,
13
+ Ollama, and other OpenAI-style servers.
13
14
  email:
14
15
  - rubyhacker@gmail.com
15
- executables: []
16
+ executables:
17
+ - raimei-chat
16
18
  extensions: []
17
19
  extra_rdoc_files: []
18
20
  files:
19
21
  - CHANGELOG.md
22
+ - Gemfile
20
23
  - LICENSE.txt
21
24
  - README.md
22
25
  - Rakefile
26
+ - exe/raimei-chat
23
27
  - lib/raimei/nim.rb
24
28
  - lib/raimei/nim/version.rb
25
29
  - sig/raimei/nim.rbs
@@ -36,7 +40,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
36
40
  requirements:
37
41
  - - ">="
38
42
  - !ruby/object:Gem::Version
39
- version: 3.2.0
43
+ version: '3.2'
40
44
  required_rubygems_version: !ruby/object:Gem::Requirement
41
45
  requirements:
42
46
  - - ">="
@@ -45,5 +49,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
49
  requirements: []
46
50
  rubygems_version: 3.7.1
47
51
  specification_version: 4
48
- summary: NVIDIA NIM / OpenAI-compatible client for Ruby (Raimei).
52
+ summary: OpenAI/NIM-compatible client for Ruby.
49
53
  test_files: []