llm_gateway 0.3.0 → 0.4.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +544 -186
  4. data/Rakefile +1 -2
  5. data/docs/migration-guide.md +135 -0
  6. data/lib/llm_gateway/adapters/adapter.rb +173 -0
  7. data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +23 -0
  8. data/lib/llm_gateway/adapters/{claude → anthropic}/bidirectional_message_mapper.rb +31 -3
  9. data/lib/llm_gateway/adapters/{claude → anthropic}/input_mapper.rb +4 -3
  10. data/lib/llm_gateway/adapters/anthropic/messages_adapter.rb +19 -0
  11. data/lib/llm_gateway/adapters/{claude → anthropic}/output_mapper.rb +1 -1
  12. data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +110 -0
  13. data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +53 -0
  14. data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +47 -0
  15. data/lib/llm_gateway/adapters/groq/option_mapper.rb +27 -0
  16. data/lib/llm_gateway/adapters/input_message_sanitizer.rb +93 -0
  17. data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +22 -0
  18. data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +31 -0
  19. data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/bidirectional_message_mapper.rb +9 -2
  20. data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/input_mapper.rb +1 -6
  21. data/lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb +65 -0
  22. data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +39 -0
  23. data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/output_mapper.rb +1 -1
  24. data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +242 -0
  25. data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +20 -0
  26. data/lib/llm_gateway/adapters/{open_ai → openai}/file_output_mapper.rb +1 -1
  27. data/lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb +39 -0
  28. data/lib/llm_gateway/adapters/{open_ai → openai}/responses/bidirectional_message_mapper.rb +52 -4
  29. data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +106 -0
  30. data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +41 -0
  31. data/lib/llm_gateway/adapters/{open_ai → openai}/responses/output_mapper.rb +1 -1
  32. data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +340 -0
  33. data/lib/llm_gateway/adapters/openai/responses_adapter.rb +20 -0
  34. data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +206 -0
  35. data/lib/llm_gateway/adapters/openai_codex/option_mapper.rb +28 -0
  36. data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +38 -0
  37. data/lib/llm_gateway/adapters/option_mapper.rb +13 -0
  38. data/lib/llm_gateway/adapters/stream_accumulator.rb +91 -0
  39. data/lib/llm_gateway/adapters/structs.rb +145 -0
  40. data/lib/llm_gateway/base_client.rb +62 -1
  41. data/lib/llm_gateway/client.rb +45 -129
  42. data/lib/llm_gateway/clients/anthropic.rb +167 -0
  43. data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +162 -0
  44. data/lib/llm_gateway/clients/claude_code/token_manager.rb +112 -0
  45. data/lib/llm_gateway/clients/groq.rb +54 -0
  46. data/lib/llm_gateway/clients/openai.rb +208 -0
  47. data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +258 -0
  48. data/lib/llm_gateway/clients/openai_codex/token_manager.rb +71 -0
  49. data/lib/llm_gateway/errors.rb +21 -0
  50. data/lib/llm_gateway/prompt.rb +12 -1
  51. data/lib/llm_gateway/provider_registry.rb +37 -0
  52. data/lib/llm_gateway/version.rb +1 -1
  53. data/lib/llm_gateway.rb +165 -14
  54. data/scripts/create_anthropic_credentials.rb +106 -0
  55. data/scripts/create_openai_codex_credentials.rb +116 -0
  56. data/scripts/generate_handoff_live_fixture.rb +169 -0
  57. data/scripts/generate_handoff_media_fixture.rb +167 -0
  58. metadata +64 -28
  59. data/lib/llm_gateway/adapters/claude/client.rb +0 -60
  60. data/lib/llm_gateway/adapters/groq/bidirectional_message_mapper.rb +0 -18
  61. data/lib/llm_gateway/adapters/groq/client.rb +0 -58
  62. data/lib/llm_gateway/adapters/groq/input_mapper.rb +0 -18
  63. data/lib/llm_gateway/adapters/groq/output_mapper.rb +0 -10
  64. data/lib/llm_gateway/adapters/open_ai/client.rb +0 -80
  65. data/lib/llm_gateway/adapters/open_ai/responses/input_mapper.rb +0 -62
  66. data/sample/claude_code_clone/agent.rb +0 -65
  67. data/sample/claude_code_clone/claude_code_clone.rb +0 -40
  68. data/sample/claude_code_clone/prompt.rb +0 -79
  69. data/sample/claude_code_clone/run.rb +0 -47
  70. data/sample/claude_code_clone/tools/bash_tool.rb +0 -54
  71. data/sample/claude_code_clone/tools/edit_tool.rb +0 -61
  72. data/sample/claude_code_clone/tools/grep_tool.rb +0 -113
  73. data/sample/claude_code_clone/tools/read_tool.rb +0 -61
  74. data/sample/claude_code_clone/tools/todowrite_tool.rb +0 -98
@@ -4,9 +4,9 @@ require "base64"
4
4
 
5
5
  module LlmGateway
6
6
  module Adapters
7
- module OpenAi
7
+ module OpenAI
8
8
  module Responses
9
- class BidirectionalMessageMapper < OpenAi::ChatCompletions::BidirectionalMessageMapper
9
+ class BidirectionalMessageMapper < OpenAI::ChatCompletions::BidirectionalMessageMapper
10
10
  def map_content(content)
11
11
  # Convert string content to text format
12
12
  #
@@ -15,6 +15,8 @@ module LlmGateway
15
15
  case content[:type]
16
16
  when "text"
17
17
  map_text_content(content)
18
+ when "image"
19
+ map_image_content(content)
18
20
  when "message"
19
21
  map_messages(content)
20
22
  when "output_text"
@@ -25,6 +27,8 @@ module LlmGateway
25
27
  map_tool_use_content(content)
26
28
  when "tool_result"
27
29
  map_tool_result_content(content)
30
+ when "reasoning"
31
+ map_reasoning_content(content)
28
32
  else
29
33
  content
30
34
  end
@@ -37,10 +41,21 @@ module LlmGateway
37
41
  end
38
42
 
39
43
  def map_tool_result_content(content)
44
+ output = content[:content]
45
+ if output.is_a?(Array)
46
+ output = output.map do |item|
47
+ if item.is_a?(Hash)
48
+ map_content(item.transform_keys(&:to_sym))
49
+ else
50
+ item
51
+ end
52
+ end
53
+ end
54
+
40
55
  {
41
56
  "type": "function_call_output",
42
57
  "call_id": content[:tool_use_id],
43
- "output": content[:content]
58
+ "output": output
44
59
  }
45
60
  end
46
61
 
@@ -54,17 +69,50 @@ module LlmGateway
54
69
 
55
70
  def map_output_text_content(content)
56
71
  {
57
- type: "text",
72
+ type: direction == LlmGateway::DIRECTION_IN ? "input_text" : "text",
58
73
  text: content[:text]
59
74
  }
60
75
  end
61
76
 
77
+ def map_reasoning_content(content)
78
+ if direction == LlmGateway::DIRECTION_IN
79
+ return { id: content[:id] } if content[:id]
80
+
81
+ content
82
+ else
83
+ {
84
+ type: "reasoning",
85
+ reasoning: normalize_reasoning_text(content[:summary]),
86
+ signature: content[:signature]
87
+ }
88
+ end
89
+ end
90
+
91
+ def map_image_content(content)
92
+ {
93
+ type: "input_image",
94
+ image_url: "data:#{content[:media_type]};base64,#{content[:data]}"
95
+ }
96
+ end
97
+
62
98
  def map_text_content(content)
63
99
  {
64
100
  type: "input_text",
65
101
  text: content[:text]
66
102
  }
67
103
  end
104
+
105
+ def normalize_reasoning_text(summary)
106
+ return summary if summary.is_a?(String)
107
+ return nil unless summary.is_a?(Array)
108
+ return nil if summary.empty?
109
+
110
+ summary.filter_map do |item|
111
+ next item if item.is_a?(String)
112
+
113
+ item[:text] || item[:summary_text] || item[:reasoning]
114
+ end.join("\n")
115
+ end
68
116
  end
69
117
  end
70
118
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require_relative "bidirectional_message_mapper"
5
+
6
+ module LlmGateway
7
+ module Adapters
8
+ module OpenAI
9
+ module Responses
10
+ class InputMapper < OpenAI::ChatCompletions::InputMapper
11
+ def self.message_mapper
12
+ BidirectionalMessageMapper.new(LlmGateway::DIRECTION_IN)
13
+ end
14
+
15
+ def self.map_tools(tools)
16
+ return tools unless tools
17
+ mapper = message_mapper
18
+
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
+ }
26
+
27
+ [ :contents, :content ].each do |key|
28
+ next unless tool[key].is_a?(Array)
29
+
30
+ mapped_tool[key] = tool[key].map do |entry|
31
+ entry.is_a?(Hash) ? mapper.map_content(entry.transform_keys(&:to_sym)) : entry
32
+ end
33
+ end
34
+
35
+ mapped_tool
36
+ end
37
+ end
38
+
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
60
+ else
61
+ {
62
+ role: msg[:role],
63
+ content: content
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
69
+
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) }
78
+
79
+ text_blocks = blocks.select { |b| b[:type] == "text" }
80
+ tool_use_blocks = blocks.select { |b| b[:type] == "tool_use" }
81
+
82
+ result = []
83
+
84
+ if text_blocks.any?
85
+ result << {
86
+ role: "assistant",
87
+ content: text_blocks.map { |b| { type: "output_text", text: b[:text] } }
88
+ }
89
+ end
90
+
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
97
+ }
98
+ end
99
+
100
+ result
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmGateway
4
+ module Adapters
5
+ module OpenAI
6
+ module Responses
7
+ module OptionMapper
8
+ include LlmGateway::Adapters::OpenAI::PromptCacheOptionMapper
9
+
10
+ VALID_REASONING_LEVELS = %w[low medium high xhigh].freeze
11
+
12
+ module_function
13
+
14
+ def map(options)
15
+ mapped_options = options.dup
16
+
17
+ max_completion_tokens = mapped_options.delete(:max_completion_tokens)
18
+ mapped_options[:max_output_tokens] = max_completion_tokens || mapped_options[:max_output_tokens] || 20_480
19
+
20
+ map_cache_key!(mapped_options)
21
+ map_prompt_cache_retention!(mapped_options)
22
+
23
+ return mapped_options unless mapped_options.key?(:reasoning)
24
+
25
+ reasoning = mapped_options.delete(:reasoning)
26
+ return mapped_options if reasoning.nil? || reasoning.to_s == "none"
27
+
28
+ mapped_options.merge(reasoning: normalize_reasoning(reasoning))
29
+ end
30
+
31
+ def normalize_reasoning(reasoning)
32
+ effort = reasoning.to_s
33
+ return { effort: effort, summary: "detailed" } if VALID_REASONING_LEVELS.include?(effort)
34
+
35
+ raise ArgumentError, "Invalid reasoning '#{reasoning}'. Use 'none', 'low', 'medium', 'high', or 'xhigh'."
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -5,7 +5,7 @@ require_relative "bidirectional_message_mapper"
5
5
 
6
6
  module LlmGateway
7
7
  module Adapters
8
- module OpenAi
8
+ module OpenAI
9
9
  module Responses
10
10
  class OutputMapper
11
11
  def self.map(data)
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../structs"
4
+
5
+ module LlmGateway
6
+ module Adapters
7
+ module OpenAI
8
+ module Responses
9
+ class StreamMapper
10
+ def map(chunk)
11
+ queued_event = shift_queued_event
12
+ return queued_event if queued_event
13
+
14
+ event_type = chunk[:event]
15
+ data = chunk[:data] || {}
16
+ raise_stream_error!(data) if event_type == "error" || data[:error] || data[:type] == "error"
17
+
18
+ case event_type
19
+ when "response.created"
20
+ stash_response(data[:response])
21
+ nil
22
+ when "response.output_item.added"
23
+ map_output_item_added(data)
24
+ when "response.output_item.done"
25
+ map_output_item_done(data)
26
+ when "response.content_part.added"
27
+ map_content_part_added(data)
28
+ when "response.content_part.done", "response.output_text.done"
29
+ map_text_done(data)
30
+ when "response.output_text.delta"
31
+ AssistantStreamEvent.new(
32
+ type: :text_delta,
33
+ content_index: content_index_for(data[:output_index] || 0),
34
+ delta: data[:delta] || ""
35
+ )
36
+ when "response.function_call_arguments.delta"
37
+ AssistantStreamEvent.new(
38
+ type: :tool_delta,
39
+ content_index: content_index_for(data[:output_index] || 0),
40
+ delta: data[:delta] || ""
41
+ )
42
+ when "response.function_call_arguments.done"
43
+ map_tool_done(data)
44
+ when "response.reasoning_summary_text.delta"
45
+ output_index = data[:output_index] || 0
46
+ mark_reasoning_has_content(output_index)
47
+ AssistantStreamReasoningEvent.new(
48
+ type: :reasoning_delta,
49
+ content_index: content_index_for(output_index),
50
+ delta: data[:delta] || "",
51
+ signature: ""
52
+ )
53
+ when "response.completed"
54
+ map_response_completed(data[:response])
55
+ else
56
+ nil
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def map_output_item_added(data)
63
+ item = data[:item] || {}
64
+ output_index = data[:output_index] || 0
65
+
66
+ case item[:type]
67
+ when "reasoning"
68
+ mark_reasoning_started(output_index)
69
+ AssistantStreamReasoningEvent.new(
70
+ type: :reasoning_start,
71
+ content_index: register_content_index(output_index),
72
+ delta: "",
73
+ signature: ""
74
+ )
75
+ when "message"
76
+ register_content_index(output_index)
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
91
+ end
92
+
93
+ def map_output_item_done(data)
94
+ item = data[:item] || {}
95
+ output_index = data[:output_index] || 0
96
+
97
+ case item[:type]
98
+ when "reasoning"
99
+ map_reasoning_done(output_index, item)
100
+ when "function_call"
101
+ map_function_call_done(output_index, item)
102
+ else
103
+ nil
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:,
116
+ delta: "",
117
+ signature: ""
118
+ )
119
+ )
120
+ mark_reasoning_completed(output_index)
121
+ return AssistantStreamReasoningEvent.new(
122
+ type: :reasoning_delta,
123
+ content_index:,
124
+ delta: summary_text,
125
+ signature: ""
126
+ )
127
+ end
128
+
129
+ mark_reasoning_completed(output_index)
130
+ AssistantStreamReasoningEvent.new(
131
+ type: :reasoning_end,
132
+ content_index:,
133
+ delta: "",
134
+ signature: ""
135
+ )
136
+ end
137
+
138
+ def map_function_call_done(output_index, item)
139
+ return nil if tool_started?(output_index)
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
+ )
149
+
150
+ AssistantToolStartEvent.new(
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
+ )
157
+ end
158
+
159
+ def map_content_part_added(data)
160
+ part = data[:part] || {}
161
+ return nil unless part[:type] == "output_text"
162
+
163
+ AssistantStreamEvent.new(
164
+ type: :text_start,
165
+ content_index: content_index_for(data[:output_index] || 0),
166
+ delta: ""
167
+ )
168
+ end
169
+
170
+ def map_text_done(data)
171
+ AssistantStreamEvent.new(
172
+ type: :text_end,
173
+ content_index: content_index_for(data[:output_index] || 0),
174
+ delta: ""
175
+ )
176
+ end
177
+
178
+ def map_tool_done(data)
179
+ AssistantStreamEvent.new(
180
+ type: :tool_end,
181
+ content_index: content_index_for(data[:output_index] || 0),
182
+ delta: ""
183
+ )
184
+ end
185
+
186
+ def map_response_completed(response)
187
+ stash_response(response)
188
+ AssistantStreamMessageEvent.new(
189
+ type: message_started? ? :message_delta : :message_start,
190
+ delta: pending_message_attributes.merge(role: pending_message_attributes[:role] || "assistant", stop_reason: stop_reason_for(response)),
191
+ usage_increment: usage_increment(response)
192
+ ).tap do
193
+ @message_started = true
194
+ clear_pending_message_attributes
195
+ end
196
+ end
197
+
198
+ def usage_increment(response)
199
+ usage = response[:usage] || {}
200
+
201
+ {
202
+ input_tokens: usage[:input_tokens] || 0,
203
+ cache_creation_input_tokens: 0,
204
+ cache_read_input_tokens: usage.dig(:input_tokens_details, :cached_tokens) || 0,
205
+ output_tokens: usage[:output_tokens] || 0,
206
+ reasoning_tokens: usage.dig(:output_tokens_details, :reasoning_tokens) || 0
207
+ }
208
+ end
209
+
210
+ def stop_reason_for(response)
211
+ output = response[:output] || []
212
+ last_item = output.last || {}
213
+
214
+ tool_state.any? || last_item[:type] == "function_call" ? "tool_use" : "stop"
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
236
+ end
237
+
238
+ def mark_reasoning_started(output_index)
239
+ reasoning_state[output_index] = :started
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)
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../adapter"
4
+ require_relative "acts_like_responses"
5
+ require_relative "../input_message_sanitizer"
6
+ require_relative "responses/input_mapper"
7
+ require_relative "responses/output_mapper"
8
+ require_relative "responses/option_mapper"
9
+ require_relative "file_output_mapper"
10
+ require_relative "responses/stream_mapper"
11
+
12
+ module LlmGateway
13
+ module Adapters
14
+ module OpenAI
15
+ class ResponsesAdapter < Adapter
16
+ include ActsLikeOpenAIResponses
17
+ end
18
+ end
19
+ end
20
+ end