langfuse-rb 0.1.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.
@@ -0,0 +1,615 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langfuse
4
+ # Observation type constants
5
+ OBSERVATION_TYPES = {
6
+ span: "span",
7
+ generation: "generation",
8
+ embedding: "embedding",
9
+ event: "event",
10
+ agent: "agent",
11
+ tool: "tool",
12
+ chain: "chain",
13
+ retriever: "retriever",
14
+ evaluator: "evaluator",
15
+ guardrail: "guardrail"
16
+ }.freeze
17
+
18
+ # Base class for all Langfuse observation wrappers.
19
+ #
20
+ # Provides unified functionality for spans, generations, events, and specialized observation types.
21
+ # Wraps OpenTelemetry spans with Langfuse-specific functionality. Uses unified `start_observation()`
22
+ # method with `as_type` parameter, aligning with langfuse-js architecture.
23
+ #
24
+ # @example Block-based API (auto-ends)
25
+ # Langfuse.observe("parent-operation", input: { query: "test" }) do |span|
26
+ # # Child span
27
+ # span.start_observation("data-processing", input: { step: "fetch" }) do |child|
28
+ # result = fetch_data
29
+ # child.update(output: result)
30
+ # end
31
+ #
32
+ # # Child generation (LLM call)
33
+ # span.start_observation("llm-call", { model: "gpt-4", input: [{ role: "user", content: "Hello" }] }, as_type: :generation) do |gen|
34
+ # response = call_llm
35
+ # gen.update(output: response, usage_details: { prompt_tokens: 100, completion_tokens: 50 })
36
+ # end
37
+ # end
38
+ #
39
+ # @example Stateful API (manual end)
40
+ # span = Langfuse.start_observation("parent-operation", { input: { query: "test" } })
41
+ #
42
+ # # Child span
43
+ # child_span = span.start_observation("data-validation", { input: { data: result } })
44
+ # validate_data
45
+ # child_span.update(output: { valid: true })
46
+ # child_span.end
47
+ #
48
+ # # Child generation (LLM call)
49
+ # gen = span.start_observation("llm-summary", {
50
+ # model: "gpt-3.5-turbo",
51
+ # input: [{ role: "user", content: "Summarize" }]
52
+ # }, as_type: :generation)
53
+ # summary = call_llm
54
+ # gen.update(output: summary, usage_details: { prompt_tokens: 50, completion_tokens: 25 })
55
+ # gen.end
56
+ #
57
+ # span.end
58
+ #
59
+ # @abstract Subclass and pass type: to super to create concrete observation types
60
+ class BaseObservation
61
+ attr_reader :otel_span, :otel_tracer, :type
62
+
63
+ # @param otel_span [OpenTelemetry::SDK::Trace::Span] The underlying OTel span
64
+ # @param otel_tracer [OpenTelemetry::SDK::Trace::Tracer] The OTel tracer
65
+ # @param attributes [Hash, Types::SpanAttributes, Types::GenerationAttributes, nil] Optional initial attributes
66
+ # @param type [String] Observation type (e.g., "span", "generation", "event")
67
+ def initialize(otel_span, otel_tracer, attributes: nil, type: nil)
68
+ @otel_span = otel_span
69
+ @otel_tracer = otel_tracer
70
+ @type = type || raise(ArgumentError, "type must be provided")
71
+
72
+ # Set initial attributes if provided
73
+ return unless attributes
74
+
75
+ update_observation_attributes(attributes.to_h)
76
+ end
77
+
78
+ # @return [String] Hex-encoded span ID (16 hex characters)
79
+ def id
80
+ @otel_span.context.span_id.unpack1("H*")
81
+ end
82
+
83
+ # @return [String] Hex-encoded trace ID (32 hex characters)
84
+ def trace_id
85
+ @otel_span.context.trace_id.unpack1("H*")
86
+ end
87
+
88
+ # @return [String] URL to view this trace in Langfuse UI
89
+ #
90
+ # @example
91
+ # span = Langfuse.observe("operation") do |obs|
92
+ # puts "View trace: #{obs.trace_url}"
93
+ # end
94
+ def trace_url
95
+ Langfuse.client.trace_url(trace_id)
96
+ end
97
+
98
+ # @param end_time [Time, Integer, nil] Optional end time (Time object or Unix timestamp in nanoseconds)
99
+ def end(end_time: nil)
100
+ @otel_span.finish(end_timestamp: end_time)
101
+ end
102
+
103
+ # Updates trace-level attributes (user_id, session_id, tags, etc.) for the entire trace.
104
+ #
105
+ # @param attrs [Hash, Types::TraceAttributes] Trace attributes to set
106
+ # @return [self]
107
+ def update_trace(attrs)
108
+ return self unless @otel_span.recording?
109
+
110
+ otel_attrs = OtelAttributes.create_trace_attributes(attrs.to_h)
111
+ otel_attrs.each { |key, value| @otel_span.set_attribute(key, value) }
112
+ self
113
+ end
114
+
115
+ # Creates a child observation within this observation's context.
116
+ #
117
+ # Supports block-based (auto-ends) and stateful (manual end) APIs. Events auto-end when created without a block.
118
+ #
119
+ # @param name [String] Descriptive name for the child observation
120
+ # @param attrs [Hash, Types::SpanAttributes, Types::GenerationAttributes, nil] Observation attributes
121
+ # @param as_type [Symbol, String] Observation type (:span, :generation, :event, etc.). Defaults to `:span`.
122
+ # @yield [observation] Optional block that receives the observation object
123
+ # @return [BaseObservation, Object] The child observation (or block return value if block given)
124
+ def start_observation(name, attrs = {}, as_type: :span, &block)
125
+ # Call module-level factory with parent context
126
+ # Skip validation to allow unknown types to fall back to Span
127
+ child = Langfuse.start_observation(
128
+ name,
129
+ attrs,
130
+ as_type: as_type,
131
+ parent_span_context: @otel_span.context,
132
+ skip_validation: true
133
+ )
134
+
135
+ if block
136
+ # Block-based API: auto-ends when block completes
137
+ # Set context and execute block
138
+ current_context = OpenTelemetry::Context.current
139
+ result = OpenTelemetry::Context.with_current(
140
+ OpenTelemetry::Trace.context_with_span(child.otel_span, parent_context: current_context)
141
+ ) do
142
+ block.call(child)
143
+ end
144
+ # Only end if not already ended (events auto-end in start_observation)
145
+ child.end unless as_type.to_s == OBSERVATION_TYPES[:event]
146
+ result
147
+ else
148
+ # Stateful API - return observation
149
+ # Events already auto-ended in start_observation
150
+ child
151
+ end
152
+ end
153
+
154
+ # Sets observation-level input attributes.
155
+ #
156
+ # @param value [Object] Input value (will be JSON-encoded)
157
+ def input=(value)
158
+ update_observation_attributes(input: value)
159
+ end
160
+
161
+ # Sets observation-level output attributes.
162
+ #
163
+ # @param value [Object] Output value (will be JSON-encoded)
164
+ def output=(value)
165
+ update_observation_attributes(output: value)
166
+ end
167
+
168
+ # @param value [Hash] Metadata hash (expanded into individual langfuse.observation.metadata.* attributes)
169
+ def metadata=(value)
170
+ update_observation_attributes(metadata: value)
171
+ end
172
+
173
+ # @param value [String] Level (DEBUG, DEFAULT, WARNING, ERROR)
174
+ def level=(value)
175
+ update_observation_attributes(level: value)
176
+ end
177
+
178
+ # @param name [String] Event name
179
+ # @param input [Object, nil] Optional event data
180
+ # @param level [String] Log level (debug, default, warning, error)
181
+ #
182
+ def event(name:, input: nil, level: "default")
183
+ attributes = {
184
+ "langfuse.observation.input" => input&.to_json,
185
+ "langfuse.observation.level" => level
186
+ }.compact
187
+
188
+ @otel_span.add_event(name, attributes: attributes)
189
+ end
190
+
191
+ # @return [OpenTelemetry::SDK::Trace::Span]
192
+ def current_span
193
+ @otel_span
194
+ end
195
+
196
+ # Protected method used by subclasses' public `update` methods.
197
+ #
198
+ # @param attrs [Hash, Types::SpanAttributes, Types::GenerationAttributes] Attributes to update
199
+ # @api private
200
+ protected
201
+
202
+ def update_observation_attributes(attrs = {}, **kwargs)
203
+ # Don't set attributes on ended spans
204
+ return unless @otel_span.recording?
205
+
206
+ # Merge keyword arguments into attrs hash
207
+ attrs_hash = if kwargs.any?
208
+ attrs.to_h.merge(kwargs)
209
+ else
210
+ attrs.to_h
211
+ end
212
+
213
+ # Use @type instance variable set during initialization
214
+ otel_attrs = OtelAttributes.create_observation_attributes(type, attrs_hash)
215
+ otel_attrs.each { |key, value| @otel_span.set_attribute(key, value) }
216
+ end
217
+
218
+ # Converts a prompt object to hash format for OtelAttributes.
219
+ #
220
+ # @param prompt [Object, Hash, nil] Prompt object or hash
221
+ # @return [Hash, Object, nil] Hash with name and version, or original prompt
222
+ # @api protected
223
+ def normalize_prompt(prompt)
224
+ case prompt
225
+ in obj if obj.respond_to?(:name) && obj.respond_to?(:version)
226
+ { name: obj.name, version: obj.version }
227
+ else
228
+ prompt
229
+ end
230
+ end
231
+ end
232
+
233
+ # General-purpose observation for tracking operations, functions, or logical units of work.
234
+ #
235
+ # @example Block-based API
236
+ # Langfuse.observe("data-processing", input: { query: "test" }) do |span|
237
+ # result = process_data
238
+ # span.update(output: result, metadata: { duration_ms: 150 })
239
+ # end
240
+ #
241
+ # @example Stateful API
242
+ # span = Langfuse.start_observation("data-processing", input: { query: "test" })
243
+ # result = process_data
244
+ # span.update(output: result)
245
+ # span.end
246
+ #
247
+ class Span < BaseObservation
248
+ def initialize(otel_span, otel_tracer, attributes: nil)
249
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:span])
250
+ end
251
+
252
+ # @param attrs [Hash, Types::SpanAttributes] Span attributes to set
253
+ # @return [self]
254
+ def update(attrs)
255
+ update_observation_attributes(attrs)
256
+ self
257
+ end
258
+ end
259
+
260
+ # Observation for LLM calls. Provides methods to set output, usage, and other LLM-specific metadata.
261
+ #
262
+ # @example Block-based API
263
+ # Langfuse.observe("chat-completion", as_type: :generation) do |gen|
264
+ # gen.model = "gpt-4"
265
+ # gen.input = [{ role: "user", content: "Hello" }]
266
+ # response = call_llm(gen.input)
267
+ # gen.output = response
268
+ # gen.usage = { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 }
269
+ # end
270
+ #
271
+ # @example Stateful API
272
+ # gen = Langfuse.start_observation("chat-completion", {
273
+ # model: "gpt-3.5-turbo",
274
+ # input: [{ role: "user", content: "Summarize this" }]
275
+ # }, as_type: :generation)
276
+ # response = call_llm(gen.input)
277
+ # gen.update(
278
+ # output: response,
279
+ # usage_details: { prompt_tokens: 50, completion_tokens: 25, total_tokens: 75 }
280
+ # )
281
+ # gen.end
282
+ #
283
+ class Generation < BaseObservation
284
+ def initialize(otel_span, otel_tracer, attributes: nil)
285
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:generation])
286
+ end
287
+
288
+ # @param attrs [Hash, Types::GenerationAttributes] Generation attributes to set
289
+ # @return [self]
290
+ def update(attrs)
291
+ update_observation_attributes(attrs)
292
+ self
293
+ end
294
+
295
+ # @param value [Hash] Usage hash with token counts (:prompt_tokens, :completion_tokens, :total_tokens)
296
+ def usage=(value)
297
+ return unless @otel_span.recording?
298
+
299
+ # Convert to Langfuse API format (camelCase keys)
300
+ usage_hash = {
301
+ promptTokens: value[:prompt_tokens] || value["prompt_tokens"],
302
+ completionTokens: value[:completion_tokens] || value["completion_tokens"],
303
+ totalTokens: value[:total_tokens] || value["total_tokens"]
304
+ }.compact
305
+
306
+ usage_json = usage_hash.to_json
307
+ @otel_span.set_attribute("langfuse.observation.usage", usage_json)
308
+ end
309
+
310
+ # @param value [String] Model name (e.g., "gpt-4", "claude-3-opus")
311
+ def model=(value)
312
+ return unless @otel_span.recording?
313
+
314
+ @otel_span.set_attribute("langfuse.observation.model", value.to_s)
315
+ end
316
+
317
+ # @param value [Hash] Model parameters (temperature, max_tokens, etc.)
318
+ def model_parameters=(value)
319
+ return unless @otel_span.recording?
320
+
321
+ # Convert to Langfuse API format (camelCase keys)
322
+ params_hash = {}
323
+ value.each do |k, v|
324
+ key_str = k.to_s
325
+ # Convert snake_case to camelCase
326
+ camel_key = key_str.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
327
+ params_hash[camel_key] = v
328
+ end
329
+ params_json = params_hash.to_json
330
+ @otel_span.set_attribute("langfuse.observation.modelParameters", params_json)
331
+ end
332
+ end
333
+
334
+ # Point-in-time occurrence. Automatically ended when created without a block.
335
+ #
336
+ # @example Creating an event
337
+ # Langfuse.observe("user-action", as_type: :event) do |event|
338
+ # event.update(input: { action: "button_click", button_id: "submit" })
339
+ # end
340
+ #
341
+ # @example Event without block (auto-ends)
342
+ # event = Langfuse.start_observation("error-occurred", {
343
+ # input: { error: "Connection timeout" },
344
+ # level: "error"
345
+ # }, as_type: :event)
346
+ # # Event is automatically ended
347
+ #
348
+ class Event < BaseObservation
349
+ def initialize(otel_span, otel_tracer, attributes: nil)
350
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:event])
351
+ end
352
+
353
+ # @param attrs [Hash, Types::SpanAttributes] Event attributes to set
354
+ # @return [self]
355
+ def update(attrs)
356
+ update_observation_attributes(attrs)
357
+ self
358
+ end
359
+ end
360
+
361
+ # Observation for tracking agent-based workflows that make decisions and use tools.
362
+ #
363
+ # @example Block-based API
364
+ # Langfuse.observe("agent-workflow", as_type: :agent) do |agent|
365
+ # agent.input = { task: "Find weather for NYC" }
366
+ # # Agent makes decisions and uses tools
367
+ # agent.start_observation("tool-call", { tool_name: "weather_api" }, as_type: :tool) do |tool|
368
+ # weather = fetch_weather("NYC")
369
+ # tool.update(output: weather)
370
+ # end
371
+ # agent.update(output: { result: "Sunny, 72°F" })
372
+ # end
373
+ #
374
+ # @example Stateful API
375
+ # agent = Langfuse.start_observation("agent-workflow", {
376
+ # input: { task: "Research topic" }
377
+ # }, as_type: :agent)
378
+ # # Agent logic here
379
+ # agent.update(output: { result: "Research complete" })
380
+ # agent.end
381
+ #
382
+ class Agent < BaseObservation
383
+ def initialize(otel_span, otel_tracer, attributes: nil)
384
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:agent])
385
+ end
386
+
387
+ # @param attrs [Hash, Types::AgentAttributes] Agent attributes to set
388
+ # @return [self]
389
+ def update(attrs)
390
+ update_observation_attributes(attrs)
391
+ self
392
+ end
393
+ end
394
+
395
+ # Observation for tracking individual tool calls and external API interactions.
396
+ #
397
+ # @example Block-based API
398
+ # Langfuse.observe("api-call", as_type: :tool) do |tool|
399
+ # tool.input = { endpoint: "/users", method: "GET" }
400
+ # response = http_client.get("/users")
401
+ # tool.update(output: response.body, metadata: { status_code: response.status })
402
+ # end
403
+ #
404
+ # @example Stateful API
405
+ # tool = Langfuse.start_observation("database-query", {
406
+ # input: { query: "SELECT * FROM users" }
407
+ # }, as_type: :tool)
408
+ # results = db.execute(tool.input[:query])
409
+ # tool.update(output: results)
410
+ # tool.end
411
+ #
412
+ class Tool < BaseObservation
413
+ def initialize(otel_span, otel_tracer, attributes: nil)
414
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:tool])
415
+ end
416
+
417
+ # @param attrs [Hash, Types::ToolAttributes] Tool attributes to set
418
+ # @return [self]
419
+ def update(attrs)
420
+ update_observation_attributes(attrs)
421
+ self
422
+ end
423
+ end
424
+
425
+ # Observation for tracking structured multi-step workflows and process chains.
426
+ #
427
+ # @example Block-based API
428
+ # Langfuse.observe("rag-pipeline", as_type: :chain) do |chain|
429
+ # chain.input = { query: "What is Ruby?" }
430
+ # # Step 1: Retrieve documents
431
+ # chain.start_observation("retrieve", { query: chain.input[:query] }, as_type: :retriever) do |ret|
432
+ # docs = vector_db.search(chain.input[:query])
433
+ # ret.update(output: docs)
434
+ # end
435
+ # # Step 2: Generate response
436
+ # chain.start_observation("generate", { model: "gpt-4" }, as_type: :generation) do |gen|
437
+ # response = llm.generate(docs)
438
+ # gen.update(output: response)
439
+ # end
440
+ # chain.update(output: { answer: "Ruby is a programming language..." })
441
+ # end
442
+ #
443
+ # @example Stateful API
444
+ # chain = Langfuse.start_observation("multi-step-process", {
445
+ # input: { data: "input_data" }
446
+ # }, as_type: :chain)
447
+ # # Chain steps here
448
+ # chain.update(output: { result: "processed_data" })
449
+ # chain.end
450
+ #
451
+ class Chain < BaseObservation
452
+ def initialize(otel_span, otel_tracer, attributes: nil)
453
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:chain])
454
+ end
455
+
456
+ # @param attrs [Hash, Types::ChainAttributes] Chain attributes to set
457
+ # @return [self]
458
+ def update(attrs)
459
+ update_observation_attributes(attrs)
460
+ self
461
+ end
462
+ end
463
+
464
+ # Observation for tracking document retrieval and search operations.
465
+ #
466
+ # @example Block-based API
467
+ # Langfuse.observe("document-search", as_type: :retriever) do |retriever|
468
+ # retriever.input = { query: "Ruby programming", top_k: 5 }
469
+ # documents = vector_db.search(retriever.input[:query], limit: retriever.input[:top_k])
470
+ # retriever.update(
471
+ # output: documents,
472
+ # metadata: { num_results: documents.length, search_time_ms: 45 }
473
+ # )
474
+ # end
475
+ #
476
+ # @example Stateful API
477
+ # retriever = Langfuse.start_observation("semantic-search", {
478
+ # input: { query: "machine learning", top_k: 10 }
479
+ # }, as_type: :retriever)
480
+ # results = search_index.query(retriever.input[:query])
481
+ # retriever.update(output: results)
482
+ # retriever.end
483
+ #
484
+ class Retriever < BaseObservation
485
+ def initialize(otel_span, otel_tracer, attributes: nil)
486
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:retriever])
487
+ end
488
+
489
+ # @param attrs [Hash, Types::RetrieverAttributes] Retriever attributes to set
490
+ # @return [self]
491
+ def update(attrs)
492
+ update_observation_attributes(attrs)
493
+ self
494
+ end
495
+ end
496
+
497
+ # Observation for tracking quality assessment and evaluation operations.
498
+ #
499
+ # @example Block-based API
500
+ # Langfuse.observe("quality-check", as_type: :evaluator) do |evaluator|
501
+ # evaluator.input = { response: "Ruby is a language", expected: "Ruby is a programming language" }
502
+ # score = calculate_similarity(evaluator.input[:response], evaluator.input[:expected])
503
+ # evaluator.update(
504
+ # output: { score: score, passed: score > 0.8 },
505
+ # metadata: { metric: "similarity" }
506
+ # )
507
+ # end
508
+ #
509
+ # @example Stateful API
510
+ # evaluator = Langfuse.start_observation("response-evaluation", {
511
+ # input: { response: llm_output, criteria: "accuracy" }
512
+ # }, as_type: :evaluator)
513
+ # evaluation_result = evaluate_response(evaluator.input[:response], evaluator.input[:criteria])
514
+ # evaluator.update(output: evaluation_result)
515
+ # evaluator.end
516
+ #
517
+ class Evaluator < BaseObservation
518
+ def initialize(otel_span, otel_tracer, attributes: nil)
519
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:evaluator])
520
+ end
521
+
522
+ # @param attrs [Hash, Types::EvaluatorAttributes] Evaluator attributes to set
523
+ # @return [self]
524
+ def update(attrs)
525
+ update_observation_attributes(attrs)
526
+ self
527
+ end
528
+ end
529
+
530
+ # Observation for tracking safety checks and compliance enforcement.
531
+ #
532
+ # @example Block-based API
533
+ # Langfuse.observe("content-moderation", as_type: :guardrail) do |guardrail|
534
+ # guardrail.input = { content: user_input }
535
+ # result = moderation_service.check(guardrail.input[:content])
536
+ # guardrail.update(
537
+ # output: { passed: result.safe, reason: result.reason },
538
+ # metadata: { check_type: "toxicity" }
539
+ # )
540
+ # end
541
+ #
542
+ # @example Stateful API
543
+ # guardrail = Langfuse.start_observation("safety-check", {
544
+ # input: { prompt: user_prompt }
545
+ # }, as_type: :guardrail)
546
+ # safety_result = safety_service.validate(guardrail.input[:prompt])
547
+ # guardrail.update(output: { safe: safety_result.safe, violations: safety_result.violations })
548
+ # guardrail.end
549
+ #
550
+ class Guardrail < BaseObservation
551
+ def initialize(otel_span, otel_tracer, attributes: nil)
552
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:guardrail])
553
+ end
554
+
555
+ # @param attrs [Hash, Types::GuardrailAttributes] Guardrail attributes to set
556
+ # @return [self]
557
+ def update(attrs)
558
+ update_observation_attributes(attrs)
559
+ self
560
+ end
561
+ end
562
+
563
+ # Observation for tracking embedding generation calls and vector operations.
564
+ #
565
+ # @example Block-based API
566
+ # Langfuse.observe("generate-embeddings", as_type: :embedding) do |embedding|
567
+ # embedding.model = "text-embedding-ada-002"
568
+ # embedding.input = ["Ruby is a language", "Python is a language"]
569
+ # vectors = embedding_service.generate(embedding.input, model: embedding.model)
570
+ # embedding.update(
571
+ # output: vectors,
572
+ # usage: { prompt_tokens: 20, total_tokens: 20 }
573
+ # )
574
+ # end
575
+ #
576
+ # @example Stateful API
577
+ # embedding = Langfuse.start_observation("vectorize", {
578
+ # model: "text-embedding-ada-002",
579
+ # input: "Convert this text to vector"
580
+ # }, as_type: :embedding)
581
+ # vector = embedding_api.create(embedding.input, model: embedding.model)
582
+ # embedding.update(
583
+ # output: vector,
584
+ # usage_details: { prompt_tokens: 10, total_tokens: 10 }
585
+ # )
586
+ # embedding.end
587
+ #
588
+ class Embedding < BaseObservation
589
+ def initialize(otel_span, otel_tracer, attributes: nil)
590
+ super(otel_span, otel_tracer, attributes: attributes, type: OBSERVATION_TYPES[:embedding])
591
+ end
592
+
593
+ # @param attrs [Hash, Types::EmbeddingAttributes] Embedding attributes to set
594
+ # @return [self]
595
+ def update(attrs)
596
+ update_observation_attributes(attrs)
597
+ self
598
+ end
599
+
600
+ # @param value [Hash] Usage hash with token counts (:prompt_tokens, :total_tokens)
601
+ def usage=(value)
602
+ update_observation_attributes(usage_details: value)
603
+ end
604
+
605
+ # @param value [String] Model name (e.g., "text-embedding-ada-002")
606
+ def model=(value)
607
+ update_observation_attributes(model: value)
608
+ end
609
+
610
+ # @param value [Hash] Model parameters (temperature, max_tokens, etc.)
611
+ def model_parameters=(value)
612
+ update_observation_attributes(model_parameters: value)
613
+ end
614
+ end
615
+ end