llm.rb 4.9.0 → 4.11.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -0
  3. data/README.md +178 -31
  4. data/data/anthropic.json +209 -242
  5. data/data/deepseek.json +15 -15
  6. data/data/google.json +553 -403
  7. data/data/openai.json +740 -535
  8. data/data/xai.json +250 -253
  9. data/data/zai.json +157 -90
  10. data/lib/llm/context/deserializer.rb +2 -1
  11. data/lib/llm/context.rb +58 -2
  12. data/lib/llm/contract/completion.rb +7 -0
  13. data/lib/llm/error.rb +4 -0
  14. data/lib/llm/eventhandler.rb +7 -0
  15. data/lib/llm/function/registry.rb +106 -0
  16. data/lib/llm/function/task.rb +39 -0
  17. data/lib/llm/function.rb +12 -7
  18. data/lib/llm/mcp/transport/http/event_handler.rb +66 -0
  19. data/lib/llm/mcp/transport/http.rb +156 -0
  20. data/lib/llm/mcp/transport/stdio.rb +7 -0
  21. data/lib/llm/mcp.rb +74 -30
  22. data/lib/llm/message.rb +9 -2
  23. data/lib/llm/provider.rb +10 -0
  24. data/lib/llm/providers/anthropic/response_adapter/completion.rb +6 -0
  25. data/lib/llm/providers/anthropic/stream_parser.rb +37 -4
  26. data/lib/llm/providers/anthropic.rb +1 -1
  27. data/lib/llm/providers/google/response_adapter/completion.rb +12 -5
  28. data/lib/llm/providers/google/stream_parser.rb +54 -11
  29. data/lib/llm/providers/google/utils.rb +30 -0
  30. data/lib/llm/providers/google.rb +2 -0
  31. data/lib/llm/providers/ollama/response_adapter/completion.rb +6 -0
  32. data/lib/llm/providers/ollama/stream_parser.rb +10 -4
  33. data/lib/llm/providers/ollama.rb +1 -1
  34. data/lib/llm/providers/openai/response_adapter/completion.rb +7 -0
  35. data/lib/llm/providers/openai/response_adapter/responds.rb +84 -10
  36. data/lib/llm/providers/openai/responses/stream_parser.rb +63 -4
  37. data/lib/llm/providers/openai/responses.rb +1 -1
  38. data/lib/llm/providers/openai/stream_parser.rb +68 -4
  39. data/lib/llm/providers/openai.rb +1 -1
  40. data/lib/llm/schema/all_of.rb +31 -0
  41. data/lib/llm/schema/any_of.rb +31 -0
  42. data/lib/llm/schema/one_of.rb +31 -0
  43. data/lib/llm/schema/parser.rb +36 -0
  44. data/lib/llm/schema.rb +45 -8
  45. data/lib/llm/stream/queue.rb +51 -0
  46. data/lib/llm/stream.rb +102 -0
  47. data/lib/llm/tool.rb +53 -47
  48. data/lib/llm/version.rb +1 -1
  49. data/lib/llm.rb +3 -2
  50. data/llm.gemspec +2 -2
  51. metadata +12 -1
data/data/zai.json CHANGED
@@ -8,19 +8,17 @@
8
8
  "name": "Z.AI",
9
9
  "doc": "https://docs.z.ai/guides/overview/pricing",
10
10
  "models": {
11
- "glm-5": {
12
- "id": "glm-5",
13
- "name": "GLM-5",
14
- "family": "glm",
11
+ "glm-4.7-flash": {
12
+ "id": "glm-4.7-flash",
13
+ "name": "GLM-4.7-Flash",
14
+ "family": "glm-flash",
15
15
  "attachment": false,
16
16
  "reasoning": true,
17
17
  "tool_call": true,
18
- "interleaved": {
19
- "field": "reasoning_content"
20
- },
21
18
  "temperature": true,
22
- "release_date": "2026-02-11",
23
- "last_updated": "2026-02-11",
19
+ "knowledge": "2025-04",
20
+ "release_date": "2026-01-19",
21
+ "last_updated": "2026-01-19",
24
22
  "modalities": {
25
23
  "input": [
26
24
  "text"
@@ -31,58 +29,66 @@
31
29
  },
32
30
  "open_weights": true,
33
31
  "cost": {
34
- "input": 1,
35
- "output": 3.2,
36
- "cache_read": 0.2,
32
+ "input": 0,
33
+ "output": 0,
34
+ "cache_read": 0,
37
35
  "cache_write": 0
38
36
  },
39
37
  "limit": {
40
- "context": 204800,
38
+ "context": 200000,
41
39
  "output": 131072
42
40
  }
43
41
  },
44
- "glm-4.5-air": {
45
- "id": "glm-4.5-air",
46
- "name": "GLM-4.5-Air",
47
- "family": "glm-air",
48
- "attachment": false,
42
+ "glm-5v-turbo": {
43
+ "id": "glm-5v-turbo",
44
+ "name": "glm-5v-turbo",
45
+ "family": "glm",
46
+ "attachment": true,
49
47
  "reasoning": true,
50
48
  "tool_call": true,
49
+ "interleaved": {
50
+ "field": "reasoning_content"
51
+ },
51
52
  "temperature": true,
52
- "knowledge": "2025-04",
53
- "release_date": "2025-07-28",
54
- "last_updated": "2025-07-28",
53
+ "release_date": "2026-04-01",
54
+ "last_updated": "2026-04-01",
55
55
  "modalities": {
56
56
  "input": [
57
- "text"
57
+ "text",
58
+ "image",
59
+ "video",
60
+ "pdf"
58
61
  ],
59
62
  "output": [
60
63
  "text"
61
64
  ]
62
65
  },
63
- "open_weights": true,
66
+ "open_weights": false,
64
67
  "cost": {
65
- "input": 0.2,
66
- "output": 1.1,
67
- "cache_read": 0.03,
68
+ "input": 1.2,
69
+ "output": 4,
70
+ "cache_read": 0.24,
68
71
  "cache_write": 0
69
72
  },
70
73
  "limit": {
71
- "context": 131072,
72
- "output": 98304
74
+ "context": 200000,
75
+ "output": 131072
73
76
  }
74
77
  },
75
- "glm-4.5": {
76
- "id": "glm-4.5",
77
- "name": "GLM-4.5",
78
+ "glm-5-turbo": {
79
+ "id": "glm-5-turbo",
80
+ "name": "GLM-5-Turbo",
78
81
  "family": "glm",
79
82
  "attachment": false,
80
83
  "reasoning": true,
81
84
  "tool_call": true,
85
+ "interleaved": {
86
+ "field": "reasoning_content"
87
+ },
88
+ "structured_output": true,
82
89
  "temperature": true,
83
- "knowledge": "2025-04",
84
- "release_date": "2025-07-28",
85
- "last_updated": "2025-07-28",
90
+ "release_date": "2026-03-16",
91
+ "last_updated": "2026-03-16",
86
92
  "modalities": {
87
93
  "input": [
88
94
  "text"
@@ -91,22 +97,22 @@
91
97
  "text"
92
98
  ]
93
99
  },
94
- "open_weights": true,
100
+ "open_weights": false,
95
101
  "cost": {
96
- "input": 0.6,
97
- "output": 2.2,
98
- "cache_read": 0.11,
102
+ "input": 1.2,
103
+ "output": 4,
104
+ "cache_read": 0.24,
99
105
  "cache_write": 0
100
106
  },
101
107
  "limit": {
102
- "context": 131072,
103
- "output": 98304
108
+ "context": 200000,
109
+ "output": 131072
104
110
  }
105
111
  },
106
- "glm-4.5-flash": {
107
- "id": "glm-4.5-flash",
108
- "name": "GLM-4.5-Flash",
109
- "family": "glm-flash",
112
+ "glm-4.5": {
113
+ "id": "glm-4.5",
114
+ "name": "GLM-4.5",
115
+ "family": "glm",
110
116
  "attachment": false,
111
117
  "reasoning": true,
112
118
  "tool_call": true,
@@ -124,9 +130,9 @@
124
130
  },
125
131
  "open_weights": true,
126
132
  "cost": {
127
- "input": 0,
128
- "output": 0,
129
- "cache_read": 0,
133
+ "input": 0.6,
134
+ "output": 2.2,
135
+ "cache_read": 0.11,
130
136
  "cache_write": 0
131
137
  },
132
138
  "limit": {
@@ -134,9 +140,9 @@
134
140
  "output": 98304
135
141
  }
136
142
  },
137
- "glm-4.7-flash": {
138
- "id": "glm-4.7-flash",
139
- "name": "GLM-4.7-Flash",
143
+ "glm-4.7-flashx": {
144
+ "id": "glm-4.7-flashx",
145
+ "name": "GLM-4.7-FlashX",
140
146
  "family": "glm-flash",
141
147
  "attachment": false,
142
148
  "reasoning": true,
@@ -155,9 +161,9 @@
155
161
  },
156
162
  "open_weights": true,
157
163
  "cost": {
158
- "input": 0,
159
- "output": 0,
160
- "cache_read": 0,
164
+ "input": 0.07,
165
+ "output": 0.4,
166
+ "cache_read": 0.01,
161
167
  "cache_write": 0
162
168
  },
163
169
  "limit": {
@@ -196,20 +202,48 @@
196
202
  "output": 131072
197
203
  }
198
204
  },
199
- "glm-4.7": {
200
- "id": "glm-4.7",
201
- "name": "GLM-4.7",
205
+ "glm-4.6v": {
206
+ "id": "glm-4.6v",
207
+ "name": "GLM-4.6V",
202
208
  "family": "glm",
203
- "attachment": false,
209
+ "attachment": true,
204
210
  "reasoning": true,
205
211
  "tool_call": true,
206
- "interleaved": {
207
- "field": "reasoning_content"
212
+ "temperature": true,
213
+ "knowledge": "2025-04",
214
+ "release_date": "2025-12-08",
215
+ "last_updated": "2025-12-08",
216
+ "modalities": {
217
+ "input": [
218
+ "text",
219
+ "image",
220
+ "video"
221
+ ],
222
+ "output": [
223
+ "text"
224
+ ]
208
225
  },
226
+ "open_weights": true,
227
+ "cost": {
228
+ "input": 0.3,
229
+ "output": 0.9
230
+ },
231
+ "limit": {
232
+ "context": 128000,
233
+ "output": 32768
234
+ }
235
+ },
236
+ "glm-4.5-flash": {
237
+ "id": "glm-4.5-flash",
238
+ "name": "GLM-4.5-Flash",
239
+ "family": "glm-flash",
240
+ "attachment": false,
241
+ "reasoning": true,
242
+ "tool_call": true,
209
243
  "temperature": true,
210
244
  "knowledge": "2025-04",
211
- "release_date": "2025-12-22",
212
- "last_updated": "2025-12-22",
245
+ "release_date": "2025-07-28",
246
+ "last_updated": "2025-07-28",
213
247
  "modalities": {
214
248
  "input": [
215
249
  "text"
@@ -220,19 +254,19 @@
220
254
  },
221
255
  "open_weights": true,
222
256
  "cost": {
223
- "input": 0.6,
224
- "output": 2.2,
225
- "cache_read": 0.11,
257
+ "input": 0,
258
+ "output": 0,
259
+ "cache_read": 0,
226
260
  "cache_write": 0
227
261
  },
228
262
  "limit": {
229
- "context": 204800,
230
- "output": 131072
263
+ "context": 131072,
264
+ "output": 98304
231
265
  }
232
266
  },
233
- "glm-5-turbo": {
234
- "id": "glm-5-turbo",
235
- "name": "GLM-5-Turbo",
267
+ "glm-5": {
268
+ "id": "glm-5",
269
+ "name": "GLM-5",
236
270
  "family": "glm",
237
271
  "attachment": false,
238
272
  "reasoning": true,
@@ -240,10 +274,9 @@
240
274
  "interleaved": {
241
275
  "field": "reasoning_content"
242
276
  },
243
- "structured_output": true,
244
277
  "temperature": true,
245
- "release_date": "2026-03-16",
246
- "last_updated": "2026-03-16",
278
+ "release_date": "2026-02-11",
279
+ "last_updated": "2026-02-11",
247
280
  "modalities": {
248
281
  "input": [
249
282
  "text"
@@ -252,18 +285,49 @@
252
285
  "text"
253
286
  ]
254
287
  },
255
- "open_weights": false,
288
+ "open_weights": true,
256
289
  "cost": {
257
- "input": 1.2,
258
- "output": 4,
259
- "cache_read": 0.24,
290
+ "input": 1,
291
+ "output": 3.2,
292
+ "cache_read": 0.2,
260
293
  "cache_write": 0
261
294
  },
262
295
  "limit": {
263
- "context": 200000,
296
+ "context": 204800,
264
297
  "output": 131072
265
298
  }
266
299
  },
300
+ "glm-4.5-air": {
301
+ "id": "glm-4.5-air",
302
+ "name": "GLM-4.5-Air",
303
+ "family": "glm-air",
304
+ "attachment": false,
305
+ "reasoning": true,
306
+ "tool_call": true,
307
+ "temperature": true,
308
+ "knowledge": "2025-04",
309
+ "release_date": "2025-07-28",
310
+ "last_updated": "2025-07-28",
311
+ "modalities": {
312
+ "input": [
313
+ "text"
314
+ ],
315
+ "output": [
316
+ "text"
317
+ ]
318
+ },
319
+ "open_weights": true,
320
+ "cost": {
321
+ "input": 0.2,
322
+ "output": 1.1,
323
+ "cache_read": 0.03,
324
+ "cache_write": 0
325
+ },
326
+ "limit": {
327
+ "context": 131072,
328
+ "output": 98304
329
+ }
330
+ },
267
331
  "glm-4.5v": {
268
332
  "id": "glm-4.5v",
269
333
  "name": "GLM-4.5V",
@@ -295,22 +359,23 @@
295
359
  "output": 16384
296
360
  }
297
361
  },
298
- "glm-4.6v": {
299
- "id": "glm-4.6v",
300
- "name": "GLM-4.6V",
362
+ "glm-4.7": {
363
+ "id": "glm-4.7",
364
+ "name": "GLM-4.7",
301
365
  "family": "glm",
302
- "attachment": true,
366
+ "attachment": false,
303
367
  "reasoning": true,
304
368
  "tool_call": true,
369
+ "interleaved": {
370
+ "field": "reasoning_content"
371
+ },
305
372
  "temperature": true,
306
373
  "knowledge": "2025-04",
307
- "release_date": "2025-12-08",
308
- "last_updated": "2025-12-08",
374
+ "release_date": "2025-12-22",
375
+ "last_updated": "2025-12-22",
309
376
  "modalities": {
310
377
  "input": [
311
- "text",
312
- "image",
313
- "video"
378
+ "text"
314
379
  ],
315
380
  "output": [
316
381
  "text"
@@ -318,12 +383,14 @@
318
383
  },
319
384
  "open_weights": true,
320
385
  "cost": {
321
- "input": 0.3,
322
- "output": 0.9
386
+ "input": 0.6,
387
+ "output": 2.2,
388
+ "cache_read": 0.11,
389
+ "cache_write": 0
323
390
  },
324
391
  "limit": {
325
- "context": 128000,
326
- "output": 32768
392
+ "context": 204800,
393
+ "output": 131072
327
394
  }
328
395
  }
329
396
  }
@@ -12,7 +12,8 @@ class LLM::Context
12
12
  returns = deserialize_returns(payload["content"]) if returns.nil?
13
13
  original_tool_calls = payload["original_tool_calls"]
14
14
  usage = payload["usage"]
15
- extra = {tool_calls:, original_tool_calls:, tools: @params[:tools], usage:}.compact
15
+ reasoning_content = payload["reasoning_content"]
16
+ extra = {tool_calls:, original_tool_calls:, tools: @params[:tools], usage:, reasoning_content:}.compact
16
17
  content = returns.nil? ? payload["content"] : returns
17
18
  LLM::Message.new(payload["role"], content, extra)
18
19
  end
data/lib/llm/context.rb CHANGED
@@ -42,6 +42,11 @@ module LLM
42
42
  # @return [LLM::Provider]
43
43
  attr_reader :llm
44
44
 
45
+ ##
46
+ # Returns the context mode
47
+ # @return [Symbol]
48
+ attr_reader :mode
49
+
45
50
  ##
46
51
  # @param [LLM::Provider] llm
47
52
  # A provider
@@ -49,10 +54,12 @@ module LLM
49
54
  # The parameters to maintain throughout the conversation.
50
55
  # Any parameter the provider supports can be included and
51
56
  # not only those listed here.
57
+ # @option params [Symbol] :mode Defaults to :completions
52
58
  # @option params [String] :model Defaults to the provider's default model
53
59
  # @option params [Array<LLM::Function>, nil] :tools Defaults to nil
54
60
  def initialize(llm, params = {})
55
61
  @llm = llm
62
+ @mode = params.delete(:mode) || :completions
56
63
  @params = {model: llm.default_model, schema: nil}.compact.merge!(params)
57
64
  @messages = LLM::Buffer.new(llm)
58
65
  end
@@ -70,6 +77,7 @@ module LLM
70
77
  # res = ctx.talk("Hello, what is your name?")
71
78
  # puts res.messages[0].content
72
79
  def talk(prompt, params = {})
80
+ return respond(prompt, params) if mode == :responses
73
81
  params = params.merge(messages: @messages.to_a)
74
82
  params = @params.merge(params)
75
83
  res = @llm.complete(prompt, params)
@@ -109,7 +117,7 @@ module LLM
109
117
  # @return [String]
110
118
  def inspect
111
119
  "#<#{self.class.name}:0x#{object_id.to_s(16)} " \
112
- "@llm=#{@llm.class}, @params=#{@params.inspect}, " \
120
+ "@llm=#{@llm.class}, @mode=#{@mode.inspect}, @params=#{@params.inspect}, " \
113
121
  "@messages=#{@messages.inspect}>"
114
122
  end
115
123
 
@@ -117,10 +125,11 @@ module LLM
117
125
  # Returns an array of functions that can be called
118
126
  # @return [Array<LLM::Function>]
119
127
  def functions
128
+ return_ids = returns.map(&:id)
120
129
  @messages
121
130
  .select(&:assistant?)
122
131
  .flat_map do |msg|
123
- fns = msg.functions.select(&:pending?)
132
+ fns = msg.functions.select { _1.pending? && !return_ids.include?(_1.id) }
124
133
  fns.each do |fn|
125
134
  fn.tracer = tracer
126
135
  fn.model = msg.model
@@ -128,6 +137,53 @@ module LLM
128
137
  end.extend(LLM::Function::Array)
129
138
  end
130
139
 
140
+ ##
141
+ # Calls a named collection of work through the context.
142
+ #
143
+ # This currently supports `:functions`, forwarding to `functions.call`.
144
+ #
145
+ # @param [Symbol] target
146
+ # The work collection to call
147
+ # @return [Array<LLM::Function::Return>]
148
+ def call(target)
149
+ case target
150
+ when :functions then functions.call
151
+ else raise ArgumentError, "Unknown target: #{target.inspect}. Expected :functions"
152
+ end
153
+ end
154
+
155
+ ##
156
+ # Returns tool returns accumulated in this context
157
+ # @return [Array<LLM::Function::Return>]
158
+ def returns
159
+ @messages
160
+ .select(&:tool_return?)
161
+ .flat_map do |msg|
162
+ LLM::Function::Return === msg.content ?
163
+ [msg.content] :
164
+ [*msg.content].grep(LLM::Function::Return)
165
+ end
166
+ end
167
+
168
+ ##
169
+ # Waits for queued tool work to finish.
170
+ #
171
+ # This prefers queued streamed tool work when the configured stream
172
+ # exposes a non-empty queue. Otherwise it falls back to waiting on
173
+ # the context's pending functions directly.
174
+ #
175
+ # @param [Symbol] strategy
176
+ # The concurrency strategy to use
177
+ # @return [Array<LLM::Function::Return>]
178
+ def wait(strategy)
179
+ stream = @params[:stream]
180
+ if LLM::Stream === stream && !stream.queue.empty?
181
+ stream.wait(strategy)
182
+ else
183
+ functions.wait(strategy)
184
+ end
185
+ end
186
+
131
187
  ##
132
188
  # Returns token usage accumulated in this context
133
189
  # @note
@@ -50,6 +50,13 @@ module LLM::Contract
50
50
  messages.find(&:assistant?).content
51
51
  end
52
52
 
53
+ ##
54
+ # @return [String, nil]
55
+ # Returns the reasoning content when the provider exposes it
56
+ def reasoning_content
57
+ messages.find(&:assistant?)&.reasoning_content
58
+ end
59
+
53
60
  ##
54
61
  # @return [Hash]
55
62
  # Returns the LLM response after parsing it as JSON
data/lib/llm/error.rb CHANGED
@@ -55,6 +55,10 @@ module LLM
55
55
  # When stuck in a tool call loop
56
56
  ToolLoopError = Class.new(Error)
57
57
 
58
+ ##
59
+ # When a tool call cannot be mapped to a local tool
60
+ NoSuchToolError = Class.new(Error)
61
+
58
62
  ##
59
63
  # When {LLM::Registry} can't map a model
60
64
  NoSuchModelError = Class.new(Error)
@@ -42,5 +42,12 @@ module LLM
42
42
  # Returns a fully constructed response body
43
43
  # @return [LLM::Object]
44
44
  def body = @parser.body
45
+
46
+ ##
47
+ # Frees parser state after streaming completes.
48
+ # @return [void]
49
+ def free
50
+ @parser.free
51
+ end
45
52
  end
46
53
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Function
4
+ ##
5
+ # The {LLM::Function::Registry LLM::Function::Registry} module provides
6
+ # shared registry behavior for functions and tools. {LLM::Tool.registry}
7
+ # stores {LLM::Tool LLM::Tool} subclasses, including dynamically created MCP
8
+ # tool subclasses, while {LLM::Function.registry} stores the functions
9
+ # derived from those tools.
10
+ #
11
+ # The registry overwrites older tool definitions with newer ones when they
12
+ # share the same tool name. In practice, tool identity is resolved by name,
13
+ # and LLMs generally do not allow two tools with the same name.
14
+ #
15
+ # Functions defined with {LLM.function} are not added to the function
16
+ # registry, since they may be closures bound to local state. Each registry
17
+ # decides how entries are keyed via {#registry_key}.
18
+ module Registry
19
+ ##
20
+ # @api private
21
+ def self.extended(klass)
22
+ klass.instance_variable_set(:@__registry, {})
23
+ klass.instance_variable_set(:@__names, {})
24
+ klass.instance_variable_set(:@__monitor, Monitor.new)
25
+ end
26
+
27
+ ##
28
+ # Returns all registered entries.
29
+ # @return [Array<LLM::Function, LLM::Tool>]
30
+ def registry
31
+ lock do
32
+ @__registry.values
33
+ end
34
+ end
35
+
36
+ ##
37
+ # Finds a registered entry by name.
38
+ # @param [String] name
39
+ # @return [LLM::Function, LLM::Tool, nil]
40
+ def find_by_name(name)
41
+ lock do
42
+ @__names[name.to_s] ||= @__registry.each_value.find do
43
+ tool_name(_1).to_s == name.to_s
44
+ end
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Clears the registry.
50
+ # @return [void]
51
+ def clear_registry!
52
+ lock do
53
+ @__registry.clear
54
+ @__names.clear
55
+ nil
56
+ end
57
+ end
58
+
59
+ ##
60
+ # Registers an entry.
61
+ # @param [LLM::Function, LLM::Tool] entry
62
+ # @api private
63
+ def register(entry)
64
+ lock do
65
+ @__registry[registry_key(entry)] = entry
66
+ @__names[tool_name(entry).to_s] = entry if tool_name(entry)
67
+ end
68
+ end
69
+
70
+ ##
71
+ # Unregisters an entry.
72
+ # @param [LLM::Function, LLM::Tool] entry
73
+ # @api private
74
+ def unregister(entry)
75
+ lock do
76
+ @__registry.delete(registry_key(entry))
77
+ @__registry.delete(entry)
78
+ @__names.delete(tool_name(entry).to_s) if tool_name(entry)
79
+ end
80
+ end
81
+
82
+ ##
83
+ # Returns the storage key for an entry.
84
+ # @param [LLM::Function, LLM::Tool] entry
85
+ # @return [Class<LLM::Tool>, String, nil]
86
+ # @api private
87
+ def registry_key(entry)
88
+ tool_name(entry) ? entry.name : entry
89
+ end
90
+
91
+ ##
92
+ # Returns the tool name, or nil for tools that are not fully initialized.
93
+ # @param [LLM::Function, LLM::Tool] entry
94
+ # @return [String, nil]
95
+ # @api private
96
+ def tool_name(entry)
97
+ entry.respond_to?(:name) ? entry.name : nil
98
+ end
99
+
100
+ ##
101
+ # @api private
102
+ def lock(&)
103
+ @__monitor.synchronize(&)
104
+ end
105
+ end
106
+ end