llm.rb 2.1.0 → 3.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/lib/llm/bot.rb +4 -4
  4. data/lib/llm/buffer.rb +0 -9
  5. data/lib/llm/contract/completion.rb +57 -0
  6. data/lib/llm/contract.rb +48 -0
  7. data/lib/llm/error.rb +22 -14
  8. data/lib/llm/eventhandler.rb +6 -4
  9. data/lib/llm/eventstream/parser.rb +18 -13
  10. data/lib/llm/function.rb +1 -1
  11. data/lib/llm/json_adapter.rb +109 -0
  12. data/lib/llm/message.rb +7 -28
  13. data/lib/llm/multipart/enumerator_io.rb +86 -0
  14. data/lib/llm/multipart.rb +32 -51
  15. data/lib/llm/object/builder.rb +6 -6
  16. data/lib/llm/object/kernel.rb +2 -2
  17. data/lib/llm/object.rb +23 -8
  18. data/lib/llm/provider.rb +11 -3
  19. data/lib/llm/providers/anthropic/error_handler.rb +1 -1
  20. data/lib/llm/providers/anthropic/files.rb +4 -5
  21. data/lib/llm/providers/anthropic/models.rb +1 -2
  22. data/lib/llm/providers/anthropic/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
  23. data/lib/llm/providers/anthropic/{format.rb → request_adapter.rb} +7 -7
  24. data/lib/llm/providers/anthropic/response_adapter/completion.rb +66 -0
  25. data/lib/llm/providers/anthropic/{response → response_adapter}/enumerable.rb +1 -1
  26. data/lib/llm/providers/anthropic/{response → response_adapter}/file.rb +1 -1
  27. data/lib/llm/providers/anthropic/{response → response_adapter}/web_search.rb +3 -3
  28. data/lib/llm/providers/anthropic/response_adapter.rb +36 -0
  29. data/lib/llm/providers/anthropic/stream_parser.rb +6 -6
  30. data/lib/llm/providers/anthropic.rb +8 -11
  31. data/lib/llm/providers/deepseek/{format/completion_format.rb → request_adapter/completion.rb} +15 -15
  32. data/lib/llm/providers/deepseek/{format.rb → request_adapter.rb} +7 -7
  33. data/lib/llm/providers/deepseek.rb +2 -2
  34. data/lib/llm/providers/gemini/audio.rb +2 -2
  35. data/lib/llm/providers/gemini/error_handler.rb +3 -3
  36. data/lib/llm/providers/gemini/files.rb +4 -7
  37. data/lib/llm/providers/gemini/images.rb +9 -14
  38. data/lib/llm/providers/gemini/models.rb +1 -2
  39. data/lib/llm/providers/gemini/{format/completion_format.rb → request_adapter/completion.rb} +14 -14
  40. data/lib/llm/providers/gemini/{format.rb → request_adapter.rb} +8 -8
  41. data/lib/llm/providers/gemini/response_adapter/completion.rb +67 -0
  42. data/lib/llm/providers/gemini/{response → response_adapter}/embedding.rb +1 -1
  43. data/lib/llm/providers/gemini/{response → response_adapter}/file.rb +1 -1
  44. data/lib/llm/providers/gemini/{response → response_adapter}/files.rb +1 -1
  45. data/lib/llm/providers/gemini/{response → response_adapter}/image.rb +3 -3
  46. data/lib/llm/providers/gemini/{response → response_adapter}/models.rb +1 -1
  47. data/lib/llm/providers/gemini/{response → response_adapter}/web_search.rb +3 -3
  48. data/lib/llm/providers/gemini/response_adapter.rb +42 -0
  49. data/lib/llm/providers/gemini/stream_parser.rb +37 -32
  50. data/lib/llm/providers/gemini.rb +10 -14
  51. data/lib/llm/providers/ollama/error_handler.rb +1 -1
  52. data/lib/llm/providers/ollama/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
  53. data/lib/llm/providers/ollama/{format.rb → request_adapter.rb} +7 -7
  54. data/lib/llm/providers/ollama/response_adapter/completion.rb +61 -0
  55. data/lib/llm/providers/ollama/{response → response_adapter}/embedding.rb +1 -1
  56. data/lib/llm/providers/ollama/response_adapter.rb +32 -0
  57. data/lib/llm/providers/ollama/stream_parser.rb +2 -2
  58. data/lib/llm/providers/ollama.rb +8 -10
  59. data/lib/llm/providers/openai/audio.rb +1 -1
  60. data/lib/llm/providers/openai/error_handler.rb +12 -2
  61. data/lib/llm/providers/openai/files.rb +3 -6
  62. data/lib/llm/providers/openai/images.rb +4 -5
  63. data/lib/llm/providers/openai/models.rb +1 -3
  64. data/lib/llm/providers/openai/moderations.rb +3 -5
  65. data/lib/llm/providers/openai/{format/completion_format.rb → request_adapter/completion.rb} +22 -22
  66. data/lib/llm/providers/openai/{format/moderation_format.rb → request_adapter/moderation.rb} +5 -5
  67. data/lib/llm/providers/openai/{format/respond_format.rb → request_adapter/respond.rb} +16 -16
  68. data/lib/llm/providers/openai/{format.rb → request_adapter.rb} +12 -12
  69. data/lib/llm/providers/openai/{response → response_adapter}/audio.rb +1 -1
  70. data/lib/llm/providers/openai/response_adapter/completion.rb +62 -0
  71. data/lib/llm/providers/openai/{response → response_adapter}/embedding.rb +1 -1
  72. data/lib/llm/providers/openai/{response → response_adapter}/enumerable.rb +1 -1
  73. data/lib/llm/providers/openai/{response → response_adapter}/file.rb +1 -1
  74. data/lib/llm/providers/openai/{response → response_adapter}/image.rb +1 -1
  75. data/lib/llm/providers/openai/{response → response_adapter}/moderations.rb +1 -1
  76. data/lib/llm/providers/openai/{response → response_adapter}/responds.rb +6 -10
  77. data/lib/llm/providers/openai/{response → response_adapter}/web_search.rb +3 -3
  78. data/lib/llm/providers/openai/response_adapter.rb +47 -0
  79. data/lib/llm/providers/openai/responses/stream_parser.rb +22 -22
  80. data/lib/llm/providers/openai/responses.rb +6 -8
  81. data/lib/llm/providers/openai/stream_parser.rb +6 -5
  82. data/lib/llm/providers/openai/vector_stores.rb +8 -9
  83. data/lib/llm/providers/openai.rb +12 -14
  84. data/lib/llm/response.rb +2 -5
  85. data/lib/llm/usage.rb +10 -0
  86. data/lib/llm/version.rb +1 -1
  87. data/lib/llm.rb +33 -1
  88. metadata +44 -35
  89. data/lib/llm/providers/anthropic/response/completion.rb +0 -39
  90. data/lib/llm/providers/gemini/response/completion.rb +0 -35
  91. data/lib/llm/providers/ollama/response/completion.rb +0 -28
  92. data/lib/llm/providers/openai/response/completion.rb +0 -40
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Format
3
+ module LLM::OpenAI::RequestAdapter
4
4
  ##
5
5
  # @private
6
- class CompletionFormat
6
+ class Completion
7
7
  ##
8
8
  # @param [LLM::Message, Hash] message
9
9
  # The message to format
@@ -12,72 +12,72 @@ module LLM::OpenAI::Format
12
12
  end
13
13
 
14
14
  ##
15
- # Formats the message for the OpenAI chat completions API
15
+ # Adapts the message for the OpenAI chat completions API
16
16
  # @return [Hash]
17
- def format
17
+ def adapt
18
18
  catch(:abort) do
19
19
  if Hash === message
20
- {role: message[:role], content: format_content(message[:content])}
20
+ {role: message[:role], content: adapt_content(message[:content])}
21
21
  elsif message.tool_call?
22
22
  {role: message.role, content: nil, tool_calls: message.extra[:original_tool_calls]}
23
23
  else
24
- format_message
24
+ adapt_message
25
25
  end
26
26
  end
27
27
  end
28
28
 
29
29
  private
30
30
 
31
- def format_message
31
+ def adapt_message
32
32
  case content
33
33
  when Array
34
- format_array
34
+ adapt_array
35
35
  else
36
- {role: message.role, content: format_content(content)}
36
+ {role: message.role, content: adapt_content(content)}
37
37
  end
38
38
  end
39
39
 
40
- def format_array
40
+ def adapt_array
41
41
  if content.empty?
42
42
  nil
43
43
  elsif returns.any?
44
- returns.map { {role: "tool", tool_call_id: _1.id, content: JSON.dump(_1.value)} }
44
+ returns.map { {role: "tool", tool_call_id: _1.id, content: LLM.json.dump(_1.value)} }
45
45
  else
46
- {role: message.role, content: content.flat_map { format_content(_1) }}
46
+ {role: message.role, content: content.flat_map { adapt_content(_1) }}
47
47
  end
48
48
  end
49
49
 
50
- def format_content(content)
50
+ def adapt_content(content)
51
51
  case content
52
52
  when LLM::Object
53
- format_object(content)
53
+ adapt_object(content)
54
54
  when String
55
55
  [{type: :text, text: content.to_s}]
56
56
  when LLM::Response
57
- format_remote_file(content)
57
+ adapt_remote_file(content)
58
58
  when LLM::Message
59
- format_content(content.content)
59
+ adapt_content(content.content)
60
60
  when LLM::Function::Return
61
- throw(:abort, {role: "tool", tool_call_id: content.id, content: JSON.dump(content.value)})
61
+ throw(:abort, {role: "tool", tool_call_id: content.id, content: LLM.json.dump(content.value)})
62
62
  else
63
63
  prompt_error!(content)
64
64
  end
65
65
  end
66
66
 
67
- def format_object(object)
67
+ def adapt_object(object)
68
68
  case object.kind
69
69
  when :image_url
70
70
  [{type: :image_url, image_url: {url: object.value}}]
71
71
  when :local_file
72
- format_local_file(object.value)
72
+ adapt_local_file(object.value)
73
73
  when :remote_file
74
- format_remote_file(object.value)
74
+ adapt_remote_file(object.value)
75
75
  else
76
76
  prompt_error!(object)
77
77
  end
78
78
  end
79
79
 
80
- def format_local_file(file)
80
+ def adapt_local_file(file)
81
81
  if file.image?
82
82
  [{type: :image_url, image_url: {url: file.to_data_uri}}]
83
83
  else
@@ -85,7 +85,7 @@ module LLM::OpenAI::Format
85
85
  end
86
86
  end
87
87
 
88
- def format_remote_file(file)
88
+ def adapt_remote_file(file)
89
89
  if file.file?
90
90
  [{type: :file, file: {file_id: file.id}}]
91
91
  else
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Format
3
+ module LLM::OpenAI::RequestAdapter
4
4
  ##
5
5
  # @private
6
- class ModerationFormat
6
+ class Moderation
7
7
  ##
8
8
  # @param [String, URI, Array<String, URI>] inputs
9
9
  # The inputs to format
10
- # @return [LLM::OpenAI::Format::ModerationFormat]
10
+ # @return [LLM::OpenAI::RequestAdapter::Moderation]
11
11
  def initialize(inputs)
12
12
  @inputs = inputs
13
13
  end
14
14
 
15
15
  ##
16
- # Formats the inputs for the OpenAI moderations API
16
+ # Adapts the inputs for the OpenAI moderations API
17
17
  # @return [Array<Hash>]
18
- def format
18
+ def adapt
19
19
  [*inputs].flat_map do |input|
20
20
  if String === input
21
21
  {type: :text, text: input}
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Format
3
+ module LLM::OpenAI::RequestAdapter
4
4
  ##
5
5
  # @private
6
- class RespondFormat
6
+ class Respond
7
7
  ##
8
8
  # @param [LLM::Message] message
9
9
  # The message to format
@@ -11,28 +11,28 @@ module LLM::OpenAI::Format
11
11
  @message = message
12
12
  end
13
13
 
14
- def format
14
+ def adapt
15
15
  catch(:abort) do
16
16
  if Hash === message
17
- {role: message[:role], content: format_content(message[:content])}
17
+ {role: message[:role], content: adapt_content(message[:content])}
18
18
  else
19
- format_message
19
+ adapt_message
20
20
  end
21
21
  end
22
22
  end
23
23
 
24
24
  private
25
25
 
26
- def format_content(content)
26
+ def adapt_content(content)
27
27
  case content
28
28
  when String
29
29
  [{type: :input_text, text: content.to_s}]
30
- when LLM::Response then format_remote_file(content)
31
- when LLM::Message then format_content(content.content)
30
+ when LLM::Response then adapt_remote_file(content)
31
+ when LLM::Message then adapt_content(content.content)
32
32
  when LLM::Object
33
33
  case content.kind
34
34
  when :image_url then [{type: :image_url, image_url: {url: content.value.to_s}}]
35
- when :remote_file then format_remote_file(content.value)
35
+ when :remote_file then adapt_remote_file(content.value)
36
36
  when :local_file then prompt_error!(content)
37
37
  else prompt_error!(content)
38
38
  end
@@ -41,26 +41,26 @@ module LLM::OpenAI::Format
41
41
  end
42
42
  end
43
43
 
44
- def format_message
44
+ def adapt_message
45
45
  case content
46
46
  when Array
47
- format_array
47
+ adapt_array
48
48
  else
49
- {role: message.role, content: format_content(content)}
49
+ {role: message.role, content: adapt_content(content)}
50
50
  end
51
51
  end
52
52
 
53
- def format_array
53
+ def adapt_array
54
54
  if content.empty?
55
55
  nil
56
56
  elsif returns.any?
57
- returns.map { {type: "function_call_output", call_id: _1.id, output: JSON.dump(_1.value)} }
57
+ returns.map { {type: "function_call_output", call_id: _1.id, output: LLM.json.dump(_1.value)} }
58
58
  else
59
- {role: message.role, content: content.flat_map { format_content(_1) }}
59
+ {role: message.role, content: content.flat_map { adapt_content(_1) }}
60
60
  end
61
61
  end
62
62
 
63
- def format_remote_file(content)
63
+ def adapt_remote_file(content)
64
64
  prompt_error!(content) unless content.file?
65
65
  file = LLM::File(content.filename)
66
66
  if file.image?
@@ -3,23 +3,23 @@
3
3
  class LLM::OpenAI
4
4
  ##
5
5
  # @private
6
- module Format
7
- require_relative "format/completion_format"
8
- require_relative "format/respond_format"
9
- require_relative "format/moderation_format"
6
+ module RequestAdapter
7
+ require_relative "request_adapter/completion"
8
+ require_relative "request_adapter/respond"
9
+ require_relative "request_adapter/moderation"
10
10
 
11
11
  ##
12
12
  # @param [Array<LLM::Message>] messages
13
- # The messages to format
13
+ # The messages to adapt
14
14
  # @param [Symbol] mode
15
- # The mode to format the messages for
15
+ # The mode to adapt the messages for
16
16
  # @return [Array<Hash>]
17
- def format(messages, mode)
17
+ def adapt(messages, mode: :complete)
18
18
  messages.filter_map do |message|
19
19
  if mode == :complete
20
- CompletionFormat.new(message).format
20
+ Completion.new(message).adapt
21
21
  else
22
- RespondFormat.new(message).format
22
+ Respond.new(message).adapt
23
23
  end
24
24
  end
25
25
  end
@@ -29,7 +29,7 @@ class LLM::OpenAI
29
29
  ##
30
30
  # @param [Hash] params
31
31
  # @return [Hash]
32
- def format_schema(params)
32
+ def adapt_schema(params)
33
33
  return {} unless params and params[:schema]
34
34
  schema = params.delete(:schema)
35
35
  schema = schema.respond_to?(:object) ? schema.object : schema
@@ -44,11 +44,11 @@ class LLM::OpenAI
44
44
  ##
45
45
  # @param [Hash] params
46
46
  # @return [Hash]
47
- def format_tools(tools)
47
+ def adapt_tools(tools)
48
48
  if tools.nil? || tools.empty?
49
49
  {}
50
50
  else
51
- {tools: tools.map { _1.respond_to?(:format) ? _1.format(self) : _1 }}
51
+ {tools: tools.map { _1.respond_to?(:adapt) ? _1.adapt(self) : _1 }}
52
52
  end
53
53
  end
54
54
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Response
3
+ module LLM::OpenAI::ResponseAdapter
4
4
  module Audio
5
5
  def audio = body.audio
6
6
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::OpenAI::ResponseAdapter
4
+ module Completion
5
+ ##
6
+ # (see LLM::Contract::Completion#messages)
7
+ def messages
8
+ body.choices.map.with_index do |choice, index|
9
+ message = choice.message
10
+ extra = {
11
+ index:, response: self,
12
+ logprobs: choice.logprobs,
13
+ tool_calls: adapt_tool_calls(message.tool_calls),
14
+ original_tool_calls: message.tool_calls
15
+ }
16
+ LLM::Message.new(message.role, message.content, extra)
17
+ end
18
+ end
19
+ alias_method :choices, :messages
20
+
21
+ ##
22
+ # (see LLM::Contract::Completion#input_tokens)
23
+ def input_tokens
24
+ body.usage["prompt_tokens"] || 0
25
+ end
26
+
27
+ ##
28
+ # (see LLM::Contract::Completion#output_tokens)
29
+ def output_tokens
30
+ body.usage["completion_tokens"] || 0
31
+ end
32
+
33
+ ##
34
+ # (see LLM::Contract::Completion#total_tokens)
35
+ def total_tokens
36
+ body.usage["total_tokens"] || 0
37
+ end
38
+
39
+ ##
40
+ # (see LLM::Contract::Completion#usage)
41
+ def usage
42
+ super
43
+ end
44
+
45
+ ##
46
+ # (see LLM::Contract::Completion#model)
47
+ def model
48
+ body.model
49
+ end
50
+
51
+ private
52
+
53
+ def adapt_tool_calls(tools)
54
+ (tools || []).filter_map do |tool|
55
+ next unless tool.function
56
+ {id: tool.id, name: tool.function.name, arguments: LLM.json.load(tool.function.arguments)}
57
+ end
58
+ end
59
+
60
+ include LLM::Contract::Completion
61
+ end
62
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Response
3
+ module LLM::OpenAI::ResponseAdapter
4
4
  module Embedding
5
5
  def embeddings = data.map { _1["embedding"] }
6
6
  def prompt_tokens = data.dig(0, "usage", "prompt_tokens")
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Response
3
+ module LLM::OpenAI::ResponseAdapter
4
4
  module Enumerable
5
5
  include ::Enumerable
6
6
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Response
3
+ module LLM::OpenAI::ResponseAdapter
4
4
  module File
5
5
  def file? = true
6
6
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Response
3
+ module LLM::OpenAI::ResponseAdapter
4
4
  module Image
5
5
  def urls
6
6
  data.filter_map { _1["url"] }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Response
3
+ module LLM::OpenAI::ResponseAdapter
4
4
  module Moderations
5
5
  ##
6
6
  # @return [Array<LLM::Response]
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Response
3
+ module LLM::OpenAI::ResponseAdapter
4
4
  module Responds
5
5
  def model = body.model
6
6
  def response_id = respond_to?(:response) ? response["id"] : id
7
- def choices = [format_message]
7
+ def choices = [adapt_message]
8
8
  def annotations = choices[0].annotations
9
9
 
10
10
  def prompt_tokens = body.usage&.input_tokens
@@ -20,11 +20,11 @@ module LLM::OpenAI::Response
20
20
 
21
21
  private
22
22
 
23
- def format_message
23
+ def adapt_message
24
24
  message = LLM::Message.new("assistant", +"", {response: self, tool_calls: []})
25
25
  output.each.with_index do |choice, index|
26
26
  if choice.type == "function_call"
27
- message.extra[:tool_calls] << format_tool(choice)
27
+ message.extra[:tool_calls] << adapt_tool(choice)
28
28
  elsif choice.content
29
29
  choice.content.each do |c|
30
30
  next unless c["type"] == "output_text"
@@ -37,12 +37,8 @@ module LLM::OpenAI::Response
37
37
  message
38
38
  end
39
39
 
40
- def format_tool(tool)
41
- LLM::Object.new(
42
- id: tool.call_id,
43
- name: tool.name,
44
- arguments: JSON.parse(tool.arguments)
45
- )
40
+ def adapt_tool(tool)
41
+ {id: tool.call_id, name: tool.name, arguments: LLM.json.load(tool.arguments)}
46
42
  end
47
43
  end
48
44
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::OpenAI::Response
3
+ module LLM::OpenAI::ResponseAdapter
4
4
  ##
5
- # The {LLM::OpenAI::Response::WebSearch LLM::OpenAI::Response::WebSearch}
5
+ # The {LLM::OpenAI::ResponseAdapter::WebSearch LLM::OpenAI::ResponseAdapter::WebSearch}
6
6
  # module provides methods for accessing web search results from a web search
7
7
  # tool call made via the {LLM::Provider#web_search LLM::Provider#web_search}
8
8
  # method.
@@ -11,7 +11,7 @@ module LLM::OpenAI::Response
11
11
  # Returns one or more search results
12
12
  # @return [Array<LLM::Object>]
13
13
  def search_results
14
- LLM::Object.from_hash(
14
+ LLM::Object.from(
15
15
  choices[0]
16
16
  .annotations
17
17
  .map { _1.slice(:title, :url) }
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::OpenAI
4
+ ##
5
+ # @private
6
+ module ResponseAdapter
7
+ require_relative "response_adapter/audio"
8
+ require_relative "response_adapter/completion"
9
+ require_relative "response_adapter/embedding"
10
+ require_relative "response_adapter/enumerable"
11
+ require_relative "response_adapter/file"
12
+ require_relative "response_adapter/image"
13
+ require_relative "response_adapter/moderations"
14
+ require_relative "response_adapter/responds"
15
+ require_relative "response_adapter/web_search"
16
+
17
+ module_function
18
+
19
+ ##
20
+ # @param [LLM::Response, Net::HTTPResponse] res
21
+ # @param [Symbol] type
22
+ # @return [LLM::Response]
23
+ def adapt(res, type:)
24
+ response = (LLM::Response === res) ? res : LLM::Response.new(res)
25
+ adapter = select(type)
26
+ response.extend(adapter)
27
+ end
28
+
29
+ ##
30
+ # @api private
31
+ def select(type)
32
+ case type
33
+ when :audio then LLM::OpenAI::ResponseAdapter::Audio
34
+ when :completion then LLM::OpenAI::ResponseAdapter::Completion
35
+ when :embedding then LLM::OpenAI::ResponseAdapter::Embedding
36
+ when :enumerable then LLM::OpenAI::ResponseAdapter::Enumerable
37
+ when :file then LLM::OpenAI::ResponseAdapter::File
38
+ when :image then LLM::OpenAI::ResponseAdapter::Image
39
+ when :moderations then LLM::OpenAI::ResponseAdapter::Moderations
40
+ when :responds then LLM::OpenAI::ResponseAdapter::Responds
41
+ when :web_search then LLM::OpenAI::ResponseAdapter::WebSearch
42
+ else
43
+ raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -6,14 +6,14 @@ class LLM::OpenAI
6
6
  class Responses::StreamParser
7
7
  ##
8
8
  # Returns the fully constructed response body
9
- # @return [LLM::Object]
9
+ # @return [Hash]
10
10
  attr_reader :body
11
11
 
12
12
  ##
13
13
  # @param [#<<] io An IO-like object
14
14
  # @return [LLM::OpenAI::Responses::StreamParser]
15
15
  def initialize(io)
16
- @body = LLM::Object.new(output: []) # Initialize with an empty output array
16
+ @body = {"output" => []}
17
17
  @io = io
18
18
  end
19
19
 
@@ -33,43 +33,43 @@ class LLM::OpenAI
33
33
  next if k == "type"
34
34
  @body[k] = v
35
35
  end
36
- @body.output ||= []
36
+ @body["output"] ||= []
37
37
  when "response.output_item.added"
38
38
  output_index = chunk["output_index"]
39
- item = LLM::Object.from_hash(chunk["item"])
40
- @body.output[output_index] = item
41
- @body.output[output_index].content ||= []
39
+ item = chunk["item"]
40
+ @body["output"][output_index] = item
41
+ @body["output"][output_index]["content"] ||= []
42
42
  when "response.content_part.added"
43
43
  output_index = chunk["output_index"]
44
44
  content_index = chunk["content_index"]
45
- part = LLM::Object.from_hash(chunk["part"])
46
- @body.output[output_index] ||= LLM::Object.new(content: [])
47
- @body.output[output_index].content ||= []
48
- @body.output[output_index].content[content_index] = part
45
+ part = chunk["part"]
46
+ @body["output"][output_index] ||= {"content" => []}
47
+ @body["output"][output_index]["content"] ||= []
48
+ @body["output"][output_index]["content"][content_index] = part
49
49
  when "response.output_text.delta"
50
50
  output_index = chunk["output_index"]
51
51
  content_index = chunk["content_index"]
52
52
  delta_text = chunk["delta"]
53
- output_item = @body.output[output_index]
54
- if output_item&.content
55
- content_part = output_item.content[content_index]
56
- if content_part && content_part.type == "output_text"
57
- content_part.text ||= ""
58
- content_part.text << delta_text
53
+ output_item = @body["output"][output_index]
54
+ if output_item && output_item["content"]
55
+ content_part = output_item["content"][content_index]
56
+ if content_part && content_part["type"] == "output_text"
57
+ content_part["text"] ||= ""
58
+ content_part["text"] << delta_text
59
59
  @io << delta_text if @io.respond_to?(:<<)
60
60
  end
61
61
  end
62
62
  when "response.output_item.done"
63
63
  output_index = chunk["output_index"]
64
- item = LLM::Object.from_hash(chunk["item"])
65
- @body.output[output_index] = item
64
+ item = chunk["item"]
65
+ @body["output"][output_index] = item
66
66
  when "response.content_part.done"
67
67
  output_index = chunk["output_index"]
68
68
  content_index = chunk["content_index"]
69
- part = LLM::Object.from_hash(chunk["part"])
70
- @body.output[output_index] ||= LLM::Object.new(content: [])
71
- @body.output[output_index].content ||= []
72
- @body.output[output_index].content[content_index] = part
69
+ part = chunk["part"]
70
+ @body["output"][output_index] ||= {"content" => []}
71
+ @body["output"][output_index]["content"] ||= []
72
+ @body["output"][output_index]["content"][content_index] = part
73
73
  end
74
74
  end
75
75
  end
@@ -14,9 +14,8 @@ class LLM::OpenAI
14
14
  # res2 = llm.responses.create "5 + 5 = X ?", role: :user, previous_response_id: res1.id
15
15
  # [res1, res2].each { llm.responses.delete(_1) }
16
16
  class Responses
17
- require_relative "response/responds"
18
17
  require_relative "responses/stream_parser"
19
- include Format
18
+ include RequestAdapter
20
19
 
21
20
  ##
22
21
  # Returns a new Responses object
@@ -38,16 +37,15 @@ class LLM::OpenAI
38
37
  def create(prompt, params = {})
39
38
  params = {role: :user, model: @provider.default_model}.merge!(params)
40
39
  tools = resolve_tools(params.delete(:tools))
41
- params = [params, format_schema(params), format_tools(tools)].inject({}, &:merge!).compact
40
+ params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
42
41
  role, stream = params.delete(:role), params.delete(:stream)
43
42
  params[:stream] = true if stream.respond_to?(:<<) || stream == true
44
43
  req = Net::HTTP::Post.new("/v1/responses", headers)
45
44
  messages = [*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
46
- body = JSON.dump({input: [format(messages, :response)].flatten}.merge!(params))
45
+ body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
47
46
  set_body_stream(req, StringIO.new(body))
48
47
  res = execute(request: req, stream:, stream_parser:)
49
- LLM::Response.new(res)
50
- .extend(LLM::OpenAI::Response::Responds)
48
+ ResponseAdapter.adapt(res, type: :responds)
51
49
  .extend(Module.new { define_method(:__tools__) { tools } })
52
50
  end
53
51
 
@@ -62,7 +60,7 @@ class LLM::OpenAI
62
60
  query = URI.encode_www_form(params)
63
61
  req = Net::HTTP::Get.new("/v1/responses/#{response_id}?#{query}", headers)
64
62
  res = execute(request: req)
65
- LLM::Response.new(res).extend(LLM::OpenAI::Response::Responds)
63
+ ResponseAdapter.adapt(res, type: :responds)
66
64
  end
67
65
 
68
66
  ##
@@ -84,7 +82,7 @@ class LLM::OpenAI
84
82
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
85
83
  end
86
84
 
87
- def format_schema(params)
85
+ def adapt_schema(params)
88
86
  return {} unless params && params[:schema]
89
87
  schema = params.delete(:schema)
90
88
  schema = schema.to_h.merge(additionalProperties: false)
@@ -6,13 +6,13 @@ class LLM::OpenAI
6
6
  class StreamParser
7
7
  ##
8
8
  # Returns the fully constructed response body
9
- # @return [LLM::Object]
9
+ # @return [Hash]
10
10
  attr_reader :body
11
11
 
12
12
  ##
13
13
  # @return [LLM::OpenAI::Chunk]
14
14
  def initialize(io)
15
- @body = LLM::Object.new
15
+ @body = {}
16
16
  @io = io
17
17
  end
18
18
 
@@ -38,8 +38,9 @@ class LLM::OpenAI
38
38
 
39
39
  def merge_choices!(choices)
40
40
  choices.each do |choice|
41
- if @body.choices[choice["index"]]
42
- target_message = @body["choices"][choice["index"]]["message"]
41
+ index = choice["index"]
42
+ if @body["choices"][index]
43
+ target_message = @body["choices"][index]["message"]
43
44
  delta = choice["delta"]
44
45
  delta.each do |key, value|
45
46
  if key == "content"
@@ -54,7 +55,7 @@ class LLM::OpenAI
54
55
  end
55
56
  else
56
57
  message_hash = {"role" => "assistant"}
57
- @body["choices"][choice["index"]] = {"message" => message_hash}
58
+ @body["choices"][index] = {"message" => message_hash}
58
59
  choice["delta"].each do |key, value|
59
60
  if key == "content"
60
61
  @io << value if @io.respond_to?(:<<)