llm.rb 15.0.2 → 15.1.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +161 -1
  3. data/README.md +134 -85
  4. data/bin/llm.rb +38 -3
  5. data/data/bedrock.json +250 -0
  6. data/data/deepinfra.json +162 -0
  7. data/data/deepseek.json +50 -0
  8. data/data/google.json +19 -21
  9. data/data/openrouter.json +14280 -0
  10. data/data/xai.json +28 -16
  11. data/docs/deepdive/advanced/context.md +8 -6
  12. data/docs/deepdive/fundamentals/agents.md +11 -10
  13. data/docs/deepdive/fundamentals/stream.md +4 -4
  14. data/lib/llm/active_record.rb +1 -1
  15. data/lib/llm/agent.rb +6 -0
  16. data/lib/llm/context.rb +28 -13
  17. data/lib/llm/cost.rb +13 -0
  18. data/lib/llm/function/fork/task.rb +14 -10
  19. data/lib/llm/provider.rb +29 -8
  20. data/lib/llm/providers/anthropic.rb +1 -1
  21. data/lib/llm/providers/bedrock/models.rb +2 -2
  22. data/lib/llm/providers/bedrock.rb +1 -1
  23. data/lib/llm/providers/google.rb +1 -1
  24. data/lib/llm/providers/ollama.rb +1 -1
  25. data/lib/llm/providers/openai/responses.rb +2 -1
  26. data/lib/llm/providers/openai.rb +1 -1
  27. data/lib/llm/providers/openrouter.rb +87 -0
  28. data/lib/llm/repl/buffer.rb +20 -5
  29. data/lib/llm/repl/input.rb +5 -4
  30. data/lib/llm/repl/markdown/table.rb +5 -2
  31. data/lib/llm/repl/node.rb +26 -1
  32. data/lib/llm/repl/stream.rb +30 -3
  33. data/lib/llm/repl/window.rb +7 -7
  34. data/lib/llm/repl.rb +1 -0
  35. data/lib/llm/skill.rb +7 -1
  36. data/lib/llm/stream.rb +8 -3
  37. data/lib/llm/tools/git.rb +2 -2
  38. data/lib/llm/tools/mkdir.rb +2 -2
  39. data/lib/llm/tools/rg.rb +2 -2
  40. data/lib/llm/tools/ruby.rb +2 -2
  41. data/lib/llm/tools/shell.rb +2 -2
  42. data/lib/llm/tools/utils.rb +1 -1
  43. data/lib/llm/transport/curb.rb +5 -3
  44. data/lib/llm/transport/http.rb +5 -2
  45. data/lib/llm/transport/persistent_http.rb +6 -4
  46. data/lib/llm/transport/utils.rb +8 -6
  47. data/lib/llm/version.rb +1 -1
  48. data/lib/llm.rb +16 -1
  49. data/llm.gemspec +2 -1
  50. metadata +19 -3
data/lib/llm/repl/node.rb CHANGED
@@ -6,6 +6,30 @@ class LLM::Repl
6
6
  # of text and optional curses attributes.
7
7
  # @api private
8
8
  class Node
9
+ ##
10
+ # Returns the display width of the given text, counting wide
11
+ # characters such as emoji as two columns.
12
+ # @param [#to_s] text
13
+ # @return [Integer]
14
+ def self.width(text)
15
+ Unicode::DisplayWidth.of(text.to_s)
16
+ end
17
+
18
+ ##
19
+ # Slices the given text so the returned piece has a display width
20
+ # of at most `width` columns.
21
+ # @param [#to_s] text
22
+ # @param [Integer] width
23
+ # @return [String]
24
+ def self.slice(text, width)
25
+ out = +""
26
+ text.to_s.each_char do |char|
27
+ break if self.width(out + char) > width
28
+ out << char
29
+ end
30
+ out
31
+ end
32
+
9
33
  ##
10
34
  # @return [String]
11
35
  attr_reader :text
@@ -24,9 +48,10 @@ class LLM::Repl
24
48
  end
25
49
 
26
50
  ##
51
+ # Returns the display width of the text
27
52
  # @return [Integer]
28
53
  def size
29
- @text.size
54
+ self.class.width(@text)
30
55
  end
31
56
  alias_method :length, :size
32
57
 
@@ -17,6 +17,7 @@ class LLM::Repl
17
17
  # @return [LLM::Repl::Stream]
18
18
  def initialize(repl, queue)
19
19
  @repl = repl
20
+ @agent = @repl.agent
20
21
  @_queue = queue
21
22
  @buffer = +""
22
23
  @tools = {}
@@ -39,6 +40,14 @@ class LLM::Repl
39
40
  @_queue.push [:status, [" ", lambda, " • #{tool.name}(#{format_args(tool)})"]]
40
41
  end
41
42
 
43
+ ##
44
+ # @param [Exception] ex
45
+ # @param [Integer] attempt
46
+ # @return [void]
47
+ def on_retry(ex, attempt)
48
+ @_queue.push [:status, retry_error(ex, attempt)]
49
+ end
50
+
42
51
  ##
43
52
  # @param [LLM::Function] tool
44
53
  # @param [LLM::Function::Return] result
@@ -56,12 +65,30 @@ class LLM::Repl
56
65
 
57
66
  private
58
67
 
68
+ ##
69
+ # @return [LLM::Agent]
70
+ def agent
71
+ @agent
72
+ end
73
+
59
74
  ##
60
75
  # @return [LLM::Repl::Node]
61
76
  def lambda
62
77
  Node.new("λ", Curses::A_BOLD | Color.red)
63
78
  end
64
79
 
80
+ ##
81
+ # @return [LLM::Repl::Node]
82
+ def retry_error(ex, attempt)
83
+ err = case ex.class.to_s
84
+ when "LLM::RateLimitError", "LLM::InsufficientQuotaError" then "Rate limited"
85
+ when "Net::ReadTimeout", "Net::OpenTimeout" then "Timed out"
86
+ else "#{ex.class}"
87
+ end
88
+ [Node.new(" 🔁 "),
89
+ Node.new("#{err} • attempt #{attempt} of #{agent.retry_budget}", Color.red | Curses::A_BOLD)]
90
+ end
91
+
65
92
  ##
66
93
  # Formats tool arguments as compact key: value pairs
67
94
  # suitable for the status line. Strings are quoted and
@@ -82,7 +109,7 @@ class LLM::Repl
82
109
  props = props.sort_by { tool.params.properties[_1].index }
83
110
  props = props.filter_map { args[_1] ? "#{_1}: #{format_value(args[_1])}" : nil }
84
111
  result = props.join(", ")
85
- result.size > max ? "#{result[0...max - 1]}…" : result
112
+ Node.width(result) > max ? "#{Node.slice(result, max - 1)}…" : result
86
113
  end
87
114
 
88
115
  ##
@@ -92,7 +119,7 @@ class LLM::Repl
92
119
  def format_value(value, max: 18)
93
120
  case value
94
121
  when String
95
- value.size > max ? "#{value[0...max]}…".inspect : value.inspect
122
+ Node.width(value) > max ? "#{Node.slice(value, max)}…".inspect : value.inspect
96
123
  when Array
97
124
  items = value.take(2).map { format_value(_1, max: 10) }
98
125
  items << "…" if value.size > 2
@@ -103,7 +130,7 @@ class LLM::Repl
103
130
  "nil"
104
131
  else
105
132
  str = value.inspect
106
- str.size > max ? "#{str[0...max]}…" : str
133
+ Node.width(str) > max ? "#{Node.slice(str, max)}…" : str
107
134
  end
108
135
  end
109
136
  end
@@ -140,11 +140,11 @@ class LLM::Repl
140
140
  remaining = buffer.width - width
141
141
  break if remaining <= 0
142
142
  text, attrs = chunk.text.to_s, chunk.attrs
143
- clipped = text[0, remaining]
143
+ clipped = Node.slice(text, remaining)
144
144
  Curses.attron(attrs) if attrs
145
145
  Curses.addstr(clipped)
146
146
  Curses.attroff(attrs) if attrs
147
- width += clipped.length
147
+ width += Node.width(clipped)
148
148
  end
149
149
  end
150
150
  last_drawn = offset + rows.size
@@ -163,10 +163,10 @@ class LLM::Repl
163
163
  Curses.clrtoeol
164
164
  status.nodes.each { addnode(_1) }
165
165
  context = status.context_bar
166
- Curses.setpos(y, [(columns - context.length) / 2, 0].max)
166
+ Curses.setpos(y, [(columns - Node.width(context)) / 2, 0].max)
167
167
  Curses.addstr(context)
168
168
  cost = status.cost.to_s
169
- Curses.setpos(y, [columns - cost.length, 0].max)
169
+ Curses.setpos(y, [columns - Node.width(cost), 0].max)
170
170
  Curses.addstr(cost)
171
171
  end
172
172
 
@@ -178,9 +178,9 @@ class LLM::Repl
178
178
  def draw_meta(offset:)
179
179
  y = Curses.lines - offset
180
180
  fill_row(y) do
181
- addnode(status.cwd, status.cwd.text[0, columns])
182
- Curses.setpos(y, [columns - status.model.text.size, 0].max)
183
- addnode(status.model, status.model.text[0, columns])
181
+ addnode(status.cwd, Node.slice(status.cwd.text, columns))
182
+ Curses.setpos(y, [columns - status.model.size, 0].max)
183
+ addnode(status.model, Node.slice(status.model.text, columns))
184
184
  end
185
185
  end
186
186
 
data/lib/llm/repl.rb CHANGED
@@ -14,6 +14,7 @@ module LLM
14
14
  class Repl
15
15
  LLM.require "curses", "~> 1.6"
16
16
  LLM.require "kramdown", "~> 2.5"
17
+ LLM.require "unicode/display_width", "~> 3.2"
17
18
 
18
19
  require_relative "repl/color"
19
20
  require_relative "repl/window"
data/lib/llm/skill.rb CHANGED
@@ -35,6 +35,11 @@ module LLM
35
35
  # @return [String]
36
36
  attr_reader :description
37
37
 
38
+ ##
39
+ # Returns the model the skill will use.
40
+ # @return [String, nil]
41
+ attr_reader :model
42
+
38
43
  ##
39
44
  # Returns the skill instructions.
40
45
  # @return [String]
@@ -90,7 +95,7 @@ module LLM
90
95
  stream = ctx.stream
91
96
  stream.on_skill_call(self)
92
97
  agent = self.agent(ctx)
93
- res = agent.talk("Solve the user's query.")
98
+ res = agent.talk("Solve the user's query.", model: model || ctx.model)
94
99
  stream.on_skill_return(agent, self, res)
95
100
  {content: res.content}
96
101
  end
@@ -156,6 +161,7 @@ module LLM
156
161
  @frontmatter = LLM::Object.from(YAML.safe_load(match[1]) || {})
157
162
  @name = @frontmatter.name || @name
158
163
  @description = @frontmatter.description || @description
164
+ @model = @frontmatter.model || nil
159
165
  @instructions = match[2]
160
166
  @inherit_tools, @tools = parse_tools(@frontmatter.tools)
161
167
  end
data/lib/llm/stream.rb CHANGED
@@ -185,12 +185,17 @@ module LLM
185
185
  end
186
186
 
187
187
  ##
188
- # Called when a request is rate limited and will be retried.
189
- # @param [LLM::RateLimitError] error
188
+ # Called when a request is retried because it was rate limited or
189
+ # timed out.
190
+ # @param [Exception] error
191
+ # The error that caused the retry
192
+ # @param [Integer] attempt
193
+ # The one-based retry attempt number
190
194
  # @return [nil]
191
- def on_rate_limit(error)
195
+ def on_retry(error, attempt)
192
196
  nil
193
197
  end
198
+ alias_method :on_rate_limit, :on_retry
194
199
 
195
200
  ##
196
201
  # Called before a skill's subagent runs.
data/lib/llm/tools/git.rb CHANGED
@@ -41,7 +41,7 @@ class LLM::Tool
41
41
  .spawn
42
42
  end
43
43
 
44
- LLM.require "test-cmd.rb", "~> 1.1"
45
- Command = Test::Cmd
44
+ LLM.require "test-cmd.rb", "~> 2.1"
45
+ Command = Test::Command
46
46
  end
47
47
  end
@@ -29,7 +29,7 @@ class LLM::Tool
29
29
  .spawn
30
30
  end
31
31
 
32
- LLM.require "test-cmd.rb", "~> 1.1"
33
- Command = Test::Cmd
32
+ LLM.require "test-cmd.rb", "~> 2.1"
33
+ Command = Test::Command
34
34
  end
35
35
  end
data/lib/llm/tools/rg.rb CHANGED
@@ -49,7 +49,7 @@ class LLM::Tool
49
49
  .spawn
50
50
  end
51
51
 
52
- LLM.require "test-cmd.rb", "~> 1.1"
53
- Command = Test::Cmd
52
+ LLM.require "test-cmd.rb", "~> 2.1"
53
+ Command = Test::Command
54
54
  end
55
55
  end
@@ -40,7 +40,7 @@ class LLM::Tool
40
40
  end
41
41
 
42
42
  require "rbconfig"
43
- LLM.require "test-cmd.rb", "~> 1.1"
44
- Command = Test::Cmd
43
+ LLM.require "test-cmd.rb", "~> 2.1"
44
+ Command = Test::Command
45
45
  end
46
46
  end
@@ -49,7 +49,7 @@ class LLM::Tool
49
49
  .spawn
50
50
  end
51
51
 
52
- LLM.require "test-cmd.rb", "~> 1.1"
53
- Command = Test::Cmd
52
+ LLM.require "test-cmd.rb", "~> 2.1"
53
+ Command = Test::Command
54
54
  end
55
55
  end
@@ -8,7 +8,7 @@ class LLM::Tool
8
8
  # Wait for a command to finish, or abort
9
9
  # with an error when it exceeds the
10
10
  # specified timeout.
11
- # @param [Test::Cmd] command
11
+ # @param [Test::Command] command
12
12
  # @param [Integer] timeout
13
13
  # @return [void]
14
14
  def wait(command:, timeout:)
@@ -28,12 +28,14 @@ class LLM::Transport
28
28
  # @param [String] host
29
29
  # @param [Integer] port
30
30
  # @param [Integer] timeout
31
+ # @param [Integer] connect_timeout
31
32
  # @param [Boolean] ssl
32
33
  # @return [LLM::Transport::Curb]
33
- def initialize(host:, port:, timeout:, ssl:)
34
+ def initialize(host:, port:, timeout:, connect_timeout:, ssl:)
34
35
  @host = host
35
36
  @port = port
36
37
  @timeout = timeout
38
+ @connect_timeout = connect_timeout
37
39
  @ssl = ssl
38
40
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
39
41
  @monitor = Monitor.new
@@ -112,13 +114,13 @@ class LLM::Transport
112
114
 
113
115
  private
114
116
 
115
- attr_reader :host, :port, :timeout, :ssl, :base_uri
117
+ attr_reader :host, :port, :timeout, :connect_timeout, :ssl, :base_uri
116
118
 
117
119
  def build_easy(request)
118
120
  LLM.require "curb" unless defined?(::Curl)
119
121
  easy = ::Curl::Easy.new(request_url(request))
120
122
  easy.timeout = timeout
121
- easy.connect_timeout = timeout
123
+ easy.connect_timeout = connect_timeout
122
124
  request.headers.each { |k, v| easy.headers[k] = v }
123
125
  easy.follow_location = true
124
126
  easy.ssl_verify_peer = false if !ssl
@@ -20,12 +20,14 @@ class LLM::Transport
20
20
  # @param [String] host
21
21
  # @param [Integer] port
22
22
  # @param [Integer] timeout
23
+ # @param [Integer] connect_timeout
23
24
  # @param [Boolean] ssl
24
25
  # @return [LLM::Transport::HTTP]
25
- def initialize(host:, port:, timeout:, ssl:)
26
+ def initialize(host:, port:, timeout:, connect_timeout:, ssl:)
26
27
  @host = host
27
28
  @port = port
28
29
  @timeout = timeout
30
+ @connect_timeout = connect_timeout
29
31
  @ssl = ssl
30
32
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
31
33
  @monitor = Monitor.new
@@ -93,10 +95,11 @@ class LLM::Transport
93
95
 
94
96
  private
95
97
 
96
- attr_reader :host, :port, :timeout, :ssl, :base_uri
98
+ attr_reader :host, :port, :timeout, :connect_timeout, :ssl, :base_uri
97
99
 
98
100
  def client
99
101
  client = Net::HTTP.new(host, port)
102
+ client.open_timeout = connect_timeout
100
103
  client.read_timeout = timeout
101
104
  client.use_ssl = ssl
102
105
  client
@@ -32,12 +32,14 @@ class LLM::Transport
32
32
  # @param [String] host
33
33
  # @param [Integer] port
34
34
  # @param [Integer] timeout
35
+ # @param [Integer] connect_timeout
35
36
  # @param [Boolean] ssl
36
37
  # @return [LLM::Transport::PersistentHTTP]
37
- def initialize(host:, port:, timeout:, ssl:)
38
+ def initialize(host:, port:, timeout:, connect_timeout:, ssl:)
38
39
  @host = host
39
40
  @port = port
40
41
  @timeout = timeout
42
+ @connect_timeout = connect_timeout
41
43
  @ssl = ssl
42
44
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
43
45
  @monitor = Monitor.new
@@ -100,7 +102,7 @@ class LLM::Transport
100
102
 
101
103
  private
102
104
 
103
- attr_reader :host, :port, :timeout, :ssl, :base_uri
105
+ attr_reader :host, :port, :timeout, :connect_timeout, :ssl, :base_uri
104
106
 
105
107
  def client
106
108
  self.class.lock do
@@ -110,14 +112,14 @@ class LLM::Transport
110
112
  LLM.require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
111
113
  client = Net::HTTP::Persistent.new(name: self.class.name)
112
114
  client.read_timeout = timeout
113
- client.open_timeout = timeout
115
+ client.open_timeout = connect_timeout
114
116
  self.class.registry[client_id] = client
115
117
  end
116
118
  end
117
119
  end
118
120
 
119
121
  def client_id
120
- "#{host}:#{port}:#{timeout}:#{ssl}"
122
+ "#{host}:#{port}:#{timeout}:#{connect_timeout}:#{ssl}"
121
123
  end
122
124
 
123
125
  def close_socket(http)
@@ -24,18 +24,19 @@ class LLM::Transport
24
24
  # @param [String] host
25
25
  # @param [Integer] port
26
26
  # @param [Integer, nil] timeout
27
+ # @param [Integer] connect_timeout
27
28
  # @param [Boolean] ssl
28
29
  # @param [Boolean] persistent
29
30
  # @param [LLM::Transport, Class, Symbol, nil] transport
30
31
  # @return [LLM::Transport]
31
- def resolve_transport(host:, port:, timeout:, ssl:, persistent:, transport:)
32
+ def resolve_transport(host:, port:, timeout:, connect_timeout: 5, ssl:, persistent:, transport:)
32
33
  if transport.nil?
33
- default_transport(host:, port:, timeout:, ssl:, persistent:)
34
+ default_transport(host:, port:, timeout:, connect_timeout:, ssl:, persistent:)
34
35
  elsif Class === transport && transport <= LLM::Transport
35
- transport.new(host:, port:, timeout:, ssl:)
36
+ transport.new(host:, port:, timeout:, connect_timeout:, ssl:)
36
37
  elsif Symbol === transport
37
38
  transport = LLM::Transport.public_send(transport)
38
- transport.new(host:, port:, timeout:, ssl:)
39
+ transport.new(host:, port:, timeout:, connect_timeout:, ssl:)
39
40
  else
40
41
  transport
41
42
  end
@@ -49,12 +50,13 @@ class LLM::Transport
49
50
  # @param [String] host
50
51
  # @param [Integer] port
51
52
  # @param [Integer, nil] timeout
53
+ # @param [Integer] connect_timeout
52
54
  # @param [Boolean] ssl
53
55
  # @param [Boolean] persistent
54
56
  # @return [LLM::Transport]
55
- def default_transport(host:, port:, timeout:, ssl:, persistent:)
57
+ def default_transport(host:, port:, timeout:, connect_timeout:, ssl:, persistent:)
56
58
  target = persistent ? LLM::Transport::PersistentHTTP : LLM::Transport::HTTP
57
- target.new(host:, port:, timeout:, ssl:)
59
+ target.new(host:, port:, timeout:, connect_timeout:, ssl:)
58
60
  end
59
61
  end
60
62
  end
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "15.0.2"
4
+ VERSION = "15.1.0"
5
5
  end
data/lib/llm.rb CHANGED
@@ -77,7 +77,9 @@ module LLM
77
77
  # @raise [LLM::LoadError]
78
78
  # When the dependency cannot be loaded
79
79
  def self.require(name, version = nil)
80
- names = {"xchan" => "xchan.rb", "net/http/persistent" => "net-http-persistent"}
80
+ names = { "xchan" => "xchan.rb",
81
+ "net/http/persistent" => "net-http-persistent",
82
+ "unicode/display_width" => "unicode-display_width" }
81
83
  gem(names[name] || name, version) if version
82
84
  super(name)
83
85
  rescue ::LoadError
@@ -260,6 +262,19 @@ module LLM
260
262
  end
261
263
  end
262
264
 
265
+ ##
266
+ # @param key (see LLM::OpenRouter#initialize)
267
+ # @param host (see LLM::OpenRouter#initialize)
268
+ # @return (see LLM::OpenRouter#initialize)
269
+ def openrouter(key: UNDEFINED, **)
270
+ lock(:require) { require_relative "llm/providers/openrouter" unless defined?(LLM::OpenRouter) }
271
+ if key == UNDEFINED
272
+ LLM::OpenRouter.new(key: key(name: __method__), **)
273
+ else
274
+ LLM::OpenRouter.new(key:, **)
275
+ end
276
+ end
277
+
263
278
  ##
264
279
  # @param key (see LLM::Alibaba#initialize)
265
280
  # @param host (see LLM::Alibaba#initialize)
data/llm.gemspec CHANGED
@@ -53,7 +53,7 @@ DESCRIPTION
53
53
  spec.add_development_dependency "yard", "~> 0.9.37"
54
54
  spec.add_development_dependency "redcarpet", "~> 3.6"
55
55
  spec.add_development_dependency "webrick", "~> 1.8"
56
- spec.add_development_dependency "test-cmd.rb", "~> 1.1.0"
56
+ spec.add_development_dependency "test-cmd.rb", "~> 2.1"
57
57
  spec.add_development_dependency "rake", "~> 13.0"
58
58
  spec.add_development_dependency "rspec", "~> 3.0"
59
59
  spec.add_development_dependency "standard", "~> 1.50"
@@ -71,4 +71,5 @@ DESCRIPTION
71
71
  spec.add_development_dependency "curb", "~> 1.3"
72
72
  spec.add_development_dependency "curses", "~> 1.6"
73
73
  spec.add_development_dependency "kramdown", "~> 2.5"
74
+ spec.add_development_dependency "unicode-display_width", "~> 3.2"
74
75
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 15.0.2
4
+ version: 15.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Gleeson
@@ -73,14 +73,14 @@ dependencies:
73
73
  requirements:
74
74
  - - "~>"
75
75
  - !ruby/object:Gem::Version
76
- version: 1.1.0
76
+ version: '2.1'
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
- version: 1.1.0
83
+ version: '2.1'
84
84
  - !ruby/object:Gem::Dependency
85
85
  name: rake
86
86
  requirement: !ruby/object:Gem::Requirement
@@ -319,6 +319,20 @@ dependencies:
319
319
  - - "~>"
320
320
  - !ruby/object:Gem::Version
321
321
  version: '2.5'
322
+ - !ruby/object:Gem::Dependency
323
+ name: unicode-display_width
324
+ requirement: !ruby/object:Gem::Requirement
325
+ requirements:
326
+ - - "~>"
327
+ - !ruby/object:Gem::Version
328
+ version: '3.2'
329
+ type: :development
330
+ prerelease: false
331
+ version_requirements: !ruby/object:Gem::Requirement
332
+ requirements:
333
+ - - "~>"
334
+ - !ruby/object:Gem::Version
335
+ version: '3.2'
322
336
  description: |
323
337
  llm.rb is an advanced runtime for building agentic AI applications on CRuby.
324
338
  It has zero runtime dependencies by default, it supports concurrent and parallel
@@ -346,6 +360,7 @@ files:
346
360
  - data/mistral.json
347
361
  - data/moonshot.json
348
362
  - data/openai.json
363
+ - data/openrouter.json
349
364
  - data/xai.json
350
365
  - data/zai.json
351
366
  - docs/deepdive.md
@@ -551,6 +566,7 @@ files:
551
566
  - lib/llm/providers/openai/schema.rb
552
567
  - lib/llm/providers/openai/stream_parser.rb
553
568
  - lib/llm/providers/openai/vector_stores.rb
569
+ - lib/llm/providers/openrouter.rb
554
570
  - lib/llm/providers/xai.rb
555
571
  - lib/llm/providers/xai/images.rb
556
572
  - lib/llm/providers/zai.rb