llm.rb 12.0.0 → 12.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.
data/data/zai.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "glm-4.7": {
12
12
  "id": "glm-4.7",
13
13
  "name": "GLM-4.7",
14
+ "description": "Mature GLM model for dependable coding, reasoning, and structured agent tasks",
14
15
  "family": "glm",
15
16
  "attachment": false,
16
17
  "reasoning": true,
@@ -50,6 +51,7 @@
50
51
  "glm-4.5v": {
51
52
  "id": "glm-4.5v",
52
53
  "name": "GLM-4.5V",
54
+ "description": "GLM vision model for visual reasoning, documents, and multimodal agents",
53
55
  "family": "glm",
54
56
  "attachment": true,
55
57
  "reasoning": true,
@@ -86,6 +88,7 @@
86
88
  "glm-4.5": {
87
89
  "id": "glm-4.5",
88
90
  "name": "GLM-4.5",
91
+ "description": "Hybrid-reasoning GLM release that made the 4.5 line broadly useful",
89
92
  "family": "glm",
90
93
  "attachment": false,
91
94
  "reasoning": true,
@@ -122,6 +125,7 @@
122
125
  "glm-4.7-flashx": {
123
126
  "id": "glm-4.7-flashx",
124
127
  "name": "GLM-4.7-FlashX",
128
+ "description": "Efficient GLM model for fast reasoning, coding, and agent workflows",
125
129
  "family": "glm-flash",
126
130
  "attachment": false,
127
131
  "reasoning": true,
@@ -158,6 +162,7 @@
158
162
  "glm-5.1": {
159
163
  "id": "glm-5.1",
160
164
  "name": "GLM-5.1",
165
+ "description": "Strong GLM coding model for agentic engineering, terminals, and repository generation",
161
166
  "family": "glm",
162
167
  "attachment": false,
163
168
  "reasoning": true,
@@ -197,6 +202,7 @@
197
202
  "glm-4.6": {
198
203
  "id": "glm-4.6",
199
204
  "name": "GLM-4.6",
205
+ "description": "Late GLM-4 workhorse for coding agents, reasoning, and structured tasks",
200
206
  "family": "glm",
201
207
  "attachment": false,
202
208
  "reasoning": true,
@@ -233,6 +239,7 @@
233
239
  "glm-5.2": {
234
240
  "id": "glm-5.2",
235
241
  "name": "GLM-5.2",
242
+ "description": "Open flagship GLM for long-horizon coding agents and million-token context work",
236
243
  "family": "glm",
237
244
  "attachment": false,
238
245
  "reasoning": true,
@@ -276,6 +283,7 @@
276
283
  "glm-4.6v": {
277
284
  "id": "glm-4.6v",
278
285
  "name": "GLM-4.6V",
286
+ "description": "GLM vision model for visual reasoning, documents, and multimodal agents",
279
287
  "family": "glm",
280
288
  "attachment": true,
281
289
  "reasoning": true,
@@ -312,6 +320,7 @@
312
320
  "glm-5v-turbo": {
313
321
  "id": "glm-5v-turbo",
314
322
  "name": "GLM-5V-Turbo",
323
+ "description": "Fast GLM vision model for screenshots, documents, and multimodal agent tasks",
315
324
  "family": "glm",
316
325
  "attachment": true,
317
326
  "reasoning": true,
@@ -353,6 +362,7 @@
353
362
  "glm-4.5-air": {
354
363
  "id": "glm-4.5-air",
355
364
  "name": "GLM-4.5-Air",
365
+ "description": "Lighter GLM-4.5 variant for fast coding assistance and cheaper agents",
356
366
  "family": "glm-air",
357
367
  "attachment": false,
358
368
  "reasoning": true,
@@ -389,6 +399,7 @@
389
399
  "glm-4.7-flash": {
390
400
  "id": "glm-4.7-flash",
391
401
  "name": "GLM-4.7-Flash",
402
+ "description": "Budget GLM lane for fast coding help, routing, and everyday automation",
392
403
  "family": "glm-flash",
393
404
  "attachment": false,
394
405
  "reasoning": true,
@@ -425,6 +436,7 @@
425
436
  "glm-4.5-flash": {
426
437
  "id": "glm-4.5-flash",
427
438
  "name": "GLM-4.5-Flash",
439
+ "description": "Efficient GLM model for fast reasoning, coding, and agent workflows",
428
440
  "family": "glm-flash",
429
441
  "attachment": false,
430
442
  "reasoning": true,
@@ -461,6 +473,7 @@
461
473
  "glm-5": {
462
474
  "id": "glm-5",
463
475
  "name": "GLM-5",
476
+ "description": "General GLM flagship for coding, analysis, and tool-heavy engineering workflows",
464
477
  "family": "glm",
465
478
  "attachment": false,
466
479
  "reasoning": true,
@@ -499,6 +512,7 @@
499
512
  "glm-5-turbo": {
500
513
  "id": "glm-5-turbo",
501
514
  "name": "GLM-5-Turbo",
515
+ "description": "Faster GLM-5 lane for coding agents that need lower latency",
502
516
  "family": "glm",
503
517
  "attachment": false,
504
518
  "reasoning": true,
data/lib/llm/agent.rb CHANGED
@@ -61,7 +61,11 @@ module LLM
61
61
  # Returns the current tools when no argument is provided
62
62
  def self.tools(*tools, &block)
63
63
  return @tools || [] if tools.empty? && !block
64
- @tools = block || tools.flatten
64
+ if tools.size == 1 and tools.grep(Symbol).any?
65
+ @tools = tools.first
66
+ else
67
+ @tools = block || tools.flatten
68
+ end
65
69
  end
66
70
 
67
71
  ##
@@ -334,6 +338,15 @@ module LLM
334
338
  @tracer || @ctx.tracer
335
339
  end
336
340
 
341
+ ##
342
+ # @param [LLM::Tracer, nil] other
343
+ # A tracer, or nil.
344
+ # @return [void]
345
+ def tracer=(other)
346
+ @ctx.tracer = other
347
+ @tracer = other
348
+ end
349
+
337
350
  ##
338
351
  # @return [LLM::Stream, #<<, nil]
339
352
  # Returns a stream object, or nil
@@ -375,6 +388,17 @@ module LLM
375
388
  @ctx.context_window
376
389
  end
377
390
 
391
+ ##
392
+ # Start a minimalist repl that can interact
393
+ # with the agent and its current state. This
394
+ # method requires the 'curses' gem to be installed
395
+ # and available to require.
396
+ # @return [void]
397
+ def repl
398
+ require_relative "repl" unless defined?(::LLM::Repl)
399
+ LLM::Repl.new(self).start
400
+ end
401
+
378
402
  ##
379
403
  # @see LLM::Context#params
380
404
  # @return [Hash]
data/lib/llm/context.rb CHANGED
@@ -421,6 +421,14 @@ module LLM
421
421
  @llm.tracer
422
422
  end
423
423
 
424
+ ##
425
+ # @param [LLM::Tracer, nil] other
426
+ # A tracer, or nil.
427
+ # @return [void]
428
+ def tracer=(other)
429
+ @llm.tracer = nil
430
+ end
431
+
424
432
  ##
425
433
  # @return [LLM::Stream, #<<, nil]
426
434
  # Returns a stream object, or nil
@@ -38,11 +38,11 @@ module LLM
38
38
  def self.dump(obj, state = nil, **options)
39
39
  require "json" unless defined?(::JSON)
40
40
  if ::JSON::State === state
41
- ::JSON.generate(obj, state)
41
+ ::JSON.generate(normalize(obj), state)
42
42
  elsif state
43
- ::JSON.dump(obj, state, **options)
43
+ ::JSON.dump(normalize(obj), state, **options)
44
44
  else
45
- ::JSON.dump(obj, **options)
45
+ ::JSON.dump(normalize(obj), **options)
46
46
  end
47
47
  end
48
48
 
@@ -59,6 +59,32 @@ module LLM
59
59
  require "json" unless defined?(::JSON)
60
60
  [::JSON::ParserError]
61
61
  end
62
+
63
+ ##
64
+ # JSON 3.0 compat
65
+ # Walks `obj` and encodes every string that is
66
+ # found into a UTF-8 compatible string.
67
+ def self.normalize(obj)
68
+ case obj
69
+ when String then normalize_string(obj)
70
+ when Array then obj.map { normalize(_1) }
71
+ when Hash then obj.map { [_1, normalize(_2)] }.to_h
72
+ when LLM::Object then obj.map { [_1, normalize(_2)] }.to_h
73
+ else obj
74
+ end
75
+ end
76
+ private_class_method :normalize
77
+
78
+ ##
79
+ # JSON 3.0 compat
80
+ # Normalizes a string as a UTF-8 encoded string
81
+ # that's compatible with the JSON spec.
82
+ def self.normalize_string(str)
83
+ return str if str.encoding == Encoding::UTF_8
84
+ str = (+str).force_encoding("UTF-8")
85
+ str.valid_encoding? ? str : str.scrub
86
+ end
87
+ private_class_method :normalize_string
62
88
  end
63
89
 
64
90
  ##
data/lib/llm/provider.rb CHANGED
@@ -286,7 +286,7 @@ class LLM::Provider
286
286
  # A tracer
287
287
  # @return [void]
288
288
  def tracer=(tracer)
289
- @tracer = tracer
289
+ @tracer = tracer || LLM::Tracer::Null.new(self)
290
290
  end
291
291
 
292
292
  ##
@@ -23,7 +23,7 @@ module LLM
23
23
  ##
24
24
  # @param (see LLM::Provider#initialize)
25
25
  # @return [LLM::LlamaCpp]
26
- def initialize(host: "localhost", port: 8080, ssl: false, **)
26
+ def initialize(host: "localhost", port: 8013, ssl: false, **)
27
27
  super
28
28
  end
29
29
 
@@ -71,11 +71,11 @@ module LLM
71
71
  end
72
72
 
73
73
  ##
74
- # Returns the default model for chat completions
75
- # @see https://ollama.com/library/qwen3 qwen3
76
- # @return [String]
74
+ # Returns nil.
75
+ # Whatever model is served by llamacpp acts as the default.
76
+ # @return [nil]
77
77
  def default_model
78
- "qwen3"
78
+ nil
79
79
  end
80
80
  end
81
81
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Repl
4
+ ##
5
+ # The {LLM::Repl::Input LLM::Repl::Input} class manages
6
+ # the editable input line shown at the bottom of the REPL.
7
+ # @api private
8
+ class Input
9
+ UP = Curses::Key::UP
10
+ DOWN = Curses::Key::DOWN
11
+ ENTER = [Curses::Key::ENTER, 10, 13]
12
+ BACKSPACE = [Curses::Key::BACKSPACE, 127]
13
+ EOF = [nil, 4]
14
+
15
+ ##
16
+ # @param [String, Symbol] provider
17
+ # @return [LLM::Repl::Input]
18
+ def initialize(provider)
19
+ @provider = provider
20
+ @buffer = +""
21
+ end
22
+
23
+ ##
24
+ # @param [LLM::Repl::Window] window
25
+ # @return [String, nil]
26
+ def readline(window)
27
+ catch(:done) do
28
+ @buffer.clear
29
+ loop do
30
+ on_char(window, window.getch)
31
+ window.redraw
32
+ end
33
+ end
34
+ end
35
+
36
+ ##
37
+ # @return [String]
38
+ def to_s
39
+ "> #{@buffer}"
40
+ end
41
+
42
+ private
43
+
44
+ def on_char(window, char)
45
+ if EOF.include?(char)
46
+ throw(:done, nil)
47
+ elsif BACKSPACE.include?(char)
48
+ @buffer.chop!
49
+ elsif ENTER.include?(char)
50
+ buf = @buffer.dup
51
+ @buffer.clear
52
+ throw(:done, buf)
53
+ elsif char == UP
54
+ window.scroll_up
55
+ elsif char == DOWN
56
+ window.scroll_down
57
+ elsif String === char
58
+ @buffer << char
59
+ else
60
+ # ???
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Repl
4
+ ##
5
+ # The {LLM::Repl::Status LLM::Repl::Status} class stores
6
+ # the small status line shown at the top of the REPL.
7
+ # @api private
8
+ class Status
9
+ ##
10
+ # @param [String, Symbol] provider
11
+ # @return [LLM::Repl::Status]
12
+ def initialize(provider)
13
+ @provider = provider
14
+ @text = "idle"
15
+ end
16
+
17
+ ##
18
+ # @return [String]
19
+ attr_reader :provider
20
+
21
+ ##
22
+ # @param [String] value
23
+ # @return [void]
24
+ attr_accessor :text
25
+
26
+ ##
27
+ # @return [String]
28
+ alias_method :to_s, :text
29
+ end
30
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Repl
4
+ ##
5
+ # The {LLM::Repl::Stream LLM::Repl::Stream} class manages
6
+ # the stream for the {LLM::Repl LLM::Repl} class. This class
7
+ # has defined hooks that receive text, tool calls, and
8
+ # tool returns.
9
+ # @api private
10
+ class Stream < LLM::Stream
11
+ ##
12
+ # @param [LLM::Repl] repl
13
+ # @return [LLM::Repl::Stream]
14
+ def initialize(repl)
15
+ @repl = repl
16
+ end
17
+
18
+ ##
19
+ # @param [String] chars
20
+ # One or more chars
21
+ # @return [void]
22
+ def on_content(chars)
23
+ @repl.write(chars)
24
+ end
25
+
26
+ ##
27
+ # @param [LLM::Function] tool
28
+ # @param [LLM::Function::Return, nil] error
29
+ # @return [void]
30
+ def on_tool_call(tool, error)
31
+ if error
32
+ @repl.status = "tool error: #{tool.name}"
33
+ else
34
+ @repl.status = "tool: #{tool.name}"
35
+ end
36
+ end
37
+
38
+ ##
39
+ # @param [LLM::Function] _tool
40
+ # @param [LLM::Function::Return] result
41
+ # @return [void]
42
+ def on_tool_return(_tool, result)
43
+ @repl.status = "tool done: #{result.name}"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Repl
4
+ ##
5
+ # The {LLM::Repl::Transcript LLM::Repl::Transcript} class
6
+ # stores streamed output for the REPL.
7
+ # @api private
8
+ class Transcript
9
+ WIDTH = 80
10
+
11
+ ##
12
+ # @return [LLM::Repl::Transcript]
13
+ def initialize
14
+ @lines = [+""]
15
+ @offset = 0
16
+ end
17
+
18
+ ##
19
+ # @param [String] chars
20
+ # @return [void]
21
+ def write(chars)
22
+ chars.each_char { write_char(_1) }
23
+ end
24
+
25
+ ##
26
+ # @return [void]
27
+ def scroll_up(height)
28
+ max = [@lines.size - height, 0].max
29
+ @offset = [@offset + 1, max].min
30
+ end
31
+
32
+ ##
33
+ # @return [void]
34
+ def scroll_down
35
+ @offset = [@offset - 1, 0].max
36
+ end
37
+
38
+ ##
39
+ # @param [Integer] height
40
+ # @return [Array<String>]
41
+ def visible(height)
42
+ last = @lines.size - 1 - @offset
43
+ first = [last - height + 1, 0].max
44
+ @lines[first..last] || []
45
+ end
46
+
47
+ private
48
+
49
+ def write_char(char)
50
+ if char == "\n"
51
+ @offset += 1 if @offset > 0
52
+ @lines << +""
53
+ elsif char == " " and @lines.last.length >= WIDTH
54
+ @offset += 1 if @offset > 0
55
+ @lines << +""
56
+ else
57
+ @lines.last << char
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Repl
4
+ ##
5
+ # The {LLM::Repl::Window LLM::Repl::Window} class draws the
6
+ # curses screen for the REPL.
7
+ # @api private
8
+ class Window
9
+ ##
10
+ # @return [LLM::Repl::Status]
11
+ attr_reader :status
12
+
13
+ ##
14
+ # @return [LLM::Repl::Transcript]
15
+ attr_reader :transcript
16
+
17
+ ##
18
+ # @return [LLM::Repl::Input]
19
+ attr_reader :input
20
+
21
+ ##
22
+ # @param [LLM::Repl::Status] status
23
+ # @param [LLM::Repl::Transcript] transcript
24
+ # @param [LLM::Repl::Input] input
25
+ # @return [LLM::Repl::Window]
26
+ def initialize(status, transcript, input)
27
+ @status = status
28
+ @transcript = transcript
29
+ @input = input
30
+ end
31
+
32
+ ##
33
+ # @yield
34
+ # @return [void]
35
+ def open
36
+ Curses.init_screen
37
+ Curses.cbreak
38
+ Curses.noecho
39
+ Curses.stdscr.keypad(true)
40
+ yield
41
+ ensure
42
+ Curses.close_screen
43
+ end
44
+
45
+ ##
46
+ # @return [void]
47
+ def redraw
48
+ Curses.clear
49
+ draw_status
50
+ draw_transcript
51
+ draw_input
52
+ Curses.refresh
53
+ end
54
+
55
+ ##
56
+ # @return [Integer]
57
+ def rows
58
+ [Curses.lines - 3, 1].max
59
+ end
60
+
61
+ ##
62
+ # @return [Object]
63
+ def getch
64
+ Curses.getch
65
+ end
66
+
67
+ ##
68
+ # @return [void]
69
+ def scroll_up
70
+ transcript.scroll_up(rows)
71
+ end
72
+
73
+ ##
74
+ # @return [void]
75
+ def scroll_down
76
+ transcript.scroll_down
77
+ end
78
+
79
+ private
80
+
81
+ def draw_status
82
+ Curses.setpos(0, 0)
83
+ Curses.addstr(status.to_s)
84
+ provider = status.provider.to_s
85
+ Curses.setpos(0, [columns - provider.length, 0].max)
86
+ Curses.addstr(provider)
87
+ end
88
+
89
+ def draw_transcript
90
+ visible = transcript.visible(rows)
91
+ visible.each_with_index do |row, index|
92
+ Curses.setpos(index + 2, 0)
93
+ Curses.addstr(row)
94
+ end
95
+ end
96
+
97
+ def draw_input
98
+ Curses.setpos(Curses.lines - 1, 0)
99
+ Curses.clrtoeol
100
+ Curses.addstr(input.to_s)
101
+ end
102
+
103
+ def columns
104
+ Curses.cols
105
+ end
106
+ end
107
+ end
data/lib/llm/repl.rb ADDED
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ LLM.require "curses"
4
+
5
+ module LLM
6
+ ##
7
+ # The {LLM::Repl LLM::Repl} class provides a small
8
+ # read-eval-print loop around an instance of
9
+ # {LLM::Agent LLM::Agent}.
10
+ #
11
+ # It can be used to keep talking to an agent after it
12
+ # has been set up or has performed a task. This can be
13
+ # useful when you want to confirm the agent handled the
14
+ # task correctly, or for it to correct course after a
15
+ # mistake was made.
16
+ class Repl
17
+ require_relative "repl/window"
18
+ require_relative "repl/status"
19
+ require_relative "repl/transcript"
20
+ require_relative "repl/input"
21
+ require_relative "repl/stream"
22
+
23
+ ##
24
+ # @param [LLM::Agent] agent
25
+ # @return [LLM::Repl]
26
+ def initialize(agent)
27
+ @agent = agent
28
+ @provider = agent.llm.name
29
+ @status = Status.new(@provider)
30
+ @transcript = Transcript.new
31
+ @input = Input.new(@provider)
32
+ @window = Window.new(@status, @transcript, @input)
33
+ @stream = Stream.new(self)
34
+ end
35
+
36
+ ##
37
+ # @return [void]
38
+ def start
39
+ window.open do
40
+ loop do
41
+ window.redraw
42
+ text = input.readline(window)
43
+ break if text.nil?
44
+ next if text.empty?
45
+ status.text = "thinking"
46
+ write("user: #{text}\n")
47
+ window.redraw
48
+ write("agent: ")
49
+ agent.talk(text, stream:)
50
+ status.text = "idle"
51
+ write("\n\n")
52
+ end
53
+ end
54
+ end
55
+
56
+ ##
57
+ # @param [String] chars
58
+ # @return [void]
59
+ def write(chars)
60
+ transcript.write(chars)
61
+ window.redraw
62
+ end
63
+
64
+ ##
65
+ # @param [String] value
66
+ # @return [void]
67
+ def status=(value)
68
+ status.text = value
69
+ window.redraw
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :agent, :provider, :stream,
75
+ :status, :transcript, :input,
76
+ :window
77
+ end
78
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Chdir LLM::Tool::Chdir} class implements
8
+ # a tool that can change the current working directory.
9
+ class Chdir < self
10
+ name "chdir"
11
+ description "change the current working directory"
12
+ parameter :path, String, "the new working directory"
13
+ required %i[path]
14
+
15
+ ##
16
+ # @param [String] path
17
+ # @return [Hash]
18
+ def call(path:)
19
+ Dir.chdir(path)
20
+ {ok: true, cwd: path}
21
+ end
22
+ end
23
+ end