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.
- checksums.yaml +4 -4
- data/.pi/skills/live-provider-testing/SKILL.md +183 -0
- data/.pi/skills/options-development/SKILL.md +131 -0
- data/CHANGELOG.md +17 -0
- data/README.md +16 -0
- data/Rakefile +1 -0
- data/lib/llm_gateway/adapters/adapter.rb +2 -35
- data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +0 -2
- data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +106 -27
- data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +0 -33
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +31 -46
- data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +48 -6
- data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +3 -2
- data/lib/llm_gateway/adapters/groq/input_mapper.rb +44 -0
- data/lib/llm_gateway/adapters/groq/option_mapper.rb +89 -4
- data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +275 -0
- data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +0 -2
- data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +0 -6
- data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +135 -72
- data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +100 -10
- data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +169 -170
- data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +0 -1
- data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +128 -68
- data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +99 -10
- data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +81 -271
- data/lib/llm_gateway/adapters/openai/responses_adapter.rb +0 -1
- data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +3 -3
- data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +0 -5
- data/lib/llm_gateway/adapters/stream_mapper.rb +50 -0
- data/lib/llm_gateway/client.rb +10 -66
- data/lib/llm_gateway/clients/groq.rb +13 -1
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +2 -8
- metadata +7 -10
- data/lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb +0 -111
- data/lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb +0 -110
- data/lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb +0 -40
- data/lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb +0 -120
- data/lib/llm_gateway/adapters/openai/responses/output_mapper.rb +0 -47
- data/lib/llm_gateway/adapters/stream_accumulator.rb +0 -91
- data/scripts/generate_handoff_live_fixture.rb +0 -169
- data/scripts/generate_handoff_media_fixture.rb +0 -167
|
@@ -1,185 +1,228 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
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
|
-
|
|
14
|
+
push_patches(patches_for(data), &block)
|
|
15
|
+
end
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
76
|
+
[ active_block_type, active_tool ]
|
|
36
77
|
end
|
|
37
78
|
|
|
38
|
-
|
|
79
|
+
def message_start_patches(data, delta)
|
|
80
|
+
return [] unless accumulator.message_hash.empty?
|
|
39
81
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
93
|
-
|
|
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
|
|
105
|
-
|
|
163
|
+
def active_tool_block
|
|
164
|
+
return nil unless accumulator.active_tool?
|
|
106
165
|
|
|
107
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
141
|
-
|
|
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
|
|
149
|
-
|
|
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
|
|
153
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
end
|
|
194
|
+
def finish_patches(finish_reason, active_block_type: accumulator.active_block_type)
|
|
195
|
+
return [] unless finish_reason
|
|
159
196
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
165
|
-
|
|
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
|
|
169
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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.
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
mapper = message_mapper
|
|
33
|
+
class << self
|
|
34
|
+
private
|
|
18
35
|
|
|
19
|
-
tools
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
47
|
+
[ :contents, :content ].each do |key|
|
|
48
|
+
next unless tool[key].is_a?(Array)
|
|
29
49
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
55
|
+
mapped_tool
|
|
56
|
+
end
|
|
36
57
|
end
|
|
37
|
-
end
|
|
38
58
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
124
|
+
{
|
|
125
|
+
type: "function_call_output",
|
|
126
|
+
call_id: content[:tool_use_id],
|
|
127
|
+
output: output
|
|
128
|
+
}
|
|
129
|
+
end
|
|
81
130
|
|
|
82
|
-
|
|
131
|
+
def map_tool_use_content(content)
|
|
132
|
+
{ id: content[:id] }
|
|
133
|
+
end
|
|
83
134
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
135
|
+
def map_output_text_content(content)
|
|
136
|
+
{
|
|
137
|
+
type: "input_text",
|
|
138
|
+
text: content[:text]
|
|
88
139
|
}
|
|
89
140
|
end
|
|
90
141
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|