llm_gateway 0.4.0 → 0.5.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.pi/skills/live-provider-testing/SKILL.md +183 -0
  3. data/.pi/skills/options-development/SKILL.md +131 -0
  4. data/CHANGELOG.md +17 -0
  5. data/README.md +16 -0
  6. data/Rakefile +1 -0
  7. data/lib/llm_gateway/adapters/adapter.rb +2 -35
  8. data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +0 -2
  9. data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +106 -27
  10. data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +0 -33
  11. data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +31 -46
  12. data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +48 -6
  13. data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +3 -2
  14. data/lib/llm_gateway/adapters/groq/input_mapper.rb +44 -0
  15. data/lib/llm_gateway/adapters/groq/option_mapper.rb +89 -4
  16. data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +275 -0
  17. data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +0 -2
  18. data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +0 -6
  19. data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +135 -72
  20. data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +100 -10
  21. data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +169 -170
  22. data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +0 -1
  23. data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +128 -68
  24. data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +99 -10
  25. data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +81 -271
  26. data/lib/llm_gateway/adapters/openai/responses_adapter.rb +0 -1
  27. data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +3 -3
  28. data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +0 -5
  29. data/lib/llm_gateway/adapters/stream_mapper.rb +50 -0
  30. data/lib/llm_gateway/client.rb +10 -66
  31. data/lib/llm_gateway/clients/groq.rb +13 -1
  32. data/lib/llm_gateway/version.rb +1 -1
  33. data/lib/llm_gateway.rb +2 -8
  34. metadata +7 -10
  35. data/lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb +0 -111
  36. data/lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb +0 -110
  37. data/lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb +0 -40
  38. data/lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb +0 -120
  39. data/lib/llm_gateway/adapters/openai/responses/output_mapper.rb +0 -47
  40. data/lib/llm_gateway/adapters/stream_accumulator.rb +0 -91
  41. data/scripts/generate_handoff_live_fixture.rb +0 -169
  42. data/scripts/generate_handoff_media_fixture.rb +0 -167
@@ -1,185 +1,228 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../structs"
3
+ require_relative "../../stream_mapper"
4
4
 
5
5
  module LlmGateway
6
6
  module Adapters
7
7
  module OpenAI
8
8
  module ChatCompletions
9
- class StreamMapper
10
- def map(chunk)
11
- queued_event = shift_queued_event
12
- return queued_event if queued_event
13
-
9
+ class StreamMapper < LlmGateway::Adapters::StreamMapper
10
+ def map(chunk, &block)
14
11
  data = chunk[:data] || {}
15
12
  raise_stream_error!(data) if chunk[:event] == "error" || data[:error] || data[:type] == "error"
16
13
 
17
- choices = data[:choices] || []
14
+ push_patches(patches_for(data), &block)
15
+ end
18
16
 
19
- if choices.empty?
20
- return message_event(
21
- delta: pending_finish_delta,
22
- usage_increment: usage_increment(data)
23
- )
24
- end
17
+ private
18
+
19
+ def patches_for(data)
20
+ choices = data[:choices] || []
21
+ return final_usage_patches(data) if choices.empty?
25
22
 
26
23
  choice = choices.first || {}
27
24
  delta = choice[:delta] || {}
28
- finish_reason = choice[:finish_reason]
29
-
30
- event = map_choice_delta(data, choice, delta)
31
- return event if event
32
-
33
- return finish_event_for(finish_reason) if finish_reason
25
+ patches = []
26
+ active_block_type = accumulator.active_block_type
27
+ active_tool = active_tool_block
28
+
29
+ append_patches(patches, message_start_patches(data, delta))
30
+
31
+ active_block_type, active_tool = append_patches(
32
+ patches,
33
+ reasoning_patches(delta[:reasoning], active_block_type:),
34
+ active_block_type,
35
+ active_tool
36
+ )
37
+ active_block_type, active_tool = append_patches(
38
+ patches,
39
+ text_patches(delta[:content], active_block_type:),
40
+ active_block_type,
41
+ active_tool
42
+ )
43
+ delta.fetch(:tool_calls, []).each do |tool_call|
44
+ active_block_type, active_tool = append_patches(
45
+ patches,
46
+ patches_for_tool_call(tool_call, active_block_type:, active_tool:),
47
+ active_block_type,
48
+ active_tool
49
+ )
50
+ end
51
+ append_patches(patches, finish_patches(choice[:finish_reason], active_block_type:))
52
+
53
+ patches
54
+ end
55
+
56
+ def append_patches(patches, new_patches, active_block_type = nil, active_tool = nil)
57
+ patches.concat(new_patches)
58
+
59
+ new_patches.each do |patch|
60
+ case patch[:type]
61
+ when :text_start
62
+ active_block_type = :text
63
+ active_tool = nil
64
+ when :reasoning_start
65
+ active_block_type = :reasoning
66
+ active_tool = nil
67
+ when :tool_start
68
+ active_block_type = :tool
69
+ active_tool = { id: patch[:id], name: patch[:name] }
70
+ when :text_end, :reasoning_end, :tool_end
71
+ active_block_type = nil
72
+ active_tool = nil
73
+ end
74
+ end
34
75
 
35
- nil
76
+ [ active_block_type, active_tool ]
36
77
  end
37
78
 
38
- private
79
+ def message_start_patches(data, delta)
80
+ return [] unless accumulator.message_hash.empty?
39
81
 
40
- def map_choice_delta(data, choice, delta)
41
- if !message_started? && delta[:tool_calls]&.any?
42
- @message_started = true
43
- stash_message_attributes(data, delta)
44
- return tool_event(delta[:tool_calls].first)
45
- end
82
+ return [] unless delta.key?(:role) ||
83
+ data[:id] ||
84
+ data[:model] ||
85
+ delta[:content] ||
86
+ delta[:reasoning] ||
87
+ delta[:tool_calls]&.any?
46
88
 
47
- if !message_started? && (delta.key?(:role) || data[:id] || data[:model])
48
- @message_started = true
49
- return AssistantStreamMessageEvent.new(
89
+ [
90
+ {
50
91
  type: :message_start,
51
92
  delta: {
52
93
  id: data[:id],
53
94
  model: data[:model],
54
- role: delta[:role]
95
+ role: delta[:role] || "assistant"
55
96
  }.compact,
56
97
  usage_increment: {}
57
- )
58
- end
59
-
60
- if (content = delta[:content]) && !content.empty?
61
- return text_event(content, choice[:index] || 0)
62
- end
98
+ }
99
+ ]
100
+ end
63
101
 
64
- return tool_event(delta[:tool_calls].first) if delta[:tool_calls]&.any?
102
+ # Groq exposes OpenAI-compatible chat completion chunks, but may include
103
+ # `delta.reasoning` before normal `delta.content`.
104
+ def reasoning_patches(reasoning, active_block_type: accumulator.active_block_type)
105
+ return [] if reasoning.to_s.empty?
65
106
 
66
- nil
107
+ [
108
+ *close_active_non_reasoning_patches(active_block_type:),
109
+ {
110
+ type: active_block_type == :reasoning ? :reasoning_delta : :reasoning_start,
111
+ delta: reasoning,
112
+ signature: ""
113
+ }
114
+ ]
67
115
  end
68
116
 
69
- def finish_event_for(finish_reason)
70
- normalized = normalize_stop_reason(finish_reason)
71
- stash_pending_finish_delta(stop_reason: normalized)
117
+ def text_patches(content, active_block_type: accumulator.active_block_type)
118
+ return [] if content.to_s.empty?
72
119
 
73
- case normalized
74
- when "tool_use"
75
- AssistantStreamEvent.new(type: :tool_end, content_index: last_started_tool_index || 0, delta: "")
76
- else
77
- AssistantStreamEvent.new(type: :text_end, content_index: last_started_text_index || 0, delta: "")
120
+ [
121
+ *close_active_non_text_patches(active_block_type:),
122
+ {
123
+ type: active_block_type == :text ? :text_delta : :text_start,
124
+ delta: content
125
+ }
126
+ ]
127
+ end
128
+
129
+ def patches_for_tool_call(tool_call, active_block_type: accumulator.active_block_type, active_tool: active_tool_block)
130
+ id = tool_call[:id]
131
+ name = tool_call.dig(:function, :name)
132
+ arguments = tool_call.dig(:function, :arguments).to_s
133
+
134
+ patches = []
135
+
136
+ if id || name
137
+ if active_block_type == :tool
138
+ patches.concat(close_active_block_patches(active_block_type:)) if new_active_tool?(id, name, active_tool:)
139
+ else
140
+ patches.concat(close_active_non_tool_patches(active_block_type:))
141
+ end
142
+
143
+ unless active_block_type == :tool && patches.empty?
144
+ patches << {
145
+ type: :tool_start,
146
+ delta: "",
147
+ id: id,
148
+ name: name
149
+ }
150
+ end
78
151
  end
79
- end
80
152
 
81
- def message_event(delta:, usage_increment: {})
82
- AssistantStreamMessageEvent.new(
83
- type: pending_message_attributes.empty? ? :message_delta : :message_start,
84
- delta: pending_message_attributes.merge(delta),
85
- usage_increment:
86
- ).tap do
87
- clear_pending_message_attributes
88
- clear_pending_finish_delta
89
- end
153
+ patches << { type: :tool_delta, delta: arguments } unless arguments.empty?
154
+ patches
90
155
  end
91
156
 
92
- def usage_increment(data)
93
- usage = data[:usage] || {}
157
+ def new_active_tool?(id, name, active_tool: active_tool_block)
158
+ return true unless active_tool
94
159
 
95
- {
96
- input_tokens: usage[:prompt_tokens] || 0,
97
- cache_creation_input_tokens: 0,
98
- cache_read_input_tokens: usage.dig(:prompt_tokens_details, :cached_tokens) || 0,
99
- output_tokens: usage[:completion_tokens] || 0,
100
- reasoning_tokens: usage.dig(:completion_tokens_details, :reasoning_tokens) || 0
101
- }
160
+ (id && active_tool[:id] != id) || (name && active_tool[:name] != name)
102
161
  end
103
162
 
104
- def text_event(content, content_index)
105
- @last_started_text_index = content_index
163
+ def active_tool_block
164
+ return nil unless accumulator.active_tool?
106
165
 
107
- if started_text_blocks.include?(content_index)
108
- AssistantStreamEvent.new(type: :text_delta, content_index:, delta: content)
109
- else
110
- started_text_blocks << content_index
111
- AssistantStreamEvent.new(type: :text_start, content_index:, delta: content)
112
- end
166
+ accumulator.blocks.reverse.find { |block| block&.fetch(:type, nil) == "tool_use" }
113
167
  end
114
168
 
115
- def tool_event(tool_call)
116
- tool_index = tool_call[:index] || 0
117
- @last_started_tool_index = tool_index
118
- function = tool_call[:function] || {}
119
- arguments = function[:arguments] || ""
120
-
121
- unless started_tool_blocks.include?(tool_index)
122
- pending_tool_calls[tool_index] = merge_tool_call(pending_tool_calls[tool_index], tool_call)
123
- pending = pending_tool_calls[tool_index]
124
-
125
- return nil unless pending[:id] && pending.dig(:function, :name)
126
-
127
- started_tool_blocks << tool_index
128
- return AssistantToolStartEvent.new(
129
- type: :tool_start,
130
- content_index: tool_index,
131
- delta: "",
132
- id: pending[:id],
133
- name: pending.dig(:function, :name)
134
- )
169
+ def close_active_block_patches(active_block_type: accumulator.active_block_type)
170
+ case active_block_type
171
+ when :text
172
+ [ { type: :text_end, delta: "" } ]
173
+ when :reasoning
174
+ [ { type: :reasoning_end, delta: "", signature: "" } ]
175
+ when :tool
176
+ [ { type: :tool_end, delta: "" } ]
177
+ else
178
+ []
135
179
  end
136
-
137
- AssistantStreamEvent.new(type: :tool_delta, content_index: tool_index, delta: arguments)
138
180
  end
139
181
 
140
- def stash_message_attributes(data, delta)
141
- @pending_message_attributes = {
142
- id: data[:id],
143
- model: data[:model],
144
- role: delta[:role]
145
- }.compact
182
+ def close_active_non_text_patches(active_block_type: accumulator.active_block_type)
183
+ active_block_type == :text ? [] : close_active_block_patches(active_block_type:)
146
184
  end
147
185
 
148
- def pending_message_attributes
149
- @pending_message_attributes ||= {}
186
+ def close_active_non_reasoning_patches(active_block_type: accumulator.active_block_type)
187
+ active_block_type == :reasoning ? [] : close_active_block_patches(active_block_type:)
150
188
  end
151
189
 
152
- def clear_pending_message_attributes
153
- @pending_message_attributes = {}
190
+ def close_active_non_tool_patches(active_block_type: accumulator.active_block_type)
191
+ active_block_type == :tool ? [] : close_active_block_patches(active_block_type:)
154
192
  end
155
193
 
156
- def stash_pending_finish_delta(delta)
157
- @pending_finish_delta = pending_finish_delta.merge(delta)
158
- end
194
+ def finish_patches(finish_reason, active_block_type: accumulator.active_block_type)
195
+ return [] unless finish_reason
159
196
 
160
- def pending_finish_delta
161
- @pending_finish_delta ||= {}
197
+ [
198
+ *close_active_block_patches(active_block_type:),
199
+ {
200
+ type: :message_delta,
201
+ delta: { stop_reason: normalize_stop_reason(finish_reason) },
202
+ usage_increment: {}
203
+ }
204
+ ]
162
205
  end
163
206
 
164
- def clear_pending_finish_delta
165
- @pending_finish_delta = {}
207
+ def final_usage_patches(data)
208
+ [
209
+ {
210
+ type: accumulator.message_hash.empty? ? :message_start : :message_delta,
211
+ delta: {},
212
+ usage_increment: usage_increment(data)
213
+ }
214
+ ]
166
215
  end
167
216
 
168
- def merge_tool_call(existing, incoming)
169
- existing ||= {}
170
- incoming ||= {}
171
-
172
- existing_function = existing[:function] || {}
173
- incoming_function = incoming[:function] || {}
217
+ def usage_increment(data)
218
+ usage = data[:usage] || {}
174
219
 
175
220
  {
176
- index: incoming[:index] || existing[:index],
177
- id: incoming[:id] || existing[:id],
178
- type: incoming[:type] || existing[:type],
179
- function: {
180
- name: incoming_function[:name] || existing_function[:name],
181
- arguments: "#{existing_function[:arguments]}#{incoming_function[:arguments]}"
182
- }
221
+ input_tokens: usage[:prompt_tokens] || 0,
222
+ cache_creation_input_tokens: 0,
223
+ cache_read_input_tokens: usage.dig(:prompt_tokens_details, :cached_tokens) || 0,
224
+ output_tokens: usage[:completion_tokens] || 0,
225
+ reasoning_tokens: usage.dig(:completion_tokens_details, :reasoning_tokens) || 0
183
226
  }
184
227
  end
185
228
 
@@ -191,50 +234,6 @@ module LlmGateway
191
234
  finish_reason
192
235
  end
193
236
  end
194
-
195
- def message_started?
196
- @message_started ||= false
197
- end
198
-
199
- def started_text_blocks
200
- @started_text_blocks ||= []
201
- end
202
-
203
- def started_tool_blocks
204
- @started_tool_blocks ||= []
205
- end
206
-
207
- def pending_tool_calls
208
- @pending_tool_calls ||= {}
209
- end
210
-
211
- def last_started_text_index
212
- @last_started_text_index
213
- end
214
-
215
- def last_started_tool_index
216
- @last_started_tool_index
217
- end
218
-
219
- def shift_queued_event
220
- queued_events.shift
221
- end
222
-
223
- def queued_events
224
- @queued_events ||= []
225
- end
226
-
227
- def raise_stream_error!(data)
228
- error = data[:error].is_a?(Hash) ? data[:error] : data
229
- message = error[:message] || "Stream error"
230
- code = error[:code] || error[:type]
231
-
232
- if LlmGateway::Errors.context_overflow_message?(message)
233
- raise LlmGateway::Errors::PromptTooLong.new(message, code)
234
- end
235
-
236
- raise LlmGateway::Errors::APIStatusError.new(message, code)
237
- end
238
237
  end
239
238
  end
240
239
  end
@@ -4,7 +4,6 @@ require_relative "../adapter"
4
4
  require_relative "acts_like_chat_completions"
5
5
  require_relative "chat_completions/input_mapper"
6
6
  require_relative "chat_completions/input_message_sanitizer"
7
- require_relative "chat_completions/output_mapper"
8
7
  require_relative "chat_completions/option_mapper"
9
8
  require_relative "file_output_mapper"
10
9
  require_relative "chat_completions/stream_mapper"
@@ -1,103 +1,163 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "base64"
4
- require_relative "bidirectional_message_mapper"
5
4
 
6
5
  module LlmGateway
7
6
  module Adapters
8
7
  module OpenAI
9
8
  module Responses
10
9
  class InputMapper < OpenAI::ChatCompletions::InputMapper
11
- def self.message_mapper
12
- BidirectionalMessageMapper.new(LlmGateway::DIRECTION_IN)
10
+ def self.map_content(content)
11
+ content = { type: "text", text: content } unless content.is_a?(Hash)
12
+
13
+ case content[:type]
14
+ when "text"
15
+ map_text_content(content)
16
+ when "image"
17
+ map_image_content(content)
18
+ when "message"
19
+ map_messages_content(content)
20
+ when "output_text"
21
+ map_output_text_content(content)
22
+ when "tool_use", "function_call"
23
+ map_tool_use_content(content)
24
+ when "tool_result"
25
+ map_tool_result_content(content)
26
+ when "reasoning"
27
+ map_reasoning_content(content)
28
+ else
29
+ content
30
+ end
13
31
  end
14
32
 
15
- def self.map_tools(tools)
16
- return tools unless tools
17
- mapper = message_mapper
33
+ class << self
34
+ private
18
35
 
19
- tools.map do |tool|
20
- mapped_tool = {
21
- type: "function",
22
- name: tool[:name],
23
- description: tool[:description],
24
- parameters: tool[:input_schema]
25
- }
36
+ def map_tools(tools)
37
+ return tools unless tools
38
+
39
+ tools.map do |tool|
40
+ mapped_tool = {
41
+ type: "function",
42
+ name: tool[:name],
43
+ description: tool[:description],
44
+ parameters: tool[:input_schema]
45
+ }
26
46
 
27
- [ :contents, :content ].each do |key|
28
- next unless tool[key].is_a?(Array)
47
+ [ :contents, :content ].each do |key|
48
+ next unless tool[key].is_a?(Array)
29
49
 
30
- mapped_tool[key] = tool[key].map do |entry|
31
- entry.is_a?(Hash) ? mapper.map_content(entry.transform_keys(&:to_sym)) : entry
50
+ mapped_tool[key] = tool[key].map do |entry|
51
+ entry.is_a?(Hash) ? map_content(entry.transform_keys(&:to_sym)) : entry
52
+ end
32
53
  end
33
- end
34
54
 
35
- mapped_tool
55
+ mapped_tool
56
+ end
36
57
  end
37
- end
38
58
 
39
- def self.map_messages(messages)
40
- return messages unless messages
41
- mapper = message_mapper
42
-
43
- messages.flat_map do |msg|
44
- if msg[:id] && msg[:content].is_a?(Array)
45
- # Full AssistantMessage#to_h — expand content for stateless multi-turn
46
- map_assistant_history_message(msg)
47
- elsif msg[:id]
48
- # Bare item-reference (e.g. manually constructed { id: "item_xxx" })
49
- msg.slice(:id)
50
- else
51
- content = if msg[:content].is_a?(Array)
52
- msg[:content].map do |content|
53
- mapper.map_content(content)
54
- end
55
- else
56
- [ mapper.map_content(msg[:content]) ]
57
- end
58
- if msg.dig(:content).is_a?(Array) && msg.dig(:content, 0, :type) == "tool_result"
59
- content
59
+ def map_messages(messages)
60
+ return messages unless messages
61
+
62
+ messages.flat_map do |msg|
63
+ if msg[:id] && msg[:content].is_a?(Array)
64
+ map_assistant_history_message(msg)
65
+ elsif msg[:id]
66
+ msg.slice(:id)
60
67
  else
61
- {
62
- role: msg[:role],
63
- content: content
64
- }
68
+ content = if msg[:content].is_a?(Array)
69
+ msg[:content].map { |content| map_content(content) }
70
+ else
71
+ [ map_content(msg[:content]) ]
72
+ end
73
+ if msg.dig(:content).is_a?(Array) && msg.dig(:content, 0, :type) == "tool_result"
74
+ content
75
+ else
76
+ {
77
+ role: msg[:role],
78
+ content: content
79
+ }
80
+ end
65
81
  end
66
82
  end
67
83
  end
68
- end
69
84
 
70
- # Map a full AssistantMessage#to_h into Responses API input items for
71
- # stateless multi-turn conversations.
72
- #
73
- # text blocks { role: "assistant", content: [{ type: "output_text", ... }] }
74
- # tool_use blocks top-level function_call items
75
- # thinking blocks → omitted (model handles reasoning internally)
76
- def self.map_assistant_history_message(msg)
77
- blocks = (msg[:content] || []).map { |b| b.transform_keys(&:to_sym) }
85
+ def map_assistant_history_message(msg)
86
+ blocks = (msg[:content] || []).map { |b| b.transform_keys(&:to_sym) }
87
+
88
+ text_blocks = blocks.select { |b| b[:type] == "text" }
89
+ tool_use_blocks = blocks.select { |b| b[:type] == "tool_use" }
90
+
91
+ result = []
92
+
93
+ if text_blocks.any?
94
+ result << {
95
+ role: "assistant",
96
+ content: text_blocks.map { |b| { type: "output_text", text: b[:text] } }
97
+ }
98
+ end
99
+
100
+ tool_use_blocks.each do |b|
101
+ result << {
102
+ type: "function_call",
103
+ call_id: b[:id],
104
+ name: b[:name],
105
+ arguments: b[:input].is_a?(Hash) ? b[:input].to_json : (b[:input] || {}).to_json
106
+ }
107
+ end
108
+
109
+ result
110
+ end
111
+
112
+ def map_messages_content(message)
113
+ message[:content].map { |content| map_content(content) }
114
+ end
115
+
116
+ def map_tool_result_content(content)
117
+ output = content[:content]
118
+ if output.is_a?(Array)
119
+ output = output.map do |item|
120
+ item.is_a?(Hash) ? map_content(item.transform_keys(&:to_sym)) : item
121
+ end
122
+ end
78
123
 
79
- text_blocks = blocks.select { |b| b[:type] == "text" }
80
- tool_use_blocks = blocks.select { |b| b[:type] == "tool_use" }
124
+ {
125
+ type: "function_call_output",
126
+ call_id: content[:tool_use_id],
127
+ output: output
128
+ }
129
+ end
81
130
 
82
- result = []
131
+ def map_tool_use_content(content)
132
+ { id: content[:id] }
133
+ end
83
134
 
84
- if text_blocks.any?
85
- result << {
86
- role: "assistant",
87
- content: text_blocks.map { |b| { type: "output_text", text: b[:text] } }
135
+ def map_output_text_content(content)
136
+ {
137
+ type: "input_text",
138
+ text: content[:text]
88
139
  }
89
140
  end
90
141
 
91
- tool_use_blocks.each do |b|
92
- result << {
93
- type: "function_call",
94
- call_id: b[:id],
95
- name: b[:name],
96
- arguments: b[:input].is_a?(Hash) ? b[:input].to_json : (b[:input] || {}).to_json
142
+ def map_reasoning_content(content)
143
+ return { id: content[:id] } if content[:id]
144
+
145
+ content
146
+ end
147
+
148
+ def map_image_content(content)
149
+ {
150
+ type: "input_image",
151
+ image_url: "data:#{content[:media_type]};base64,#{content[:data]}"
97
152
  }
98
153
  end
99
154
 
100
- result
155
+ def map_text_content(content)
156
+ {
157
+ type: "input_text",
158
+ text: content[:text]
159
+ }
160
+ end
101
161
  end
102
162
  end
103
163
  end