llm.rb 11.3.0 → 12.0.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +270 -1
  3. data/LICENSE +92 -17
  4. data/README.md +209 -647
  5. data/data/anthropic.json +433 -249
  6. data/data/bedrock.json +2097 -1055
  7. data/data/deepinfra.json +993 -0
  8. data/data/deepseek.json +53 -28
  9. data/data/google.json +389 -771
  10. data/data/openai.json +1053 -771
  11. data/data/xai.json +133 -292
  12. data/data/zai.json +249 -141
  13. data/lib/llm/a2a.rb +2 -2
  14. data/lib/llm/active_record/acts_as_agent.rb +10 -41
  15. data/lib/llm/active_record/acts_as_llm.rb +25 -0
  16. data/lib/llm/active_record.rb +3 -3
  17. data/lib/llm/agent.rb +1 -1
  18. data/lib/llm/context.rb +9 -5
  19. data/lib/llm/contract/completion.rb +2 -2
  20. data/lib/llm/cost.rb +1 -1
  21. data/lib/llm/function/fiber_group.rb +2 -2
  22. data/lib/llm/function/task_group.rb +2 -2
  23. data/lib/llm/function/thread_group.rb +3 -3
  24. data/lib/llm/pipe.rb +1 -1
  25. data/lib/llm/provider.rb +2 -2
  26. data/lib/llm/providers/anthropic/request_adapter.rb +1 -1
  27. data/lib/llm/providers/bedrock/request_adapter/completion.rb +5 -5
  28. data/lib/llm/providers/bedrock/request_adapter.rb +3 -3
  29. data/lib/llm/providers/bedrock/response_adapter/completion.rb +2 -2
  30. data/lib/llm/providers/bedrock/response_adapter.rb +2 -2
  31. data/lib/llm/providers/deepinfra/audio.rb +66 -0
  32. data/lib/llm/providers/deepinfra/images.rb +90 -0
  33. data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
  34. data/lib/llm/providers/deepinfra.rb +100 -0
  35. data/lib/llm/providers/deepseek/images.rb +109 -0
  36. data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
  37. data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
  38. data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
  39. data/lib/llm/providers/deepseek.rb +4 -2
  40. data/lib/llm/providers/google/request_adapter.rb +23 -6
  41. data/lib/llm/providers/google.rb +4 -4
  42. data/lib/llm/providers/ollama/request_adapter.rb +1 -1
  43. data/lib/llm/providers/openai/audio.rb +6 -2
  44. data/lib/llm/providers/openai/images.rb +9 -50
  45. data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
  46. data/lib/llm/providers/openai/request_adapter.rb +1 -1
  47. data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
  48. data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
  49. data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
  50. data/lib/llm/providers/openai/responses.rb +1 -0
  51. data/lib/llm/providers/openai/stream_parser.rb +5 -6
  52. data/lib/llm/providers/openai.rb +2 -2
  53. data/lib/llm/providers/xai/images.rb +49 -26
  54. data/lib/llm/providers/xai.rb +2 -2
  55. data/lib/llm/registry.rb +2 -2
  56. data/lib/llm/response.rb +11 -1
  57. data/lib/llm/schema/leaf.rb +7 -1
  58. data/lib/llm/schema/object.rb +1 -1
  59. data/lib/llm/schema/renderer.rb +121 -0
  60. data/lib/llm/schema.rb +30 -0
  61. data/lib/llm/sequel/agent.rb +2 -43
  62. data/lib/llm/sequel/plugin.rb +25 -7
  63. data/lib/llm/stream.rb +1 -1
  64. data/lib/llm/tool.rb +2 -2
  65. data/lib/llm/tracer/telemetry.rb +4 -6
  66. data/lib/llm/tracer.rb +9 -21
  67. data/lib/llm/transport/execution.rb +16 -1
  68. data/lib/llm/transport/http.rb +2 -2
  69. data/lib/llm/transport/net_http_adapter.rb +1 -1
  70. data/lib/llm/transport/persistent_http.rb +1 -1
  71. data/lib/llm/transport/response/http.rb +1 -1
  72. data/lib/llm/uridata.rb +16 -0
  73. data/lib/llm/utils.rb +1 -1
  74. data/lib/llm/version.rb +1 -1
  75. data/lib/llm.rb +20 -8
  76. data/llm.gemspec +14 -26
  77. data/resources/deepdive.md +966 -0
  78. metadata +26 -29
  79. data/lib/llm/tracer/langsmith.rb +0 -144
@@ -4,30 +4,21 @@ class LLM::XAI
4
4
  ##
5
5
  # The {LLM::XAI::Images LLM::XAI::Images} class provides an interface
6
6
  # for [xAI's images API](https://docs.x.ai/docs/guides/image-generations).
7
- # xAI supports multiple response formats: temporary URLs, or binary strings
8
- # encoded in base64. The default is to return base64-encoded image data.
7
+ # xAI returns base64-encoded image data.
9
8
  #
10
- # @example Temporary URLs
9
+ # @example
11
10
  # #!/usr/bin/env ruby
12
11
  # require "llm"
13
- # require "open-uri"
14
- # require "fileutils"
15
12
  #
16
13
  # llm = LLM.xai(key: ENV["KEY"])
17
- # res = llm.images.create prompt: "A dog on a rocket to the moon",
18
- # response_format: "url"
19
- # FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
20
- # "rocket.png"
21
- #
22
- # @example Binary strings
23
- # #!/usr/bin/env ruby
24
- # require "llm"
25
- #
26
- # llm = LLM.xai(key: ENV["KEY"])
27
- # res = llm.images.create prompt: "A dog on a rocket to the moon",
28
- # response_format: "b64_json"
14
+ # res = llm.images.create prompt: "A dog on a rocket to the moon"
29
15
  # IO.copy_stream res.images[0], "rocket.png"
30
16
  class Images < LLM::OpenAI::Images
17
+ ##
18
+ # @api private
19
+ PATTERN = %r{\A(?:https?://|data:)}
20
+ private_constant :PATTERN
21
+
31
22
  ##
32
23
  # Create an image
33
24
  # @example
@@ -40,20 +31,52 @@ class LLM::XAI
40
31
  # @param [Hash] params Other parameters (see xAI docs)
41
32
  # @raise (see LLM::Provider#request)
42
33
  # @return [LLM::Response]
43
- def create(prompt:, model: "grok-imagine-image", **params)
44
- super
34
+ def create(prompt:, model: "grok-imagine-image-quality", **params)
35
+ req = LLM::Transport::Request.post(path("/images/generations"), headers)
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")
38
+ res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
39
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
40
+ res
45
41
  end
46
42
 
47
43
  ##
48
- # @raise [NotImplementedError]
49
- def edit(model: "grok-imagine-image", **)
50
- raise NotImplementedError
44
+ # Edit an image
45
+ # @example
46
+ # llm = LLM.xai(key: ENV["KEY"])
47
+ # res = llm.images.edit(image: "/images/book.png", prompt: "The book is floating in the clouds")
48
+ # IO.copy_stream res.images[0], "floating-book.png"
49
+ # @see https://docs.x.ai/docs/guides/image-generations xAI docs
50
+ # @param [String, LLM::File, File] image The image to edit
51
+ # @param [String] prompt The prompt
52
+ # @param [String] model The model to use
53
+ # @param [Hash] params Other parameters (see xAI docs)
54
+ # @raise (see LLM::Provider#request)
55
+ # @return [LLM::Response]
56
+ def edit(image:, prompt:, model: "grok-imagine-image-quality", **params)
57
+ req = LLM::Transport::Request.post(path("/images/edits"), headers)
58
+ req.body = LLM.json.dump({
59
+ prompt:,
60
+ model:,
61
+ image: image_url(image),
62
+ response_format: "b64_json"
63
+ }.merge!(params))
64
+ res, span, tracer = execute(request: req, operation: "request")
65
+ res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
66
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
67
+ res
51
68
  end
52
69
 
53
- ##
54
- # @raise [NotImplementedError]
55
- def create_variation(model: "grok-imagine-image", **)
56
- raise NotImplementedError
70
+ private
71
+
72
+ def image_url(image)
73
+ case image
74
+ when String
75
+ url = image.match?(PATTERN) ? image : LLM.File(image).to_data_uri
76
+ else
77
+ url = LLM.File(image).to_data_uri
78
+ end
79
+ {url:, type: "image_url"}
57
80
  end
58
81
  end
59
82
  end
@@ -70,10 +70,10 @@ module LLM
70
70
 
71
71
  ##
72
72
  # Returns the default model for chat completions
73
- # #see https://docs.x.ai/docs/models grok-4-0709
73
+ # #see https://docs.x.ai/docs/models grok-4.3
74
74
  # @return [String]
75
75
  def default_model
76
- "grok-4-0709"
76
+ "grok-4.3"
77
77
  end
78
78
  end
79
79
  end
data/lib/llm/registry.rb CHANGED
@@ -12,7 +12,7 @@ class LLM::Registry
12
12
  ##
13
13
  # @raise [LLM::Error]
14
14
  # Might raise an error
15
- # @param [Symbol]
15
+ # @param [Symbol] name
16
16
  # A provider name
17
17
  # @return [LLM::Registry]
18
18
  def self.for(name)
@@ -71,7 +71,7 @@ class LLM::Registry
71
71
  end
72
72
 
73
73
  ##
74
- # Similar to #{find} but returns the block's return value
74
+ # Similar to `#find` but returns the block's return value
75
75
  # @return [Object, nil]
76
76
  def find_map(pair)
77
77
  result = nil
data/lib/llm/response.rb CHANGED
@@ -14,7 +14,7 @@ module LLM
14
14
  # through {#res}. When the default net/http transport is in use,
15
15
  # {LLM::Transport::Response::HTTP
16
16
  # LLM::Transport::Response::HTTP} keeps the
17
- # original {Net::HTTPResponse Net::HTTPResponse} available through
17
+ # original `Net::HTTPResponse` available through
18
18
  # its own {LLM::Transport::Response::HTTP#res #res}.
19
19
  class Response
20
20
  require "json"
@@ -56,6 +56,16 @@ module LLM
56
56
  @res.success?
57
57
  end
58
58
 
59
+ ##
60
+ # Returns the provider response id when present.
61
+ # @return [String, nil]
62
+ def id
63
+ return nil unless LLM::Object === body
64
+ body.id ||
65
+ body.responseId || body.response_id ||
66
+ body.requestId || body.request_id
67
+ end
68
+
59
69
  ##
60
70
  # Returns true if the response is from the Files API
61
71
  # @return [Boolean]
@@ -95,7 +95,7 @@ class LLM::Schema
95
95
  ##
96
96
  # @return [Hash]
97
97
  def to_h
98
- {description: @description, default: @default, enum: @enum}.compact
98
+ {description: @description, default: @default, enum: @enum, const: @const}.compact
99
99
  end
100
100
 
101
101
  ##
@@ -104,6 +104,12 @@ class LLM::Schema
104
104
  to_h.to_json(options)
105
105
  end
106
106
 
107
+ ##
108
+ # @return [String]
109
+ def to_s
110
+ LLM::Schema::Renderer.render(self)
111
+ end
112
+
107
113
  ##
108
114
  # @param [LLM::Schema::Leaf] other
109
115
  # An object to compare
@@ -12,7 +12,7 @@ class LLM::Schema
12
12
  attr_reader :properties
13
13
 
14
14
  ##
15
- # @param params [Hash]
15
+ # @param [Hash] properties
16
16
  # A hash of properties
17
17
  # @return [LLM::Schema::Object]
18
18
  def initialize(properties)
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Schema
4
+ ##
5
+ # Internal renderer for prompt-friendly schema output.
6
+ # @api private
7
+ module Renderer
8
+ extend self
9
+
10
+ ##
11
+ # Render a schema node as a human-readable string.
12
+ # @param [LLM::Schema::Leaf] node
13
+ # The schema node to render
14
+ # @param [Integer] indent
15
+ # The indentation level
16
+ # @param [String, Symbol, nil] name
17
+ # The property name for nested nodes
18
+ # @param [Boolean] root
19
+ # Whether the node is the root schema object
20
+ # @return [String]
21
+ def render(node, indent: 0, name: nil, root: false)
22
+ line = (" " * indent).to_s
23
+ if name
24
+ line << name.to_s
25
+ line << "?" unless node.required?
26
+ line << ": "
27
+ end
28
+ line << type_name(node)
29
+ metadata = metadata_for(node, include_required: !root)
30
+ line << " (#{metadata.join(", ")})" unless metadata.empty?
31
+ line << " - #{node.description}" if node.respond_to?(:description) && node.description
32
+ nested = nested_lines(node, indent: indent + 2)
33
+ ([line] + nested).join("\n")
34
+ end
35
+
36
+ private
37
+
38
+ ##
39
+ # Render nested lines for compound schema nodes.
40
+ # @param [LLM::Schema::Leaf] node
41
+ # The schema node
42
+ # @param [Integer] indent
43
+ # The indentation level
44
+ # @return [Array<String>]
45
+ def nested_lines(node, indent:)
46
+ case node
47
+ when LLM::Schema::Object
48
+ node.properties.map { |key, val| render(val, indent:, name: key) }
49
+ when LLM::Schema::Array
50
+ items = node.to_h[:items]
51
+ items.is_a?(LLM::Schema::Object) ? [render(items, indent:, name: "items")] : []
52
+ else
53
+ []
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Return the printable type name for a schema node.
59
+ # @param [LLM::Schema::Leaf] node
60
+ # The schema node
61
+ # @return [String]
62
+ def type_name(node)
63
+ h = node.to_h
64
+ return "array<#{inline_type(h[:items])}>" if node.is_a?(LLM::Schema::Array)
65
+ return "anyOf<#{inline_types(h[:anyOf])}>" if node.is_a?(LLM::Schema::AnyOf)
66
+ return "oneOf<#{inline_types(h[:oneOf])}>" if node.is_a?(LLM::Schema::OneOf)
67
+ return "allOf<#{inline_types(h[:allOf])}>" if node.is_a?(LLM::Schema::AllOf)
68
+ h[:type] || "unknown"
69
+ end
70
+
71
+ ##
72
+ # Return the inline type description for a nested node.
73
+ # @param [LLM::Schema::Leaf, Object] node
74
+ # The nested schema node
75
+ # @return [String]
76
+ def inline_type(node)
77
+ return type_name(node) if node.is_a?(LLM::Schema::Leaf)
78
+ node.inspect
79
+ end
80
+
81
+ ##
82
+ # Return the inline type description for a list of nodes.
83
+ # @param [Array<LLM::Schema::Leaf>] values
84
+ # The union members
85
+ # @return [String]
86
+ def inline_types(values)
87
+ values.map { inline_type(_1) }.join(", ")
88
+ end
89
+
90
+ ##
91
+ # Extract printable metadata for a schema node.
92
+ # @param [LLM::Schema::Leaf] node
93
+ # The schema node
94
+ # @param [Boolean] include_required
95
+ # Whether to include the required marker
96
+ # @return [Array<String>]
97
+ def metadata_for(node, include_required:)
98
+ h = node.to_h.dup
99
+ details = []
100
+ details << "required" if include_required && node.required?
101
+ details << "default: #{value(node.default)}" if node.default
102
+ details << "enum: #{node.enum.map { value(_1) }.join(" | ")}" if node.enum
103
+ details << "const: #{value(node.const)}" if node.const
104
+ h.except(:type, :description, :default, :enum, :const, :required, :properties, :items, :anyOf, :oneOf, :allOf)
105
+ .each { |key, val| details << "#{key}: #{value(val)}" }
106
+ details
107
+ end
108
+
109
+ ##
110
+ # Convert a scalar value into its printable representation.
111
+ # @param [Object] val
112
+ # The value to render
113
+ # @return [String]
114
+ def value(val)
115
+ case val
116
+ when ::String then val.inspect
117
+ else val.to_s
118
+ end
119
+ end
120
+ end
121
+ end
data/lib/llm/schema.rb CHANGED
@@ -34,6 +34,7 @@
34
34
  class LLM::Schema
35
35
  require_relative "schema/version"
36
36
  require_relative "schema/parser"
37
+ require_relative "schema/renderer"
37
38
  require_relative "schema/leaf"
38
39
  require_relative "schema/object"
39
40
  require_relative "schema/array"
@@ -121,6 +122,19 @@ class LLM::Schema
121
122
  end
122
123
  end
123
124
 
125
+ ##
126
+ # @param [Hash] defaults
127
+ # @return [LLM::Schema::Object]
128
+ def self.defaults(defaults)
129
+ lock do
130
+ object.tap do |schema|
131
+ defaults.each do |name, val|
132
+ Utils.fetch(schema.properties, name).default(val)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
124
138
  ##
125
139
  # @api private
126
140
  # @return [LLM::Schema]
@@ -139,6 +153,14 @@ class LLM::Schema
139
153
  end
140
154
  end
141
155
 
156
+ ##
157
+ # Render the schema as a prompt-friendly string.
158
+ # @return [String]
159
+ def self.to_s
160
+ Renderer.render(object, root: true)
161
+ end
162
+ (class << self; self; end).alias_method(:inspect, :to_s)
163
+
142
164
  ##
143
165
  # @api private
144
166
  def self.lock(&)
@@ -220,4 +242,12 @@ class LLM::Schema
220
242
  def null
221
243
  Null.new
222
244
  end
245
+
246
+ ##
247
+ # Render a schema leaf as a prompt-friendly string.
248
+ # @return [String]
249
+ def to_s
250
+ self.class.to_s
251
+ end
252
+ alias_method :inspect, :to_s
223
253
  end
@@ -7,8 +7,7 @@ module LLM::Sequel
7
7
  # This wrapper reuses the same record-backed runtime surface as
8
8
  # {LLM::Sequel::Plugin}, but builds an {LLM::Agent LLM::Agent} instead of an
9
9
  # {LLM::Context LLM::Context}. Agent defaults such as model, tools, schema,
10
- # instructions, and concurrency are configured on the model class and
11
- # forwarded to an internal agent subclass.
10
+ # instructions, and concurrency are configured on an internal agent subclass.
12
11
  module Agent
13
12
  require_relative "plugin"
14
13
  EMPTY_HASH = LLM::Sequel::Plugin::EMPTY_HASH
@@ -25,7 +24,7 @@ module LLM::Sequel
25
24
  options = DEFAULTS.merge(options)
26
25
  model.db.extension :pg_json if %i[json jsonb].include?(options[:format])
27
26
  model.instance_variable_set(:@llm_agent_options, options.freeze)
28
- model.instance_exec(&block) if block
27
+ block_given? ? model.instance_exec(model.agent, &block) : nil
29
28
  end
30
29
 
31
30
  module ClassMethods
@@ -33,46 +32,6 @@ module LLM::Sequel
33
32
  @llm_agent_options || Agent::DEFAULTS
34
33
  end
35
34
 
36
- def model(model = nil, &block)
37
- return agent.model if model.nil? && !block
38
- agent.model(model, &block)
39
- end
40
-
41
- def tools(*tools, &block)
42
- return agent.tools if tools.empty? && !block
43
- agent.tools(*tools, &block)
44
- end
45
-
46
- def skills(*skills, &block)
47
- return agent.skills if skills.empty? && !block
48
- agent.skills(*skills, &block)
49
- end
50
-
51
- def schema(schema = nil, &block)
52
- return agent.schema if schema.nil? && !block
53
- agent.schema(schema, &block)
54
- end
55
-
56
- def instructions(instructions = nil)
57
- return agent.instructions if instructions.nil?
58
- agent.instructions(instructions)
59
- end
60
-
61
- def concurrency(concurrency = nil)
62
- return agent.concurrency if concurrency.nil?
63
- agent.concurrency(concurrency)
64
- end
65
-
66
- def confirm(*tool_names, &block)
67
- return agent.confirm if tool_names.empty? && !block
68
- agent.confirm(*tool_names, &block)
69
- end
70
-
71
- def tracer(tracer = nil, &block)
72
- return agent.tracer if tracer.nil? && !block
73
- agent.tracer(tracer, &block)
74
- end
75
-
76
35
  def agent
77
36
  @agent ||= Class.new(LLM::Agent)
78
37
  end
@@ -16,6 +16,13 @@ module LLM::Sequel
16
16
  # JSON typecasting for the model. `provider:`, `context:`, and `tracer:`
17
17
  # can also be configured as symbols that are called on the model.
18
18
  module Plugin
19
+ DEFAULTS = {
20
+ data_column: :data,
21
+ format: :string,
22
+ provider: :set_provider,
23
+ context: :set_context,
24
+ tracer: :set_tracer
25
+ }.freeze
19
26
  EMPTY_HASH = {}.freeze
20
27
 
21
28
  ##
@@ -94,13 +101,6 @@ module LLM::Sequel
94
101
  end
95
102
  end
96
103
  end
97
- DEFAULTS = {
98
- data_column: :data,
99
- format: :string,
100
- tracer: nil,
101
- provider: nil,
102
- context: EMPTY_HASH
103
- }.freeze
104
104
 
105
105
  ##
106
106
  # Called by Sequel when the plugin is first applied to a model class.
@@ -304,6 +304,24 @@ module LLM::Sequel
304
304
 
305
305
  private
306
306
 
307
+ ##
308
+ # @return [LLM::Provider]
309
+ def set_provider
310
+ raise NotImplementedError, "implement the set_provider callback"
311
+ end
312
+
313
+ ##
314
+ # @return [Hash]
315
+ def set_context
316
+ Plugin::EMPTY_HASH.dup
317
+ end
318
+
319
+ ##
320
+ # @return [LLM::Tracer]
321
+ def set_tracer
322
+ nil
323
+ end
324
+
307
325
  ##
308
326
  # @return [LLM::Context]
309
327
  def ctx
data/lib/llm/stream.rb CHANGED
@@ -15,7 +15,7 @@ module LLM
15
15
  # therefore block streaming progress and should generally return as
16
16
  # quickly as possible.
17
17
  #
18
- # The most common callback is {#on_content}, which also maps to {#<<}.
18
+ # The most common callback is {#on_content}, which also maps to `#<<`.
19
19
  # Providers may also call {#on_reasoning_content} and {#on_tool_call} when
20
20
  # that data is available. Runtime features such as context compaction may
21
21
  # also emit lifecycle callbacks like {#on_transform} or {#on_compaction}.
data/lib/llm/tool.rb CHANGED
@@ -63,7 +63,7 @@ class LLM::Tool
63
63
  ##
64
64
  # @param [LLM::A2A] a2a
65
65
  # The A2A client that will execute the tool call
66
- # @param [LLM::A2A::Card::Skill]
66
+ # @param [LLM::A2A::Card::Skill] skill
67
67
  # An A2A tool
68
68
  # @return [Class<LLM::Tool>]
69
69
  # Returns a subclass of LLM::Tool
@@ -124,7 +124,7 @@ class LLM::Tool
124
124
 
125
125
  ##
126
126
  # Registers the tool as a function when inherited
127
- # @param [Class] klass The subclass
127
+ # @param [Class] tool The subclass
128
128
  # @return [void]
129
129
  def self.inherited(tool)
130
130
  LLM.lock(:inherited) do
@@ -30,8 +30,7 @@ module LLM
30
30
  # require "llm"
31
31
  # require "opentelemetry-exporter-otlp"
32
32
  #
33
- # endpoint = "https://api.smith.langchain.com/otel/v1/traces"
34
- # exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint:)
33
+ # exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
35
34
  #
36
35
  # llm = LLM.openai(key: ENV["KEY"])
37
36
  # llm.tracer = LLM::Tracer::Telemetry.new(llm, exporter:)
@@ -59,7 +58,6 @@ module LLM
59
58
  # @return [self]
60
59
  def start_trace(trace_group_id: nil, name: "llm", attributes: {}, metadata: nil)
61
60
  return self if trace_group_id.to_s.empty?
62
-
63
61
  span_context = span_context_from_trace_group_id(trace_group_id.to_s)
64
62
  parent_ctx = ::OpenTelemetry::Trace.context_with_span(
65
63
  ::OpenTelemetry::Trace.non_recording_span(span_context)
@@ -316,7 +314,7 @@ module LLM
316
314
  set_span_attributes(span, consume_extra_outputs.merge(outputs || {}))
317
315
  finish_metadata = consume_finish_metadata_proc(res)
318
316
  metadata = (metadata || {}).merge(finish_metadata || {})
319
- set_span_attributes(span, metadata.transform_keys { "langsmith.metadata.#{_1}" })
317
+ set_span_attributes(span, metadata.transform_keys { "llm.metadata.#{_1}" })
320
318
  span.add_event("gen_ai.request.finish")
321
319
  span.tap(&:finish)
322
320
  end
@@ -326,7 +324,7 @@ module LLM
326
324
  "gen_ai.operation.name" => operation
327
325
  }.merge!(finish_attributes(operation, res)).compact
328
326
  chunks_json = retrieval_chunks_json(res)
329
- attributes["langsmith.metadata.chunks"] = chunks_json if chunks_json
327
+ attributes["llm.metadata.chunks"] = chunks_json if chunks_json
330
328
  attributes.each { span.set_attribute(_1, _2) }
331
329
  span.add_event("gen_ai.request.finish")
332
330
  span.tap(&:finish)
@@ -334,7 +332,7 @@ module LLM
334
332
 
335
333
  ##
336
334
  # @api private
337
- # Serialize retrieval response chunks for span attributes (e.g. langsmith.metadata.chunks).
335
+ # Serialize retrieval response chunks for span attributes.
338
336
  # Returns a JSON string or nil when res has no data.
339
337
  def consume_finish_metadata_proc(res)
340
338
  key = LLM::Tracer::FINISH_METADATA_PROC_KEY
data/lib/llm/tracer.rb CHANGED
@@ -11,7 +11,6 @@ module LLM
11
11
  class Tracer
12
12
  require_relative "tracer/logger"
13
13
  require_relative "tracer/telemetry"
14
- require_relative "tracer/langsmith"
15
14
  require_relative "tracer/null"
16
15
 
17
16
  ##
@@ -45,7 +44,7 @@ module LLM
45
44
  # @param [Object, nil] span
46
45
  # @param [String] model
47
46
  # @param [Hash, nil] outputs Optional span attributes (e.g. gen_ai.output.messages) from llm.rb or caller.
48
- # @param [Hash, nil] metadata Optional metadata (emitted as langsmith.metadata.*) from llm.rb or caller.
47
+ # @param [Hash, nil] metadata Optional metadata from llm.rb or caller.
49
48
  # @return [void]
50
49
  def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil)
51
50
  raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
@@ -110,8 +109,7 @@ module LLM
110
109
  # @param [Hash] attributes
111
110
  # OpenTelemetry attributes to set on the root span.
112
111
  # @param [Hash, nil] metadata
113
- # Optional. Trace-level metadata merged into the trace (e.g. langsmith.metadata.*).
114
- # Only used by tracers that support it (e.g. {LLM::Tracer::Langsmith}).
112
+ # Optional. Trace-level metadata merged into the trace by tracers that support it.
115
113
  # @return [self]
116
114
  def start_trace(trace_group_id: nil, name: "llm", attributes: {}, metadata: nil)
117
115
  self
@@ -150,11 +148,10 @@ module LLM
150
148
  ##
151
149
  # Merges extra attributes for the current trace/span. Used by applications
152
150
  # (e.g. chatbot) to add metadata, span inputs, or span outputs to the next
153
- # span or to the trace. No-op by default; {LLM::Tracer::Langsmith} merges
154
- # into fiber-local storage and emits them as langsmith/GenAI attributes.
151
+ # span or to the trace. No-op by default.
155
152
  #
156
153
  # @param [Hash, nil] metadata
157
- # Key-value pairs merged into trace/span metadata (e.g. langsmith.metadata.*).
154
+ # Key-value pairs merged into trace/span metadata.
158
155
  # @param [Hash, nil] inputs
159
156
  # Key-value pairs set on the next span at start (e.g. gen_ai.input.messages).
160
157
  # Consumed when the span is created.
@@ -169,9 +166,9 @@ module LLM
169
166
  ##
170
167
  # Optional: set a proc to supply metadata when the next chat span finishes.
171
168
  # The proc is called with the response (res) and should return a Hash of
172
- # metadata (e.g. { intent: "...", confidence: 1.0 }) to merge onto the span
173
- # as langsmith.metadata.*. Cleared after use. Used by apps to attach
174
- # routing/intent that is only known after the response.
169
+ # metadata (e.g. { intent: "...", confidence: 1.0 }) to merge onto the span.
170
+ # Cleared after use. Used by apps to attach routing/intent that is only
171
+ # known after the response.
175
172
  #
176
173
  # @param [Proc, nil] proc (res) -> Hash or nil
177
174
  # @return [self]
@@ -182,19 +179,10 @@ module LLM
182
179
 
183
180
  FINISH_METADATA_PROC_KEY = :"llm.tracer.finish_metadata_proc"
184
181
 
185
- ##
186
- # Returns the current extra bag (metadata, inputs, outputs) for the current
187
- # thread/trace. Used by subclasses; default returns empty hashes.
188
- #
189
- # @return [Hash] { metadata: {}, inputs: {}, outputs: {} }
190
- def current_extra
191
- {}
192
- end
193
-
194
182
  ##
195
183
  # Returns and clears extra inputs for the next span. Called by the telemetry
196
- # tracer when starting a span. Subclasses (e.g. Langsmith) override to
197
- # return fiber-local inputs; default returns {}.
184
+ # tracer when starting a span. Subclasses can override to return stored
185
+ # inputs; default returns {}.
198
186
  #
199
187
  # @return [Hash] Attribute key => value to set on the span at start
200
188
  def consume_extra_inputs
@@ -59,9 +59,24 @@ class LLM::Transport
59
59
  # @return [LLM::Object, String]
60
60
  def parse_response(res)
61
61
  case res["content-type"]
62
- when %r{\Aapplication/json\s*} then LLM::Object.from(LLM.json.load(res.body))
62
+ when %r{\Aapplication/json\s*}
63
+ body = read_body(res.body)
64
+ LLM::Object.from(LLM.json.load(body))
63
65
  else res.body
64
66
  end
65
67
  end
68
+
69
+ ##
70
+ # @param [#class] body
71
+ # @return [String]
72
+ def read_body(body)
73
+ case body.class.to_s
74
+ when "Net::ReadAdapter"
75
+ str = +""
76
+ body.read_body { str << _1 }
77
+ str
78
+ else body
79
+ end
80
+ end
66
81
  end
67
82
  end
@@ -5,7 +5,7 @@ require "net/http"
5
5
  class LLM::Transport
6
6
  ##
7
7
  # The {LLM::Transport::HTTP LLM::Transport::HTTP} transport is the
8
- # built-in adapter for Ruby's {Net::HTTP Net::HTTP}. It manages
8
+ # built-in adapter for Ruby's `Net::HTTP`. It manages
9
9
  # transient HTTP connections, tracks active requests by owner, and
10
10
  # interrupts in-flight requests when needed.
11
11
  #
@@ -69,7 +69,7 @@ class LLM::Transport
69
69
 
70
70
  ##
71
71
  # Performs a request on the current HTTP transport.
72
- # Accepts both {Net::HTTPRequest} and {LLM::Transport::Request}.
72
+ # Accepts both `Net::HTTPRequest` and {LLM::Transport::Request}.
73
73
  #
74
74
  # @param [Net::HTTPRequest, LLM::Transport::Request] request
75
75
  # @param [Fiber] owner