llm.rb 4.14.0 → 4.16.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.
@@ -31,7 +31,7 @@ class LLM::OpenAI
31
31
  # @return [LLM::Response]
32
32
  def all(**params)
33
33
  query = URI.encode_www_form(params)
34
- req = Net::HTTP::Get.new("/v1/vector_stores?#{query}", headers)
34
+ req = Net::HTTP::Get.new(path("/vector_stores?#{query}"), headers)
35
35
  res, span, tracer = execute(request: req, operation: "request")
36
36
  res = ResponseAdapter.adapt(res, type: :enumerable)
37
37
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -47,7 +47,7 @@ class LLM::OpenAI
47
47
  # @return [LLM::Response]
48
48
  # @see https://platform.openai.com/docs/api-reference/vector_stores/create OpenAI docs
49
49
  def create(name:, file_ids: nil, **params)
50
- req = Net::HTTP::Post.new("/v1/vector_stores", headers)
50
+ req = Net::HTTP::Post.new(path("/vector_stores"), headers)
51
51
  req.body = LLM.json.dump(params.merge({name:, file_ids:}).compact)
52
52
  res, span, tracer = execute(request: req, operation: "request")
53
53
  res = LLM::Response.new(res)
@@ -72,7 +72,7 @@ class LLM::OpenAI
72
72
  # @see https://platform.openai.com/docs/api-reference/vector_stores/retrieve OpenAI docs
73
73
  def get(vector:)
74
74
  vector_id = vector.respond_to?(:id) ? vector.id : vector
75
- req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}", headers)
75
+ req = Net::HTTP::Get.new(path("/vector_stores/#{vector_id}"), headers)
76
76
  res, span, tracer = execute(request: req, operation: "request")
77
77
  res = LLM::Response.new(res)
78
78
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -89,7 +89,7 @@ class LLM::OpenAI
89
89
  # @see https://platform.openai.com/docs/api-reference/vector_stores/modify OpenAI docs
90
90
  def modify(vector:, name: nil, **params)
91
91
  vector_id = vector.respond_to?(:id) ? vector.id : vector
92
- req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}", headers)
92
+ req = Net::HTTP::Post.new(path("/vector_stores/#{vector_id}"), headers)
93
93
  req.body = LLM.json.dump(params.merge({name:}).compact)
94
94
  res, span, tracer = execute(request: req, operation: "request")
95
95
  res = LLM::Response.new(res)
@@ -105,7 +105,7 @@ class LLM::OpenAI
105
105
  # @see https://platform.openai.com/docs/api-reference/vector_stores/delete OpenAI docs
106
106
  def delete(vector:)
107
107
  vector_id = vector.respond_to?(:id) ? vector.id : vector
108
- req = Net::HTTP::Delete.new("/v1/vector_stores/#{vector_id}", headers)
108
+ req = Net::HTTP::Delete.new(path("/vector_stores/#{vector_id}"), headers)
109
109
  res, span, tracer = execute(request: req, operation: "request")
110
110
  res = LLM::Response.new(res)
111
111
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -122,7 +122,7 @@ class LLM::OpenAI
122
122
  # @see https://platform.openai.com/docs/api-reference/vector_stores/search OpenAI docs
123
123
  def search(vector:, query:, **params)
124
124
  vector_id = vector.respond_to?(:id) ? vector.id : vector
125
- req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/search", headers)
125
+ req = Net::HTTP::Post.new(path("/vector_stores/#{vector_id}/search"), headers)
126
126
  req.body = LLM.json.dump(params.merge({query:}).compact)
127
127
  res, span, tracer = execute(request: req, operation: "retrieval")
128
128
  res = ResponseAdapter.adapt(res, type: :enumerable)
@@ -140,7 +140,7 @@ class LLM::OpenAI
140
140
  def all_files(vector:, **params)
141
141
  vector_id = vector.respond_to?(:id) ? vector.id : vector
142
142
  query = URI.encode_www_form(params)
143
- req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}/files?#{query}", headers)
143
+ req = Net::HTTP::Get.new(path("/vector_stores/#{vector_id}/files?#{query}"), headers)
144
144
  res, span, tracer = execute(request: req, operation: "request")
145
145
  res = ResponseAdapter.adapt(res, type: :enumerable)
146
146
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -159,7 +159,7 @@ class LLM::OpenAI
159
159
  def add_file(vector:, file:, attributes: nil, **params)
160
160
  vector_id = vector.respond_to?(:id) ? vector.id : vector
161
161
  file_id = file.respond_to?(:id) ? file.id : file
162
- req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/files", headers)
162
+ req = Net::HTTP::Post.new(path("/vector_stores/#{vector_id}/files"), headers)
163
163
  req.body = LLM.json.dump(params.merge({file_id:, attributes:}).compact)
164
164
  res, span, tracer = execute(request: req, operation: "request")
165
165
  res = LLM::Response.new(res)
@@ -190,7 +190,7 @@ class LLM::OpenAI
190
190
  def update_file(vector:, file:, attributes:, **params)
191
191
  vector_id = vector.respond_to?(:id) ? vector.id : vector
192
192
  file_id = file.respond_to?(:id) ? file.id : file
193
- req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/files/#{file_id}", headers)
193
+ req = Net::HTTP::Post.new(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
194
194
  req.body = LLM.json.dump(params.merge({attributes:}).compact)
195
195
  res, span, tracer = execute(request: req, operation: "request")
196
196
  res = LLM::Response.new(res)
@@ -209,7 +209,7 @@ class LLM::OpenAI
209
209
  vector_id = vector.respond_to?(:id) ? vector.id : vector
210
210
  file_id = file.respond_to?(:id) ? file.id : file
211
211
  query = URI.encode_www_form(params)
212
- req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}/files/#{file_id}?#{query}", headers)
212
+ req = Net::HTTP::Get.new(path("/vector_stores/#{vector_id}/files/#{file_id}?#{query}"), headers)
213
213
  res, span, tracer = execute(request: req, operation: "request")
214
214
  res = LLM::Response.new(res)
215
215
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -226,7 +226,7 @@ class LLM::OpenAI
226
226
  def delete_file(vector:, file:)
227
227
  vector_id = vector.respond_to?(:id) ? vector.id : vector
228
228
  file_id = file.respond_to?(:id) ? file.id : file
229
- req = Net::HTTP::Delete.new("/v1/vector_stores/#{vector_id}/files/#{file_id}", headers)
229
+ req = Net::HTTP::Delete.new(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
230
230
  res, span, tracer = execute(request: req, operation: "request")
231
231
  res = LLM::Response.new(res)
232
232
  tracer.on_request_finish(operation: "request", res:, span:)
@@ -259,7 +259,7 @@ class LLM::OpenAI
259
259
 
260
260
  private
261
261
 
262
- [:headers, :execute, :set_body_stream].each do |m|
262
+ [:path, :headers, :execute, :set_body_stream].each do |m|
263
263
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
264
264
  end
265
265
  end
@@ -32,8 +32,8 @@ module LLM
32
32
 
33
33
  ##
34
34
  # @param key (see LLM::Provider#initialize)
35
- def initialize(**)
36
- super(host: HOST, **)
35
+ def initialize(base_path: "/v1", **)
36
+ super(host: HOST, base_path:, **)
37
37
  end
38
38
 
39
39
  ##
@@ -52,7 +52,7 @@ module LLM
52
52
  # @raise (see LLM::Provider#request)
53
53
  # @return (see LLM::Provider#embed)
54
54
  def embed(input, model: "text-embedding-3-small", **params)
55
- req = Net::HTTP::Post.new("/v1/embeddings", headers)
55
+ req = Net::HTTP::Post.new(path("/embeddings"), headers)
56
56
  req.body = LLM.json.dump({input:, model:}.merge!(params))
57
57
  res, span, tracer = execute(request: req, operation: "embeddings", model:)
58
58
  res = ResponseAdapter.adapt(res, type: :embedding)
@@ -187,7 +187,7 @@ module LLM
187
187
  private
188
188
 
189
189
  def completions_path
190
- "/v1/chat/completions"
190
+ path("/chat/completions")
191
191
  end
192
192
 
193
193
  def headers
data/lib/llm/response.rb CHANGED
@@ -2,10 +2,18 @@
2
2
 
3
3
  module LLM
4
4
  ##
5
- # {LLM::Response LLM::Response} encapsulates a response
6
- # from an LLM provider. It is returned by all methods
7
- # that make requests to a provider, and sometimes extended
8
- # with provider-specific functionality.
5
+ # {LLM::Response LLM::Response} is the normalized base shape for
6
+ # provider and endpoint responses in llm.rb.
7
+ #
8
+ # Provider calls return an instance of this class, then extend it
9
+ # with provider-, endpoint-, or context-specific modules so response
10
+ # handling can share one common surface without flattening away
11
+ # specialized behavior.
12
+ #
13
+ # The normalized response still keeps the original
14
+ # {Net::HTTPResponse Net::HTTPResponse} available through {#res}
15
+ # when callers need direct access to raw HTTP details such as
16
+ # headers, status codes, or unadapted bodies.
9
17
  class Response
10
18
  require "json"
11
19
 
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Sequel
4
+ ##
5
+ # Sequel plugin for persisting {LLM::Context LLM::Context} state.
6
+ #
7
+ # This plugin maps model columns onto provider selection, model
8
+ # selection, usage accounting, and serialized context data while
9
+ # leaving application-specific concerns such as credentials,
10
+ # associations, and UI shaping to the host app.
11
+ #
12
+ # Context state can be stored as a JSON string (`format: :string`, the
13
+ # default) or as a structured object (`format: :json` / `:jsonb`) for
14
+ # databases such as PostgreSQL that can persist JSON natively.
15
+ # `:json` and `:jsonb` expect a real JSON column type with Sequel handling
16
+ # JSON typecasting for the model.
17
+ module Plugin
18
+ EMPTY_HASH = {}.freeze
19
+ DEFAULT_USAGE_COLUMNS = {
20
+ input_tokens: :input_tokens,
21
+ output_tokens: :output_tokens,
22
+ total_tokens: :total_tokens
23
+ }.freeze
24
+ DEFAULTS = {
25
+ provider_column: :provider,
26
+ model_column: :model,
27
+ data_column: :data,
28
+ format: :string,
29
+ usage_columns: DEFAULT_USAGE_COLUMNS,
30
+ provider: EMPTY_HASH,
31
+ context: EMPTY_HASH
32
+ }.freeze
33
+
34
+ ##
35
+ # Called by Sequel when the plugin is first applied to a model class.
36
+ #
37
+ # This hook installs the plugin's class- and instance-level behavior on
38
+ # the target model. It runs before {configure}, so it should only attach
39
+ # methods and not depend on per-model plugin options.
40
+ #
41
+ # @param [Class] model
42
+ # @return [void]
43
+ def self.apply(model, **)
44
+ model.extend ClassMethods
45
+ model.include InstanceMethods
46
+ end
47
+
48
+ ##
49
+ # Called by Sequel after {apply} with the options passed to
50
+ # `plugin :llm, ...`.
51
+ #
52
+ # This hook merges plugin defaults with the model's explicit settings and
53
+ # stores the resolved configuration on the model class for later use by
54
+ # instance methods such as {InstanceMethods#llm} and {InstanceMethods#ctx}.
55
+ #
56
+ # @param [Class] model
57
+ # @param [Hash] options
58
+ # @option options [Symbol] :format
59
+ # Storage format for the serialized context. Use `:string` for text
60
+ # columns, or `:json` / `:jsonb` for structured JSON columns with Sequel
61
+ # JSON typecasting enabled.
62
+ # @return [void]
63
+ def self.configure(model, options = EMPTY_HASH)
64
+ options = DEFAULTS.merge(options)
65
+ usage_columns = DEFAULT_USAGE_COLUMNS.merge(options[:usage_columns] || EMPTY_HASH)
66
+ model.instance_variable_set(
67
+ :@llm_plugin_options,
68
+ options.merge(usage_columns: usage_columns.freeze).freeze
69
+ )
70
+ end
71
+ end
72
+
73
+ module Plugin::ClassMethods
74
+ ##
75
+ # @return [Hash]
76
+ def llm_plugin_options
77
+ @llm_plugin_options || DEFAULTS
78
+ end
79
+ end
80
+
81
+ module Plugin::InstanceMethods
82
+ ##
83
+ # Continues the stored context with new input and flushes it.
84
+ # @see LLM::Context#talk
85
+ # @return [LLM::Response]
86
+ def talk(...)
87
+ ctx.talk(...).tap { flush }
88
+ end
89
+
90
+ ##
91
+ # Continues the stored context through the Responses API and flushes it.
92
+ # @see LLM::Context#respond
93
+ # @return [LLM::Response]
94
+ def respond(...)
95
+ ctx.respond(...).tap { flush }
96
+ end
97
+
98
+ ##
99
+ # Waits for queued tool work to finish.
100
+ # @see LLM::Context#wait
101
+ # @return [Array<LLM::Function::Return>]
102
+ def wait(...)
103
+ ctx.wait(...)
104
+ end
105
+
106
+ ##
107
+ # Calls into the stored context.
108
+ # @see LLM::Context#call
109
+ # @return [Object]
110
+ def call(...)
111
+ ctx.call(...)
112
+ end
113
+
114
+ ##
115
+ # @see LLM::Context#messages
116
+ # @return [Array<LLM::Message>]
117
+ def messages
118
+ ctx.messages
119
+ end
120
+
121
+ ##
122
+ # @note The bang is used because Sequel reserves `model` for the
123
+ # underlying model class on instances.
124
+ # @see LLM::Context#model
125
+ # @return [String]
126
+ def model!
127
+ ctx.model
128
+ end
129
+
130
+ ##
131
+ # @see LLM::Context#functions
132
+ # @return [Array<LLM::Function>]
133
+ def functions
134
+ ctx.functions
135
+ end
136
+
137
+ ##
138
+ # @see LLM::Context#cost
139
+ # @return [LLM::Cost]
140
+ def cost
141
+ ctx.cost
142
+ end
143
+
144
+ ##
145
+ # @see LLM::Context#context_window
146
+ # @return [Integer]
147
+ def context_window
148
+ ctx.context_window
149
+ rescue LLM::NoSuchModelError, LLM::NoSuchRegistryError
150
+ 0
151
+ end
152
+
153
+ ##
154
+ # Returns usage from the mapped usage columns.
155
+ # @return [LLM::Object]
156
+ def usage
157
+ LLM::Object.from(
158
+ input_tokens: self[columns[:input_tokens]] || 0,
159
+ output_tokens: self[columns[:output_tokens]] || 0,
160
+ total_tokens: self[columns[:total_tokens]] || 0
161
+ )
162
+ end
163
+
164
+ private
165
+
166
+ ##
167
+ # Returns the resolved provider instance for this record.
168
+ # @return [LLM::Provider]
169
+ def llm
170
+ options = self.class.llm_plugin_options
171
+ provider = self[columns[:provider_column]]
172
+ kwargs = resolve_options(options[:provider])
173
+ @llm ||= LLM.method(provider).call(**kwargs)
174
+ end
175
+
176
+ ##
177
+ # @return [LLM::Context]
178
+ def ctx
179
+ @ctx ||= begin
180
+ options = self.class.llm_plugin_options
181
+ params = resolve_options(options[:context]).dup
182
+ params[:model] ||= self[columns[:model_column]]
183
+ ctx = LLM::Context.new(llm, params.compact)
184
+ data = self[columns[:data_column]]
185
+ if data.nil? || data == ""
186
+ ctx
187
+ else
188
+ string = case options[:format]
189
+ when :string then data
190
+ when :json, :jsonb then LLM.json.dump(data)
191
+ else raise ArgumentError, "Unknown format: #{options[:format].inspect}"
192
+ end
193
+ ctx.restore(string:)
194
+ end
195
+ end
196
+ end
197
+
198
+ ##
199
+ # @return [void]
200
+ def flush
201
+ options = self.class.llm_plugin_options
202
+ update({
203
+ columns[:data_column] => serialize_context(options[:format]),
204
+ columns[:input_tokens] => ctx.usage.input_tokens,
205
+ columns[:output_tokens] => ctx.usage.output_tokens,
206
+ columns[:total_tokens] => ctx.usage.total_tokens
207
+ })
208
+ end
209
+
210
+ ##
211
+ # @return [Hash]
212
+ def resolve_option(option)
213
+ case option
214
+ when Proc then instance_exec(&option)
215
+ when Hash then option.dup
216
+ else option
217
+ end
218
+ end
219
+
220
+ ##
221
+ # @return [Hash]
222
+ def resolve_options(option)
223
+ case option
224
+ when Proc, Hash then resolve_option(option)
225
+ else EMPTY_HASH.dup
226
+ end
227
+ end
228
+
229
+ def serialize_context(format)
230
+ case format
231
+ when :string then ctx.to_json
232
+ when :json, :jsonb then ctx.to_h
233
+ else raise ArgumentError, "Unknown format: #{format.inspect}"
234
+ end
235
+ end
236
+
237
+ def columns
238
+ @columns ||= begin
239
+ options = self.class.llm_plugin_options
240
+ usage_columns = options[:usage_columns]
241
+ {
242
+ provider_column: options[:provider_column],
243
+ model_column: options[:model_column],
244
+ data_column: options[:data_column],
245
+ input_tokens: usage_columns[:input_tokens],
246
+ output_tokens: usage_columns[:output_tokens],
247
+ total_tokens: usage_columns[:total_tokens]
248
+ }.freeze
249
+ end
250
+ end
251
+ end
252
+ end
@@ -54,9 +54,9 @@ class LLM::Stream
54
54
  private
55
55
 
56
56
  def fire_hooks(tasks, results)
57
- results.each_with_index do |ret, idx|
57
+ results.each_with_index do |result, idx|
58
58
  tool = tasks[idx]&.function
59
- @stream.on_tool_return(tool, ret) if tool
59
+ @stream.on_tool_return(tool, result) if tool
60
60
  end
61
61
  results
62
62
  end
data/lib/llm/stream.rb CHANGED
@@ -86,10 +86,10 @@ module LLM
86
86
  # `tool.spawn(:fiber)`, or `tool.spawn(:task)`.
87
87
  # @param [LLM::Function] tool
88
88
  # The tool that returned.
89
- # @param [LLM::Function::Return] ret
89
+ # @param [LLM::Function::Return] result
90
90
  # The completed tool return.
91
91
  # @return [nil]
92
- def on_tool_return(tool, ret)
92
+ def on_tool_return(tool, result)
93
93
  nil
94
94
  end
95
95
 
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "4.14.0"
4
+ VERSION = "4.16.0"
5
5
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ require "llm/sequel/plugin"
6
+ Llm = LLM::Sequel::Plugin
7
+ end
8
+ 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: 4.14.0
4
+ version: 4.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri
@@ -228,6 +228,8 @@ files:
228
228
  - data/xai.json
229
229
  - data/zai.json
230
230
  - lib/llm.rb
231
+ - lib/llm/active_record.rb
232
+ - lib/llm/active_record/acts_as_llm.rb
231
233
  - lib/llm/agent.rb
232
234
  - lib/llm/bot.rb
233
235
  - lib/llm/buffer.rb
@@ -367,6 +369,7 @@ files:
367
369
  - lib/llm/schema/parser.rb
368
370
  - lib/llm/schema/string.rb
369
371
  - lib/llm/schema/version.rb
372
+ - lib/llm/sequel/plugin.rb
370
373
  - lib/llm/server_tool.rb
371
374
  - lib/llm/session.rb
372
375
  - lib/llm/stream.rb
@@ -381,6 +384,7 @@ files:
381
384
  - lib/llm/usage.rb
382
385
  - lib/llm/utils.rb
383
386
  - lib/llm/version.rb
387
+ - lib/sequel/plugins/llm.rb
384
388
  - llm.gemspec
385
389
  homepage: https://github.com/llmrb/llm.rb
386
390
  licenses: