llm.rb 4.3.1 → 4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b04602c6e101c35e64efe688c889da97675ebd112293f3da5b762456a3c15673
4
- data.tar.gz: 20d63516d9d81661842d29d4e7c34325103cc60bd698f7195b59cc5be44a2ccc
3
+ metadata.gz: 357446a155ea5c66f1f1de5e2172f021bfa339c1006bae3f35a18b1c1ad173a7
4
+ data.tar.gz: 4726be4c9133aa0da37771c6a16ba1eab27771acee6826a259f624a199cf8088
5
5
  SHA512:
6
- metadata.gz: 5ff6f85d0ec1c469e9ca9e5cfe310f28293b270f974dd9974d00bbf3b1cf6eb44e52351bf4b9d37e338c33349230bbd98f35d9bdbefe891b6dc402f734721654
7
- data.tar.gz: 71dd34c25c103c4dd874a9f86ce3628b0abf81538234d3243dbbdf03f685369dab7a052b041c6ef7c088518ae024a4894b09cceaed59a569ad33b0e1af1d5186
6
+ metadata.gz: b97ee9fc6594633d4176d21651a9625e8cd7c55d7d66d9e3a8a0bf5314df957447e7b4af431f57cd5e9c47408ceefc08babbb868af05d9ef7b887e543c6914a8
7
+ data.tar.gz: 5881e618855cf3c9830fcc5edad571b2cb015a514ab99fcd4134e09402fda6ff51e7d034d50e96988739747a0dc5520b40c4938b9f1a0ed9af328dde22a48c99
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <p align="center">
5
5
  <a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
6
6
  <a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
7
- <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.3.1-green.svg?" alt="Version"></a>
7
+ <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.4.0-green.svg?" alt="Version"></a>
8
8
  </p>
9
9
 
10
10
  ## About
data/lib/llm/provider.rb CHANGED
@@ -39,6 +39,7 @@ class LLM::Provider
39
39
  @client = persistent ? persistent_client : transient_client
40
40
  @tracer = LLM::Tracer::Null.new(self)
41
41
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
42
+ @headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
42
43
  end
43
44
 
44
45
  ##
@@ -195,7 +196,7 @@ class LLM::Provider
195
196
  # @return [LLM::Provider]
196
197
  # Returns self
197
198
  def with(headers:)
198
- tap { (@headers ||= {}).merge!(headers) }
199
+ tap { @headers.merge!(headers) }
199
200
  end
200
201
 
201
202
  ##
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+
3
5
  module LLM
4
6
  ##
5
7
  # The {LLM::Tracer::Telemetry LLM::Tracer::Telemetry} tracer provides
@@ -46,9 +48,46 @@ module LLM
46
48
  def initialize(provider, options = {})
47
49
  super
48
50
  @exporter = options.delete(:exporter)
51
+ @root_span = nil
52
+ @root_context = nil
49
53
  setup!
50
54
  end
51
55
 
56
+ ##
57
+ # When +trace_group_id+ is provided, it is converted to an OpenTelemetry
58
+ # trace_id (via a deterministic 16-byte hash) so all spans until {#stop_trace}
59
+ # share that trace_id and appear as one trace in OTLP/Langfuse.
60
+ #
61
+ # @param (see LLM::Tracer#start_trace)
62
+ # @return [self]
63
+ def start_trace(trace_group_id: nil, name: "llm", attributes: {})
64
+ return self if trace_group_id.to_s.empty?
65
+
66
+ span_context = span_context_from_trace_group_id(trace_group_id.to_s)
67
+ parent_ctx = ::OpenTelemetry::Trace.context_with_span(
68
+ ::OpenTelemetry::Trace.non_recording_span(span_context)
69
+ )
70
+ attrs = attributes.compact
71
+ attrs["llm.trace_group_id"] = trace_group_id.to_s
72
+ @root_span = @tracer.start_span(
73
+ name,
74
+ kind: :server,
75
+ attributes: attrs,
76
+ with_parent: parent_ctx
77
+ )
78
+ @root_context = ::OpenTelemetry::Trace.context_with_span(@root_span)
79
+ self
80
+ end
81
+
82
+ ##
83
+ # @return [self]
84
+ def stop_trace
85
+ @root_span&.finish
86
+ @root_span = nil
87
+ @root_context = nil
88
+ self
89
+ end
90
+
52
91
  ##
53
92
  # @param (see LLM::Tracer#on_request_start)
54
93
  def on_request_start(operation:, model: nil)
@@ -96,7 +135,7 @@ module LLM
96
135
  "server.port" => provider_port
97
136
  }.compact
98
137
  span_name = ["execute_tool", name].compact.join(" ")
99
- span = @tracer.start_span(span_name.empty? ? "gen_ai.tool" : span_name, kind: :client, attributes:)
138
+ span = create_span(span_name.empty? ? "gen_ai.tool" : span_name, attributes:)
100
139
  span.add_event("gen_ai.tool.start")
101
140
  span
102
141
  end
@@ -155,6 +194,30 @@ module LLM
155
194
 
156
195
  private
157
196
 
197
+ ##
198
+ # @api private
199
+ def create_span(name, kind: :client, attributes: {})
200
+ opts = {kind:, attributes:}
201
+ opts[:with_parent] = @root_context if @root_context
202
+ @tracer.start_span(name, **opts)
203
+ end
204
+
205
+ ##
206
+ # Converts a string trace_group_id to an OpenTelemetry SpanContext so all
207
+ # spans created with this context share the same trace_id.
208
+ # @api private
209
+ def span_context_from_trace_group_id(trace_group_id)
210
+ trace_id = Digest::MD5.digest(trace_group_id)
211
+ trace_id = ::OpenTelemetry::Trace.generate_trace_id if trace_id == ::OpenTelemetry::Trace::INVALID_TRACE_ID
212
+ span_id = Digest::SHA256.digest(trace_group_id)[0, 8]
213
+ span_id = ::OpenTelemetry::Trace.generate_span_id if span_id == ::OpenTelemetry::Trace::INVALID_SPAN_ID
214
+ ::OpenTelemetry::Trace::SpanContext.new(
215
+ trace_id:,
216
+ span_id:,
217
+ trace_flags: ::OpenTelemetry::Trace::TraceFlags::SAMPLED
218
+ )
219
+ end
220
+
158
221
  ##
159
222
  # @api private
160
223
  def setup!
@@ -209,7 +272,7 @@ module LLM
209
272
  "server.port" => provider_port
210
273
  }.compact
211
274
  span_name = [operation, model].compact.join(" ")
212
- span = @tracer.start_span(span_name.empty? ? "gen_ai.request" : span_name, kind: :client, attributes:)
275
+ span = create_span(span_name.empty? ? "gen_ai.request" : span_name, attributes:)
213
276
  span.add_event("gen_ai.request.start")
214
277
  span
215
278
  end
@@ -221,7 +284,7 @@ module LLM
221
284
  "server.address" => provider_host,
222
285
  "server.port" => provider_port
223
286
  }.compact
224
- span = @tracer.start_span(operation, kind: :client, attributes:)
287
+ span = create_span(operation, attributes:)
225
288
  span.add_event("gen_ai.request.start")
226
289
  span
227
290
  end
data/lib/llm/tracer.rb CHANGED
@@ -89,6 +89,31 @@ module LLM
89
89
  raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
90
90
  end
91
91
 
92
+ ##
93
+ # Opens a trace group so subsequent LLM spans share the same OpenTelemetry
94
+ # trace_id (and appear as one trace in backends like Langfuse).
95
+ # When +trace_group_id+ is a string, it is used to derive the trace_id.
96
+ #
97
+ # @param [String, nil] trace_group_id
98
+ # Optional. When present, converted to a 16-byte trace_id so all spans
99
+ # created until {#stop_trace} are grouped in one trace.
100
+ # @param [String] name
101
+ # Name for the root span (e.g. "chatbot.turn").
102
+ # @param [Hash] attributes
103
+ # OpenTelemetry attributes to set on the root span.
104
+ # @return [self]
105
+ def start_trace(trace_group_id: nil, name: "llm", attributes: {})
106
+ self
107
+ end
108
+
109
+ ##
110
+ # Finishes the trace group started by {#start_trace}. Safe to call even if
111
+ # no trace is active.
112
+ # @return [self]
113
+ def stop_trace
114
+ self
115
+ end
116
+
92
117
  ##
93
118
  # @return [String]
94
119
  def inspect
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.3.1"
4
+ VERSION = "4.4.0"
5
5
  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.3.1
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri