raimei-nim 0.1.1 → 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: 34cd5b478c9e975b72d433101f62c1cf5949097b844dfaa3bbbd8b1512803263
4
- data.tar.gz: bd9081697d14c34807ece3d9e50beebcb9da994a7660d9617d165fa60655cabb
3
+ metadata.gz: abc44189bf4b4c7c97e31627d738bcadfb4a74b216f434e86e03a3e0b7b59b62
4
+ data.tar.gz: 7a042805a56694d8d2a2a917c9094cd2c79ac5986b0afc0e694265f8b5e376d5
5
5
  SHA512:
6
- metadata.gz: d3c9e767389ab1856d05285067ed38f459a61d6ffa736e8727b69b36651909e3f6bbda27eeacaf6daa2c8f8794e3d7a6c803d6dc9bff6a2cb6e5a44b05af795f
7
- data.tar.gz: d4e21bcce8052155ff694636547c9ab79cdc418da4635d1f6b66935e44a8dc9ca6855da04f29ceee727c788decdea7ff2a26eb6b59d32213837b67f2d231dcc5
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/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.1"
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.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hal Fulton
@@ -13,14 +13,17 @@ description: Streaming SSE client with timeouts/retries; works with NIM/TRT-LLM,
13
13
  Ollama, and other OpenAI-style servers.
14
14
  email:
15
15
  - rubyhacker@gmail.com
16
- executables: []
16
+ executables:
17
+ - raimei-chat
17
18
  extensions: []
18
19
  extra_rdoc_files: []
19
20
  files:
20
21
  - CHANGELOG.md
22
+ - Gemfile
21
23
  - LICENSE.txt
22
24
  - README.md
23
25
  - Rakefile
26
+ - exe/raimei-chat
24
27
  - lib/raimei/nim.rb
25
28
  - lib/raimei/nim/version.rb
26
29
  - sig/raimei/nim.rbs