llm.rb 11.3.1 → 12.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +354 -2
  3. data/LICENSE +93 -17
  4. data/README.md +226 -616
  5. data/data/anthropic.json +322 -426
  6. data/data/bedrock.json +2634 -1144
  7. data/data/deepinfra.json +1513 -0
  8. data/data/deepseek.json +57 -28
  9. data/data/google.json +411 -771
  10. data/data/openai.json +1104 -771
  11. data/data/xai.json +141 -292
  12. data/data/zai.json +263 -141
  13. data/lib/llm/active_record/acts_as_agent.rb +3 -41
  14. data/lib/llm/active_record/acts_as_llm.rb +18 -0
  15. data/lib/llm/active_record.rb +3 -3
  16. data/lib/llm/agent.rb +25 -1
  17. data/lib/llm/context.rb +17 -5
  18. data/lib/llm/contract/completion.rb +2 -2
  19. data/lib/llm/json_adapter.rb +29 -3
  20. data/lib/llm/provider.rb +3 -3
  21. data/lib/llm/providers/deepinfra/audio.rb +66 -0
  22. data/lib/llm/providers/deepinfra/images.rb +90 -0
  23. data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
  24. data/lib/llm/providers/deepinfra.rb +100 -0
  25. data/lib/llm/providers/deepseek/images.rb +109 -0
  26. data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
  27. data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
  28. data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
  29. data/lib/llm/providers/deepseek.rb +4 -2
  30. data/lib/llm/providers/google/request_adapter.rb +22 -5
  31. data/lib/llm/providers/google.rb +4 -4
  32. data/lib/llm/providers/llamacpp.rb +5 -5
  33. data/lib/llm/providers/openai/audio.rb +6 -2
  34. data/lib/llm/providers/openai/images.rb +9 -50
  35. data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
  36. data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
  37. data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
  38. data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
  39. data/lib/llm/providers/openai/responses.rb +1 -0
  40. data/lib/llm/providers/openai/stream_parser.rb +5 -6
  41. data/lib/llm/providers/openai.rb +2 -2
  42. data/lib/llm/providers/xai/images.rb +49 -26
  43. data/lib/llm/providers/xai.rb +2 -2
  44. data/lib/llm/repl/input.rb +64 -0
  45. data/lib/llm/repl/status.rb +30 -0
  46. data/lib/llm/repl/stream.rb +46 -0
  47. data/lib/llm/repl/transcript.rb +61 -0
  48. data/lib/llm/repl/window.rb +107 -0
  49. data/lib/llm/repl.rb +78 -0
  50. data/lib/llm/response.rb +10 -0
  51. data/lib/llm/schema/leaf.rb +7 -1
  52. data/lib/llm/schema/renderer.rb +121 -0
  53. data/lib/llm/schema.rb +30 -0
  54. data/lib/llm/sequel/agent.rb +2 -43
  55. data/lib/llm/sequel/plugin.rb +25 -7
  56. data/lib/llm/tools/chdir.rb +23 -0
  57. data/lib/llm/tools/git.rb +41 -0
  58. data/lib/llm/tools/mkdir.rb +32 -0
  59. data/lib/llm/tools/pwd.rb +20 -0
  60. data/lib/llm/tools/read_file.rb +40 -0
  61. data/lib/llm/tools/rg.rb +46 -0
  62. data/lib/llm/tools/shell.rb +48 -0
  63. data/lib/llm/tools/swap_text.rb +25 -0
  64. data/lib/llm/tools/write_file.rb +24 -0
  65. data/lib/llm/tools.rb +5 -0
  66. data/lib/llm/tracer/telemetry.rb +4 -6
  67. data/lib/llm/tracer.rb +9 -21
  68. data/lib/llm/transport/execution.rb +16 -1
  69. data/lib/llm/transport/net_http_adapter.rb +1 -1
  70. data/lib/llm/uridata.rb +16 -0
  71. data/lib/llm/version.rb +1 -1
  72. data/lib/llm.rb +9 -0
  73. data/llm.gemspec +5 -18
  74. data/resources/deepdive.md +829 -263
  75. metadata +31 -18
  76. data/lib/llm/tracer/langsmith.rb +0 -144
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
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Chdir LLM::Tool::Chdir} class implements
8
+ # a tool that can change the current working directory.
9
+ class Chdir < self
10
+ name "chdir"
11
+ description "change the current working directory"
12
+ parameter :path, String, "the new working directory"
13
+ required %i[path]
14
+
15
+ ##
16
+ # @param [String] path
17
+ # @return [Hash]
18
+ def call(path:)
19
+ Dir.chdir(path)
20
+ {ok: true, cwd: path}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Git LLM::Tool::Git} class implements
8
+ # a tool that can perform a select number of git actions.
9
+ # The actions it can perform are read-only - at least for
10
+ # the time being.
11
+ class Git < self
12
+ name "git"
13
+ description "perform an action with git"
14
+ parameter :action, Enum["log", "diff", "commit", "checkout", "branch", "show"], "the git operation to perform"
15
+ parameter :arguments, Array[String], "one or more arguments for the git action"
16
+ required %i[action]
17
+
18
+ ##
19
+ # @param [String] path
20
+ # @param [Integer] start
21
+ # @param [Integer] stop
22
+ # @return [Hash]
23
+ def call(action:, arguments: nil)
24
+ command = spawn(action:, arguments:)
25
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
26
+ end
27
+
28
+ private
29
+
30
+ def spawn(action:, arguments:)
31
+ Command
32
+ .new("git")
33
+ .argv(action)
34
+ .argv(*[*arguments])
35
+ .spawn
36
+ end
37
+
38
+ LLM.require "test-cmd.rb"
39
+ Command = Test::Cmd
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tool
4
+ ##
5
+ # The {LLM::Tool::Mkdir LLM::Tool::Mkdir} class implements
6
+ # a tool that can create a tree of new directories.
7
+ class Mkdir < self
8
+ name "mkdir"
9
+ description "create a new directory"
10
+ parameter :path, String, "the path to the directory"
11
+
12
+ ##
13
+ # @param [String] path
14
+ # @return [Hash]
15
+ def call(path:)
16
+ command = spawn(path:)
17
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
18
+ end
19
+
20
+ private
21
+
22
+ def spawn(path:)
23
+ Command
24
+ .new("mkdir")
25
+ .argv("-p", path)
26
+ .spawn
27
+ end
28
+
29
+ LLM.require "test-cmd.rb"
30
+ Command = Test::Cmd
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Pwd LLM::Tool::Pwd} class implements
8
+ # a tool that can reveal the current working directory.
9
+ class Pwd < self
10
+ name "pwd"
11
+ description "returns the current working directory"
12
+
13
+ ##
14
+ # @param [String] path
15
+ # @return [Hash]
16
+ def call
17
+ {ok: true, cwd: Dir.getwd}
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::ReadFile LLM::Tool::ReadFile} class implements
8
+ # a tool that can read the contents of a file. The tool accepts
9
+ # two optional offsets: a start line, and a stop line. Without
10
+ # either the entire file contents are read into memory.
11
+ class ReadFile < self
12
+ name "read-file"
13
+ description "read the contents of a file"
14
+ parameter :path, String, "the path to the file"
15
+ parameter :start, Integer, "start line number"
16
+ parameter :stop, Integer, "stop line number"
17
+ required %i[path]
18
+
19
+ ##
20
+ # @param [String] path
21
+ # @param [Integer] start
22
+ # @param [Integer] stop
23
+ # @return [Hash]
24
+ def call(path:, start: 1, stop: -1)
25
+ content, cursor = nil, 1
26
+ File.open(path, "r") do |f|
27
+ while cursor < start
28
+ f.gets
29
+ cursor += 1
30
+ end
31
+ if stop == -1
32
+ content = f.read
33
+ else
34
+ content = start.upto(stop).map { f.gets }.join
35
+ end
36
+ end
37
+ {ok: true, content:}
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Rg LLM::Tool::Rg} class implements
8
+ # a frontend to the popular 'rg' tool. The tool can
9
+ # recursively search the current working directory
10
+ # for one or more patterns.
11
+ class Rg < self
12
+ name "rg"
13
+ description "recursively search the current directory for lines matching a pattern"
14
+ parameter :patterns, Array[String], "one or more search patterns"
15
+ parameter :path, String, "the path where the search is performed (default is cwd)"
16
+ required %i[patterns]
17
+
18
+ ##
19
+ # @param [String] pattern
20
+ # @return [Hash]
21
+ def call(patterns:, path: Dir.getwd)
22
+ validate!(patterns:, path:)
23
+ command = spawn(patterns:, path:)
24
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
25
+ end
26
+
27
+ private
28
+
29
+ def validate!(patterns:, path:)
30
+ if path == "/"
31
+ raise RuntimeError, "you can't search from the root of the filesystem"
32
+ elsif patterns == ["."]
33
+ raise RuntimeError, "narrow your search"
34
+ end
35
+ end
36
+
37
+ def spawn(patterns:, path:)
38
+ Command.new("rg")
39
+ .argv(*[*patterns].flat_map { ["-e", _1] }, path)
40
+ .spawn
41
+ end
42
+
43
+ LLM.require "test-cmd.rb"
44
+ Command = Test::Cmd
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_string_literal
4
+
5
+ class LLM::Tool
6
+ ##
7
+ # The {LLM::Tool::Shell} class implements a tool that can
8
+ # spawn a command. That can be dangerous given a low-quality
9
+ # model, or a high-quality model that simply makes a bad
10
+ # decision. The risk can be reduced through a confirmation
11
+ # step such as {LLM::Agent.confirm LLM::Agent.confirm}, or
12
+ # by managing the tool loop manually through
13
+ # {LLM::Context LLM::Context}.
14
+ class Shell < self
15
+ name "shell"
16
+ description "run a shell command"
17
+ parameter :name, String, "the command name"
18
+ parameter :arguments, Array[String], "one or more command arguments"
19
+ required %i[name]
20
+
21
+ ##
22
+ # @param [String] name
23
+ # The name of a command
24
+ # @param [Array<String>] arguments
25
+ # One or more command-line arguments
26
+ # @return [Hash]
27
+ def call(name:, arguments: nil)
28
+ command = spawn(name:, arguments:)
29
+ {ok: command.success?, stdout: command.stdout, stderr: command.stderr}
30
+ end
31
+
32
+ private
33
+
34
+ ##
35
+ # @param [String] name
36
+ # @param [Array<String>] arguments
37
+ # @return [Command]
38
+ def spawn(name:, arguments:)
39
+ Command
40
+ .new(name)
41
+ .argv(*[*arguments])
42
+ .spawn
43
+ end
44
+
45
+ LLM.require "test-cmd.rb"
46
+ Command = Test::Cmd
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tool
4
+ ##
5
+ # The {LLM::Tool::SwapText LLM::Tool::SwapText} class
6
+ # implements a tool that can substitute one piece of
7
+ # text for another piece of text in a given file.
8
+ class SwapText < self
9
+ name "swap-text"
10
+ description "Replace an exact snippet in a file"
11
+ parameter :path, String, "Path to file"
12
+ parameter :before, String, "Exact text to replace"
13
+ parameter :after, String, "Replacement text"
14
+ parameter :expected_count, Integer, "How many matches should be replaced"
15
+ required %i[path before after]
16
+
17
+ def call(path:, before:, after:, expected_count: 1)
18
+ content = File.read(path)
19
+ count = content.scan(before).length
20
+ raise "expected #{expected_count} match(es), found #{count}" unless count == expected_count.to_i
21
+ File.write(path, content.sub(before, after))
22
+ {ok: true, replaced: count}
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tool
4
+ ##
5
+ # The {LLM::Tool::WriteFile LLM::Tool::WriteFile} class
6
+ # implements a tool that can write a given string to a
7
+ # given file path.
8
+ class WriteFile < self
9
+ name "write-file"
10
+ description "write to a file"
11
+ parameter :path, String, "The file path"
12
+ parameter :content, String, "The file content"
13
+ required %i[path content]
14
+
15
+ ##
16
+ # @param [String] path
17
+ # @param [String] content
18
+ # @return [Hash]
19
+ def call(path:, content:)
20
+ File.open(path, "w") { _1.write(content) }
21
+ {ok: true}
22
+ end
23
+ end
24
+ end
data/lib/llm/tools.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.join(__dir__, "tools", "*.rb")].each do
4
+ require(_1)
5
+ end
@@ -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
@@ -21,7 +21,7 @@ class LLM::Transport
21
21
  when :put then ::Net::HTTP::Put.new(path, headers)
22
22
  when :patch then ::Net::HTTP::Patch.new(path, headers)
23
23
  when :delete then ::Net::HTTP::Delete.new(path, headers)
24
- else ::Net::HTTP::GenericRequest.new(method, path, nil, headers)
24
+ else ::Net::HTTPGenericRequest.new(method, path, nil, headers)
25
25
  end
26
26
  if req.body
27
27
  http_req.body = req.body
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ class URIData < Struct.new(:content_type, :encoding_type, :encoded, :decoded)
5
+ ##
6
+ # @param [String] str
7
+ # A string
8
+ # @return [URIData]
9
+ def self.parse(str)
10
+ _, data = str.split(":")
11
+ content_type, data = data.split(";")
12
+ encoding_type, data = data.split(",")
13
+ URIData.new(content_type, encoding_type, data, StringIO.new(data.unpack1("m0")))
14
+ end
15
+ end
16
+ end
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "11.3.1"
4
+ VERSION = "12.1.0"
5
5
  end