llm.rb 12.2.0 → 12.3.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/openai.json CHANGED
@@ -124,6 +124,99 @@
124
124
  "output": 168
125
125
  }
126
126
  },
127
+ "gpt-5.6": {
128
+ "id": "gpt-5.6",
129
+ "name": "GPT-5.6",
130
+ "description": "Frontier GPT-5.6 model for complex professional work, coding, and agentic workflows",
131
+ "family": "gpt",
132
+ "attachment": true,
133
+ "reasoning": true,
134
+ "reasoning_options": [
135
+ {
136
+ "type": "effort",
137
+ "values": [
138
+ "none",
139
+ "low",
140
+ "medium",
141
+ "high",
142
+ "xhigh",
143
+ "max"
144
+ ]
145
+ }
146
+ ],
147
+ "tool_call": true,
148
+ "structured_output": true,
149
+ "temperature": false,
150
+ "knowledge": "2026-02-16",
151
+ "release_date": "2026-07-09",
152
+ "last_updated": "2026-07-09",
153
+ "modalities": {
154
+ "input": [
155
+ "text",
156
+ "image",
157
+ "pdf"
158
+ ],
159
+ "output": [
160
+ "text"
161
+ ]
162
+ },
163
+ "open_weights": false,
164
+ "limit": {
165
+ "context": 1050000,
166
+ "input": 922000,
167
+ "output": 128000
168
+ },
169
+ "experimental": {
170
+ "modes": {
171
+ "fast": {
172
+ "cost": {
173
+ "input": 10,
174
+ "output": 60,
175
+ "cache_read": 1,
176
+ "cache_write": 12.5
177
+ },
178
+ "provider": {
179
+ "body": {
180
+ "service_tier": "priority"
181
+ }
182
+ }
183
+ },
184
+ "pro": {
185
+ "provider": {
186
+ "body": {
187
+ "reasoning": {
188
+ "mode": "pro"
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ },
195
+ "cost": {
196
+ "input": 5,
197
+ "output": 30,
198
+ "cache_read": 0.5,
199
+ "cache_write": 6.25,
200
+ "tiers": [
201
+ {
202
+ "input": 10,
203
+ "output": 45,
204
+ "cache_read": 1,
205
+ "cache_write": 12.5,
206
+ "tier": {
207
+ "type": "context",
208
+ "size": 272000
209
+ }
210
+ }
211
+ ],
212
+ "context_over_200k": {
213
+ "input": 10,
214
+ "output": 45,
215
+ "cache_read": 1,
216
+ "cache_write": 12.5
217
+ }
218
+ }
219
+ },
127
220
  "gpt-5": {
128
221
  "id": "gpt-5",
129
222
  "name": "GPT-5",
@@ -930,6 +1023,99 @@
930
1023
  "output": 0
931
1024
  }
932
1025
  },
1026
+ "gpt-5.6-luna": {
1027
+ "id": "gpt-5.6-luna",
1028
+ "name": "GPT-5.6 Luna",
1029
+ "description": "Cost-efficient GPT-5.6 model for fast, high-volume workloads",
1030
+ "family": "gpt-nano",
1031
+ "attachment": true,
1032
+ "reasoning": true,
1033
+ "reasoning_options": [
1034
+ {
1035
+ "type": "effort",
1036
+ "values": [
1037
+ "none",
1038
+ "low",
1039
+ "medium",
1040
+ "high",
1041
+ "xhigh",
1042
+ "max"
1043
+ ]
1044
+ }
1045
+ ],
1046
+ "tool_call": true,
1047
+ "structured_output": true,
1048
+ "temperature": false,
1049
+ "knowledge": "2026-02-16",
1050
+ "release_date": "2026-07-09",
1051
+ "last_updated": "2026-07-09",
1052
+ "modalities": {
1053
+ "input": [
1054
+ "text",
1055
+ "image",
1056
+ "pdf"
1057
+ ],
1058
+ "output": [
1059
+ "text"
1060
+ ]
1061
+ },
1062
+ "open_weights": false,
1063
+ "limit": {
1064
+ "context": 1050000,
1065
+ "input": 922000,
1066
+ "output": 128000
1067
+ },
1068
+ "experimental": {
1069
+ "modes": {
1070
+ "fast": {
1071
+ "cost": {
1072
+ "input": 2,
1073
+ "output": 12,
1074
+ "cache_read": 0.2,
1075
+ "cache_write": 2.5
1076
+ },
1077
+ "provider": {
1078
+ "body": {
1079
+ "service_tier": "priority"
1080
+ }
1081
+ }
1082
+ },
1083
+ "pro": {
1084
+ "provider": {
1085
+ "body": {
1086
+ "reasoning": {
1087
+ "mode": "pro"
1088
+ }
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ },
1094
+ "cost": {
1095
+ "input": 1,
1096
+ "output": 6,
1097
+ "cache_read": 0.1,
1098
+ "cache_write": 1.25,
1099
+ "tiers": [
1100
+ {
1101
+ "input": 2,
1102
+ "output": 9,
1103
+ "cache_read": 0.2,
1104
+ "cache_write": 2.5,
1105
+ "tier": {
1106
+ "type": "context",
1107
+ "size": 272000
1108
+ }
1109
+ }
1110
+ ],
1111
+ "context_over_200k": {
1112
+ "input": 2,
1113
+ "output": 9,
1114
+ "cache_read": 0.2,
1115
+ "cache_write": 2.5
1116
+ }
1117
+ }
1118
+ },
933
1119
  "gpt-5.1-codex-mini": {
934
1120
  "id": "gpt-5.1-codex-mini",
935
1121
  "name": "GPT-5.1 Codex mini",
@@ -974,6 +1160,149 @@
974
1160
  "cache_read": 0.025
975
1161
  }
976
1162
  },
1163
+ "gpt-realtime-2.1": {
1164
+ "id": "gpt-realtime-2.1",
1165
+ "name": "GPT-Realtime-2.1",
1166
+ "description": "Realtime speech-to-speech model with configurable reasoning, tool use, and robust voice-agent behavior",
1167
+ "family": "gpt",
1168
+ "attachment": true,
1169
+ "reasoning": true,
1170
+ "reasoning_options": [
1171
+ {
1172
+ "type": "effort",
1173
+ "values": [
1174
+ "minimal",
1175
+ "low",
1176
+ "medium",
1177
+ "high",
1178
+ "xhigh"
1179
+ ]
1180
+ }
1181
+ ],
1182
+ "tool_call": true,
1183
+ "structured_output": false,
1184
+ "temperature": false,
1185
+ "knowledge": "2024-09-30",
1186
+ "release_date": "2026-07-06",
1187
+ "last_updated": "2026-07-06",
1188
+ "modalities": {
1189
+ "input": [
1190
+ "text",
1191
+ "audio",
1192
+ "image"
1193
+ ],
1194
+ "output": [
1195
+ "text",
1196
+ "audio"
1197
+ ]
1198
+ },
1199
+ "open_weights": false,
1200
+ "limit": {
1201
+ "context": 128000,
1202
+ "input": 96000,
1203
+ "output": 32000
1204
+ },
1205
+ "cost": {
1206
+ "input": 4,
1207
+ "output": 24,
1208
+ "cache_read": 0.4,
1209
+ "input_audio": 32,
1210
+ "output_audio": 64
1211
+ }
1212
+ },
1213
+ "gpt-5.6-terra": {
1214
+ "id": "gpt-5.6-terra",
1215
+ "name": "GPT-5.6 Terra",
1216
+ "description": "Balanced GPT-5.6 model for capable, cost-efficient everyday work",
1217
+ "family": "gpt-mini",
1218
+ "attachment": true,
1219
+ "reasoning": true,
1220
+ "reasoning_options": [
1221
+ {
1222
+ "type": "effort",
1223
+ "values": [
1224
+ "none",
1225
+ "low",
1226
+ "medium",
1227
+ "high",
1228
+ "xhigh",
1229
+ "max"
1230
+ ]
1231
+ }
1232
+ ],
1233
+ "tool_call": true,
1234
+ "structured_output": true,
1235
+ "temperature": false,
1236
+ "knowledge": "2026-02-16",
1237
+ "release_date": "2026-07-09",
1238
+ "last_updated": "2026-07-09",
1239
+ "modalities": {
1240
+ "input": [
1241
+ "text",
1242
+ "image",
1243
+ "pdf"
1244
+ ],
1245
+ "output": [
1246
+ "text"
1247
+ ]
1248
+ },
1249
+ "open_weights": false,
1250
+ "limit": {
1251
+ "context": 1050000,
1252
+ "input": 922000,
1253
+ "output": 128000
1254
+ },
1255
+ "experimental": {
1256
+ "modes": {
1257
+ "fast": {
1258
+ "cost": {
1259
+ "input": 5,
1260
+ "output": 30,
1261
+ "cache_read": 0.5,
1262
+ "cache_write": 6.25
1263
+ },
1264
+ "provider": {
1265
+ "body": {
1266
+ "service_tier": "priority"
1267
+ }
1268
+ }
1269
+ },
1270
+ "pro": {
1271
+ "provider": {
1272
+ "body": {
1273
+ "reasoning": {
1274
+ "mode": "pro"
1275
+ }
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1280
+ },
1281
+ "cost": {
1282
+ "input": 2.5,
1283
+ "output": 15,
1284
+ "cache_read": 0.25,
1285
+ "cache_write": 3.125,
1286
+ "tiers": [
1287
+ {
1288
+ "input": 5,
1289
+ "output": 22.5,
1290
+ "cache_read": 0.5,
1291
+ "cache_write": 6.25,
1292
+ "tier": {
1293
+ "type": "context",
1294
+ "size": 272000
1295
+ }
1296
+ }
1297
+ ],
1298
+ "context_over_200k": {
1299
+ "input": 5,
1300
+ "output": 22.5,
1301
+ "cache_read": 0.5,
1302
+ "cache_write": 6.25
1303
+ }
1304
+ }
1305
+ },
977
1306
  "gpt-5.1-chat-latest": {
978
1307
  "id": "gpt-5.1-chat-latest",
979
1308
  "name": "GPT-5.1 Chat",
@@ -1851,6 +2180,99 @@
1851
2180
  "cache_read": 0.075
1852
2181
  }
1853
2182
  },
2183
+ "gpt-5.6-sol": {
2184
+ "id": "gpt-5.6-sol",
2185
+ "name": "GPT-5.6 Sol",
2186
+ "description": "Frontier GPT-5.6 model for complex professional work, coding, and agentic workflows",
2187
+ "family": "gpt",
2188
+ "attachment": true,
2189
+ "reasoning": true,
2190
+ "reasoning_options": [
2191
+ {
2192
+ "type": "effort",
2193
+ "values": [
2194
+ "none",
2195
+ "low",
2196
+ "medium",
2197
+ "high",
2198
+ "xhigh",
2199
+ "max"
2200
+ ]
2201
+ }
2202
+ ],
2203
+ "tool_call": true,
2204
+ "structured_output": true,
2205
+ "temperature": false,
2206
+ "knowledge": "2026-02-16",
2207
+ "release_date": "2026-07-09",
2208
+ "last_updated": "2026-07-09",
2209
+ "modalities": {
2210
+ "input": [
2211
+ "text",
2212
+ "image",
2213
+ "pdf"
2214
+ ],
2215
+ "output": [
2216
+ "text"
2217
+ ]
2218
+ },
2219
+ "open_weights": false,
2220
+ "limit": {
2221
+ "context": 1050000,
2222
+ "input": 922000,
2223
+ "output": 128000
2224
+ },
2225
+ "experimental": {
2226
+ "modes": {
2227
+ "fast": {
2228
+ "cost": {
2229
+ "input": 10,
2230
+ "output": 60,
2231
+ "cache_read": 1,
2232
+ "cache_write": 12.5
2233
+ },
2234
+ "provider": {
2235
+ "body": {
2236
+ "service_tier": "priority"
2237
+ }
2238
+ }
2239
+ },
2240
+ "pro": {
2241
+ "provider": {
2242
+ "body": {
2243
+ "reasoning": {
2244
+ "mode": "pro"
2245
+ }
2246
+ }
2247
+ }
2248
+ }
2249
+ }
2250
+ },
2251
+ "cost": {
2252
+ "input": 5,
2253
+ "output": 30,
2254
+ "cache_read": 0.5,
2255
+ "cache_write": 6.25,
2256
+ "tiers": [
2257
+ {
2258
+ "input": 10,
2259
+ "output": 45,
2260
+ "cache_read": 1,
2261
+ "cache_write": 12.5,
2262
+ "tier": {
2263
+ "type": "context",
2264
+ "size": 272000
2265
+ }
2266
+ }
2267
+ ],
2268
+ "context_over_200k": {
2269
+ "input": 10,
2270
+ "output": 45,
2271
+ "cache_read": 1,
2272
+ "cache_write": 12.5
2273
+ }
2274
+ }
2275
+ },
1854
2276
  "gpt-5-codex": {
1855
2277
  "id": "gpt-5-codex",
1856
2278
  "name": "GPT-5-Codex",
@@ -75,6 +75,17 @@ module LLM::ActiveRecord
75
75
  @llm
76
76
  end
77
77
 
78
+ ##
79
+ # @note
80
+ # This method does not persist to the database,
81
+ # but it can inspect and alter runtime state in
82
+ # a way that is temporary.
83
+ # @param (see LLM::Agent#repl)
84
+ # @return (see LLM::Agent#repl)
85
+ def repl(**params)
86
+ ctx.repl(**params)
87
+ end
88
+
78
89
  private
79
90
 
80
91
  ##
data/lib/llm/agent.rb CHANGED
@@ -398,17 +398,25 @@ module LLM
398
398
  # By default this method disables the tracer for
399
399
  # the duration of the repl session, and restores
400
400
  # it afterwards.
401
- # @param [Boolean] trace
401
+ # @param [Boolean] tracer
402
402
  # When true, the tracer is kept alive during the
403
403
  # repl session. Default is false.
404
+ # @param [Array<LLM::Tool>] tools
405
+ # Extra tools to attach for the repl session
406
+ # @param [Array<String>] skills
407
+ # Extra skills to attach for the repl session
404
408
  # @return [void]
405
- def repl(trace: false)
406
- if !trace
407
- previous = tracer
409
+ def repl(tracer: false, trace: nil, tools: [], skills: [])
410
+ if trace != nil
411
+ warn "llm.rb: trace option is deprecated, use tracer instead"
412
+ tracer = trace
413
+ end
414
+ if !tracer
415
+ previous = self.tracer
408
416
  self.tracer = nil
409
417
  end
410
418
  require_relative "repl" unless defined?(::LLM::Repl)
411
- LLM::Repl.new(self).start
419
+ LLM::Repl.new(agent: self, tools:, skills:).start
412
420
  ensure
413
421
  if !trace
414
422
  self.tracer = previous
@@ -526,7 +534,7 @@ module LLM
526
534
  max = params.key?(:tool_attempts) ? params.delete(:tool_attempts) : 25
527
535
  max = Integer(max) if max
528
536
  stream = params[:stream] || @ctx.params[:stream]
529
- stream.extra[:concurrency] = concurrency if LLM::Stream === stream
537
+ params[:stream] = LLM::Stream.try(stream, extra: {concurrency:})
530
538
  res = talk.call(apply_instructions(prompt), params)
531
539
  while @ctx.functions?
532
540
  if max
data/lib/llm/compactor.rb CHANGED
@@ -71,13 +71,13 @@ class LLM::Compactor
71
71
  retention_window = [config[:retention_window], messages.size].min
72
72
  return nil unless messages.size > retention_window
73
73
  stream = ctx.params[:stream]
74
- stream.on_compaction(ctx, self) if LLM::Stream === stream
74
+ stream.on_compaction(ctx, self)
75
75
  recent = retained_messages
76
76
  older = messages[0...(messages.size - recent.size)]
77
77
  summary = LLM::Message.new(ctx.llm.user_role, "[Previous conversation summary]\n\n#{summarize(older)}", {compaction: true})
78
78
  ctx.messages.replace([*ctx.messages.take_while(&:system?), summary, *recent])
79
79
  ctx.compacted = true
80
- stream.on_compaction_finish(ctx, self) if LLM::Stream === stream
80
+ stream.on_compaction_finish(ctx, self)
81
81
  summary
82
82
  end
83
83
 
data/lib/llm/context.rb CHANGED
@@ -92,6 +92,8 @@ module LLM
92
92
  @params[:tools] = tools unless tools.empty?
93
93
  @params[:store] ||= false if @mode == :responses
94
94
  @messages = LLM::Buffer.new(llm)
95
+ extra = @params.slice(:model, :tools).merge!(ctx: self, tracer:)
96
+ @params[:stream] = LLM::Stream.try(@params[:stream], extra:)
95
97
  end
96
98
 
97
99
  ##
@@ -310,10 +312,7 @@ module LLM
310
312
  # A list of functions to exclude from the wait
311
313
  # @return [Array<LLM::Function::Return>]
312
314
  def wait(strategy, except: [])
313
- if LLM::Stream === stream && !stream.queue.empty?
314
- @queue = stream.queue
315
- @queue.wait
316
- else
315
+ if stream.queue.empty?
317
316
  tools = except.empty? ? functions : functions - except
318
317
  guards = guarded_returns(tools:)
319
318
  return guards if guards
@@ -321,6 +320,9 @@ module LLM
321
320
  returns = @queue.wait
322
321
  emit_tool_returns(tools, returns)
323
322
  returns
323
+ else
324
+ @queue = stream.queue
325
+ @queue.wait
324
326
  end
325
327
  ensure
326
328
  @queue = nil
@@ -502,25 +504,11 @@ module LLM
502
504
 
503
505
  private
504
506
 
505
- ##
506
- # Binds runtime metadata onto an active stream.
507
- # @api private
508
- def bind!(stream, model, tools)
509
- return unless LLM::Stream === stream
510
- @stream = stream
511
- stream.extra[:ctx] = self
512
- stream.extra[:tracer] = tracer
513
- stream.extra[:model] = model
514
- stream.extra[:tools] = tools
515
- end
516
-
517
507
  ##
518
508
  # Returns the bound stream queue, if available.
519
509
  # @api private
520
510
  def queue
521
- [@queue, stream&.queue].compact.first
522
- rescue NoMethodError
523
- nil
511
+ [@queue, stream.queue].compact.first
524
512
  end
525
513
 
526
514
  ##
@@ -543,12 +531,13 @@ module LLM
543
531
  # Rewrites a prompt and params through the configured transformer.
544
532
  # @api private
545
533
  def transform(prompt, params)
534
+ transformer = self.transformer
546
535
  return [prompt, params] unless transformer
547
536
  stream = params[:stream]
548
- stream.on_transform(self, transformer) if LLM::Stream === stream
537
+ stream.on_transform(self, transformer)
549
538
  transformer.call(self, prompt, params)
550
539
  ensure
551
- stream.on_transform_finish(self, transformer) if LLM::Stream === stream
540
+ stream.on_transform_finish(self, transformer) if transformer
552
541
  end
553
542
 
554
543
  ##
@@ -556,8 +545,10 @@ module LLM
556
545
  # @api private
557
546
  def respond(prompt, params)
558
547
  params = @params.merge(params)
548
+ extra = params.slice(:model, :tools).merge!(ctx: self, tracer:)
549
+ params[:stream] = LLM::Stream.try(params[:stream], extra:)
559
550
  prompt, params = transform(prompt, params)
560
- bind!(params[:stream], params[:model], params[:tools])
551
+ @stream = params[:stream]
561
552
  res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
562
553
  input = res_id ? [] : @messages.to_a
563
554
  params = params.merge(previous_response_id: res_id, input:).compact
@@ -570,8 +561,10 @@ module LLM
570
561
  def complete(prompt, params)
571
562
  params = params.merge(messages: @messages.to_a)
572
563
  params = @params.merge(params)
564
+ extra = params.slice(:model, :tools).merge!(ctx: self, tracer:)
565
+ params[:stream] = LLM::Stream.try(params[:stream], extra:)
573
566
  prompt, params = transform(prompt, params)
574
- bind!(params[:stream], params[:model], params[:tools])
567
+ @stream = params[:stream]
575
568
  [prompt, params, @llm.complete(prompt, params)]
576
569
  end
577
570
 
@@ -590,7 +583,6 @@ module LLM
590
583
  # Emits tool return callbacks for directly waited function work.
591
584
  # @api private
592
585
  def emit_tool_returns(tools, returns)
593
- return unless LLM::Stream === stream
594
586
  returns.each_with_index { |result, index| stream.on_tool_return(tools[index], result) }
595
587
  end
596
588
 
data/lib/llm/object.rb CHANGED
@@ -148,6 +148,19 @@ class LLM::Object < BasicObject
148
148
  SINGLETON.from @h.merge(other)
149
149
  end
150
150
 
151
+ ##
152
+ # @param [Hash, #to_h] other
153
+ # The hash to merge in place
154
+ # @return [LLM::Object]
155
+ # Returns self
156
+ def merge!(other = UNDEFINED)
157
+ return SINGLETON.get(@h, :merge!) if other.equal?(UNDEFINED)
158
+ other = ::Hash.try_convert(other)
159
+ raise TypeError, "#{other} cannot be coerced into a Hash" unless other
160
+ @h.merge!(other)
161
+ self
162
+ end
163
+
151
164
  ##
152
165
  # @param [#to_s, #to_sym] k
153
166
  # The key name
data/lib/llm/provider.rb CHANGED
@@ -71,6 +71,16 @@ class LLM::Provider
71
71
  raise NotImplementedError
72
72
  end
73
73
 
74
+ ##
75
+ # @note
76
+ # This feature is not implemented by all providers,
77
+ # and it will raise NotImplementedError for providers
78
+ # that do not support it.
79
+ # @return [LLM::Response]
80
+ def ocr(...)
81
+ raise NotImplementedError
82
+ end
83
+
74
84
  ##
75
85
  # Provides an interface to the chat completions API
76
86
  # @example
@@ -332,13 +342,6 @@ class LLM::Provider
332
342
  transport.request_owner
333
343
  end
334
344
 
335
- ##
336
- # @param [Object] stream
337
- # @return [Boolean]
338
- def streamable?(stream)
339
- LLM::Stream === stream || stream.respond_to?(:<<)
340
- end
341
-
342
345
  ##
343
346
  # @return [Boolean]
344
347
  # Returns true when an API key is configured
@@ -151,9 +151,8 @@ module LLM
151
151
  params = {role: :user, model: default_model, max_tokens: 1024}.merge!(params)
152
152
  tools = resolve_tools(params.delete(:tools))
153
153
  params = [params, adapt_tools(tools)].inject({}, &:merge!).compact
154
- role, stream = params.delete(:role), params.delete(:stream)
155
- params[:stream] = true if streamable?(stream) || stream == true
156
- [params, stream, tools, role]
154
+ role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
155
+ [params.merge!(stream: stream.enabled?), stream, tools, role]
157
156
  end
158
157
 
159
158
  def build_complete_request(prompt, params, role)