prompt_builder 0.1.2 → 0.3.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/CHANGELOG.md +45 -0
- data/README.md +145 -5
- data/VERSION +1 -1
- data/lib/prompt_builder/errors.rb +8 -0
- data/lib/prompt_builder/items/message.rb +40 -16
- data/lib/prompt_builder/response.rb +59 -0
- data/lib/prompt_builder/serializers/base.rb +28 -0
- data/lib/prompt_builder/serializers/chat_completion/request.rb +30 -9
- data/lib/prompt_builder/serializers/chat_completion/response.rb +12 -0
- data/lib/prompt_builder/serializers/converse/request.rb +60 -16
- data/lib/prompt_builder/serializers/converse/response.rb +15 -0
- data/lib/prompt_builder/serializers/gemini/request.rb +30 -9
- data/lib/prompt_builder/serializers/gemini/response.rb +9 -0
- data/lib/prompt_builder/serializers/messages/request.rb +36 -10
- data/lib/prompt_builder/serializers/messages/response.rb +9 -0
- data/lib/prompt_builder/serializers/open_responses/request.rb +6 -0
- data/lib/prompt_builder/serializers/open_responses/response.rb +10 -0
- data/lib/prompt_builder/session.rb +236 -11
- data/prompt_builder.gemspec +0 -2
- metadata +2 -16
|
@@ -16,6 +16,18 @@ module PromptBuilder
|
|
|
16
16
|
class << self
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
+
# OpenAI errors arrive as {"error" => {"message" => ..., "type" =>
|
|
20
|
+
# ..., "code" => ...}}; some compatible servers return a bare string
|
|
21
|
+
# under "error".
|
|
22
|
+
def error_response_message(hash)
|
|
23
|
+
error = hash["error"]
|
|
24
|
+
return nil if error.nil? || hash.key?("choices")
|
|
25
|
+
|
|
26
|
+
return error.to_s unless error.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
[error["code"] || error["type"], error["message"]].compact.join(": ")
|
|
29
|
+
end
|
|
30
|
+
|
|
19
31
|
def deserialize_response(hash)
|
|
20
32
|
validate_response!(hash)
|
|
21
33
|
|
|
@@ -8,6 +8,9 @@ module PromptBuilder
|
|
|
8
8
|
class Converse < Base
|
|
9
9
|
# Request serializer for the Amazon Bedrock Converse API format.
|
|
10
10
|
#
|
|
11
|
+
# Required session fields (an UnsupportedFormatError is raised when missing):
|
|
12
|
+
# - +model+ — populates the required +modelId+ parameter
|
|
13
|
+
#
|
|
11
14
|
# === Unsupported Open Responses features
|
|
12
15
|
#
|
|
13
16
|
# These session fields are not supported and are silently omitted from the
|
|
@@ -19,7 +22,8 @@ module PromptBuilder
|
|
|
19
22
|
# - +parallel_tool_calls+ — parallel tool call control is not supported
|
|
20
23
|
# - +presence_penalty+ — not supported by the Converse API
|
|
21
24
|
# - +prompt_cache_key+ / +prompt_cache_retention+ — explicit prompt cache keys are not supported
|
|
22
|
-
# - +reasoning+ — extended thinking is not supported on the Converse endpoint
|
|
25
|
+
# - +reasoning+ — extended thinking is not supported on the Converse endpoint;
|
|
26
|
+
# a warning is issued (once per process) when +session.reasoning+ is set
|
|
23
27
|
# - +safety_identifier+ — no equivalent user-safety field on the Converse endpoint
|
|
24
28
|
# - +store+ — server-side response storage is not supported
|
|
25
29
|
# - +stream+ — SSE streaming is handled outside the Converse request payload
|
|
@@ -35,6 +39,10 @@ module PromptBuilder
|
|
|
35
39
|
# - +tool_choice: "none"+ has no Converse representation and is omitted
|
|
36
40
|
#
|
|
37
41
|
# Input content restrictions:
|
|
42
|
+
# - System and developer messages are hoisted out of conversational order
|
|
43
|
+
# into the top-level +system+ parameter, with +instructions+ merged last
|
|
44
|
+
# (instructions apply to the current request, matching Open Responses
|
|
45
|
+
# semantics)
|
|
38
46
|
# - +Reasoning+ items are silently skipped
|
|
39
47
|
# - +RefusalContent+ is dropped silently (a parsed response refusal can
|
|
40
48
|
# stay in session history without breaking subsequent request_payload calls)
|
|
@@ -64,8 +72,11 @@ module PromptBuilder
|
|
|
64
72
|
# - Prompt management variables (+prompt_variables+)
|
|
65
73
|
#
|
|
66
74
|
# Prompt caching markers (+cachePoint+) are emitted via the +cache_point+
|
|
67
|
-
#
|
|
68
|
-
#
|
|
75
|
+
# extra on system/message content, tool definitions (appended to the
|
|
76
|
+
# +toolConfig.tools+ array), and +FunctionCallOutput+ items (appended to
|
|
77
|
+
# the message content after the +toolResult+ block). Cross-region routing
|
|
78
|
+
# via inference profiles is selected through the model id and needs no
|
|
79
|
+
# request field.
|
|
69
80
|
class Request < Base
|
|
70
81
|
IMAGE_MEDIA_TYPE_FORMATS = {
|
|
71
82
|
"image/jpeg" => "jpeg",
|
|
@@ -110,6 +121,13 @@ module PromptBuilder
|
|
|
110
121
|
}.freeze
|
|
111
122
|
|
|
112
123
|
class << self
|
|
124
|
+
# Reset the once-per-process reasoning warning. Primarily used in tests.
|
|
125
|
+
#
|
|
126
|
+
# @return [void]
|
|
127
|
+
def reset_reasoning_warning!
|
|
128
|
+
@reasoning_warning_issued = false
|
|
129
|
+
end
|
|
130
|
+
|
|
113
131
|
private
|
|
114
132
|
|
|
115
133
|
def serialize_request(session)
|
|
@@ -118,7 +136,11 @@ module PromptBuilder
|
|
|
118
136
|
ctx = {document_name_counts: Hash.new(0)}
|
|
119
137
|
|
|
120
138
|
h = {}
|
|
121
|
-
|
|
139
|
+
raise UnsupportedFormatError, "Converse format requires session.model" unless session.model
|
|
140
|
+
|
|
141
|
+
warn_reasoning_unsupported! if session.reasoning
|
|
142
|
+
|
|
143
|
+
h["modelId"] = session.model
|
|
122
144
|
|
|
123
145
|
system = build_system(session)
|
|
124
146
|
h["system"] = system unless system.empty?
|
|
@@ -149,11 +171,22 @@ module PromptBuilder
|
|
|
149
171
|
h["toolConfig"] = tool_config if tool_config
|
|
150
172
|
|
|
151
173
|
# Session extra: recognized keys for Converse API
|
|
152
|
-
|
|
174
|
+
extra = session.extra
|
|
175
|
+
apply_session_extra!(h, extra) unless extra.empty?
|
|
153
176
|
|
|
154
177
|
h
|
|
155
178
|
end
|
|
156
179
|
|
|
180
|
+
# The Converse endpoint has no reasoning/extended thinking parameter,
|
|
181
|
+
# so the reasoning config is dropped. Warn once per process instead of
|
|
182
|
+
# dropping it silently.
|
|
183
|
+
def warn_reasoning_unsupported!
|
|
184
|
+
return if @reasoning_warning_issued
|
|
185
|
+
|
|
186
|
+
@reasoning_warning_issued = true
|
|
187
|
+
warn "PromptBuilder: session.reasoning is not supported by the Converse format and was omitted from the request payload"
|
|
188
|
+
end
|
|
189
|
+
|
|
157
190
|
def apply_session_extra!(h, extra)
|
|
158
191
|
if extra.key?("stop_sequences")
|
|
159
192
|
inference_config = h["inferenceConfig"] ||= {}
|
|
@@ -169,8 +202,6 @@ module PromptBuilder
|
|
|
169
202
|
def build_system(session)
|
|
170
203
|
parts = []
|
|
171
204
|
|
|
172
|
-
parts << {"text" => session.instructions} if session.instructions
|
|
173
|
-
|
|
174
205
|
session.items.each do |item|
|
|
175
206
|
next unless item.is_a?(Items::Message)
|
|
176
207
|
next unless item.role == "system" || item.role == "developer"
|
|
@@ -186,6 +217,11 @@ module PromptBuilder
|
|
|
186
217
|
end
|
|
187
218
|
end
|
|
188
219
|
|
|
220
|
+
# Instructions come last: in the Open Responses API they apply to
|
|
221
|
+
# the current request, so they must follow any system messages
|
|
222
|
+
# accumulated in the conversation history.
|
|
223
|
+
parts << {"text" => session.instructions} if session.instructions
|
|
224
|
+
|
|
189
225
|
parts
|
|
190
226
|
end
|
|
191
227
|
|
|
@@ -224,10 +260,14 @@ module PromptBuilder
|
|
|
224
260
|
}]
|
|
225
261
|
}
|
|
226
262
|
when Items::FunctionCallOutput
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
263
|
+
# The Converse ToolResultContentBlock union has no cachePoint
|
|
264
|
+
# member; the marker must be a sibling content block after the
|
|
265
|
+
# toolResult block.
|
|
266
|
+
content = [serialize_tool_result(item, ctx)]
|
|
267
|
+
if item.extra && item.extra["cache_point"]
|
|
268
|
+
content << {"cachePoint" => {"type" => "default"}}
|
|
269
|
+
end
|
|
270
|
+
raw_messages << {"role" => "user", "content" => content}
|
|
231
271
|
when Items::Reasoning, Items::Compaction, Items::ItemReference
|
|
232
272
|
# Reasoning, Compaction, and ItemReference items are not supported
|
|
233
273
|
# in the request, so ignore them rather than raising an error.
|
|
@@ -240,7 +280,7 @@ module PromptBuilder
|
|
|
240
280
|
|
|
241
281
|
def serialize_system_content(content)
|
|
242
282
|
case content
|
|
243
|
-
when Content::InputText
|
|
283
|
+
when Content::InputText, Content::Text
|
|
244
284
|
{"text" => content.text}
|
|
245
285
|
when Content::OutputText
|
|
246
286
|
validate_output_text!(content)
|
|
@@ -268,7 +308,7 @@ module PromptBuilder
|
|
|
268
308
|
|
|
269
309
|
def serialize_content(content, role:, ctx: nil)
|
|
270
310
|
case content
|
|
271
|
-
when Content::InputText, Content::OutputText
|
|
311
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
272
312
|
validate_output_text!(content) if content.is_a?(Content::OutputText)
|
|
273
313
|
{"text" => content.text}
|
|
274
314
|
when Content::InputImage
|
|
@@ -487,7 +527,7 @@ module PromptBuilder
|
|
|
487
527
|
|
|
488
528
|
def serialize_tool_result_content(content, ctx)
|
|
489
529
|
case content
|
|
490
|
-
when Content::InputText, Content::OutputText
|
|
530
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
491
531
|
validate_output_text!(content) if content.is_a?(Content::OutputText)
|
|
492
532
|
{"text" => content.text}
|
|
493
533
|
when Content::InputImage
|
|
@@ -585,13 +625,17 @@ module PromptBuilder
|
|
|
585
625
|
end
|
|
586
626
|
|
|
587
627
|
def build_tools(session)
|
|
588
|
-
session.tool_definitions.
|
|
628
|
+
session.tool_definitions.flat_map do |definition|
|
|
589
629
|
tool_spec = {"name" => definition.name}
|
|
590
630
|
tool_spec["description"] = definition.description if definition.description
|
|
591
631
|
tool_spec["inputSchema"] = {
|
|
592
632
|
"json" => definition.parameters || {"type" => "object", "properties" => {}}
|
|
593
633
|
}
|
|
594
|
-
{"toolSpec" => tool_spec}
|
|
634
|
+
tools = [{"toolSpec" => tool_spec}]
|
|
635
|
+
# The Converse Tool union type has a cachePoint member, so the
|
|
636
|
+
# marker is its own entry in the tools array.
|
|
637
|
+
tools << {"cachePoint" => {"type" => "default"}} if definition.extra["cache_point"]
|
|
638
|
+
tools
|
|
595
639
|
end
|
|
596
640
|
end
|
|
597
641
|
|
|
@@ -10,6 +10,21 @@ module PromptBuilder
|
|
|
10
10
|
class << self
|
|
11
11
|
private
|
|
12
12
|
|
|
13
|
+
# Bedrock exception bodies carry a top-level "message" (the exception
|
|
14
|
+
# class travels in the x-amzn-ErrorType header) or a top-level
|
|
15
|
+
# "__type", while misrouted requests get a Coral service envelope
|
|
16
|
+
# like {"Output" => {"__type" => "..."}, "Version" => "1.0"}.
|
|
17
|
+
def error_response_message(hash)
|
|
18
|
+
return nil if hash.key?("output")
|
|
19
|
+
|
|
20
|
+
coral_output = hash["Output"].is_a?(Hash) ? hash["Output"] : {}
|
|
21
|
+
error_type = hash["__type"] || coral_output["__type"]
|
|
22
|
+
message = hash["message"] || hash["Message"] || coral_output["message"] || coral_output["Message"]
|
|
23
|
+
return nil unless error_type || message
|
|
24
|
+
|
|
25
|
+
[error_type, message].compact.join(": ")
|
|
26
|
+
end
|
|
27
|
+
|
|
13
28
|
def deserialize_response(hash)
|
|
14
29
|
require_response_key!(hash, "output")
|
|
15
30
|
require_response_key!(hash, "stopReason")
|
|
@@ -7,6 +7,11 @@ module PromptBuilder
|
|
|
7
7
|
class Gemini < Base
|
|
8
8
|
# Request serializer for the Google Gemini API format.
|
|
9
9
|
#
|
|
10
|
+
# Required session fields (an UnsupportedFormatError is raised when missing):
|
|
11
|
+
# - +model+ — not part of the payload (it belongs in the request URL) but
|
|
12
|
+
# validated so an incomplete session fails at serialization time
|
|
13
|
+
# - at least one user/model message (the +contents+ array must not be empty)
|
|
14
|
+
#
|
|
10
15
|
# === Unsupported Open Responses features
|
|
11
16
|
#
|
|
12
17
|
# These session fields are not supported and are silently omitted from the
|
|
@@ -22,6 +27,12 @@ module PromptBuilder
|
|
|
22
27
|
# - +truncation+ — server-side context truncation is not supported
|
|
23
28
|
#
|
|
24
29
|
# Input content restrictions:
|
|
30
|
+
# - System and developer messages are hoisted out of conversational order
|
|
31
|
+
# into the top-level +systemInstruction+ field, with +instructions+ merged
|
|
32
|
+
# last (instructions apply to the current request, matching Open Responses
|
|
33
|
+
# semantics)
|
|
34
|
+
# - Only text content (+InputText+/+OutputText+/+Text+) survives in system
|
|
35
|
+
# and developer messages; other content is silently omitted
|
|
25
36
|
# - +InputImage+ content is only supported in user messages (assistant images are omitted)
|
|
26
37
|
# - +InputImage+ with +image_url+ requires either base64 +data+ or a URL;
|
|
27
38
|
# content without +image_url+ or +file_id+ raises
|
|
@@ -115,7 +126,11 @@ module PromptBuilder
|
|
|
115
126
|
system_instruction = build_system_instruction(session)
|
|
116
127
|
h["systemInstruction"] = system_instruction if system_instruction
|
|
117
128
|
|
|
118
|
-
|
|
129
|
+
contents = build_contents(session)
|
|
130
|
+
if contents.empty?
|
|
131
|
+
raise UnsupportedFormatError, "Gemini format requires at least one user/model message"
|
|
132
|
+
end
|
|
133
|
+
h["contents"] = contents
|
|
119
134
|
|
|
120
135
|
generation_config = build_generation_config(session)
|
|
121
136
|
h["generationConfig"] = generation_config unless generation_config.empty?
|
|
@@ -127,7 +142,8 @@ module PromptBuilder
|
|
|
127
142
|
h["toolConfig"] = tool_config if tool_config
|
|
128
143
|
|
|
129
144
|
# Session extra: recognized keys for Gemini API
|
|
130
|
-
|
|
145
|
+
extra = session.extra
|
|
146
|
+
apply_session_extra!(h, extra) unless extra.empty?
|
|
131
147
|
|
|
132
148
|
# Gemini selects streaming via endpoint (:streamGenerateContent)
|
|
133
149
|
# rather than a request body field, so session.stream is a no-op
|
|
@@ -153,19 +169,24 @@ module PromptBuilder
|
|
|
153
169
|
def build_system_instruction(session)
|
|
154
170
|
parts = []
|
|
155
171
|
|
|
156
|
-
if session.instructions
|
|
157
|
-
parts << {"text" => session.instructions}
|
|
158
|
-
end
|
|
159
|
-
|
|
160
172
|
session.items.each do |item|
|
|
161
173
|
next unless item.is_a?(Items::Message)
|
|
162
174
|
next unless item.role == "system" || item.role == "developer"
|
|
163
175
|
|
|
164
176
|
item.content.each do |content|
|
|
165
|
-
|
|
177
|
+
if content.is_a?(Content::InputText) || content.is_a?(Content::OutputText) || content.is_a?(Content::Text)
|
|
178
|
+
parts << {"text" => content.text}
|
|
179
|
+
end
|
|
166
180
|
end
|
|
167
181
|
end
|
|
168
182
|
|
|
183
|
+
# Instructions come last: in the Open Responses API they apply to
|
|
184
|
+
# the current request, so they must follow any system messages
|
|
185
|
+
# accumulated in the conversation history.
|
|
186
|
+
if session.instructions
|
|
187
|
+
parts << {"text" => session.instructions}
|
|
188
|
+
end
|
|
189
|
+
|
|
169
190
|
return nil if parts.empty?
|
|
170
191
|
|
|
171
192
|
{"parts" => parts}
|
|
@@ -265,7 +286,7 @@ module PromptBuilder
|
|
|
265
286
|
# types are silently omitted.
|
|
266
287
|
text = output.filter_map do |content|
|
|
267
288
|
case content
|
|
268
|
-
when Content::InputText, Content::OutputText
|
|
289
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
269
290
|
content.text
|
|
270
291
|
end
|
|
271
292
|
end.join("\n")
|
|
@@ -298,7 +319,7 @@ module PromptBuilder
|
|
|
298
319
|
|
|
299
320
|
def serialize_content(content, role:)
|
|
300
321
|
case content
|
|
301
|
-
when Content::InputText, Content::OutputText
|
|
322
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
302
323
|
part = {"text" => content.text}
|
|
303
324
|
if content.extra && content.extra["thought_signature"]
|
|
304
325
|
part["thoughtSignature"] = content.extra["thought_signature"]
|
|
@@ -63,6 +63,15 @@ module PromptBuilder
|
|
|
63
63
|
class << self
|
|
64
64
|
private
|
|
65
65
|
|
|
66
|
+
# Gemini errors arrive as
|
|
67
|
+
# {"error" => {"code" => 400, "message" => ..., "status" => ...}}.
|
|
68
|
+
def error_response_message(hash)
|
|
69
|
+
error = hash["error"]
|
|
70
|
+
return nil unless error.is_a?(Hash) && !hash.key?("candidates")
|
|
71
|
+
|
|
72
|
+
[error["status"] || error["code"], error["message"]].compact.join(": ")
|
|
73
|
+
end
|
|
74
|
+
|
|
66
75
|
def deserialize_response(hash)
|
|
67
76
|
require_response_key!(hash, "candidates")
|
|
68
77
|
require_response_key!(hash, "modelVersion")
|
|
@@ -5,6 +5,10 @@ module PromptBuilder
|
|
|
5
5
|
class Messages < Base
|
|
6
6
|
# Request serializer for the Anthropic Messages API format.
|
|
7
7
|
#
|
|
8
|
+
# Required session fields (an UnsupportedFormatError is raised when missing):
|
|
9
|
+
# - +model+
|
|
10
|
+
# - +max_output_tokens+ — populates the required +max_tokens+ parameter
|
|
11
|
+
#
|
|
8
12
|
# === Unsupported Open Responses features
|
|
9
13
|
#
|
|
10
14
|
# These session fields are not supported and are silently omitted from the
|
|
@@ -27,9 +31,17 @@ module PromptBuilder
|
|
|
27
31
|
# - +service_tier+ — only +auto+ and +standard_only+ are accepted
|
|
28
32
|
# - +text+ — +format.type=json_schema+ is mapped to +output_config.format+
|
|
29
33
|
# - +reasoning+ — +budget_tokens+, +display+, +effort+, and +type+ are forwarded;
|
|
30
|
-
# +temperature+ must be unset
|
|
34
|
+
# +temperature+ must be unset, +top_p+ must be >= 0.95, and
|
|
35
|
+
# +max_output_tokens+ must be greater than +budget_tokens+ when reasoning
|
|
36
|
+
# is enabled
|
|
31
37
|
#
|
|
32
38
|
# Input content restrictions:
|
|
39
|
+
# - System and developer messages are hoisted out of conversational order
|
|
40
|
+
# into the top-level +system+ parameter, with +instructions+ merged last
|
|
41
|
+
# (instructions apply to the current request, matching Open Responses
|
|
42
|
+
# semantics)
|
|
43
|
+
# - Only text content (+InputText+/+OutputText+/+Text+) survives in system
|
|
44
|
+
# and developer messages; other content is silently omitted
|
|
33
45
|
# - +InputVideo+ content is not supported and is omitted
|
|
34
46
|
# - +RefusalContent+ is dropped silently (a parsed refusal can stay in
|
|
35
47
|
# session history without breaking subsequent request_payload calls)
|
|
@@ -95,7 +107,11 @@ module PromptBuilder
|
|
|
95
107
|
raise UnsupportedFormatError, "Messages format requires session.model" unless session.model
|
|
96
108
|
|
|
97
109
|
h["model"] = session.model
|
|
98
|
-
|
|
110
|
+
unless session.max_output_tokens
|
|
111
|
+
raise UnsupportedFormatError,
|
|
112
|
+
"Messages format requires session.max_output_tokens to populate the required max_tokens field"
|
|
113
|
+
end
|
|
114
|
+
h["max_tokens"] = session.max_output_tokens
|
|
99
115
|
h["temperature"] = session.temperature if session.temperature
|
|
100
116
|
h["top_p"] = session.top_p if session.top_p
|
|
101
117
|
effective_metadata = build_effective_metadata(session)
|
|
@@ -105,7 +121,8 @@ module PromptBuilder
|
|
|
105
121
|
h["stream"] = session.stream unless session.stream.nil?
|
|
106
122
|
|
|
107
123
|
# Session extra: recognized keys for Messages API
|
|
108
|
-
|
|
124
|
+
extra = session.extra
|
|
125
|
+
apply_session_extra!(h, extra) unless extra.empty?
|
|
109
126
|
|
|
110
127
|
output_config = build_output_config(session)
|
|
111
128
|
h["output_config"] = output_config unless output_config.empty?
|
|
@@ -249,6 +266,12 @@ module PromptBuilder
|
|
|
249
266
|
def validate_thinking_compatibility!(session, thinking)
|
|
250
267
|
return unless thinking
|
|
251
268
|
|
|
269
|
+
budget = thinking["budget_tokens"]
|
|
270
|
+
if budget && session.max_output_tokens && session.max_output_tokens <= budget
|
|
271
|
+
raise UnsupportedFormatError,
|
|
272
|
+
"Messages format requires max_output_tokens (max_tokens) to be greater than reasoning.budget_tokens"
|
|
273
|
+
end
|
|
274
|
+
|
|
252
275
|
if session.temperature
|
|
253
276
|
raise UnsupportedFormatError,
|
|
254
277
|
"Messages format does not support temperature when thinking is enabled"
|
|
@@ -263,16 +286,12 @@ module PromptBuilder
|
|
|
263
286
|
def build_system(session)
|
|
264
287
|
parts = []
|
|
265
288
|
|
|
266
|
-
if session.instructions
|
|
267
|
-
parts << {"type" => "text", "text" => session.instructions}
|
|
268
|
-
end
|
|
269
|
-
|
|
270
289
|
session.items.each do |item|
|
|
271
290
|
next unless item.is_a?(Items::Message)
|
|
272
291
|
next unless item.role == "system" || item.role == "developer"
|
|
273
292
|
|
|
274
293
|
item.content.each do |content|
|
|
275
|
-
if content.is_a?(Content::InputText)
|
|
294
|
+
if content.is_a?(Content::InputText) || content.is_a?(Content::OutputText) || content.is_a?(Content::Text)
|
|
276
295
|
part = {"type" => "text", "text" => content.text}
|
|
277
296
|
if content.extra && content.extra["cache_control"]
|
|
278
297
|
part["cache_control"] = content.extra["cache_control"]
|
|
@@ -282,6 +301,13 @@ module PromptBuilder
|
|
|
282
301
|
end
|
|
283
302
|
end
|
|
284
303
|
|
|
304
|
+
# Instructions come last: in the Open Responses API they apply to
|
|
305
|
+
# the current request, so they must follow any system messages
|
|
306
|
+
# accumulated in the conversation history.
|
|
307
|
+
if session.instructions
|
|
308
|
+
parts << {"type" => "text", "text" => session.instructions}
|
|
309
|
+
end
|
|
310
|
+
|
|
285
311
|
parts
|
|
286
312
|
end
|
|
287
313
|
|
|
@@ -347,7 +373,7 @@ module PromptBuilder
|
|
|
347
373
|
|
|
348
374
|
def serialize_content_block(content, role:)
|
|
349
375
|
case content
|
|
350
|
-
when Content::InputText
|
|
376
|
+
when Content::InputText, Content::Text
|
|
351
377
|
{"type" => "text", "text" => content.text}
|
|
352
378
|
when Content::OutputText
|
|
353
379
|
text = {"type" => "text", "text" => content.text}
|
|
@@ -476,7 +502,7 @@ module PromptBuilder
|
|
|
476
502
|
# Document blocks are rejected by the API.
|
|
477
503
|
def serialize_tool_result_content(content)
|
|
478
504
|
case content
|
|
479
|
-
when Content::InputText, Content::OutputText
|
|
505
|
+
when Content::InputText, Content::OutputText, Content::Text
|
|
480
506
|
{"type" => "text", "text" => content.text}
|
|
481
507
|
when Content::InputImage
|
|
482
508
|
serialize_content(content, role: "user")
|
|
@@ -10,6 +10,15 @@ module PromptBuilder
|
|
|
10
10
|
class << self
|
|
11
11
|
private
|
|
12
12
|
|
|
13
|
+
# Anthropic errors arrive as
|
|
14
|
+
# {"type" => "error", "error" => {"type" => ..., "message" => ...}}.
|
|
15
|
+
def error_response_message(hash)
|
|
16
|
+
error = hash["error"]
|
|
17
|
+
return nil unless error.is_a?(Hash) && !hash.key?("content")
|
|
18
|
+
|
|
19
|
+
[error["type"], error["message"]].compact.join(": ")
|
|
20
|
+
end
|
|
21
|
+
|
|
13
22
|
def deserialize_response(hash)
|
|
14
23
|
require_response_key!(hash, "content")
|
|
15
24
|
require_response_key!(hash, "stop_reason")
|
|
@@ -4,6 +4,9 @@ module PromptBuilder
|
|
|
4
4
|
module Serializers
|
|
5
5
|
class OpenResponses < Base
|
|
6
6
|
# Request serializer for the OpenAI Open Responses API format.
|
|
7
|
+
#
|
|
8
|
+
# Required session fields (an UnsupportedFormatError is raised when missing):
|
|
9
|
+
# - +model+
|
|
7
10
|
class Request < Base
|
|
8
11
|
# Content extra keys that are part of the Open Responses API schema and
|
|
9
12
|
# must survive extra-stripping (the canonical content classes have no
|
|
@@ -20,9 +23,12 @@ module PromptBuilder
|
|
|
20
23
|
# @param session [Session] the session to export
|
|
21
24
|
# @return [Hash] the serialized request payload
|
|
22
25
|
def request_payload(session)
|
|
26
|
+
raise UnsupportedFormatError, "Open Responses format requires session.model" unless session.model
|
|
27
|
+
|
|
23
28
|
payload = session.to_h
|
|
24
29
|
items = apply_server_state!(payload, session)
|
|
25
30
|
payload.delete("extra")
|
|
31
|
+
payload.delete("response_boundary_index")
|
|
26
32
|
strip_extra(payload, items, session.tool_definitions)
|
|
27
33
|
normalize_content_urls!(payload)
|
|
28
34
|
strip_non_replayable_reasoning!(payload)
|
|
@@ -8,6 +8,16 @@ module PromptBuilder
|
|
|
8
8
|
class << self
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
+
# Open Responses request errors arrive as a bare {"error" => {...}}
|
|
12
|
+
# envelope. A failed response object also carries an "error" field,
|
|
13
|
+
# but it has a "status" and parses into a failed Response instead.
|
|
14
|
+
def error_response_message(hash)
|
|
15
|
+
error = hash["error"]
|
|
16
|
+
return nil unless error.is_a?(Hash) && !hash.key?("status")
|
|
17
|
+
|
|
18
|
+
[error["code"] || error["type"], error["message"]].compact.join(": ")
|
|
19
|
+
end
|
|
20
|
+
|
|
11
21
|
def deserialize_response(hash)
|
|
12
22
|
require_response_key!(hash, "status")
|
|
13
23
|
require_response_key!(hash, "object")
|