Skip to content

Commit 23c4d38

Browse files
ebeigartsclaude
andauthored
Instrument complete instead of ask (#16)
RubyLLM's Rails/ActiveRecord integration (acts_as_chat) defines its own ask method that calls RubyLLM::Chat#complete directly, bypassing the instrumentation's patched #ask method. By instrumenting #complete instead, all LLM API calls are captured regardless of the entry point. This also means tool call flows now produce one chat span per API call rather than a single span wrapping the entire ask loop, giving better visibility into each individual LLM request. Closes #15 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c75f29f commit 23c4d38

2 files changed

Lines changed: 36 additions & 3 deletions

File tree

lib/opentelemetry/instrumentation/ruby_llm/patches/chat.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module Instrumentation
55
module RubyLLM
66
module Patches
77
module Chat
8-
def ask(message = nil, with: nil, &)
8+
def complete(&)
99
provider = @model&.provider || "unknown"
1010
model_id = @model&.id || "unknown"
1111

test/instrumentation_test.rb

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_records_error_on_api_failure
6767
assert_equal OpenTelemetry::Trace::Status::ERROR, span.status.code
6868
end
6969

70-
def test_ask_still_works_when_instrumentation_fails
70+
def test_complete_still_works_when_instrumentation_fails
7171
stub_request(:post, "https://api.openai.com/v1/chat/completions")
7272
.to_return(
7373
status: 200,
@@ -92,6 +92,39 @@ def test_ask_still_works_when_instrumentation_fails
9292
assert_equal "Hello!", response.content
9393
end
9494

95+
def test_instruments_complete_called_directly
96+
stub_request(:post, "https://api.openai.com/v1/chat/completions")
97+
.to_return(
98+
status: 200,
99+
headers: { "Content-Type" => "application/json" },
100+
body: {
101+
id: "chatcmpl-123",
102+
object: "chat.completion",
103+
model: "gpt-4o-mini",
104+
choices: [{
105+
index: 0,
106+
message: { role: "assistant", content: "Hello, world!" },
107+
finish_reason: "stop"
108+
}],
109+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }
110+
}.to_json
111+
)
112+
113+
chat = RubyLLM.chat(model: "gpt-4o-mini")
114+
chat.add_message(role: :user, content: "Hi")
115+
chat.complete
116+
117+
spans = EXPORTER.finished_spans
118+
assert_equal 1, spans.length
119+
120+
span = spans.first
121+
assert_equal "chat gpt-4o-mini", span.name
122+
assert_equal "chat", span.attributes["gen_ai.operation.name"]
123+
assert_equal "openai", span.attributes["gen_ai.provider.name"]
124+
assert_equal 10, span.attributes["gen_ai.usage.input_tokens"]
125+
assert_equal 5, span.attributes["gen_ai.usage.output_tokens"]
126+
end
127+
95128
def test_creates_span_for_tool_call
96129
calculator = Class.new(RubyLLM::Tool) do
97130
def self.name = "calculator"
@@ -155,7 +188,7 @@ def execute(expression:)
155188
chat_spans = spans.select { |s| s.name.include?("chat ") }
156189

157190
assert_equal 1, tool_spans.length
158-
assert_equal 1, chat_spans.length
191+
assert_equal 2, chat_spans.length
159192

160193
tool_span = tool_spans.first
161194
assert_equal OpenTelemetry::Trace::SpanKind::INTERNAL, tool_span.kind

0 commit comments

Comments
 (0)