llm.rb 15.2.2 → 15.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +197 -3
- data/README.md +180 -64
- data/bin/llm.rb +9 -2
- data/data/alibaba.json +45 -0
- data/data/anthropic.json +67 -0
- data/data/bedrock.json +1466 -397
- data/data/deepinfra.json +148 -16
- data/data/deepseek.json +3 -0
- data/data/mistral.json +42 -0
- data/data/openai.json +186 -0
- data/data/openrouter.json +1538 -378
- data/data/xai.json +53 -20
- data/data/zai.json +90 -4
- data/docs/deepdive/advanced/compaction.md +1 -2
- data/docs/deepdive/advanced/context.md +214 -1
- data/docs/deepdive/advanced/guard.md +9 -57
- data/docs/deepdive/features/builtin_tools.md +14 -16
- data/docs/deepdive/features/console.md +5 -0
- data/docs/deepdive/features/database.md +85 -10
- data/docs/deepdive/fundamentals/agents.md +7 -8
- data/docs/deepdive/fundamentals/providers.md +45 -5
- data/docs/deepdive/fundamentals/schema.md +73 -0
- data/docs/deepdive/fundamentals/tools.md +80 -27
- data/docs/deepdive/media/audio.md +8 -19
- data/docs/deepdive/media/images.md +8 -10
- data/docs/deepdive/media/ocr.md +1 -3
- data/docs/deepdive/reference/cost.md +48 -0
- data/docs/deepdive/reference/tracer.md +76 -0
- data/docs/deepdive.md +1 -1
- data/lib/llm/active_record/message.rb +113 -0
- data/lib/llm/active_record.rb +1 -0
- data/lib/llm/agent.rb +44 -19
- data/lib/llm/console/buffer.rb +9 -1
- data/lib/llm/console.rb +6 -1
- data/lib/llm/context/deserializer.rb +10 -3
- data/lib/llm/context.rb +62 -26
- data/lib/llm/guard.rb +2 -8
- data/lib/llm/message.rb +18 -7
- data/lib/llm/provider.rb +74 -16
- data/lib/llm/providers/alibaba.rb +15 -0
- data/lib/llm/providers/anthropic/error_handler.rb +5 -2
- data/lib/llm/providers/anthropic/files.rb +12 -12
- data/lib/llm/providers/anthropic/models.rb +2 -2
- data/lib/llm/providers/anthropic.rb +5 -3
- data/lib/llm/providers/bedrock/error_handler.rb +3 -2
- data/lib/llm/providers/bedrock/models.rb +5 -3
- data/lib/llm/providers/bedrock.rb +5 -3
- data/lib/llm/providers/deepinfra/audio.rb +4 -4
- data/lib/llm/providers/deepinfra/images.rb +4 -4
- data/lib/llm/providers/google/error_handler.rb +5 -2
- data/lib/llm/providers/google/files.rb +10 -10
- data/lib/llm/providers/google/images.rb +2 -2
- data/lib/llm/providers/google/models.rb +2 -2
- data/lib/llm/providers/google.rb +7 -7
- data/lib/llm/providers/mistral.rb +3 -1
- data/lib/llm/providers/ollama/error_handler.rb +5 -2
- data/lib/llm/providers/ollama/models.rb +2 -2
- data/lib/llm/providers/ollama.rb +7 -5
- data/lib/llm/providers/openai/audio.rb +6 -6
- data/lib/llm/providers/openai/error_handler.rb +5 -2
- data/lib/llm/providers/openai/files.rb +10 -10
- data/lib/llm/providers/openai/images.rb +4 -4
- data/lib/llm/providers/openai/models.rb +2 -2
- data/lib/llm/providers/openai/moderations.rb +2 -2
- data/lib/llm/providers/openai/request_adapter.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +8 -8
- data/lib/llm/providers/openai/vector_stores.rb +22 -22
- data/lib/llm/providers/openai.rb +10 -8
- data/lib/llm/providers/xai/images.rb +4 -4
- data/lib/llm/schema.rb +24 -0
- data/lib/llm/tracer/telemetry.rb +4 -4
- data/lib/llm/tracer.rb +11 -3
- data/lib/llm/transport/execution.rb +8 -4
- data/lib/llm/utils.rb +13 -0
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +2 -2
- metadata +5 -5
- data/lib/llm/guard/loop.rb +0 -89
|
@@ -35,13 +35,13 @@ class LLM::OpenAI
|
|
|
35
35
|
req = LLM::Transport::Request.post(path("/audio/speech"), headers)
|
|
36
36
|
req.body = LLM.json.dump({input:, voice:, model:, response_format:}.merge!(params))
|
|
37
37
|
io = StringIO.new("".b)
|
|
38
|
-
res, span, tracer = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
|
|
38
|
+
res, span, tracer, request_id = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
|
|
39
39
|
content_type = res["content-type"].to_s.split(";").first
|
|
40
40
|
content_type = content_type.empty? ? LLM::Mime[".#{response_format}"] : content_type
|
|
41
41
|
data = "data:#{content_type};base64,#{[io.string].pack("m0")}"
|
|
42
42
|
res.body = LLM::Object.from(audio: data)
|
|
43
43
|
res = ResponseAdapter.adapt(LLM::Response.new(res), type: :audio)
|
|
44
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
44
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
|
|
45
45
|
res
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -62,9 +62,9 @@ class LLM::OpenAI
|
|
|
62
62
|
req = LLM::Transport::Request.post(path("/audio/transcriptions"), headers)
|
|
63
63
|
req["content-type"] = multi.content_type
|
|
64
64
|
transport.set_body_stream(req, multi.body)
|
|
65
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
65
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
66
66
|
res = LLM::Response.new(res)
|
|
67
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
67
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
|
|
68
68
|
res
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -86,9 +86,9 @@ class LLM::OpenAI
|
|
|
86
86
|
req = LLM::Transport::Request.post(path("/audio/translations"), headers)
|
|
87
87
|
req["content-type"] = multi.content_type
|
|
88
88
|
transport.set_body_stream(req, multi.body)
|
|
89
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
89
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
90
90
|
res = LLM::Response.new(res)
|
|
91
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
91
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
|
|
92
92
|
res
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -21,10 +21,13 @@ class LLM::OpenAI
|
|
|
21
21
|
# The span
|
|
22
22
|
# @param [LLM::Transport::Response, Net::HTTPResponse] res
|
|
23
23
|
# The response from the server
|
|
24
|
+
# @param [String] request_id
|
|
25
|
+
# The id of the request that failed
|
|
24
26
|
# @return [LLM::OpenAI::ErrorHandler]
|
|
25
|
-
def initialize(tracer, span, res)
|
|
27
|
+
def initialize(tracer, span, res, request_id)
|
|
26
28
|
@tracer = tracer
|
|
27
29
|
@span = span
|
|
30
|
+
@request_id = request_id
|
|
28
31
|
@res = LLM::Transport::Response.from(res)
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -33,7 +36,7 @@ class LLM::OpenAI
|
|
|
33
36
|
# Raises a subclass of {LLM::Error LLM::Error}
|
|
34
37
|
def raise_error!
|
|
35
38
|
ex = error
|
|
36
|
-
@tracer.on_request_error(ex:, span:)
|
|
39
|
+
@tracer.on_request_error(ex:, span:, request_id: @request_id)
|
|
37
40
|
ensure
|
|
38
41
|
raise(ex) if ex
|
|
39
42
|
end
|
|
@@ -41,9 +41,9 @@ class LLM::OpenAI
|
|
|
41
41
|
def all(**params)
|
|
42
42
|
query = URI.encode_www_form(params)
|
|
43
43
|
req = LLM::Transport::Request.get(path("/files?#{query}"), headers)
|
|
44
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
44
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
45
45
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
46
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
46
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
47
47
|
res
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -63,9 +63,9 @@ class LLM::OpenAI
|
|
|
63
63
|
req = LLM::Transport::Request.post(path("/files"), headers)
|
|
64
64
|
req["content-type"] = multi.content_type
|
|
65
65
|
transport.set_body_stream(req, multi.body)
|
|
66
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
66
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
67
67
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
68
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
68
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
69
69
|
res
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -84,9 +84,9 @@ class LLM::OpenAI
|
|
|
84
84
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
85
85
|
query = URI.encode_www_form(params)
|
|
86
86
|
req = LLM::Transport::Request.get(path("/files/#{file_id}?#{query}"), headers)
|
|
87
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
87
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
88
88
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
89
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
89
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
90
90
|
res
|
|
91
91
|
end
|
|
92
92
|
|
|
@@ -107,9 +107,9 @@ class LLM::OpenAI
|
|
|
107
107
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
108
108
|
req = LLM::Transport::Request.get(path("/files/#{file_id}/content?#{query}"), headers)
|
|
109
109
|
io = StringIO.new("".b)
|
|
110
|
-
res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
|
|
110
|
+
res, span, tracer, request_id = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
|
|
111
111
|
res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
|
|
112
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
112
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
113
113
|
res
|
|
114
114
|
end
|
|
115
115
|
|
|
@@ -126,9 +126,9 @@ class LLM::OpenAI
|
|
|
126
126
|
def delete(file:)
|
|
127
127
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
128
128
|
req = LLM::Transport::Request.delete(path("/files/#{file_id}"), headers)
|
|
129
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
129
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
130
130
|
res = LLM::Response.new(res)
|
|
131
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
131
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
132
132
|
res
|
|
133
133
|
end
|
|
134
134
|
|
|
@@ -38,9 +38,9 @@ class LLM::OpenAI
|
|
|
38
38
|
def create(prompt:, model: "gpt-image-1-mini", output_format: "png", **params)
|
|
39
39
|
req = LLM::Transport::Request.post(path("/images/generations"), headers)
|
|
40
40
|
req.body = LLM.json.dump({prompt:, n: 1, model:, output_format:}.merge!(params))
|
|
41
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
41
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
42
42
|
res = ResponseAdapter.adapt(res, type: :image)
|
|
43
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
43
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
|
|
44
44
|
res
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -64,9 +64,9 @@ class LLM::OpenAI
|
|
|
64
64
|
req = LLM::Transport::Request.post(path("/images/edits"), headers)
|
|
65
65
|
req["content-type"] = multi.content_type
|
|
66
66
|
transport.set_body_stream(req, multi.body)
|
|
67
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
67
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
68
68
|
res = ResponseAdapter.adapt(res, type: :image)
|
|
69
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
69
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
|
|
70
70
|
res
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -40,9 +40,9 @@ class LLM::OpenAI
|
|
|
40
40
|
def all(**params)
|
|
41
41
|
query = URI.encode_www_form(params)
|
|
42
42
|
req = LLM::Transport::Request.get(path("/models?#{query}"), headers)
|
|
43
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
43
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
44
44
|
res = ResponseAdapter.adapt(res, type: :models)
|
|
45
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
45
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
46
46
|
res
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -50,9 +50,9 @@ class LLM::OpenAI
|
|
|
50
50
|
req = LLM::Transport::Request.post(path("/moderations"), headers)
|
|
51
51
|
input = RequestAdapter::Moderation.new(input).adapt
|
|
52
52
|
req.body = LLM.json.dump({input:, model:}.merge!(params))
|
|
53
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
53
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
54
54
|
res = ResponseAdapter.adapt(res, type: :moderations)
|
|
55
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
55
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
|
|
56
56
|
res
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -54,10 +54,10 @@ class LLM::OpenAI
|
|
|
54
54
|
params.delete(:input)
|
|
55
55
|
body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
|
|
56
56
|
transport.set_body_stream(req, StringIO.new(body))
|
|
57
|
-
res, span, tracer = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
|
|
57
|
+
res, span, tracer, request_id = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
|
|
58
58
|
res = ResponseAdapter.adapt(res, type: :responds)
|
|
59
59
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
60
|
-
tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
|
|
60
|
+
tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:, request_id:)
|
|
61
61
|
res
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -71,9 +71,9 @@ class LLM::OpenAI
|
|
|
71
71
|
response_id = response.respond_to?(:id) ? response.id : response
|
|
72
72
|
query = URI.encode_www_form(params)
|
|
73
73
|
req = LLM::Transport::Request.get(path("/responses/#{response_id}?#{query}"), headers)
|
|
74
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
74
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
75
75
|
res = ResponseAdapter.adapt(res, type: :responds)
|
|
76
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
76
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
77
77
|
res
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -86,9 +86,9 @@ class LLM::OpenAI
|
|
|
86
86
|
def delete(response)
|
|
87
87
|
response_id = response.respond_to?(:id) ? response.id : response
|
|
88
88
|
req = LLM::Transport::Request.delete(path("/responses/#{response_id}"), headers)
|
|
89
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
89
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
90
90
|
res = LLM::Response.new(res)
|
|
91
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
91
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
92
92
|
res
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -98,7 +98,7 @@ class LLM::OpenAI
|
|
|
98
98
|
def adapt_function(fn)
|
|
99
99
|
{
|
|
100
100
|
type: "function", name: fn.name, description: fn.description,
|
|
101
|
-
parameters: fn.params
|
|
101
|
+
parameters: LLM::Schema::Utils.close(fn.params), strict: false
|
|
102
102
|
}.compact
|
|
103
103
|
end
|
|
104
104
|
|
|
@@ -112,7 +112,7 @@ class LLM::OpenAI
|
|
|
112
112
|
return {} unless params && params[:schema]
|
|
113
113
|
schema = params.delete(:schema)
|
|
114
114
|
schema = schema.respond_to?(:object) ? schema.object : schema
|
|
115
|
-
schema =
|
|
115
|
+
schema = LLM::Schema::Utils.close(schema)
|
|
116
116
|
name = "JSONSchema"
|
|
117
117
|
{text: {format: {type: "json_schema", name:, schema:}}}
|
|
118
118
|
end
|
|
@@ -32,9 +32,9 @@ class LLM::OpenAI
|
|
|
32
32
|
def all(**params)
|
|
33
33
|
query = URI.encode_www_form(params)
|
|
34
34
|
req = LLM::Transport::Request.get(path("/vector_stores?#{query}"), headers)
|
|
35
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
35
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
36
36
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
37
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
37
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
38
38
|
res
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -49,9 +49,9 @@ class LLM::OpenAI
|
|
|
49
49
|
def create(name:, file_ids: nil, **params)
|
|
50
50
|
req = LLM::Transport::Request.post(path("/vector_stores"), headers)
|
|
51
51
|
req.body = LLM.json.dump(params.merge({name:, file_ids:}).compact)
|
|
52
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
52
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
53
53
|
res = LLM::Response.new(res)
|
|
54
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
54
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
55
55
|
res
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -73,9 +73,9 @@ class LLM::OpenAI
|
|
|
73
73
|
def get(vector:)
|
|
74
74
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
75
75
|
req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}"), headers)
|
|
76
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
76
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
77
77
|
res = LLM::Response.new(res)
|
|
78
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
78
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
79
79
|
res
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -91,9 +91,9 @@ class LLM::OpenAI
|
|
|
91
91
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
92
92
|
req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}"), headers)
|
|
93
93
|
req.body = LLM.json.dump(params.merge({name:}).compact)
|
|
94
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
94
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
95
95
|
res = LLM::Response.new(res)
|
|
96
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
96
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
97
97
|
res
|
|
98
98
|
end
|
|
99
99
|
|
|
@@ -106,9 +106,9 @@ class LLM::OpenAI
|
|
|
106
106
|
def delete(vector:)
|
|
107
107
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
108
108
|
req = LLM::Transport::Request.delete(path("/vector_stores/#{vector_id}"), headers)
|
|
109
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
109
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
110
110
|
res = LLM::Response.new(res)
|
|
111
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
111
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
112
112
|
res
|
|
113
113
|
end
|
|
114
114
|
|
|
@@ -124,9 +124,9 @@ class LLM::OpenAI
|
|
|
124
124
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
125
125
|
req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/search"), headers)
|
|
126
126
|
req.body = LLM.json.dump(params.merge({query:}).compact)
|
|
127
|
-
res, span, tracer = execute(request: req, operation: "retrieval")
|
|
127
|
+
res, span, tracer, request_id = execute(request: req, operation: "retrieval")
|
|
128
128
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
129
|
-
tracer.on_request_finish(operation: "retrieval", res:, span:)
|
|
129
|
+
tracer.on_request_finish(operation: "retrieval", res:, span:, request_id:)
|
|
130
130
|
res
|
|
131
131
|
end
|
|
132
132
|
|
|
@@ -141,9 +141,9 @@ class LLM::OpenAI
|
|
|
141
141
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
142
142
|
query = URI.encode_www_form(params)
|
|
143
143
|
req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}/files?#{query}"), headers)
|
|
144
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
144
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
145
145
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
146
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
146
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
147
147
|
res
|
|
148
148
|
end
|
|
149
149
|
|
|
@@ -161,9 +161,9 @@ class LLM::OpenAI
|
|
|
161
161
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
162
162
|
req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/files"), headers)
|
|
163
163
|
req.body = LLM.json.dump(params.merge({file_id:, attributes:}).compact)
|
|
164
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
164
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
165
165
|
res = LLM::Response.new(res)
|
|
166
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
166
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
167
167
|
res
|
|
168
168
|
end
|
|
169
169
|
alias_method :create_file, :add_file
|
|
@@ -192,9 +192,9 @@ class LLM::OpenAI
|
|
|
192
192
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
193
193
|
req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
|
|
194
194
|
req.body = LLM.json.dump(params.merge({attributes:}).compact)
|
|
195
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
195
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
196
196
|
res = LLM::Response.new(res)
|
|
197
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
197
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
198
198
|
res
|
|
199
199
|
end
|
|
200
200
|
|
|
@@ -210,9 +210,9 @@ class LLM::OpenAI
|
|
|
210
210
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
211
211
|
query = URI.encode_www_form(params)
|
|
212
212
|
req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}/files/#{file_id}?#{query}"), headers)
|
|
213
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
213
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
214
214
|
res = LLM::Response.new(res)
|
|
215
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
215
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
216
216
|
res
|
|
217
217
|
end
|
|
218
218
|
|
|
@@ -227,9 +227,9 @@ class LLM::OpenAI
|
|
|
227
227
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
228
228
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
229
229
|
req = LLM::Transport::Request.delete(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
|
|
230
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
230
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
231
231
|
res = LLM::Response.new(res)
|
|
232
|
-
tracer.on_request_finish(operation: "request", res:, span:)
|
|
232
|
+
tracer.on_request_finish(operation: "request", res:, span:, request_id:)
|
|
233
233
|
res
|
|
234
234
|
end
|
|
235
235
|
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -54,9 +54,9 @@ module LLM
|
|
|
54
54
|
def embed(input, model: "text-embedding-3-small", **params)
|
|
55
55
|
req = LLM::Transport::Request.post(path("/embeddings"), headers)
|
|
56
56
|
req.body = LLM.json.dump({input:, model:}.merge!(params))
|
|
57
|
-
res, span, tracer = execute(request: req, operation: "embeddings", model:)
|
|
57
|
+
res, span, tracer, request_id = execute(request: req, operation: "embeddings", model:)
|
|
58
58
|
res = ResponseAdapter.adapt(res, type: :embedding)
|
|
59
|
-
tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
|
|
59
|
+
tracer.on_request_finish(operation: "embeddings", model:, res:, span:, request_id:)
|
|
60
60
|
res
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -74,10 +74,10 @@ module LLM
|
|
|
74
74
|
params, stream, tools, role = normalize_complete_params(params)
|
|
75
75
|
req, messages = build_complete_request(prompt, params, role)
|
|
76
76
|
tracer.set_request_metadata(user_input: extract_user_input(messages, fallback: prompt))
|
|
77
|
-
res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
77
|
+
res, span, tracer, request_id = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
78
78
|
res = ResponseAdapter.adapt(res, type: :completion)
|
|
79
79
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
80
|
-
tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
|
|
80
|
+
tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:, request_id:)
|
|
81
81
|
res
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -156,10 +156,10 @@ module LLM
|
|
|
156
156
|
# @param [LLM::Function] fn
|
|
157
157
|
# @return [Hash]
|
|
158
158
|
def adapt_function(fn)
|
|
159
|
-
params = fn.params
|
|
159
|
+
params = LLM::Schema::Utils.close(fn.params)
|
|
160
160
|
{
|
|
161
|
-
type: "function",
|
|
162
|
-
function: {name: fn.name, description: fn.description, parameters: params}
|
|
161
|
+
type: "function",
|
|
162
|
+
function: {name: fn.name, description: fn.description, parameters: params, strict: false}
|
|
163
163
|
}.compact
|
|
164
164
|
end
|
|
165
165
|
|
|
@@ -203,7 +203,9 @@ module LLM
|
|
|
203
203
|
|
|
204
204
|
def headers
|
|
205
205
|
lock do
|
|
206
|
-
(@headers || {})
|
|
206
|
+
(@headers || {})
|
|
207
|
+
.merge(temporary_headers)
|
|
208
|
+
.merge(
|
|
207
209
|
"Content-Type" => "application/json",
|
|
208
210
|
"Authorization" => "Bearer #{@key}"
|
|
209
211
|
)
|
|
@@ -34,9 +34,9 @@ class LLM::XAI
|
|
|
34
34
|
def create(prompt:, model: "grok-imagine-image-quality", **params)
|
|
35
35
|
req = LLM::Transport::Request.post(path("/images/generations"), headers)
|
|
36
36
|
req.body = LLM.json.dump({prompt:, n: 1, model:, response_format: "b64_json"}.merge!(params))
|
|
37
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
37
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
38
38
|
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
39
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
39
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
|
|
40
40
|
res
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -61,9 +61,9 @@ class LLM::XAI
|
|
|
61
61
|
image: image_url(image),
|
|
62
62
|
response_format: "b64_json"
|
|
63
63
|
}.merge!(params))
|
|
64
|
-
res, span, tracer = execute(request: req, operation: "request")
|
|
64
|
+
res, span, tracer, request_id = execute(request: req, operation: "request")
|
|
65
65
|
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
66
|
-
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
66
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
|
|
67
67
|
res
|
|
68
68
|
end
|
|
69
69
|
|
data/lib/llm/schema.rb
CHANGED
|
@@ -81,6 +81,30 @@ class LLM::Schema
|
|
|
81
81
|
def fetch(properties, name)
|
|
82
82
|
properties[name] || properties.fetch(name.to_s)
|
|
83
83
|
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Returns a copy of a JSON schema where every object is
|
|
87
|
+
# closed with "additionalProperties": false. OpenAI and
|
|
88
|
+
# Azure reject tool parameters that leave an object open,
|
|
89
|
+
# and they validate nested objects as well as the root.
|
|
90
|
+
# @param [Object] node
|
|
91
|
+
# A schema, a Hash of properties, or an Array of either
|
|
92
|
+
# @return [Object]
|
|
93
|
+
def close(node)
|
|
94
|
+
case node
|
|
95
|
+
when LLM::Schema::Leaf
|
|
96
|
+
close(node.to_h)
|
|
97
|
+
when LLM::Object
|
|
98
|
+
close(node.to_h)
|
|
99
|
+
when Hash
|
|
100
|
+
node.to_h { |key, value| [key, close(value)] }
|
|
101
|
+
.then { _1[:type] == "object" ? _1.merge(additionalProperties: false) : _1 }
|
|
102
|
+
when Array
|
|
103
|
+
node.map { close(_1) }
|
|
104
|
+
else
|
|
105
|
+
node
|
|
106
|
+
end
|
|
107
|
+
end
|
|
84
108
|
end
|
|
85
109
|
|
|
86
110
|
##
|
data/lib/llm/tracer/telemetry.rb
CHANGED
|
@@ -86,7 +86,7 @@ module LLM
|
|
|
86
86
|
|
|
87
87
|
##
|
|
88
88
|
# @param (see LLM::Tracer#on_request_start)
|
|
89
|
-
def on_request_start(operation:, model: nil, inputs: nil)
|
|
89
|
+
def on_request_start(operation:, model: nil, inputs: nil, request_id:)
|
|
90
90
|
case operation
|
|
91
91
|
when "chat" then start_chat(operation:, model:, inputs:)
|
|
92
92
|
when "retrieval" then start_retrieval(operation:)
|
|
@@ -96,7 +96,7 @@ module LLM
|
|
|
96
96
|
|
|
97
97
|
##
|
|
98
98
|
# @param (see LLM::Tracer#on_request_finish)
|
|
99
|
-
def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil)
|
|
99
|
+
def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil, request_id:)
|
|
100
100
|
return nil unless span
|
|
101
101
|
case operation
|
|
102
102
|
when "chat" then finish_chat(operation:, model:, res:, span:, outputs:, metadata:)
|
|
@@ -107,7 +107,7 @@ module LLM
|
|
|
107
107
|
|
|
108
108
|
##
|
|
109
109
|
# @param (see LLM::Tracer#on_request_error)
|
|
110
|
-
def on_request_error(ex:, span:)
|
|
110
|
+
def on_request_error(ex:, span:, request_id:)
|
|
111
111
|
return nil unless span
|
|
112
112
|
attributes = {"error.type" => ex.class.to_s}.compact
|
|
113
113
|
attributes.each { span.set_attribute(_1, _2) }
|
|
@@ -305,7 +305,7 @@ module LLM
|
|
|
305
305
|
"gen_ai.operation.name" => operation,
|
|
306
306
|
"gen_ai.request.model" => model,
|
|
307
307
|
"gen_ai.response.id" => res.id,
|
|
308
|
-
"gen_ai.response.model" => model,
|
|
308
|
+
"gen_ai.response.model" => res.model,
|
|
309
309
|
"gen_ai.usage.input_tokens" => res.usage.input_tokens,
|
|
310
310
|
"gen_ai.usage.output_tokens" => res.usage.output_tokens,
|
|
311
311
|
"output.value" => serialize_request_value(output_value)
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -66,8 +66,12 @@ module LLM
|
|
|
66
66
|
# @param [String] operation
|
|
67
67
|
# @param [String] model
|
|
68
68
|
# @param [Hash, nil] inputs Optional span attributes (e.g. gen_ai.input.messages) from llm.rb or caller.
|
|
69
|
+
# @param [String] request_id
|
|
70
|
+
# The id of the request. It is a UUIDv7, and it is shared by
|
|
71
|
+
# {#on_request_start}, {#on_request_finish}, and {#on_request_error}
|
|
72
|
+
# for the same request.
|
|
69
73
|
# @return [void]
|
|
70
|
-
def on_request_start(operation:, model: nil, inputs: nil)
|
|
74
|
+
def on_request_start(operation:, model: nil, inputs: nil, request_id:)
|
|
71
75
|
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
72
76
|
end
|
|
73
77
|
|
|
@@ -79,8 +83,10 @@ module LLM
|
|
|
79
83
|
# @param [String] model
|
|
80
84
|
# @param [Hash, nil] outputs Optional span attributes (e.g. gen_ai.output.messages) from llm.rb or caller.
|
|
81
85
|
# @param [Hash, nil] metadata Optional metadata from llm.rb or caller.
|
|
86
|
+
# @param [String] request_id
|
|
87
|
+
# The id of the request, as passed to {#on_request_start}
|
|
82
88
|
# @return [void]
|
|
83
|
-
def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil)
|
|
89
|
+
def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil, request_id:)
|
|
84
90
|
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
85
91
|
end
|
|
86
92
|
|
|
@@ -88,8 +94,10 @@ module LLM
|
|
|
88
94
|
# Called when an LLM provider request fails.
|
|
89
95
|
# @param [LLM::Error] ex
|
|
90
96
|
# @param [Object, nil] span
|
|
97
|
+
# @param [String] request_id
|
|
98
|
+
# The id of the request, as passed to {#on_request_start}
|
|
91
99
|
# @return [void]
|
|
92
|
-
def on_request_error(ex:, span:)
|
|
100
|
+
def on_request_error(ex:, span:, request_id:)
|
|
93
101
|
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
94
102
|
end
|
|
95
103
|
|
|
@@ -33,10 +33,11 @@ class LLM::Transport
|
|
|
33
33
|
stream &&= LLM::Object.from(streamer: stream, parser: stream_parser, decoder: stream_decoder)
|
|
34
34
|
owner = transport.request_owner
|
|
35
35
|
tracer = self.tracer
|
|
36
|
-
|
|
36
|
+
request_id = SecureRandom.uuid_v7
|
|
37
|
+
span = tracer.on_request_start(operation:, model:, inputs:, request_id:)
|
|
37
38
|
res = transport.request(request, owner:, stream:, &b)
|
|
38
39
|
res = LLM::Transport::Response.from(res)
|
|
39
|
-
[handle_response(res, tracer, span), span, tracer]
|
|
40
|
+
[handle_response(res, tracer, span, request_id), span, tracer, request_id]
|
|
40
41
|
rescue *transport.interrupt_errors
|
|
41
42
|
raise LLM::Interrupt, "request interrupted" if transport.interrupted?(owner)
|
|
42
43
|
raise
|
|
@@ -48,9 +49,12 @@ class LLM::Transport
|
|
|
48
49
|
# The response to handle
|
|
49
50
|
# @param [Object, nil] span
|
|
50
51
|
# The span
|
|
52
|
+
# @param [String] request_id
|
|
53
|
+
# The id of the request being handled
|
|
51
54
|
# @return [LLM::Transport::Response]
|
|
52
|
-
def handle_response(res, tracer, span)
|
|
53
|
-
res.ok? ? res.body = parse_response(res) :
|
|
55
|
+
def handle_response(res, tracer, span, request_id)
|
|
56
|
+
res.ok? ? res.body = parse_response(res) :
|
|
57
|
+
error_handler.new(tracer, span, res, request_id).raise_error!
|
|
54
58
|
res
|
|
55
59
|
end
|
|
56
60
|
|
data/lib/llm/utils.rb
CHANGED
|
@@ -48,6 +48,19 @@ module LLM
|
|
|
48
48
|
(defined?(::Sequel::Model) and ::Sequel::Model === obj)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
##
|
|
52
|
+
# Returns the UTC time encoded in a UUIDv7, or nil when the
|
|
53
|
+
# given value is not a UUIDv7. The first 48 bits of a UUIDv7
|
|
54
|
+
# are a Unix millisecond timestamp.
|
|
55
|
+
# @param [Object] id
|
|
56
|
+
# @return [Time, nil]
|
|
57
|
+
def timestamp(id)
|
|
58
|
+
hex = id.to_s.delete("-")
|
|
59
|
+
return nil unless hex.match?(/\A\h{32}\z/)
|
|
60
|
+
return nil unless hex[12] == "7"
|
|
61
|
+
Time.at(hex[0, 12].to_i(16) / 1000.0).utc
|
|
62
|
+
end
|
|
63
|
+
|
|
51
64
|
##
|
|
52
65
|
# Normalizes an HTTP API base path.
|
|
53
66
|
#
|
data/lib/llm/version.rb
CHANGED
data/llm.gemspec
CHANGED
|
@@ -35,9 +35,9 @@ DESCRIPTION
|
|
|
35
35
|
spec.executables = ["llm.rb"]
|
|
36
36
|
spec.require_paths = ["lib"]
|
|
37
37
|
spec.post_install_message = "\n" \
|
|
38
|
-
"
|
|
38
|
+
"Want to see what I'm building with llm.rb? " \
|
|
39
39
|
"\n" \
|
|
40
|
-
"
|
|
40
|
+
"Checkout the https://r.uby.dev website." \
|
|
41
41
|
"\n\n"
|
|
42
42
|
|
|
43
43
|
spec.add_development_dependency "webmock", "~> 3.24.0"
|