opentelemetry-instrumentation-ruby_llm 0.6.0 → 0.7.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/.github/workflows/main.yml +1 -0
- data/Appraisals +4 -0
- data/Gemfile +2 -0
- data/README.md +92 -1
- data/gemfiles/ruby_llm_1.12.1.gemfile +18 -0
- data/gemfiles/ruby_llm_1.8.0.gemfile +2 -0
- data/gemfiles/ruby_llm_1_latest.gemfile +2 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/instrumentation.rb +26 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/message_formatter.rb +97 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/patches/agent.rb +84 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/patches/chat.rb +15 -28
- data/lib/opentelemetry/instrumentation/ruby_llm/patches/chat_methods.rb +17 -0
- data/lib/opentelemetry/instrumentation/ruby_llm/version.rb +1 -1
- data/test/active_record_chat_methods_test.rb +195 -0
- data/test/agent_instrumentation_test.rb +245 -0
- data/test/instrumentation_test.rb +194 -236
- data/test/test_helper.rb +30 -0
- metadata +7 -1
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
require "test_helper"
|
|
2
2
|
|
|
3
3
|
class InstrumentationTest < Minitest::Test
|
|
4
|
+
include ChatCompletionStubs
|
|
5
|
+
|
|
4
6
|
def setup
|
|
5
7
|
EXPORTER.reset
|
|
6
8
|
|
|
7
9
|
RubyLLM.configure do |c|
|
|
8
10
|
c.openai_api_key = "fake-key-for-testing"
|
|
11
|
+
c.anthropic_api_key = "fake-key-for-testing"
|
|
9
12
|
end
|
|
10
13
|
end
|
|
11
14
|
|
|
@@ -30,29 +33,12 @@ class InstrumentationTest < Minitest::Test
|
|
|
30
33
|
assert_equal "1.8.0", OpenTelemetry::Instrumentation::RubyLLM::Instrumentation::MINIMUM_RUBY_LLM_VERSION
|
|
31
34
|
end
|
|
32
35
|
|
|
36
|
+
def test_agent_minimum_ruby_llm_version_is_pinned_at_1_12_1
|
|
37
|
+
assert_equal "1.12.1", OpenTelemetry::Instrumentation::RubyLLM::Instrumentation::AGENT_MINIMUM_RUBY_LLM_VERSION
|
|
38
|
+
end
|
|
39
|
+
|
|
33
40
|
def test_creates_span_with_attributes
|
|
34
|
-
|
|
35
|
-
.to_return(
|
|
36
|
-
status: 200,
|
|
37
|
-
headers: { "Content-Type" => "application/json" },
|
|
38
|
-
body: {
|
|
39
|
-
id: "chatcmpl-123",
|
|
40
|
-
object: "chat.completion",
|
|
41
|
-
model: "gpt-4o-mini",
|
|
42
|
-
choices: [
|
|
43
|
-
{
|
|
44
|
-
index: 0,
|
|
45
|
-
message: { role: "assistant", content: "Hello, world!" },
|
|
46
|
-
finish_reason: "stop"
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
usage: {
|
|
50
|
-
prompt_tokens: 10,
|
|
51
|
-
completion_tokens: 5,
|
|
52
|
-
total_tokens: 15
|
|
53
|
-
}
|
|
54
|
-
}.to_json
|
|
55
|
-
)
|
|
41
|
+
stub_chat_completion
|
|
56
42
|
|
|
57
43
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
58
44
|
chat.ask("Hi")
|
|
@@ -73,17 +59,9 @@ class InstrumentationTest < Minitest::Test
|
|
|
73
59
|
end
|
|
74
60
|
|
|
75
61
|
def test_marks_streaming_chat_requests
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
headers: { "Content-Type" => "application/json" },
|
|
80
|
-
body: {
|
|
81
|
-
id: "chatcmpl-123",
|
|
82
|
-
model: "gpt-4o-mini",
|
|
83
|
-
choices: [{ index: 0, message: { role: "assistant", content: "Hi" }, finish_reason: "stop" }],
|
|
84
|
-
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
|
|
85
|
-
}.to_json
|
|
86
|
-
)
|
|
62
|
+
stub_chat_completion(
|
|
63
|
+
chat_completion_body(content: "Hi", usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 })
|
|
64
|
+
)
|
|
87
65
|
|
|
88
66
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
89
67
|
chat.ask("Hi") { |_chunk| }
|
|
@@ -92,28 +70,58 @@ class InstrumentationTest < Minitest::Test
|
|
|
92
70
|
assert_equal true, span.attributes["gen_ai.request.stream"]
|
|
93
71
|
end
|
|
94
72
|
|
|
95
|
-
def
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
73
|
+
def test_records_openai_prompt_cache_read_tokens
|
|
74
|
+
# OpenAI exposes only `cached_tokens` via `prompt_tokens_details.cached_tokens`.
|
|
75
|
+
# Its provider in ruby_llm does not surface a `cache_creation_tokens` value
|
|
76
|
+
# until 1.15.0 (we only assert the cache-read attribute here).
|
|
77
|
+
# The accessor itself was added in ruby_llm 1.9.0.
|
|
78
|
+
skip "cached_tokens accessor not available before ruby_llm 1.9.0" unless RubyLLM::Message.instance_methods.include?(:cached_tokens)
|
|
79
|
+
stub_chat_completion(
|
|
80
|
+
chat_completion_body(
|
|
81
|
+
content: "Hello!",
|
|
82
|
+
usage: {
|
|
83
|
+
prompt_tokens: 100,
|
|
84
|
+
completion_tokens: 5,
|
|
85
|
+
total_tokens: 105,
|
|
86
|
+
prompt_tokens_details: { cached_tokens: 75 }
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
92
|
+
chat.ask("Hi")
|
|
93
|
+
|
|
94
|
+
span = EXPORTER.finished_spans.first
|
|
95
|
+
assert_equal 75, span.attributes["gen_ai.usage.cache_read.input_tokens"]
|
|
96
|
+
assert_equal 0, span.attributes["gen_ai.usage.cache_creation.input_tokens"]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_records_anthropic_prompt_cache_tokens
|
|
100
|
+
# Anthropic's provider surfaces both `cached_tokens` (via
|
|
101
|
+
# `cache_read_input_tokens`) and `cache_creation_tokens` (via
|
|
102
|
+
# `cache_creation_input_tokens`). Accessors were added in ruby_llm 1.9.0.
|
|
103
|
+
skip "cache token accessors not available before ruby_llm 1.9.0" unless RubyLLM::Message.instance_methods.include?(:cache_creation_tokens)
|
|
104
|
+
stub_request(:post, "https://api.anthropic.com/v1/messages")
|
|
100
105
|
.to_return(
|
|
101
106
|
status: 200,
|
|
102
107
|
headers: { "Content-Type" => "application/json" },
|
|
103
108
|
body: {
|
|
104
|
-
id: "
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
id: "msg_cache",
|
|
110
|
+
type: "message",
|
|
111
|
+
role: "assistant",
|
|
112
|
+
model: "claude-3-5-sonnet-20241022",
|
|
113
|
+
content: [{ type: "text", text: "Hello!" }],
|
|
114
|
+
stop_reason: "end_turn",
|
|
107
115
|
usage: {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
input_tokens: 100,
|
|
117
|
+
output_tokens: 5,
|
|
118
|
+
cache_read_input_tokens: 75,
|
|
119
|
+
cache_creation_input_tokens: 20
|
|
112
120
|
}
|
|
113
121
|
}.to_json
|
|
114
122
|
)
|
|
115
123
|
|
|
116
|
-
chat = RubyLLM.chat(model: "
|
|
124
|
+
chat = RubyLLM.chat(model: "claude-3-5-sonnet-20241022")
|
|
117
125
|
chat.ask("Hi")
|
|
118
126
|
|
|
119
127
|
span = EXPORTER.finished_spans.first
|
|
@@ -140,22 +148,7 @@ class InstrumentationTest < Minitest::Test
|
|
|
140
148
|
end
|
|
141
149
|
|
|
142
150
|
def test_instruments_complete_called_directly
|
|
143
|
-
|
|
144
|
-
.to_return(
|
|
145
|
-
status: 200,
|
|
146
|
-
headers: { "Content-Type" => "application/json" },
|
|
147
|
-
body: {
|
|
148
|
-
id: "chatcmpl-123",
|
|
149
|
-
object: "chat.completion",
|
|
150
|
-
model: "gpt-4o-mini",
|
|
151
|
-
choices: [{
|
|
152
|
-
index: 0,
|
|
153
|
-
message: { role: "assistant", content: "Hello, world!" },
|
|
154
|
-
finish_reason: "stop"
|
|
155
|
-
}],
|
|
156
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
157
|
-
}.to_json
|
|
158
|
-
)
|
|
151
|
+
stub_chat_completion
|
|
159
152
|
|
|
160
153
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
161
154
|
chat.add_message(role: :user, content: "Hi")
|
|
@@ -183,47 +176,20 @@ class InstrumentationTest < Minitest::Test
|
|
|
183
176
|
end
|
|
184
177
|
end
|
|
185
178
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
role: "assistant",
|
|
199
|
-
content: nil,
|
|
200
|
-
tool_calls: [{
|
|
201
|
-
id: "call_abc123",
|
|
202
|
-
type: "function",
|
|
203
|
-
function: { name: "calculator", arguments: '{"expression":"2+2"}' }
|
|
204
|
-
}]
|
|
205
|
-
},
|
|
206
|
-
finish_reason: "tool_calls"
|
|
207
|
-
}],
|
|
208
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
209
|
-
}.to_json
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
status: 200,
|
|
213
|
-
headers: { "Content-Type" => "application/json" },
|
|
214
|
-
body: {
|
|
215
|
-
id: "chatcmpl-456",
|
|
216
|
-
object: "chat.completion",
|
|
217
|
-
model: "gpt-4o-mini",
|
|
218
|
-
choices: [{
|
|
219
|
-
index: 0,
|
|
220
|
-
message: { role: "assistant", content: "The answer is 4" },
|
|
221
|
-
finish_reason: "stop"
|
|
222
|
-
}],
|
|
223
|
-
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 }
|
|
224
|
-
}.to_json
|
|
225
|
-
}
|
|
179
|
+
stub_chat_completion(
|
|
180
|
+
chat_completion_body(
|
|
181
|
+
content: nil,
|
|
182
|
+
tool_calls: [{
|
|
183
|
+
id: "call_abc123",
|
|
184
|
+
type: "function",
|
|
185
|
+
function: { name: "calculator", arguments: '{"expression":"2+2"}' }
|
|
186
|
+
}]
|
|
187
|
+
),
|
|
188
|
+
chat_completion_body(
|
|
189
|
+
content: "The answer is 4",
|
|
190
|
+
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 }
|
|
226
191
|
)
|
|
192
|
+
)
|
|
227
193
|
|
|
228
194
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
229
195
|
chat.with_tool(calculator)
|
|
@@ -249,6 +215,41 @@ class InstrumentationTest < Minitest::Test
|
|
|
249
215
|
assert_equal "function", tool_span.attributes["gen_ai.tool.type"]
|
|
250
216
|
end
|
|
251
217
|
|
|
218
|
+
def test_truncates_tool_result_to_configured_max_length
|
|
219
|
+
long_value = "x" * 1000
|
|
220
|
+
echo = Class.new(RubyLLM::Tool) do
|
|
221
|
+
def self.name = "echo"
|
|
222
|
+
description "Echoes a long string"
|
|
223
|
+
|
|
224
|
+
define_method(:execute) { long_value }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
stub_chat_completion(
|
|
228
|
+
chat_completion_body(
|
|
229
|
+
content: nil,
|
|
230
|
+
tool_calls: [{
|
|
231
|
+
id: "call_echo",
|
|
232
|
+
type: "function",
|
|
233
|
+
function: { name: "echo", arguments: "{}" }
|
|
234
|
+
}]
|
|
235
|
+
),
|
|
236
|
+
chat_completion_body(
|
|
237
|
+
content: "done",
|
|
238
|
+
usage: { prompt_tokens: 20, completion_tokens: 5, total_tokens: 25 }
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:tool_result_max_length] = 700
|
|
243
|
+
|
|
244
|
+
chat = RubyLLM.chat(model: "gpt-4o-mini").with_tool(echo)
|
|
245
|
+
chat.ask("echo please")
|
|
246
|
+
|
|
247
|
+
tool_span = EXPORTER.finished_spans.find { |s| s.name.start_with?("execute_tool ") }
|
|
248
|
+
assert_equal "x" * 700, tool_span.attributes["gen_ai.tool.call.result"]
|
|
249
|
+
ensure
|
|
250
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:tool_result_max_length] = 500
|
|
251
|
+
end
|
|
252
|
+
|
|
252
253
|
def test_records_error_when_tool_raises
|
|
253
254
|
boom = Class.new(RubyLLM::Tool) do
|
|
254
255
|
def self.name = "boom"
|
|
@@ -259,29 +260,17 @@ class InstrumentationTest < Minitest::Test
|
|
|
259
260
|
end
|
|
260
261
|
end
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
message: {
|
|
272
|
-
role: "assistant",
|
|
273
|
-
content: nil,
|
|
274
|
-
tool_calls: [{
|
|
275
|
-
id: "call_x",
|
|
276
|
-
type: "function",
|
|
277
|
-
function: { name: "boom", arguments: "{}" }
|
|
278
|
-
}]
|
|
279
|
-
},
|
|
280
|
-
finish_reason: "tool_calls"
|
|
281
|
-
}],
|
|
282
|
-
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
|
|
283
|
-
}.to_json
|
|
263
|
+
stub_chat_completion(
|
|
264
|
+
chat_completion_body(
|
|
265
|
+
content: nil,
|
|
266
|
+
tool_calls: [{
|
|
267
|
+
id: "call_x",
|
|
268
|
+
type: "function",
|
|
269
|
+
function: { name: "boom", arguments: "{}" }
|
|
270
|
+
}],
|
|
271
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
|
|
284
272
|
)
|
|
273
|
+
)
|
|
285
274
|
|
|
286
275
|
chat = RubyLLM.chat(model: "gpt-4o-mini").with_tool(boom)
|
|
287
276
|
assert_raises(ArgumentError) { chat.ask("trigger") }
|
|
@@ -292,22 +281,7 @@ class InstrumentationTest < Minitest::Test
|
|
|
292
281
|
end
|
|
293
282
|
|
|
294
283
|
def test_does_not_capture_content_by_default
|
|
295
|
-
|
|
296
|
-
.to_return(
|
|
297
|
-
status: 200,
|
|
298
|
-
headers: { "Content-Type" => "application/json" },
|
|
299
|
-
body: {
|
|
300
|
-
id: "chatcmpl-123",
|
|
301
|
-
object: "chat.completion",
|
|
302
|
-
model: "gpt-4o-mini",
|
|
303
|
-
choices: [{
|
|
304
|
-
index: 0,
|
|
305
|
-
message: { role: "assistant", content: "Hello, world!" },
|
|
306
|
-
finish_reason: "stop"
|
|
307
|
-
}],
|
|
308
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
309
|
-
}.to_json
|
|
310
|
-
)
|
|
284
|
+
stub_chat_completion
|
|
311
285
|
|
|
312
286
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
313
287
|
chat.with_instructions("You are helpful")
|
|
@@ -322,22 +296,7 @@ class InstrumentationTest < Minitest::Test
|
|
|
322
296
|
def test_captures_content_when_enabled
|
|
323
297
|
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = true
|
|
324
298
|
|
|
325
|
-
|
|
326
|
-
.to_return(
|
|
327
|
-
status: 200,
|
|
328
|
-
headers: { "Content-Type" => "application/json" },
|
|
329
|
-
body: {
|
|
330
|
-
id: "chatcmpl-123",
|
|
331
|
-
object: "chat.completion",
|
|
332
|
-
model: "gpt-4o-mini",
|
|
333
|
-
choices: [{
|
|
334
|
-
index: 0,
|
|
335
|
-
message: { role: "assistant", content: "Hello, world!" },
|
|
336
|
-
finish_reason: "stop"
|
|
337
|
-
}],
|
|
338
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
339
|
-
}.to_json
|
|
340
|
-
)
|
|
299
|
+
stub_chat_completion
|
|
341
300
|
|
|
342
301
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
343
302
|
chat.with_instructions("You are helpful")
|
|
@@ -409,22 +368,7 @@ class InstrumentationTest < Minitest::Test
|
|
|
409
368
|
end
|
|
410
369
|
|
|
411
370
|
def test_with_otel_attributes_sets_span_attributes
|
|
412
|
-
|
|
413
|
-
.to_return(
|
|
414
|
-
status: 200,
|
|
415
|
-
headers: { "Content-Type" => "application/json" },
|
|
416
|
-
body: {
|
|
417
|
-
id: "chatcmpl-123",
|
|
418
|
-
object: "chat.completion",
|
|
419
|
-
model: "gpt-4o-mini",
|
|
420
|
-
choices: [{
|
|
421
|
-
index: 0,
|
|
422
|
-
message: { role: "assistant", content: "Hello!" },
|
|
423
|
-
finish_reason: "stop"
|
|
424
|
-
}],
|
|
425
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
426
|
-
}.to_json
|
|
427
|
-
)
|
|
371
|
+
stub_chat_completion(chat_completion_body(content: "Hello!"))
|
|
428
372
|
|
|
429
373
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
430
374
|
chat.with_otel_attributes(
|
|
@@ -439,22 +383,7 @@ class InstrumentationTest < Minitest::Test
|
|
|
439
383
|
end
|
|
440
384
|
|
|
441
385
|
def test_with_otel_attributes_returns_self_for_chaining
|
|
442
|
-
|
|
443
|
-
.to_return(
|
|
444
|
-
status: 200,
|
|
445
|
-
headers: { "Content-Type" => "application/json" },
|
|
446
|
-
body: {
|
|
447
|
-
id: "chatcmpl-123",
|
|
448
|
-
object: "chat.completion",
|
|
449
|
-
model: "gpt-4o-mini",
|
|
450
|
-
choices: [{
|
|
451
|
-
index: 0,
|
|
452
|
-
message: { role: "assistant", content: "Hello!" },
|
|
453
|
-
finish_reason: "stop"
|
|
454
|
-
}],
|
|
455
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
456
|
-
}.to_json
|
|
457
|
-
)
|
|
386
|
+
stub_chat_completion(chat_completion_body(content: "Hello!"))
|
|
458
387
|
|
|
459
388
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
460
389
|
result = chat.with_otel_attributes("custom.category" => "test")
|
|
@@ -463,22 +392,7 @@ class InstrumentationTest < Minitest::Test
|
|
|
463
392
|
end
|
|
464
393
|
|
|
465
394
|
def test_with_otel_attributes_evaluates_callables
|
|
466
|
-
|
|
467
|
-
.to_return(
|
|
468
|
-
status: 200,
|
|
469
|
-
headers: { "Content-Type" => "application/json" },
|
|
470
|
-
body: {
|
|
471
|
-
id: "chatcmpl-123",
|
|
472
|
-
object: "chat.completion",
|
|
473
|
-
model: "gpt-4o-mini",
|
|
474
|
-
choices: [{
|
|
475
|
-
index: 0,
|
|
476
|
-
message: { role: "assistant", content: "Hello!" },
|
|
477
|
-
finish_reason: "stop"
|
|
478
|
-
}],
|
|
479
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
480
|
-
}.to_json
|
|
481
|
-
)
|
|
395
|
+
stub_chat_completion(chat_completion_body(content: "Hello!"))
|
|
482
396
|
|
|
483
397
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
484
398
|
chat.with_otel_attributes(
|
|
@@ -493,22 +407,7 @@ class InstrumentationTest < Minitest::Test
|
|
|
493
407
|
end
|
|
494
408
|
|
|
495
409
|
def test_works_without_otel_attributes
|
|
496
|
-
|
|
497
|
-
.to_return(
|
|
498
|
-
status: 200,
|
|
499
|
-
headers: { "Content-Type" => "application/json" },
|
|
500
|
-
body: {
|
|
501
|
-
id: "chatcmpl-123",
|
|
502
|
-
object: "chat.completion",
|
|
503
|
-
model: "gpt-4o-mini",
|
|
504
|
-
choices: [{
|
|
505
|
-
index: 0,
|
|
506
|
-
message: { role: "assistant", content: "Hello!" },
|
|
507
|
-
finish_reason: "stop"
|
|
508
|
-
}],
|
|
509
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
510
|
-
}.to_json
|
|
511
|
-
)
|
|
410
|
+
stub_chat_completion(chat_completion_body(content: "Hello!"))
|
|
512
411
|
|
|
513
412
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
514
413
|
response = chat.ask("Hi")
|
|
@@ -516,25 +415,84 @@ class InstrumentationTest < Minitest::Test
|
|
|
516
415
|
assert_equal "Hello!", response.content
|
|
517
416
|
end
|
|
518
417
|
|
|
418
|
+
def test_captures_ruby_llm_content_with_attachments
|
|
419
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = true
|
|
420
|
+
|
|
421
|
+
stub_chat_completion(chat_completion_body(content: "A cat."))
|
|
422
|
+
|
|
423
|
+
content = RubyLLM::Content.new("What is this?", "https://example.com/cat.png")
|
|
424
|
+
|
|
425
|
+
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
426
|
+
chat.add_message(role: :user, content: content)
|
|
427
|
+
chat.complete
|
|
428
|
+
|
|
429
|
+
span = EXPORTER.finished_spans.first
|
|
430
|
+
input_messages = JSON.parse(span.attributes["gen_ai.input.messages"])
|
|
431
|
+
assert_equal(
|
|
432
|
+
[
|
|
433
|
+
{ "type" => "text", "content" => "What is this?" },
|
|
434
|
+
{
|
|
435
|
+
"type" => "uri",
|
|
436
|
+
"modality" => "image",
|
|
437
|
+
"mime_type" => "image/png",
|
|
438
|
+
"uri" => "https://example.com/cat.png"
|
|
439
|
+
}
|
|
440
|
+
],
|
|
441
|
+
input_messages[0]["parts"]
|
|
442
|
+
)
|
|
443
|
+
ensure
|
|
444
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = false
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def test_captures_ruby_llm_content_raw
|
|
448
|
+
skip "RubyLLM::Content::Raw not available before ruby_llm 1.9.0" unless defined?(RubyLLM::Content::Raw)
|
|
449
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = true
|
|
450
|
+
|
|
451
|
+
stub_chat_completion(chat_completion_body(content: "Acknowledged."))
|
|
452
|
+
|
|
453
|
+
raw = RubyLLM::Content::Raw.new([{ type: "text", text: "raw payload" }])
|
|
454
|
+
|
|
455
|
+
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
456
|
+
chat.add_message(role: :user, content: raw)
|
|
457
|
+
chat.complete
|
|
458
|
+
|
|
459
|
+
span = EXPORTER.finished_spans.first
|
|
460
|
+
input_messages = JSON.parse(span.attributes["gen_ai.input.messages"])
|
|
461
|
+
assert_equal(
|
|
462
|
+
[{ "type" => "raw", "content" => [{ "type" => "text", "text" => "raw payload" }].to_json }],
|
|
463
|
+
input_messages[0]["parts"]
|
|
464
|
+
)
|
|
465
|
+
ensure
|
|
466
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = false
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def test_captures_ruby_llm_content_raw_system_instructions
|
|
470
|
+
skip "RubyLLM::Content::Raw not available before ruby_llm 1.9.0" unless defined?(RubyLLM::Content::Raw)
|
|
471
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = true
|
|
472
|
+
|
|
473
|
+
stub_chat_completion(chat_completion_body(content: "Acknowledged."))
|
|
474
|
+
|
|
475
|
+
raw_block = RubyLLM::Content::Raw.new([{ type: "text", text: "You are helpful" }])
|
|
476
|
+
|
|
477
|
+
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
478
|
+
chat.add_message(role: :system, content: raw_block)
|
|
479
|
+
chat.add_message(role: :user, content: "Hi")
|
|
480
|
+
chat.complete
|
|
481
|
+
|
|
482
|
+
span = EXPORTER.finished_spans.first
|
|
483
|
+
system_instructions = JSON.parse(span.attributes["gen_ai.system_instructions"])
|
|
484
|
+
assert_equal(
|
|
485
|
+
[{ "type" => "raw", "content" => [{ "type" => "text", "text" => "You are helpful" }].to_json }],
|
|
486
|
+
system_instructions
|
|
487
|
+
)
|
|
488
|
+
ensure
|
|
489
|
+
OpenTelemetry::Instrumentation::RubyLLM::Instrumentation.instance.config[:capture_content] = false
|
|
490
|
+
end
|
|
491
|
+
|
|
519
492
|
def test_captures_content_when_enabled_via_env_var
|
|
520
493
|
ENV["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = "true"
|
|
521
494
|
|
|
522
|
-
|
|
523
|
-
.to_return(
|
|
524
|
-
status: 200,
|
|
525
|
-
headers: { "Content-Type" => "application/json" },
|
|
526
|
-
body: {
|
|
527
|
-
id: "chatcmpl-123",
|
|
528
|
-
object: "chat.completion",
|
|
529
|
-
model: "gpt-4o-mini",
|
|
530
|
-
choices: [{
|
|
531
|
-
index: 0,
|
|
532
|
-
message: { role: "assistant", content: "Hello, world!" },
|
|
533
|
-
finish_reason: "stop"
|
|
534
|
-
}],
|
|
535
|
-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
|
|
536
|
-
}.to_json
|
|
537
|
-
)
|
|
495
|
+
stub_chat_completion
|
|
538
496
|
|
|
539
497
|
chat = RubyLLM.chat(model: "gpt-4o-mini")
|
|
540
498
|
chat.ask("Hi")
|
data/test/test_helper.rb
CHANGED
|
@@ -6,6 +6,36 @@ require "ruby_llm"
|
|
|
6
6
|
require "opentelemetry/sdk"
|
|
7
7
|
require "opentelemetry-instrumentation-ruby_llm"
|
|
8
8
|
|
|
9
|
+
module ChatCompletionStubs
|
|
10
|
+
DEFAULT_USAGE = { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }.freeze
|
|
11
|
+
|
|
12
|
+
def chat_completion_body(content: "Hello, world!", model: "gpt-4o-mini", tool_calls: nil, usage: DEFAULT_USAGE)
|
|
13
|
+
message = { role: "assistant", content: content }
|
|
14
|
+
message[:tool_calls] = tool_calls if tool_calls
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
id: "chatcmpl-123",
|
|
18
|
+
object: "chat.completion",
|
|
19
|
+
model: model,
|
|
20
|
+
choices: [{
|
|
21
|
+
index: 0,
|
|
22
|
+
message: message,
|
|
23
|
+
finish_reason: tool_calls ? "tool_calls" : "stop"
|
|
24
|
+
}],
|
|
25
|
+
usage: usage
|
|
26
|
+
}.to_json
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def stub_chat_completion(*bodies)
|
|
30
|
+
bodies = [chat_completion_body] if bodies.empty?
|
|
31
|
+
responses = bodies.map do |body|
|
|
32
|
+
{ status: 200, headers: { "Content-Type" => "application/json" }, body: body }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
stub_request(:post, "https://api.openai.com/v1/chat/completions").to_return(*responses)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
9
39
|
EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new
|
|
10
40
|
span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER)
|
|
11
41
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opentelemetry-instrumentation-ruby_llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Clarissa Borges
|
|
@@ -61,14 +61,20 @@ files:
|
|
|
61
61
|
- example/trace_demonstration_with_langfuse_and_tools.rb
|
|
62
62
|
- example/trace_demonstration_with_langfuse_ingredient_search.rb
|
|
63
63
|
- example/trace_demonstration_with_tools.rb
|
|
64
|
+
- gemfiles/ruby_llm_1.12.1.gemfile
|
|
64
65
|
- gemfiles/ruby_llm_1.8.0.gemfile
|
|
65
66
|
- gemfiles/ruby_llm_1_latest.gemfile
|
|
66
67
|
- lib/opentelemetry-instrumentation-ruby_llm.rb
|
|
67
68
|
- lib/opentelemetry/instrumentation/ruby_llm/instrumentation.rb
|
|
69
|
+
- lib/opentelemetry/instrumentation/ruby_llm/message_formatter.rb
|
|
70
|
+
- lib/opentelemetry/instrumentation/ruby_llm/patches/agent.rb
|
|
68
71
|
- lib/opentelemetry/instrumentation/ruby_llm/patches/chat.rb
|
|
72
|
+
- lib/opentelemetry/instrumentation/ruby_llm/patches/chat_methods.rb
|
|
69
73
|
- lib/opentelemetry/instrumentation/ruby_llm/patches/embedding.rb
|
|
70
74
|
- lib/opentelemetry/instrumentation/ruby_llm/version.rb
|
|
71
75
|
- opentelemetry-instrumentation-ruby_llm.gemspec
|
|
76
|
+
- test/active_record_chat_methods_test.rb
|
|
77
|
+
- test/agent_instrumentation_test.rb
|
|
72
78
|
- test/instrumentation_test.rb
|
|
73
79
|
- test/test_helper.rb
|
|
74
80
|
homepage: https://github.com/thoughtbot/opentelemetry-instrumentation-ruby_llm
|