llm.rb 4.8.0 → 4.10.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +356 -583
  3. data/data/anthropic.json +770 -0
  4. data/data/deepseek.json +75 -0
  5. data/data/google.json +1050 -0
  6. data/data/openai.json +1421 -0
  7. data/data/xai.json +792 -0
  8. data/data/zai.json +330 -0
  9. data/lib/llm/agent.rb +42 -41
  10. data/lib/llm/bot.rb +1 -263
  11. data/lib/llm/buffer.rb +7 -0
  12. data/lib/llm/{session → context}/deserializer.rb +4 -3
  13. data/lib/llm/context.rb +292 -0
  14. data/lib/llm/cost.rb +26 -0
  15. data/lib/llm/error.rb +8 -0
  16. data/lib/llm/function/array.rb +61 -0
  17. data/lib/llm/function/fiber_group.rb +91 -0
  18. data/lib/llm/function/task_group.rb +89 -0
  19. data/lib/llm/function/thread_group.rb +94 -0
  20. data/lib/llm/function.rb +75 -10
  21. data/lib/llm/mcp/command.rb +108 -0
  22. data/lib/llm/mcp/error.rb +31 -0
  23. data/lib/llm/mcp/pipe.rb +82 -0
  24. data/lib/llm/mcp/rpc.rb +118 -0
  25. data/lib/llm/mcp/transport/http/event_handler.rb +66 -0
  26. data/lib/llm/mcp/transport/http.rb +122 -0
  27. data/lib/llm/mcp/transport/stdio.rb +85 -0
  28. data/lib/llm/mcp.rb +116 -0
  29. data/lib/llm/message.rb +13 -11
  30. data/lib/llm/model.rb +2 -2
  31. data/lib/llm/prompt.rb +17 -7
  32. data/lib/llm/provider.rb +32 -17
  33. data/lib/llm/providers/anthropic/files.rb +3 -3
  34. data/lib/llm/providers/anthropic.rb +19 -4
  35. data/lib/llm/providers/deepseek.rb +10 -3
  36. data/lib/llm/providers/{gemini → google}/audio.rb +6 -6
  37. data/lib/llm/providers/{gemini → google}/error_handler.rb +2 -2
  38. data/lib/llm/providers/{gemini → google}/files.rb +11 -11
  39. data/lib/llm/providers/{gemini → google}/images.rb +7 -7
  40. data/lib/llm/providers/{gemini → google}/models.rb +5 -5
  41. data/lib/llm/providers/{gemini → google}/request_adapter/completion.rb +7 -3
  42. data/lib/llm/providers/{gemini → google}/request_adapter.rb +1 -1
  43. data/lib/llm/providers/{gemini → google}/response_adapter/completion.rb +7 -7
  44. data/lib/llm/providers/{gemini → google}/response_adapter/embedding.rb +1 -1
  45. data/lib/llm/providers/{gemini → google}/response_adapter/file.rb +1 -1
  46. data/lib/llm/providers/{gemini → google}/response_adapter/files.rb +1 -1
  47. data/lib/llm/providers/{gemini → google}/response_adapter/image.rb +1 -1
  48. data/lib/llm/providers/{gemini → google}/response_adapter/models.rb +1 -1
  49. data/lib/llm/providers/{gemini → google}/response_adapter/web_search.rb +2 -2
  50. data/lib/llm/providers/{gemini → google}/response_adapter.rb +8 -8
  51. data/lib/llm/providers/{gemini → google}/stream_parser.rb +3 -3
  52. data/lib/llm/providers/{gemini.rb → google.rb} +41 -26
  53. data/lib/llm/providers/llamacpp.rb +10 -3
  54. data/lib/llm/providers/ollama.rb +19 -4
  55. data/lib/llm/providers/openai/files.rb +3 -3
  56. data/lib/llm/providers/openai/response_adapter/completion.rb +9 -1
  57. data/lib/llm/providers/openai/response_adapter/responds.rb +9 -1
  58. data/lib/llm/providers/openai/responses.rb +9 -1
  59. data/lib/llm/providers/openai/stream_parser.rb +2 -0
  60. data/lib/llm/providers/openai.rb +19 -4
  61. data/lib/llm/providers/xai.rb +10 -3
  62. data/lib/llm/providers/zai.rb +9 -2
  63. data/lib/llm/registry.rb +81 -0
  64. data/lib/llm/schema/all_of.rb +31 -0
  65. data/lib/llm/schema/any_of.rb +31 -0
  66. data/lib/llm/schema/one_of.rb +31 -0
  67. data/lib/llm/schema/parser.rb +145 -0
  68. data/lib/llm/schema.rb +49 -8
  69. data/lib/llm/server_tool.rb +5 -5
  70. data/lib/llm/session.rb +10 -1
  71. data/lib/llm/tool.rb +88 -6
  72. data/lib/llm/tracer/logger.rb +1 -1
  73. data/lib/llm/tracer/telemetry.rb +7 -7
  74. data/lib/llm/tracer.rb +3 -3
  75. data/lib/llm/usage.rb +5 -0
  76. data/lib/llm/version.rb +1 -1
  77. data/lib/llm.rb +39 -6
  78. data/llm.gemspec +45 -8
  79. metadata +86 -28
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::MCP
4
+ ##
5
+ # The {LLM::MCP::RPC} module provides the JSON-RPC interface used by
6
+ # {LLM::MCP}. MCP uses JSON-RPC to exchange messages between a client
7
+ # and a server. A client sends a method name and its parameters as a
8
+ # request, and the server replies with either a result or an error.
9
+ #
10
+ # This module is responsible for composing those requests, applying
11
+ # the defaults needed by built-in MCP methods such as initialize,
12
+ # and reading responses for request methods. Notifications are sent
13
+ # without waiting for a response, and errors are raised as
14
+ # {LLM::MCP::Error}.
15
+ # @private
16
+ module RPC
17
+ ##
18
+ # Sends a method over the transport.
19
+ # @param [LLM::MCP::Transport] transport
20
+ # The transport to write to
21
+ # @param [String] method
22
+ # The method name to call
23
+ # @param [Hash] params
24
+ # The parameters to send with the method call
25
+ # @return [Object, nil]
26
+ # The result of the method call, or nil if it's a notification
27
+ def call(transport, method, params = {})
28
+ message = {jsonrpc: "2.0", method:, params: default_params(method).merge(params)}
29
+ if notification?(method)
30
+ transport.write(message)
31
+ nil
32
+ else
33
+ @request_id = (@request_id || -1) + 1
34
+ id = @request_id
35
+ transport.write(message.merge(id:))
36
+ recv(transport, id)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ ##
43
+ # Reads a response from the transport.
44
+ # @param [LLM::MCP::Transport] transport
45
+ # The transport to read from
46
+ # @param [Integer] id
47
+ # The request id to wait for
48
+ # @raise [LLM::MCP::Error]
49
+ # When the MCP process returns an error
50
+ # @return [Object, nil]
51
+ # The result returned by the MCP process
52
+ def recv(transport, id)
53
+ poll(timeout:, ex: [IO::WaitReadable]) do
54
+ loop do
55
+ res = transport.read_nonblock
56
+ next unless res["id"] == id
57
+ if res["error"]
58
+ raise LLM::MCP::Error.from(response: res)
59
+ else
60
+ break res["result"]
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ ##
67
+ # Returns default parameters for built-in methods.
68
+ # @param [String] method
69
+ # The method name
70
+ # @return [Hash]
71
+ def default_params(method)
72
+ case method
73
+ when "initialize"
74
+ {protocolVersion: "2025-03-26", capabilities: {}}
75
+ else
76
+ {}
77
+ end
78
+ end
79
+
80
+ ##
81
+ # Returns true when the method is a notification.
82
+ # @param [String] method
83
+ # The method name
84
+ # @return [Boolean]
85
+ def notification?(method)
86
+ method.to_s.start_with?("notifications/")
87
+ end
88
+
89
+ ##
90
+ # Returns the maximum amount of time to wait when reading from an MCP process.
91
+ # @return [Integer]
92
+ def timeout
93
+ @timeout ||= 5
94
+ end
95
+
96
+ ##
97
+ # Runs a block until it succeeds, times out, or raises an unhandled exception.
98
+ # @param [Integer] timeout
99
+ # The timeout for the block, in seconds
100
+ # @param [Array<Class>] ex
101
+ # The exceptions to retry when raised
102
+ # @yield
103
+ # The block to run
104
+ # @raise [LLM::MCP::TimeoutError]
105
+ # When the block takes longer than the timeout
106
+ # @return [Object]
107
+ def poll(timeout:, ex: [])
108
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
109
+ loop do
110
+ return yield
111
+ rescue *ex
112
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
113
+ raise LLM::MCP::TimeoutError, "MCP process timed out" if duration > timeout
114
+ sleep 0.05
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::MCP::Transport
4
+ ##
5
+ # The {LLM::MCP::Transport::HTTP::EventHandler LLM::MCP::Transport::HTTP::EventHandler}
6
+ # class adapts generic server-sent event callbacks into decoded JSON-RPC
7
+ # messages for {LLM::MCP::Transport::HTTP LLM::MCP::Transport::HTTP}.
8
+ # It accumulates event data until a blank line terminates the current
9
+ # event, then parses the payload as JSON and yields it to the callback
10
+ # given at initialization.
11
+ # @private
12
+ class HTTP::EventHandler
13
+ ##
14
+ # @yieldparam [Hash] message
15
+ # A decoded JSON-RPC message
16
+ # @return [LLM::MCP::Transport::HTTP::EventHandler]
17
+ def initialize(&on_message)
18
+ @on_message = on_message
19
+ reset
20
+ end
21
+
22
+ ##
23
+ # Receives the SSE event name.
24
+ # @param [LLM::EventStream::Event] event
25
+ # The event stream event
26
+ # @return [void]
27
+ def on_event(event)
28
+ @event = event.value
29
+ end
30
+
31
+ ##
32
+ # Receives one line of SSE data.
33
+ # @param [LLM::EventStream::Event] event
34
+ # The event stream event
35
+ # @return [void]
36
+ def on_data(event)
37
+ @data << event.value.to_s
38
+ end
39
+
40
+ # The generic event stream parser dispatches one line at a time.
41
+ # A blank line terminates the current SSE event.
42
+ # @param [LLM::EventStream::Event] event
43
+ # The event stream event
44
+ # @return [void]
45
+ def on_chunk(event)
46
+ flush if event.chunk == "\n"
47
+ end
48
+
49
+ private
50
+
51
+ def flush
52
+ return reset if @data.empty? && @event.nil?
53
+ payload = @data.join("\n")
54
+ reset
55
+ return if payload.empty? || payload == "[DONE]"
56
+ @on_message.call(LLM.json.load(payload))
57
+ rescue *LLM.json.parser_error
58
+ reset
59
+ end
60
+
61
+ def reset
62
+ @event = nil
63
+ @data = []
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::MCP::Transport
4
+ ##
5
+ # The {LLM::MCP::Transport::HTTP LLM::MCP::Transport::HTTP} class
6
+ # provides an HTTP transport for {LLM::MCP LLM::MCP}. It sends
7
+ # JSON-RPC messages with HTTP POST requests and buffers response
8
+ # messages for non-blocking reads.
9
+ class HTTP
10
+ require_relative "http/event_handler"
11
+
12
+ ##
13
+ # @param [String] url
14
+ # The URL for the MCP HTTP endpoint
15
+ # @param [Hash] headers
16
+ # Extra headers to send with requests
17
+ # @param [Integer, nil] timeout
18
+ # The timeout in seconds. Defaults to nil
19
+ # @return [LLM::MCP::Transport::HTTP]
20
+ def initialize(url:, headers: {}, timeout: nil)
21
+ @uri = URI.parse(url)
22
+ @use_ssl = @uri.scheme == "https"
23
+ @headers = headers
24
+ @timeout = timeout
25
+ @queue = []
26
+ @monitor = Monitor.new
27
+ @running = false
28
+ end
29
+
30
+ ##
31
+ # Starts the HTTP transport.
32
+ # @raise [LLM::MCP::Error]
33
+ # When the transport is already running
34
+ # @return [void]
35
+ def start
36
+ lock do
37
+ raise LLM::MCP::Error, "MCP transport is already running" if running?
38
+ @queue.clear
39
+ @running = true
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Stops the HTTP transport and closes the connection.
45
+ # This method is idempotent.
46
+ # @return [void]
47
+ def stop
48
+ lock do
49
+ return nil unless running?
50
+ @running = false
51
+ nil
52
+ end
53
+ end
54
+
55
+ ##
56
+ # Writes a JSON-RPC message via HTTP POST.
57
+ # @param [Hash] message
58
+ # The JSON-RPC message
59
+ # @raise [LLM::MCP::Error]
60
+ # When the transport is not running or the HTTP request fails
61
+ # @return [void]
62
+ def write(message)
63
+ raise LLM::MCP::Error, "MCP transport is not running" unless running?
64
+ http = Net::HTTP.start(uri.host, uri.port, use_ssl:, open_timeout: timeout, read_timeout: timeout)
65
+ req = Net::HTTP::Post.new(uri.path, headers.merge("content-type" => "application/json"))
66
+ req.body = LLM.json.dump(message)
67
+ http.request(req) do |res|
68
+ unless Net::HTTPSuccess === res
69
+ raise LLM::MCP::Error, "MCP transport write failed with HTTP #{res.code}"
70
+ end
71
+ read(res)
72
+ end
73
+ end
74
+
75
+ ##
76
+ # Reads the next queued message without blocking.
77
+ # @raise [LLM::MCP::Error]
78
+ # When the transport is not running
79
+ # @raise [IO::WaitReadable]
80
+ # When no complete message is available to read
81
+ # @return [Hash]
82
+ def read_nonblock
83
+ lock do
84
+ raise LLM::MCP::Error, "MCP transport is not running" unless running?
85
+ raise IO::WaitReadable if @queue.empty?
86
+ @queue.shift
87
+ end
88
+ end
89
+
90
+ ##
91
+ # @return [Boolean]
92
+ # Returns true when the MCP server connection is alive
93
+ def running?
94
+ @running
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :uri, :use_ssl, :headers, :timeout
100
+
101
+ def enqueue(message)
102
+ lock { @queue << message }
103
+ end
104
+
105
+ def read(res)
106
+ if res["content-type"].to_s.include?("text/event-stream")
107
+ parser = LLM::EventStream::Parser.new
108
+ parser.register EventHandler.new { enqueue(_1) }
109
+ res.read_body { parser << _1 }
110
+ parser.free
111
+ else
112
+ body = +""
113
+ res.read_body { body << _1 }
114
+ enqueue(LLM.json.load(body)) unless body.empty?
115
+ end
116
+ end
117
+
118
+ def lock(&)
119
+ @monitor.synchronize(&)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::MCP::Transport
4
+ ##
5
+ # The {LLM::MCP::Transport::Stdio LLM::MCP::Transport::Stdio} class
6
+ # provides a stdio transport for {LLM::MCP LLM::MCP}. It sends JSON-RPC
7
+ # messages to an MCP process over stdin and stdout and delegates process
8
+ # lifecycle management to {LLM::MCP::Command LLM::MCP::Command}.
9
+ class Stdio
10
+ ##
11
+ # Returns a new Stdio transport instance.
12
+ # @param command [LLM::MCP::Command]
13
+ # The command to run for the MCP process
14
+ # @return [LLM::MCP::Transport::Stdio]
15
+ def initialize(command:)
16
+ @command = command
17
+ end
18
+
19
+ ##
20
+ # Starts an MCP process over a stdio transport.
21
+ # This method is non-blocking and returns immediately.
22
+ # @raise [LLM::Error]
23
+ # When the transport is already running
24
+ # @return [void]
25
+ def start
26
+ if command.alive?
27
+ raise LLM::MCP::Error, "MCP transport is already running"
28
+ else
29
+ command.start
30
+ end
31
+ end
32
+
33
+ ##
34
+ # Closes the connection to the MCP process.
35
+ # This method is idempotent and can be called multiple times without error.
36
+ # @return [void]
37
+ def stop
38
+ command.stop
39
+ end
40
+
41
+ ##
42
+ # Writes a message to the MCP process.
43
+ # @param [Hash] message
44
+ # The message to write
45
+ # @raise [LLM::Error]
46
+ # When the transport is not running
47
+ # @return [void]
48
+ def write(message)
49
+ if command.alive?
50
+ command.write(LLM.json.dump(message))
51
+ else
52
+ raise LLM::MCP::Error, "MCP transport is not running"
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Reads a message from the MCP process without blocking.
58
+ # @raise [LLM::Error]
59
+ # When the transport is not running
60
+ # @raise [IO::WaitReadable]
61
+ # When no complete message is available to read
62
+ # @return [Hash]
63
+ # The next message from the MCP process
64
+ def read_nonblock
65
+ if command.alive?
66
+ LLM.json.load(command.read_nonblock)
67
+ else
68
+ raise LLM::MCP::Error, "MCP transport is not running"
69
+ end
70
+ end
71
+
72
+ ##
73
+ # Waits for the command to exit.
74
+ # This method is blocking and will return only after the
75
+ # process has exited.
76
+ # @return [void]
77
+ def wait
78
+ command.wait
79
+ end
80
+
81
+ private
82
+
83
+ attr_reader :command, :stdin, :stdout, :stderr
84
+ end
85
+ end
data/lib/llm/mcp.rb ADDED
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The {LLM::MCP LLM::MCP} class provides access to servers that
5
+ # implement the Model Context Protocol. MCP defines a standard way for
6
+ # clients and servers to exchange capabilities such as tools, prompts,
7
+ # resources, and other structured interactions.
8
+ #
9
+ # In llm.rb, {LLM::MCP LLM::MCP} currently supports stdio and HTTP
10
+ # transports and focuses on discovering tools that can be used through
11
+ # {LLM::Context LLM::Context} and {LLM::Agent LLM::Agent}.
12
+ class LLM::MCP
13
+ require "monitor"
14
+ require_relative "mcp/error"
15
+ require_relative "mcp/command"
16
+ require_relative "mcp/rpc"
17
+ require_relative "mcp/pipe"
18
+ require_relative "mcp/transport/http"
19
+ require_relative "mcp/transport/stdio"
20
+
21
+ include RPC
22
+
23
+ ##
24
+ # @param [LLM::Provider, nil] llm
25
+ # The provider to use for MCP transports that need one
26
+ # @param [Hash, nil] stdio The configuration for the stdio transport
27
+ # @option stdio [Array<String>] :argv
28
+ # The command to run for the MCP process
29
+ # @option stdio [Hash] :env
30
+ # The environment variables to set for the MCP process
31
+ # @option stdio [String, nil] :cwd
32
+ # The working directory for the MCP process
33
+ # @param [Hash, nil] http The configuration for the HTTP transport
34
+ # @option http [String] :url
35
+ # The URL for the MCP HTTP endpoint
36
+ # @option http [Hash] :headers
37
+ # Extra headers for requests
38
+ # @param [Integer] timeout The maximum amount of time to wait when reading from an MCP process
39
+ # @return [LLM::MCP] A new MCP instance
40
+ def initialize(llm = nil, stdio: nil, http: nil, timeout: 30)
41
+ @llm = llm
42
+ @monitor = Monitor.new
43
+ @timeout = timeout
44
+ if stdio && http
45
+ raise ArgumentError, "stdio and http are mutually exclusive"
46
+ elsif stdio
47
+ @command = Command.new(**stdio)
48
+ @transport = Transport::Stdio.new(command:)
49
+ elsif http
50
+ @transport = Transport::HTTP.new(**http, timeout:)
51
+ else
52
+ raise ArgumentError, "stdio or http is required"
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Starts the MCP process.
58
+ # @return [void]
59
+ def start
60
+ lock do
61
+ transport.start
62
+ call(transport, "initialize", {clientInfo: {name: "llm.rb", version: LLM::VERSION}})
63
+ call(transport, "notifications/initialized")
64
+ end
65
+ end
66
+
67
+ ##
68
+ # Stops the MCP process.
69
+ # @return [void]
70
+ def stop
71
+ lock do
72
+ transport.stop
73
+ nil
74
+ end
75
+ end
76
+
77
+ ##
78
+ # Returns the tools provided by the MCP process.
79
+ # @return [Array<Class<LLM::Tool>>]
80
+ def tools
81
+ lock do
82
+ res = call(transport, "tools/list")
83
+ res["tools"].map { LLM::Tool.mcp(self, _1) }
84
+ end
85
+ end
86
+
87
+ ##
88
+ # Calls a tool by name with the given arguments
89
+ # @param [String] name The name of the tool to call
90
+ # @param [Hash] arguments The arguments to pass to the tool
91
+ # @return [Object] The result of the tool call
92
+ def call_tool(name, arguments = {})
93
+ lock do
94
+ res = call(transport, "tools/call", {name:, arguments:})
95
+ adapt_tool_result(res)
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ attr_reader :llm, :command, :transport, :timeout
102
+
103
+ def adapt_tool_result(result)
104
+ if result["structuredContent"]
105
+ result["structuredContent"]
106
+ elsif result["content"]
107
+ {content: result["content"]}
108
+ else
109
+ result
110
+ end
111
+ end
112
+
113
+ def lock(&)
114
+ @monitor.synchronize(&)
115
+ end
116
+ end
data/lib/llm/message.rb CHANGED
@@ -26,7 +26,7 @@ module LLM
26
26
  def initialize(role, content, extra = {})
27
27
  @role = role.to_s
28
28
  @content = content
29
- @extra = extra
29
+ @extra = LLM::Object.from(extra)
30
30
  end
31
31
 
32
32
  ##
@@ -34,8 +34,9 @@ module LLM
34
34
  # @return [Hash]
35
35
  def to_h
36
36
  {role:, content:,
37
- tools: @extra[:tool_calls],
38
- original_tool_calls: extra[:original_tool_calls]}.compact
37
+ tools: extra.tool_calls,
38
+ usage:,
39
+ original_tool_calls: extra.original_tool_calls}.compact
39
40
  end
40
41
 
41
42
  ##
@@ -69,8 +70,9 @@ module LLM
69
70
  ##
70
71
  # @return [Array<LLM::Function>]
71
72
  def functions
72
- @functions ||= tool_calls.map do |fn|
73
- function = available_tools.find { _1.name.to_s == fn["name"] }.dup
73
+ @functions ||= tool_calls.filter_map do |fn|
74
+ function = available_tools.find { _1.name.to_s == fn["name"] } || next
75
+ function = function.dup
74
76
  function.tap { _1.id = fn.id }
75
77
  function.tap { _1.arguments = fn.arguments }
76
78
  end
@@ -119,7 +121,7 @@ module LLM
119
121
  # @return [LLM::Response, nil]
120
122
  # Returns the response associated with the message, or nil
121
123
  def response
122
- extra[:response]
124
+ extra.response
123
125
  end
124
126
 
125
127
  ##
@@ -129,7 +131,7 @@ module LLM
129
131
  # Returns annotations associated with the message
130
132
  # @return [Array<LLM::Object>]
131
133
  def annotations
132
- @annotations ||= LLM::Object.from(extra["annotations"] || [])
134
+ @annotations ||= LLM::Object.from(extra.annotations || [])
133
135
  end
134
136
 
135
137
  ##
@@ -139,8 +141,7 @@ module LLM
139
141
  # Returns token usage statistics
140
142
  # @return [LLM::Object, nil]
141
143
  def usage
142
- return nil unless response
143
- @usage ||= response.usage
144
+ @usage ||= extra.usage || response&.usage
144
145
  end
145
146
  alias_method :token_usage, :usage
146
147
 
@@ -163,11 +164,12 @@ module LLM
163
164
  private
164
165
 
165
166
  def tool_calls
166
- @tool_calls ||= LLM::Object.from(@extra[:tool_calls] || [])
167
+ @tool_calls ||= LLM::Object.from(extra.tool_calls || [])
167
168
  end
168
169
 
169
170
  def available_tools
170
- response&.__tools__ || []
171
+ tools = extra.tools || response&.__tools__ || []
172
+ tools.map { _1.respond_to?(:function) ? _1.function : _1 }
171
173
  end
172
174
  end
173
175
  end
data/lib/llm/model.rb CHANGED
@@ -34,7 +34,7 @@ class LLM::Model
34
34
  # @return [Boolean]
35
35
  def chat?
36
36
  return true if anthropic?
37
- return [*(raw.supportedGenerationMethods || [])].include?("generateContent") if gemini?
37
+ return [*(raw.supportedGenerationMethods || [])].include?("generateContent") if google?
38
38
  openai_compatible_chat?
39
39
  end
40
40
 
@@ -96,7 +96,7 @@ class LLM::Model
96
96
  raw.type == "model" && raw.key?(:display_name) && raw.key?(:created_at)
97
97
  end
98
98
 
99
- def gemini?
99
+ def google?
100
100
  raw.key?(:supportedGenerationMethods)
101
101
  end
102
102
 
data/lib/llm/prompt.rb CHANGED
@@ -5,20 +5,20 @@
5
5
  # a single request from multiple role-aware messages.
6
6
  # A prompt is not just a string. It is an ordered chain of
7
7
  # messages with explicit roles (for example `system` and `user`).
8
- # Use {LLM::Session#prompt} when building a prompt inside a session.
8
+ # Use {LLM::Context#prompt} when building a prompt inside a session.
9
9
  # Use `LLM::Prompt.new(provider)` directly when you want to construct
10
10
  # or pass prompt objects around explicitly.
11
11
  #
12
12
  # @example
13
13
  # llm = LLM.openai(key: ENV["KEY"])
14
- # ses = LLM::Session.new(llm)
14
+ # ctx = LLM::Context.new(llm)
15
15
  #
16
- # prompt = ses.prompt do
16
+ # prompt = ctx.prompt do
17
17
  # system "Your task is to assist the user"
18
18
  # user "Hello. Can you assist me?"
19
19
  # end
20
20
  #
21
- # res = ses.talk(prompt)
21
+ # res = ctx.talk(prompt)
22
22
  class LLM::Prompt
23
23
  ##
24
24
  # @param [LLM::Provider] provider
@@ -57,7 +57,7 @@ class LLM::Prompt
57
57
  # The message content
58
58
  # @return [void]
59
59
  def user(content)
60
- chat(content, role: @provider.user_role)
60
+ talk(content, role: @provider.user_role)
61
61
  end
62
62
 
63
63
  ##
@@ -65,7 +65,7 @@ class LLM::Prompt
65
65
  # The message content
66
66
  # @return [void]
67
67
  def system(content)
68
- chat(content, role: @provider.system_role)
68
+ talk(content, role: @provider.system_role)
69
69
  end
70
70
 
71
71
  ##
@@ -73,7 +73,7 @@ class LLM::Prompt
73
73
  # The message content
74
74
  # @return [void]
75
75
  def developer(content)
76
- chat(content, role: @provider.developer_role)
76
+ talk(content, role: @provider.developer_role)
77
77
  end
78
78
 
79
79
  ##
@@ -82,4 +82,14 @@ class LLM::Prompt
82
82
  def to_a
83
83
  @buffer.dup
84
84
  end
85
+
86
+ ##
87
+ # Returns true when two prompts have the same buffer
88
+ # @param [LLM::Prompt] other
89
+ # @return [Boolean]
90
+ def ==(other)
91
+ return false unless LLM::Prompt === other
92
+ @buffer == other.to_a
93
+ end
94
+ alias_method :eql?, :==
85
95
  end