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
|
@@ -5,27 +5,110 @@ module LlmGateway
|
|
|
5
5
|
module OpenAI
|
|
6
6
|
module Responses
|
|
7
7
|
module OptionMapper
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
DEFAULT_MAX_OUTPUT_TOKENS = 20_480
|
|
10
9
|
VALID_REASONING_LEVELS = %w[low medium high xhigh].freeze
|
|
11
10
|
|
|
11
|
+
# Source: https://developers.openai.com/api/reference/resources/responses/methods/create/index.md
|
|
12
|
+
# API: OpenAI Responses Create; accessed 2026-05-18.
|
|
13
|
+
# Body parameters listed by the API reference: background,
|
|
14
|
+
# context_management, conversation, include, input, instructions,
|
|
15
|
+
# max_output_tokens, max_tool_calls, metadata, model,
|
|
16
|
+
# parallel_tool_calls, previous_response_id, prompt, prompt_cache_key,
|
|
17
|
+
# prompt_cache_retention, reasoning, safety_identifier, service_tier,
|
|
18
|
+
# store, stream, stream_options, temperature, text, tool_choice, tools,
|
|
19
|
+
# top_logprobs, top_p, truncation, user.
|
|
20
|
+
# This mapper intentionally excludes transcript/tool/system structural
|
|
21
|
+
# fields (input, instructions, tools) from option handling.
|
|
22
|
+
VALID_OPTIONS = %i[
|
|
23
|
+
background
|
|
24
|
+
context_management
|
|
25
|
+
conversation
|
|
26
|
+
include
|
|
27
|
+
max_output_tokens
|
|
28
|
+
max_tool_calls
|
|
29
|
+
metadata
|
|
30
|
+
model
|
|
31
|
+
parallel_tool_calls
|
|
32
|
+
previous_response_id
|
|
33
|
+
prompt
|
|
34
|
+
prompt_cache_key
|
|
35
|
+
prompt_cache_retention
|
|
36
|
+
reasoning
|
|
37
|
+
safety_identifier
|
|
38
|
+
service_tier
|
|
39
|
+
store
|
|
40
|
+
stream
|
|
41
|
+
stream_options
|
|
42
|
+
temperature
|
|
43
|
+
text
|
|
44
|
+
tool_choice
|
|
45
|
+
top_logprobs
|
|
46
|
+
top_p
|
|
47
|
+
truncation
|
|
48
|
+
user
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
MANAGED_OPTIONS = %i[
|
|
52
|
+
max_completion_tokens
|
|
53
|
+
response_format
|
|
54
|
+
cache_key
|
|
55
|
+
cache_retention
|
|
56
|
+
].freeze
|
|
57
|
+
|
|
12
58
|
module_function
|
|
13
59
|
|
|
14
60
|
def map(options)
|
|
15
|
-
mapped_options = options.
|
|
61
|
+
mapped_options = options.reject { |key, _| MANAGED_OPTIONS.include?(key) }
|
|
62
|
+
mapped_options[:max_output_tokens] = options[:max_completion_tokens] || options[:max_output_tokens] || DEFAULT_MAX_OUTPUT_TOKENS
|
|
16
63
|
|
|
17
|
-
|
|
18
|
-
mapped_options[:
|
|
64
|
+
cache_key = options[:cache_key]
|
|
65
|
+
mapped_options[:prompt_cache_key] = cache_key unless cache_key.nil?
|
|
19
66
|
|
|
20
|
-
|
|
21
|
-
|
|
67
|
+
cache_retention = options[:cache_retention]
|
|
68
|
+
mapped_options[:prompt_cache_retention] = normalize_cache_retention(cache_retention) \
|
|
69
|
+
unless cache_retention.nil?
|
|
22
70
|
|
|
23
|
-
|
|
71
|
+
if mapped_options[:prompt_cache_key] && !mapped_options[:prompt_cache_retention]
|
|
72
|
+
mapped_options[:prompt_cache_retention] = normalize_cache_retention("short")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if cache_retention.to_s == "none"
|
|
76
|
+
mapped_options.delete(:prompt_cache_key)
|
|
77
|
+
mapped_options.delete(:prompt_cache_retention)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
response_format = options[:response_format]
|
|
81
|
+
mapped_options[:text] = text_with_response_format(mapped_options[:text], response_format) unless response_format.nil?
|
|
24
82
|
|
|
25
83
|
reasoning = mapped_options.delete(:reasoning)
|
|
26
|
-
|
|
84
|
+
mapped_options[:reasoning] = normalize_reasoning(reasoning) \
|
|
85
|
+
unless reasoning.nil? || reasoning.to_s == "none"
|
|
86
|
+
|
|
87
|
+
validate_options!(mapped_options)
|
|
88
|
+
mapped_options
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_options!(mapped_options)
|
|
92
|
+
unknown_options = mapped_options.keys - VALID_OPTIONS
|
|
93
|
+
return if unknown_options.empty?
|
|
94
|
+
|
|
95
|
+
raise ArgumentError,
|
|
96
|
+
"Unknown OpenAI Responses options: #{unknown_options.join(', ')}. " \
|
|
97
|
+
"Valid options: #{VALID_OPTIONS.join(', ')}."
|
|
98
|
+
end
|
|
27
99
|
|
|
28
|
-
|
|
100
|
+
def normalize_cache_retention(cache_retention)
|
|
101
|
+
case cache_retention.to_s
|
|
102
|
+
when "short"
|
|
103
|
+
"in_memory"
|
|
104
|
+
when "long"
|
|
105
|
+
"24h"
|
|
106
|
+
when "none"
|
|
107
|
+
nil
|
|
108
|
+
else
|
|
109
|
+
raise ArgumentError,
|
|
110
|
+
"Invalid cache_retention '#{cache_retention}'. Use 'short', 'long', or 'none'."
|
|
111
|
+
end
|
|
29
112
|
end
|
|
30
113
|
|
|
31
114
|
def normalize_reasoning(reasoning)
|
|
@@ -34,6 +117,12 @@ module LlmGateway
|
|
|
34
117
|
|
|
35
118
|
raise ArgumentError, "Invalid reasoning '#{reasoning}'. Use 'none', 'low', 'medium', 'high', or 'xhigh'."
|
|
36
119
|
end
|
|
120
|
+
|
|
121
|
+
def text_with_response_format(text, response_format)
|
|
122
|
+
text_options = text ? text.dup : {}
|
|
123
|
+
text_options[:format] = response_format.is_a?(String) ? { type: response_format } : response_format
|
|
124
|
+
text_options
|
|
125
|
+
end
|
|
37
126
|
end
|
|
38
127
|
end
|
|
39
128
|
end
|
|
@@ -1,198 +1,124 @@
|
|
|
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 Responses
|
|
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
|
event_type = chunk[:event]
|
|
15
12
|
data = chunk[:data] || {}
|
|
16
13
|
raise_stream_error!(data) if event_type == "error" || data[:error] || data[:type] == "error"
|
|
17
14
|
|
|
15
|
+
push_patches(patches_for(event_type, data), &block)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def patches_for(event_type, data)
|
|
18
21
|
case event_type
|
|
19
22
|
when "response.created"
|
|
20
|
-
|
|
21
|
-
nil
|
|
23
|
+
response_created_patches(data[:response])
|
|
22
24
|
when "response.output_item.added"
|
|
23
|
-
|
|
24
|
-
when "response.output_item.done"
|
|
25
|
-
map_output_item_done(data)
|
|
25
|
+
output_item_added_patches(data)
|
|
26
26
|
when "response.content_part.added"
|
|
27
|
-
|
|
28
|
-
when "response.content_part.done"
|
|
29
|
-
|
|
27
|
+
content_part_added_patches(data)
|
|
28
|
+
when "response.content_part.done"
|
|
29
|
+
content_part_done_patches(data)
|
|
30
30
|
when "response.output_text.delta"
|
|
31
|
-
|
|
32
|
-
type: :text_delta,
|
|
33
|
-
content_index: content_index_for(data[:output_index] || 0),
|
|
34
|
-
delta: data[:delta] || ""
|
|
35
|
-
)
|
|
31
|
+
[ { type: :text_delta, delta: data[:delta] || "" } ]
|
|
36
32
|
when "response.function_call_arguments.delta"
|
|
37
|
-
|
|
38
|
-
type: :tool_delta,
|
|
39
|
-
content_index: content_index_for(data[:output_index] || 0),
|
|
40
|
-
delta: data[:delta] || ""
|
|
41
|
-
)
|
|
33
|
+
[ { type: :tool_delta, delta: data[:delta] || "" } ]
|
|
42
34
|
when "response.function_call_arguments.done"
|
|
43
|
-
|
|
35
|
+
[ { type: :tool_end, delta: "" } ]
|
|
36
|
+
when "response.reasoning_summary_part.added"
|
|
37
|
+
[ { type: :reasoning_start, delta: "", signature: "" } ]
|
|
44
38
|
when "response.reasoning_summary_text.delta"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
type: :reasoning_delta,
|
|
49
|
-
content_index: content_index_for(output_index),
|
|
50
|
-
delta: data[:delta] || "",
|
|
51
|
-
signature: ""
|
|
52
|
-
)
|
|
39
|
+
[ { type: :reasoning_delta, delta: data[:delta] || "", signature: "" } ]
|
|
40
|
+
when "response.reasoning_summary_part.done"
|
|
41
|
+
[ { type: :reasoning_end, delta: "", signature: "" } ]
|
|
53
42
|
when "response.completed"
|
|
54
|
-
|
|
43
|
+
response_completed_patches(data[:response])
|
|
55
44
|
else
|
|
56
|
-
|
|
45
|
+
[]
|
|
57
46
|
end
|
|
58
47
|
end
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def map_output_item_added(data)
|
|
63
|
-
item = data[:item] || {}
|
|
64
|
-
output_index = data[:output_index] || 0
|
|
49
|
+
def response_created_patches(response)
|
|
50
|
+
response ||= {}
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ensure_message_started(role: item[:role] || "assistant")
|
|
78
|
-
when "function_call"
|
|
79
|
-
stash_role("assistant")
|
|
80
|
-
mark_tool_started(output_index)
|
|
81
|
-
AssistantToolStartEvent.new(
|
|
82
|
-
type: :tool_start,
|
|
83
|
-
content_index: register_content_index(output_index),
|
|
84
|
-
delta: "",
|
|
85
|
-
id: item[:call_id] || item[:id],
|
|
86
|
-
name: item[:name]
|
|
87
|
-
)
|
|
88
|
-
else
|
|
89
|
-
nil
|
|
90
|
-
end
|
|
52
|
+
[
|
|
53
|
+
{
|
|
54
|
+
type: :message_start,
|
|
55
|
+
delta: {
|
|
56
|
+
id: response[:id],
|
|
57
|
+
model: response[:model],
|
|
58
|
+
role: "assistant"
|
|
59
|
+
}.compact,
|
|
60
|
+
usage_increment: {}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
91
63
|
end
|
|
92
64
|
|
|
93
|
-
def
|
|
65
|
+
def output_item_added_patches(data)
|
|
94
66
|
item = data[:item] || {}
|
|
95
|
-
output_index = data[:output_index] || 0
|
|
96
67
|
|
|
97
68
|
case item[:type]
|
|
98
|
-
when "
|
|
99
|
-
|
|
69
|
+
when "message"
|
|
70
|
+
return [] unless accumulator.message_hash.empty?
|
|
71
|
+
|
|
72
|
+
[
|
|
73
|
+
{
|
|
74
|
+
type: :message_start,
|
|
75
|
+
delta: { role: item[:role] || "assistant" },
|
|
76
|
+
usage_increment: {}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
100
79
|
when "function_call"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def map_reasoning_done(output_index, item)
|
|
108
|
-
content_index = content_index_for(output_index)
|
|
109
|
-
summary_text = extract_reasoning_summary_text(item)
|
|
110
|
-
|
|
111
|
-
if reasoning_started_without_content?(output_index) && !summary_text.empty?
|
|
112
|
-
queue_event(
|
|
113
|
-
AssistantStreamReasoningEvent.new(
|
|
114
|
-
type: :reasoning_end,
|
|
115
|
-
content_index:,
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
type: :tool_start,
|
|
116
83
|
delta: "",
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
content_index:,
|
|
124
|
-
delta: summary_text,
|
|
125
|
-
signature: ""
|
|
126
|
-
)
|
|
84
|
+
id: item[:call_id] || item[:id],
|
|
85
|
+
name: item[:name]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
else
|
|
89
|
+
[]
|
|
127
90
|
end
|
|
128
|
-
|
|
129
|
-
mark_reasoning_completed(output_index)
|
|
130
|
-
AssistantStreamReasoningEvent.new(
|
|
131
|
-
type: :reasoning_end,
|
|
132
|
-
content_index:,
|
|
133
|
-
delta: "",
|
|
134
|
-
signature: ""
|
|
135
|
-
)
|
|
136
91
|
end
|
|
137
92
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
mark_tool_started(output_index)
|
|
142
|
-
queue_event(
|
|
143
|
-
AssistantStreamEvent.new(
|
|
144
|
-
type: :tool_end,
|
|
145
|
-
content_index: content_index_for(output_index),
|
|
146
|
-
delta: ""
|
|
147
|
-
)
|
|
148
|
-
)
|
|
93
|
+
def content_part_added_patches(data)
|
|
94
|
+
part = data[:part] || {}
|
|
95
|
+
return [] unless part[:type] == "output_text"
|
|
149
96
|
|
|
150
|
-
|
|
151
|
-
type: :tool_start,
|
|
152
|
-
content_index: register_content_index(output_index),
|
|
153
|
-
delta: "",
|
|
154
|
-
id: item[:call_id] || item[:id],
|
|
155
|
-
name: item[:name]
|
|
156
|
-
)
|
|
97
|
+
[ { type: :text_start, delta: "" } ]
|
|
157
98
|
end
|
|
158
99
|
|
|
159
|
-
def
|
|
100
|
+
def content_part_done_patches(data)
|
|
160
101
|
part = data[:part] || {}
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
AssistantStreamEvent.new(
|
|
164
|
-
type: :text_start,
|
|
165
|
-
content_index: content_index_for(data[:output_index] || 0),
|
|
166
|
-
delta: ""
|
|
167
|
-
)
|
|
168
|
-
end
|
|
102
|
+
return [] unless part.empty? || part[:type] == "output_text"
|
|
169
103
|
|
|
170
|
-
|
|
171
|
-
AssistantStreamEvent.new(
|
|
172
|
-
type: :text_end,
|
|
173
|
-
content_index: content_index_for(data[:output_index] || 0),
|
|
174
|
-
delta: ""
|
|
175
|
-
)
|
|
104
|
+
[ { type: :text_end, delta: "" } ]
|
|
176
105
|
end
|
|
177
106
|
|
|
178
|
-
def
|
|
179
|
-
|
|
180
|
-
type: :tool_end,
|
|
181
|
-
content_index: content_index_for(data[:output_index] || 0),
|
|
182
|
-
delta: ""
|
|
183
|
-
)
|
|
184
|
-
end
|
|
107
|
+
def response_completed_patches(response)
|
|
108
|
+
response ||= {}
|
|
185
109
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
110
|
+
[
|
|
111
|
+
{
|
|
112
|
+
type: accumulator.message_hash.empty? ? :message_start : :message_delta,
|
|
113
|
+
delta: {
|
|
114
|
+
id: response[:id],
|
|
115
|
+
model: response[:model],
|
|
116
|
+
role: "assistant",
|
|
117
|
+
stop_reason: stop_reason_for(response)
|
|
118
|
+
}.compact,
|
|
119
|
+
usage_increment: usage_increment(response)
|
|
120
|
+
}
|
|
121
|
+
]
|
|
196
122
|
end
|
|
197
123
|
|
|
198
124
|
def usage_increment(response)
|
|
@@ -211,127 +137,11 @@ module LlmGateway
|
|
|
211
137
|
output = response[:output] || []
|
|
212
138
|
last_item = output.last || {}
|
|
213
139
|
|
|
214
|
-
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def ensure_message_started(role: "assistant")
|
|
218
|
-
return nil if message_started?
|
|
219
|
-
|
|
220
|
-
@message_started = true
|
|
221
|
-
AssistantStreamMessageEvent.new(
|
|
222
|
-
type: :message_start,
|
|
223
|
-
delta: pending_message_attributes.merge(role: role).compact,
|
|
224
|
-
usage_increment: {}
|
|
225
|
-
).tap do
|
|
226
|
-
clear_pending_message_attributes
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def extract_reasoning_summary_text(item)
|
|
231
|
-
Array(item[:summary]).filter_map do |summary|
|
|
232
|
-
next summary[:text] if summary.is_a?(Hash) && summary[:text]
|
|
233
|
-
next summary[:summary] if summary.is_a?(Hash) && summary[:summary]
|
|
234
|
-
next summary if summary.is_a?(String)
|
|
235
|
-
end.join
|
|
140
|
+
tool_seen? || last_item[:type] == "function_call" ? "tool_use" : "stop"
|
|
236
141
|
end
|
|
237
142
|
|
|
238
|
-
def
|
|
239
|
-
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def mark_reasoning_has_content(output_index)
|
|
243
|
-
reasoning_state[output_index] = :has_content
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def mark_reasoning_completed(output_index)
|
|
247
|
-
reasoning_state[output_index] = :completed
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def reasoning_started_without_content?(output_index)
|
|
251
|
-
reasoning_state[output_index] == :started
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def reasoning_state
|
|
255
|
-
@reasoning_state ||= {}
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def mark_tool_started(output_index)
|
|
259
|
-
tool_state[output_index] = :started
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def tool_started?(output_index)
|
|
263
|
-
tool_state[output_index] == :started
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def tool_state
|
|
267
|
-
@tool_state ||= {}
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def stash_response(response)
|
|
271
|
-
response ||= {}
|
|
272
|
-
@pending_message_attributes = pending_message_attributes.merge(
|
|
273
|
-
id: response[:id],
|
|
274
|
-
model: response[:model]
|
|
275
|
-
).compact
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def stash_role(role)
|
|
279
|
-
@pending_message_attributes = pending_message_attributes.merge(role:)
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def pending_message_attributes
|
|
283
|
-
@pending_message_attributes ||= {}
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def clear_pending_message_attributes
|
|
287
|
-
@pending_message_attributes = {}
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def register_content_index(output_index)
|
|
291
|
-
content_index_map[output_index] ||= next_content_index!
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def content_index_for(output_index)
|
|
295
|
-
content_index_map.fetch(output_index) { register_content_index(output_index) }
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def next_content_index!
|
|
299
|
-
@next_content_index ||= 0
|
|
300
|
-
current = @next_content_index
|
|
301
|
-
@next_content_index += 1
|
|
302
|
-
current
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def content_index_map
|
|
306
|
-
@content_index_map ||= {}
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def message_started?
|
|
310
|
-
@message_started ||= false
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def queue_event(event)
|
|
314
|
-
queued_events << event
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def shift_queued_event
|
|
318
|
-
queued_events.shift
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def queued_events
|
|
322
|
-
@queued_events ||= []
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
def raise_stream_error!(data)
|
|
326
|
-
error = data[:error].is_a?(Hash) ? data[:error] : data
|
|
327
|
-
message = error[:message] || "Stream error"
|
|
328
|
-
code = error[:code] || error[:type]
|
|
329
|
-
|
|
330
|
-
if LlmGateway::Errors.context_overflow_message?(message)
|
|
331
|
-
raise LlmGateway::Errors::PromptTooLong.new(message, code)
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
raise LlmGateway::Errors::APIStatusError.new(message, code)
|
|
143
|
+
def tool_seen?
|
|
144
|
+
accumulator.blocks.any? { |content_block| content_block && content_block[:type] == "tool_use" }
|
|
335
145
|
end
|
|
336
146
|
end
|
|
337
147
|
end
|
|
@@ -4,7 +4,6 @@ require_relative "../adapter"
|
|
|
4
4
|
require_relative "acts_like_responses"
|
|
5
5
|
require_relative "../input_message_sanitizer"
|
|
6
6
|
require_relative "responses/input_mapper"
|
|
7
|
-
require_relative "responses/output_mapper"
|
|
8
7
|
require_relative "responses/option_mapper"
|
|
9
8
|
require_relative "file_output_mapper"
|
|
10
9
|
require_relative "responses/stream_mapper"
|
|
@@ -26,7 +26,7 @@ module LlmGateway
|
|
|
26
26
|
def self.map_messages(messages)
|
|
27
27
|
return messages unless messages.is_a?(Array)
|
|
28
28
|
|
|
29
|
-
mapper
|
|
29
|
+
mapper = self
|
|
30
30
|
stripped = strip_reasoning_blocks(messages)
|
|
31
31
|
|
|
32
32
|
mapped = stripped.each_with_object([]) do |msg, acc|
|
|
@@ -85,7 +85,7 @@ module LlmGateway
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# Ensure assistant messages carry "output_text" rather than "input_text".
|
|
88
|
-
# The
|
|
88
|
+
# The base Responses input mapper maps plain text blocks to "input_text";
|
|
89
89
|
# Codex is strict about directionality and rejects "input_text" on the
|
|
90
90
|
# assistant side.
|
|
91
91
|
def self.normalize_assistant_content_types(messages)
|
|
@@ -114,7 +114,7 @@ module LlmGateway
|
|
|
114
114
|
# signature *is* the serialised item)
|
|
115
115
|
# - tool_use / function_call → top-level function_call item
|
|
116
116
|
# - text / *_text variants → output_text inside an assistant content block
|
|
117
|
-
# - anything else → delegated to the
|
|
117
|
+
# - anything else → delegated to the Responses input mapper
|
|
118
118
|
def self.map_assistant_content(content, mapper)
|
|
119
119
|
text_parts = []
|
|
120
120
|
items = []
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../adapter"
|
|
4
4
|
require_relative "../openai/acts_like_responses"
|
|
5
|
-
require_relative "../openai/responses/output_mapper"
|
|
6
5
|
require_relative "option_mapper"
|
|
7
6
|
require_relative "../openai/responses/stream_mapper"
|
|
8
7
|
require_relative "../openai/file_output_mapper"
|
|
@@ -25,10 +24,6 @@ module LlmGateway
|
|
|
25
24
|
OptionMapper
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
def perform_chat(messages, tools:, system:, **options)
|
|
29
|
-
client.chat_codex(messages, tools: tools, system: system, **options)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
27
|
def perform_stream(messages, tools:, system:, **options, &block)
|
|
33
28
|
client.stream_codex(messages, tools: tools, system: system, **options, &block)
|
|
34
29
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "normalized_stream_accumulator"
|
|
4
|
+
|
|
5
|
+
module LlmGateway
|
|
6
|
+
module Adapters
|
|
7
|
+
class StreamMapper
|
|
8
|
+
def result
|
|
9
|
+
accumulator.result
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def accumulator
|
|
15
|
+
@accumulator ||= LlmGateway::Adapters::NormalizedStreamAccumulator.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def push_patches(patches, &block)
|
|
19
|
+
patches.each do |patch|
|
|
20
|
+
accumulator.push(patch, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def raise_stream_error!(data, overload_codes: [])
|
|
27
|
+
error = stream_error_payload(data)
|
|
28
|
+
message = error[:message] || error["message"] || "Stream error"
|
|
29
|
+
code = error[:code] || error["code"] || error[:type] || error["type"]
|
|
30
|
+
|
|
31
|
+
if LlmGateway::Errors.context_overflow_message?(message)
|
|
32
|
+
raise LlmGateway::Errors::PromptTooLong.new(message, code)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if Array(overload_codes).any? { |overload_code| overload_code.to_s == code.to_s }
|
|
36
|
+
raise LlmGateway::Errors::OverloadError.new(message, code)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
raise LlmGateway::Errors::APIStatusError.new(message, code)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stream_error_payload(data)
|
|
43
|
+
data ||= {}
|
|
44
|
+
error = data[:error] || data["error"]
|
|
45
|
+
|
|
46
|
+
error.is_a?(Hash) ? error : data
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|