prompt_builder 0.1.0 → 0.1.2
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 +19 -10
- data/README.md +11 -10
- data/VERSION +1 -1
- data/lib/prompt_builder/errors.rb +3 -0
- data/lib/prompt_builder/response.rb +8 -0
- data/lib/prompt_builder/serializers/base.rb +16 -0
- data/lib/prompt_builder/serializers/chat_completion/request.rb +5 -1
- data/lib/prompt_builder/serializers/chat_completion/response.rb +9 -2
- data/lib/prompt_builder/serializers/converse/request.rb +11 -9
- data/lib/prompt_builder/serializers/converse/response.rb +3 -0
- data/lib/prompt_builder/serializers/gemini/response.rb +3 -0
- data/lib/prompt_builder/serializers/messages/request.rb +24 -3
- data/lib/prompt_builder/serializers/messages/response.rb +3 -0
- data/lib/prompt_builder/serializers/open_responses/request.rb +69 -26
- data/lib/prompt_builder/serializers/open_responses/response.rb +3 -0
- data/lib/prompt_builder/session.rb +3 -3
- data/lib/prompt_builder.rb +5 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e762383ea51918fa77b5962f0137775551a37da1c228a37d24364011c8d21f73
|
|
4
|
+
data.tar.gz: 64e2a4e22feb0a8e0e5752f4807b79452c3cb700dd8b5740500f11cbe0b4217e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7eb3a4806f9cd7a24e00652659b7a5d641ae5877187bd714afd0e4eaa86c7a7901eebcfdf11202497d0b98cce03f9a0fec10f1840883f91b4b87be3a9f2c387
|
|
7
|
+
data.tar.gz: 2b731def4d25b2233b208d3fb3d8867ee17cce9356cb84b3d57d3d3043116af5eb55ca7888de00c329c3637b209bbc70ea11dada040bc5f97f9701c85a0fa8ee
|
data/CHANGELOG.md
CHANGED
|
@@ -4,20 +4,29 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 0.1.2
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `Session#add_function_call_output` raised a `NameError`; it now correctly builds an `Items::FunctionCallOutput`.
|
|
12
|
+
- Provider-specific `extra` data on items, content blocks, and tool definitions (e.g. `cache_control`, `cache_point`, `thought_signature`, `media_type`) leaked into Open Responses request payloads and would be rejected as unknown parameters. It is now stripped from `request_payload(:open_responses)`.
|
|
13
|
+
- The Messages serializer emitted all `OutputText.annotations` as text-block `citations`, including annotation shapes from other providers (e.g. Chat Completions `url_citation`) that the Anthropic API rejects. Only valid Anthropic citation types are now emitted.
|
|
14
|
+
- The Chat Completions serializer emitted tool messages with an empty content array when a tool result contained no text content; it now collapses to an empty string.
|
|
15
|
+
- The Chat Completions response parser duplicated message-level `annotations` and `logprobs` onto every text block; they are now attached only to the first.
|
|
16
|
+
- `PromptBuilder.parse_data_url` now accepts data URLs with media type parameters (e.g. `data:text/plain;charset=utf-8;base64,...`).
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `Response#provider_data` as an alias for `Response#extra`.
|
|
21
|
+
|
|
22
|
+
## 0.1.1
|
|
8
23
|
|
|
9
24
|
### Changed
|
|
10
25
|
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
types, enum values, sub-keys, `tool_choice` variants, and `Compaction`/`ItemReference`
|
|
14
|
-
items are dropped from request payloads; unrecognized blocks, candidates, and choices
|
|
15
|
-
are skipped when parsing responses. `UnsupportedFormatError` is still raised for
|
|
16
|
-
missing required data (e.g. no `model`, no messages, a `json_schema` format without a
|
|
17
|
-
schema), structural problems, genuine conflicts, unparseable function-call arguments,
|
|
18
|
-
and streaming response chunks.
|
|
26
|
+
- Responses will now raise an `UnsupportedFormatError` if the response shape is missing key elements.
|
|
27
|
+
- Removed the default `max_tokens` value of 4096 from the request serializer. If `max_output_tokens` is not set on the session, the request will simply omit the `max_tokens` parameter, allowing the API to apply its own defaults.
|
|
19
28
|
|
|
20
|
-
## 1.0
|
|
29
|
+
## 0.1.0
|
|
21
30
|
|
|
22
31
|
### Added
|
|
23
32
|
|
data/README.md
CHANGED
|
@@ -78,11 +78,11 @@ Once you've built a session, serialize it to a request payload for the API you w
|
|
|
78
78
|
**Open Responses API**:
|
|
79
79
|
|
|
80
80
|
```ruby
|
|
81
|
-
payload = session.to_h
|
|
82
|
-
# or
|
|
83
81
|
payload = session.request_payload(:open_responses)
|
|
84
82
|
```
|
|
85
83
|
|
|
84
|
+
Note that `session.to_h` is *not* a request payload — it is the persistence format used by `Session.from_h` and keeps provider-specific `extra` data, canonical `url` content keys, and non-replayable items that the Open Responses API would reject. Always build request payloads through `request_payload`.
|
|
85
|
+
|
|
86
86
|
**OpenAI Chat Completions API**:
|
|
87
87
|
|
|
88
88
|
```ruby
|
|
@@ -117,7 +117,7 @@ uri = URI("https://api.openai.com/v1/responses")
|
|
|
117
117
|
request = Net::HTTP::Post.new(uri)
|
|
118
118
|
request["Authorization"] = "Bearer #{api_key}"
|
|
119
119
|
request["Content-Type"] = "application/json"
|
|
120
|
-
request.body = JSON.generate(session.
|
|
120
|
+
request.body = JSON.generate(session.request_payload(:open_responses))
|
|
121
121
|
|
|
122
122
|
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
123
123
|
http.request(request)
|
|
@@ -535,7 +535,7 @@ The following table shows which session configuration fields are supported by ea
|
|
|
535
535
|
⁹ `RefusalContent` blocks are dropped silently by all request serializers so a parsed refusal can sit in session history without breaking subsequent `request_payload` calls. A message left empty after stripping is omitted entirely.
|
|
536
536
|
¹⁰ Chat Completions sends `InputFile` as a `{"type": "file", "file": {...}}` content block. `file_id` (Files API) and base64 data URL (with optional `filename`/`media_type`) are supported. A URL-only `InputFile` (non-data URL) is omitted because Chat Completions has no remote-URL form for files.
|
|
537
537
|
¹¹ `OutputText.annotations` (e.g. URL citations from `web_search_options`) are parsed onto the assistant message and round-trip through session history, but are dropped silently on Chat Completions and Converse request serialization since those formats have no request-side equivalent. `OutputText.logprobs` is likewise dropped.
|
|
538
|
-
¹² Messages text-block `citations` are parsed into `OutputText.annotations` and emitted back as `citations` when serializing Messages history. Document/tool-result citation opt-ins are still not modeled.
|
|
538
|
+
¹² Messages text-block `citations` are parsed into `OutputText.annotations` and emitted back as `citations` when serializing Messages history. Annotations from other providers (e.g. a Chat Completions `url_citation`) are silently dropped since they are not valid Anthropic citation shapes. Document/tool-result citation opt-ins are still not modeled.
|
|
539
539
|
¹³ Converse `requestMetadata` only accepts string key/value pairs. This serializer stringifies scalar metadata values and silently omits nested arrays or objects.
|
|
540
540
|
|
|
541
541
|
### Features Not Accessible Through Open Responses
|
|
@@ -617,7 +617,7 @@ Request-side mappings worth calling out:
|
|
|
617
617
|
| `InputFile` with data URL (+ optional `filename`/`media_type`) | `{"type": "file", "file": {"filename": ..., "file_data": "data:<media_type>;base64,..."}}` |
|
|
618
618
|
| `InputImage` with data URL (+ `media_type`) | `{"type": "image_url", "image_url": {"url": "data:<media_type>;base64,..."}}` |
|
|
619
619
|
|
|
620
|
-
|
|
620
|
+
Chat Completions request features with no canonical Open Responses field are available through `session.extra` (`stop`, `seed`, `logit_bias`, `n`, `prediction`, `web_search_options`, `modalities`, `audio`). Features not exposed at all include `input_audio` content blocks, custom tools, `tool_choice: {"type": "allowed_tools", ...}`, and the deprecated `functions`, `function_call`, `max_tokens`, and `user` fields. Use the modern canonical fields when available (`tools`, `tool_choice`, `max_output_tokens`, `safety_identifier`, `prompt_cache_key`).
|
|
621
621
|
|
|
622
622
|
Response-side behavior and limitations:
|
|
623
623
|
|
|
@@ -676,7 +676,7 @@ Content and message restrictions:
|
|
|
676
676
|
- Assistant messages map to `role: "model"`; consecutive same-role turns are merged automatically.
|
|
677
677
|
- `InputImage`, `InputFile`, and `InputVideo` are only supported in user messages; the same content on an assistant message is omitted.
|
|
678
678
|
- `InputImage` accepts any URL, a base64 data URL (with required `media_type`), or a Files API `file_id`.
|
|
679
|
-
- `InputFile` accepts any URL, a base64 data URL, or `file_id`. `media_type` is required when not inferable from a `filename` or URL extension. Recognized extensions
|
|
679
|
+
- `InputFile` accepts any URL, a base64 data URL, or `file_id`. `media_type` is required when not inferable from a `filename` or URL extension. Recognized extensions cover documents (`pdf`, `txt`, `md`/`markdown`, `html`/`htm`, `csv`, `json`, `xml`, `rtf`), images (`png`, `jpg`/`jpeg`, `webp`, `heic`, `heif`), audio (`mp3`, `wav`, `aiff`, `aac`, `ogg`, `flac`), and video (`mp4`, `mov`, `webm`, `mpeg`/`mpg`).
|
|
680
680
|
- `InputVideo` requires a URL; raw bytes are not modeled.
|
|
681
681
|
- `RefusalContent` is dropped silently so a parsed Chat Completions refusal can sit in session history without breaking subsequent serialization.
|
|
682
682
|
- `Reasoning` items round-trip via `parts[].thought` with `thoughtSignature` preserved. `redacted_thinking`, `summary`, and unknown reasoning block types are silently skipped.
|
|
@@ -689,7 +689,7 @@ Response-side limitations:
|
|
|
689
689
|
|
|
690
690
|
- Unknown response `Part` shapes (`inlineData`, `fileData`, `executableCode`, `codeExecutionResult`, `videoMetadata`, server-side `toolCall`/`toolResponse`, or any `Part` without a recognized content key) are silently skipped.
|
|
691
691
|
- Only `candidates[0]` is parsed. When `candidateCount > 1`, additional candidates are dropped (their index is preserved on `provider_data`).
|
|
692
|
-
- Function-call `id` from the response is
|
|
692
|
+
- Function-call `id` from the response is preserved when present; otherwise the parser synthesizes `gemini_call_<seed>_<n>` so multiple calls in one response share a deterministic seed.
|
|
693
693
|
- `finishReason` mappings: `STOP` → `completed`; `MAX_TOKENS` → `incomplete`; `SAFETY`, `RECITATION`, `OTHER`, `BLOCKLIST`, `PROHIBITED_CONTENT`, `SPII`, `MALFORMED_FUNCTION_CALL`, `IMAGE_SAFETY`, `LANGUAGE`, `UNEXPECTED_TOOL_CALL`, `TOO_MANY_TOOL_CALLS`, `MODEL_ARMOR` → `failed`. `FINISH_REASON_UNSPECIFIED` is treated as nil.
|
|
694
694
|
- An empty `candidates` array combined with `promptFeedback.blockReason` is mapped to `failed`.
|
|
695
695
|
- `usageMetadata.cachedContentTokenCount`, `toolUsePromptTokenCount`, `promptTokensDetails`, `cacheTokensDetails`, and `toolUsePromptTokensDetails` populate `response.usage.input_tokens_details`. `thoughtsTokenCount` and `candidatesTokensDetails` populate `response.usage.output_tokens_details`.
|
|
@@ -725,14 +725,15 @@ Content and message restrictions:
|
|
|
725
725
|
- `FunctionCall.arguments` must parse to a JSON object; non-object JSON values raise (Bedrock's `toolUse.input` requires an object).
|
|
726
726
|
- `FunctionCallOutput.output` content supports `InputText`/`OutputText`, `InputImage`, `InputFile`, and `InputVideo`, mapping to Converse `text`, `image`, `document`, and `video` tool result blocks. Converse also supports `json` and `searchResult` tool result blocks, but this gem has no canonical content types for them, so unsupported content is omitted. `FunctionCallOutput.status` is mapped: `completed` → `success`, `failed`/`incomplete` → `error`, anything else passes through or is dropped.
|
|
727
727
|
- `tool_choice: "none"` and `tool_choice` without registered tools are both omitted.
|
|
728
|
-
- Converse API request features
|
|
728
|
+
- Converse API request features with no canonical Open Responses field are available through `session.extra` (`stop_sequences`, `guardrail_config`, `additional_model_request_fields`, `additional_model_response_field_paths`, `performance_config`, `prompt_variables`) and content `extra` (`cache_point`). Features not exposed at all include `guardContent`, audio blocks, and `searchResult` blocks.
|
|
729
|
+
- The serialized payload includes `modelId` as a convenience; the Converse REST endpoint takes the model id in the URL path (`/model/{modelId}/converse`), and AWS REST-JSON services ignore unrecognized body members. Remove it from the body if you prefer a strictly minimal payload.
|
|
729
730
|
|
|
730
731
|
Response-side limitations:
|
|
731
732
|
|
|
732
733
|
- Unknown content block keys (e.g. `citationsContent`, `guardContent`) are silently skipped.
|
|
733
734
|
- `metrics.latencyMs`, `trace` (guardrail and prompt-router trace events), `additionalModelResponseFields`, `performanceConfig`, and the raw `serviceTier` object are exposed on `response.provider_data`. `response.service_tier` is also populated from `serviceTier.type`.
|
|
734
|
-
- `stopReason` mappings: `end_turn` / `tool_use` / `stop_sequence` → `completed`, `max_tokens` / `model_context_window_exceeded` → `incomplete`, `guardrail_intervened` / `content_filtered` / `malformed_model_output` / `malformed_tool_use` → `failed`. Unlike the Messages serializer, the matched stop sequence text is not surfaced separately because Converse does not echo it back unless you request provider-specific fields through `additionalModelResponseFieldPaths
|
|
735
|
-
- `usage.cacheReadInputTokens` and `usage.cacheWriteInputTokens` populate `response.usage.input_tokens_details["cached_tokens"]` and `["cache_creation_input_tokens"]`. Cache writes
|
|
735
|
+
- `stopReason` mappings: `end_turn` / `tool_use` / `stop_sequence` → `completed`, `max_tokens` / `model_context_window_exceeded` → `incomplete`, `guardrail_intervened` / `content_filtered` / `malformed_model_output` / `malformed_tool_use` → `failed`. Unlike the Messages serializer, the matched stop sequence text is not surfaced separately because Converse does not echo it back unless you request provider-specific fields through `additionalModelResponseFieldPaths` (available via the `additional_model_response_field_paths` session extra; the requested fields appear on `response.provider_data`).
|
|
736
|
+
- `usage.cacheReadInputTokens` and `usage.cacheWriteInputTokens` populate `response.usage.input_tokens_details["cached_tokens"]` and `["cache_creation_input_tokens"]`. Cache writes require `cachePoint` markers in the request, which are emitted via the `cache_point` content extra.
|
|
736
737
|
|
|
737
738
|
## Installation
|
|
738
739
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.2
|
|
@@ -125,6 +125,14 @@ module PromptBuilder
|
|
|
125
125
|
FIELDS.each { |f| attr_reader f }
|
|
126
126
|
attr_reader :text_config, :output, :usage, :extra
|
|
127
127
|
|
|
128
|
+
# @!method provider_data
|
|
129
|
+
# Provider-specific response metadata with no canonical Open Responses
|
|
130
|
+
# slot (e.g. Gemini grounding metadata, Chat Completions system_fingerprint).
|
|
131
|
+
# Alias for +extra+.
|
|
132
|
+
#
|
|
133
|
+
# @return [Hash, nil]
|
|
134
|
+
alias_method :provider_data, :extra
|
|
135
|
+
|
|
128
136
|
BOOLEAN_FIELDS.each { |f| alias_method("#{f}?", f) }
|
|
129
137
|
|
|
130
138
|
# Create a new Response.
|
|
@@ -31,6 +31,22 @@ module PromptBuilder
|
|
|
31
31
|
def deserialize_response(_hash)
|
|
32
32
|
raise NotImplementedError
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
# Ensure a response payload is a Hash containing the key that identifies
|
|
36
|
+
# it as a well-formed response for this format. Raised before parsing so
|
|
37
|
+
# malformed bodies (e.g. provider error envelopes) fail loudly with the
|
|
38
|
+
# offending body rather than producing a silently empty Response.
|
|
39
|
+
#
|
|
40
|
+
# @param hash [Object] the raw response payload
|
|
41
|
+
# @param key [String] a key that must be present in a valid payload
|
|
42
|
+
# @raise [UnexpectedPayloadError] if the key is missing
|
|
43
|
+
def require_response_key!(hash, key)
|
|
44
|
+
return if hash.is_a?(Hash) && hash.key?(key)
|
|
45
|
+
|
|
46
|
+
body = JSON.generate(hash)[0..200]
|
|
47
|
+
raise UnexpectedPayloadError,
|
|
48
|
+
"unexpected response payload, missing #{key.inspect}: #{body}"
|
|
49
|
+
end
|
|
34
50
|
end
|
|
35
51
|
end
|
|
36
52
|
end
|
|
@@ -228,12 +228,16 @@ module PromptBuilder
|
|
|
228
228
|
|
|
229
229
|
# Only text content is supported in tool output; other content types
|
|
230
230
|
# are silently omitted.
|
|
231
|
-
output.filter_map do |content|
|
|
231
|
+
content = output.filter_map do |content|
|
|
232
232
|
case content
|
|
233
233
|
when Content::InputText, Content::OutputText
|
|
234
234
|
serialize_text_content(content)
|
|
235
235
|
end
|
|
236
236
|
end
|
|
237
|
+
|
|
238
|
+
# Chat Completions rejects tool messages with an empty content
|
|
239
|
+
# array; collapse to an empty string like the other serializers.
|
|
240
|
+
content.empty? ? "" : content
|
|
237
241
|
end
|
|
238
242
|
|
|
239
243
|
def flush_tool_calls!(messages, pending_tool_calls, last_assistant_msg = nil)
|
|
@@ -36,6 +36,10 @@ module PromptBuilder
|
|
|
36
36
|
content: [Content::RefusalContent.new(refusal: message["refusal"])]
|
|
37
37
|
)
|
|
38
38
|
elsif message["content"].is_a?(Array)
|
|
39
|
+
# logprobs and annotations are reported once per message, so
|
|
40
|
+
# attach them to the first text block only to avoid duplicating
|
|
41
|
+
# them across blocks on round-trip.
|
|
42
|
+
first_text_block = true
|
|
39
43
|
contents = message["content"].each_with_object([]) do |block, acc|
|
|
40
44
|
case block["type"]
|
|
41
45
|
when "text", nil
|
|
@@ -44,9 +48,10 @@ module PromptBuilder
|
|
|
44
48
|
|
|
45
49
|
acc << Content::OutputText.new(
|
|
46
50
|
text: text,
|
|
47
|
-
logprobs: logprobs_content,
|
|
48
|
-
annotations: annotations
|
|
51
|
+
logprobs: first_text_block ? logprobs_content : [],
|
|
52
|
+
annotations: first_text_block ? annotations : []
|
|
49
53
|
)
|
|
54
|
+
first_text_block = false
|
|
50
55
|
when "refusal"
|
|
51
56
|
acc << Content::RefusalContent.new(refusal: block["refusal"]) if block["refusal"]
|
|
52
57
|
else
|
|
@@ -109,6 +114,8 @@ module PromptBuilder
|
|
|
109
114
|
if hash["object"] == "chat.completion.chunk"
|
|
110
115
|
raise UnsupportedFormatError, "Chat Completions streaming chunks are not supported"
|
|
111
116
|
end
|
|
117
|
+
require_response_key!(hash, "choices")
|
|
118
|
+
require_response_key!(hash, "model")
|
|
112
119
|
# Responses with multiple choices have no canonical multi-candidate
|
|
113
120
|
# representation; only the first choice is parsed (handled by the
|
|
114
121
|
# caller using choices[0]).
|
|
@@ -55,15 +55,17 @@ module PromptBuilder
|
|
|
55
55
|
# === Features in Converse not available through Open Responses
|
|
56
56
|
#
|
|
57
57
|
# The following Converse parameters cannot be set through the Open Responses
|
|
58
|
-
# canonical format:
|
|
59
|
-
# - +stopSequences+ — custom stop sequences
|
|
60
|
-
# - Guardrail policies (+
|
|
61
|
-
# - Per-model passthrough fields (+
|
|
62
|
-
# - Requested provider response fields (+
|
|
63
|
-
# -
|
|
64
|
-
# -
|
|
65
|
-
#
|
|
66
|
-
#
|
|
58
|
+
# canonical format but are recognized via session +extra+ keys:
|
|
59
|
+
# - +stopSequences+ — custom stop sequences (+stop_sequences+)
|
|
60
|
+
# - Guardrail policies (+guardrail_config+)
|
|
61
|
+
# - Per-model passthrough fields (+additional_model_request_fields+)
|
|
62
|
+
# - Requested provider response fields (+additional_model_response_field_paths+)
|
|
63
|
+
# - +performanceConfig+ latency settings (+performance_config+)
|
|
64
|
+
# - Prompt management variables (+prompt_variables+)
|
|
65
|
+
#
|
|
66
|
+
# Prompt caching markers (+cachePoint+) are emitted via the +cache_point+
|
|
67
|
+
# content extra. Cross-region routing via inference profiles is selected
|
|
68
|
+
# through the model id and needs no request field.
|
|
67
69
|
class Request < Base
|
|
68
70
|
IMAGE_MEDIA_TYPE_FORMATS = {
|
|
69
71
|
"image/jpeg" => "jpeg",
|
|
@@ -11,6 +11,9 @@ module PromptBuilder
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def deserialize_response(hash)
|
|
14
|
+
require_response_key!(hash, "output")
|
|
15
|
+
require_response_key!(hash, "stopReason")
|
|
16
|
+
|
|
14
17
|
usage_hash = hash["usage"]
|
|
15
18
|
usage = if usage_hash
|
|
16
19
|
cache_read = usage_hash["cacheReadInputTokens"]
|
|
@@ -64,6 +64,9 @@ module PromptBuilder
|
|
|
64
64
|
private
|
|
65
65
|
|
|
66
66
|
def deserialize_response(hash)
|
|
67
|
+
require_response_key!(hash, "candidates")
|
|
68
|
+
require_response_key!(hash, "modelVersion")
|
|
69
|
+
|
|
67
70
|
usage = build_usage(hash["usageMetadata"])
|
|
68
71
|
|
|
69
72
|
# Only the first candidate is parsed; additional candidates have no
|
|
@@ -71,12 +71,22 @@ module PromptBuilder
|
|
|
71
71
|
# - +anthropic-beta+ headers and API versioning (this gem produces no HTTP
|
|
72
72
|
# request — set headers in your HTTP client)
|
|
73
73
|
class Request < Base
|
|
74
|
-
DEFAULT_MAX_TOKENS = 4096
|
|
75
74
|
SUPPORTED_METADATA_KEYS = ["user_id"].freeze
|
|
76
75
|
EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"].freeze
|
|
77
76
|
SUPPORTED_THINKING_TYPES = ["adaptive", "disabled", "enabled"].freeze
|
|
78
77
|
SUPPORTED_TOOL_CHOICE_TYPES = ["any", "auto", "none", "tool"].freeze
|
|
79
78
|
|
|
79
|
+
# Citation types accepted by the Messages API on text blocks. Annotations
|
|
80
|
+
# parsed from other providers (e.g. Chat Completions url_citation) have
|
|
81
|
+
# no Anthropic equivalent and are silently dropped.
|
|
82
|
+
SUPPORTED_CITATION_TYPES = [
|
|
83
|
+
"char_location",
|
|
84
|
+
"content_block_location",
|
|
85
|
+
"page_location",
|
|
86
|
+
"search_result_location",
|
|
87
|
+
"web_search_result_location"
|
|
88
|
+
].freeze
|
|
89
|
+
|
|
80
90
|
class << self
|
|
81
91
|
private
|
|
82
92
|
|
|
@@ -85,7 +95,7 @@ module PromptBuilder
|
|
|
85
95
|
raise UnsupportedFormatError, "Messages format requires session.model" unless session.model
|
|
86
96
|
|
|
87
97
|
h["model"] = session.model
|
|
88
|
-
h["max_tokens"] = session.max_output_tokens
|
|
98
|
+
h["max_tokens"] = session.max_output_tokens if session.max_output_tokens
|
|
89
99
|
h["temperature"] = session.temperature if session.temperature
|
|
90
100
|
h["top_p"] = session.top_p if session.top_p
|
|
91
101
|
effective_metadata = build_effective_metadata(session)
|
|
@@ -341,7 +351,8 @@ module PromptBuilder
|
|
|
341
351
|
{"type" => "text", "text" => content.text}
|
|
342
352
|
when Content::OutputText
|
|
343
353
|
text = {"type" => "text", "text" => content.text}
|
|
344
|
-
|
|
354
|
+
citations = serialize_citations(content.annotations)
|
|
355
|
+
text["citations"] = citations unless citations.empty?
|
|
345
356
|
text
|
|
346
357
|
when Content::InputImage
|
|
347
358
|
# Assistant image content is not supported; omit it.
|
|
@@ -425,6 +436,16 @@ module PromptBuilder
|
|
|
425
436
|
end
|
|
426
437
|
end
|
|
427
438
|
|
|
439
|
+
# Only annotations that are valid Anthropic citation objects can be
|
|
440
|
+
# replayed as citations. Annotations from other providers (e.g. a
|
|
441
|
+
# Chat Completions url_citation) are silently dropped so a
|
|
442
|
+
# cross-provider session history doesn't produce an invalid request.
|
|
443
|
+
def serialize_citations(annotations)
|
|
444
|
+
annotations.select do |annotation|
|
|
445
|
+
annotation.is_a?(Hash) && SUPPORTED_CITATION_TYPES.include?(annotation["type"])
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
428
449
|
# FunctionCallOutput.status values that map to tool_result.is_error.
|
|
429
450
|
# "incomplete" and "failed" cover the OR canonical statuses; "error"
|
|
430
451
|
# is accepted as a convenience alias.
|
|
@@ -5,6 +5,15 @@ module PromptBuilder
|
|
|
5
5
|
class OpenResponses < Base
|
|
6
6
|
# Request serializer for the OpenAI Open Responses API format.
|
|
7
7
|
class Request < Base
|
|
8
|
+
# Content extra keys that are part of the Open Responses API schema and
|
|
9
|
+
# must survive extra-stripping (the canonical content classes have no
|
|
10
|
+
# dedicated attribute for them).
|
|
11
|
+
CANONICAL_EXTRA_KEYS = {
|
|
12
|
+
"input_file" => ["file_id"].freeze,
|
|
13
|
+
"input_image" => ["file_id"].freeze
|
|
14
|
+
}.freeze
|
|
15
|
+
private_constant :CANONICAL_EXTRA_KEYS
|
|
16
|
+
|
|
8
17
|
class << self
|
|
9
18
|
# Export a session to Open Responses API request payload.
|
|
10
19
|
#
|
|
@@ -12,9 +21,9 @@ module PromptBuilder
|
|
|
12
21
|
# @return [Hash] the serialized request payload
|
|
13
22
|
def request_payload(session)
|
|
14
23
|
payload = session.to_h
|
|
15
|
-
apply_server_state!(payload, session)
|
|
24
|
+
items = apply_server_state!(payload, session)
|
|
16
25
|
payload.delete("extra")
|
|
17
|
-
strip_extra(payload)
|
|
26
|
+
strip_extra(payload, items, session.tool_definitions)
|
|
18
27
|
normalize_content_urls!(payload)
|
|
19
28
|
strip_non_replayable_reasoning!(payload)
|
|
20
29
|
strip_output_only_fields!(payload)
|
|
@@ -28,36 +37,78 @@ module PromptBuilder
|
|
|
28
37
|
# When the session is in server-state mode, replace the full input
|
|
29
38
|
# array with only items added after the last response boundary and
|
|
30
39
|
# include previous_response_id for the API to resolve history.
|
|
40
|
+
# Returns the item objects backing the payload's input array.
|
|
31
41
|
def apply_server_state!(payload, session)
|
|
32
|
-
return if session.local_state?
|
|
42
|
+
return session.items if session.local_state?
|
|
33
43
|
|
|
34
44
|
payload.delete("input")
|
|
35
|
-
new_items = session.items[session.response_boundary_index..]
|
|
36
|
-
payload["input"] = new_items.map(&:to_h)
|
|
45
|
+
new_items = session.items[session.response_boundary_index..] || []
|
|
46
|
+
payload["input"] = new_items.map(&:to_h) unless new_items.empty?
|
|
47
|
+
new_items
|
|
37
48
|
end
|
|
38
49
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
|
|
50
|
+
# Remove provider-specific extra data from items, content blocks, and
|
|
51
|
+
# tool definitions since it is not part of the Open Responses API
|
|
52
|
+
# schema. Items, content blocks, and tool definitions serialize their
|
|
53
|
+
# extras flat into the hash, so the extra keys must be looked up on
|
|
54
|
+
# the backing objects rather than removed as a nested "extra" key.
|
|
55
|
+
def strip_extra(payload, items, tool_definitions)
|
|
43
56
|
input = payload["input"]
|
|
44
57
|
if input.is_a?(Array)
|
|
45
|
-
input.
|
|
46
|
-
next unless
|
|
58
|
+
input.each_with_index do |item_hash, index|
|
|
59
|
+
next unless item_hash.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
item = items[index]
|
|
62
|
+
next unless item
|
|
47
63
|
|
|
48
|
-
item.
|
|
64
|
+
strip_extra_keys!(item_hash, item.extra)
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
strip_extra_from_blocks!(
|
|
66
|
+
case item
|
|
67
|
+
when Items::Message
|
|
68
|
+
strip_extra_from_blocks!(item_hash["content"], item.content)
|
|
69
|
+
when Items::FunctionCallOutput
|
|
70
|
+
strip_extra_from_blocks!(item_hash["output"], item.output) if item.output.is_a?(Array)
|
|
55
71
|
end
|
|
56
72
|
end
|
|
57
73
|
end
|
|
58
74
|
|
|
59
75
|
tools = payload["tools"]
|
|
60
|
-
|
|
76
|
+
if tools.is_a?(Array)
|
|
77
|
+
tools.each_with_index do |tool_hash, index|
|
|
78
|
+
next unless tool_hash.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
definition = tool_definitions[index]
|
|
81
|
+
strip_extra_keys!(tool_hash, definition.extra) if definition
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def strip_extra_from_blocks!(blocks, contents)
|
|
87
|
+
return unless blocks.is_a?(Array)
|
|
88
|
+
|
|
89
|
+
blocks.each_with_index do |block, index|
|
|
90
|
+
next unless block.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
content = contents[index]
|
|
93
|
+
next unless content.respond_to?(:extra)
|
|
94
|
+
|
|
95
|
+
keep = CANONICAL_EXTRA_KEYS.fetch(block["type"], [])
|
|
96
|
+
strip_extra_keys!(block, content.extra, keep: keep)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def strip_extra_keys!(hash, extra, keep: [])
|
|
101
|
+
return unless extra.is_a?(Hash)
|
|
102
|
+
|
|
103
|
+
extra.each_key do |key|
|
|
104
|
+
# "type" is canonical on every item and content block; an extra
|
|
105
|
+
# named "type" never survives serialization, so deleting it here
|
|
106
|
+
# would corrupt the block.
|
|
107
|
+
next if key == "type"
|
|
108
|
+
next if keep.include?(key)
|
|
109
|
+
|
|
110
|
+
hash.delete(key)
|
|
111
|
+
end
|
|
61
112
|
end
|
|
62
113
|
|
|
63
114
|
# Convert canonical "url" keys back to Open Responses API keys:
|
|
@@ -201,14 +252,6 @@ module PromptBuilder
|
|
|
201
252
|
normalized.empty? ? nil : normalized
|
|
202
253
|
end
|
|
203
254
|
|
|
204
|
-
def strip_extra_from_blocks!(blocks)
|
|
205
|
-
blocks.each do |block|
|
|
206
|
-
next unless block.is_a?(Hash)
|
|
207
|
-
|
|
208
|
-
block.delete("extra")
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
255
|
# Normalize text.format for the Responses API. If the format uses
|
|
213
256
|
# the Chat Completions nested json_schema sub-object, flatten it
|
|
214
257
|
# so name/schema/strict/description sit directly under format.
|
|
@@ -210,10 +210,10 @@ module PromptBuilder
|
|
|
210
210
|
# Add a tool call output to the conversation.
|
|
211
211
|
#
|
|
212
212
|
# @param call_id [String] the tool call identifier
|
|
213
|
-
# @param result [String,
|
|
214
|
-
# @return [Items::
|
|
213
|
+
# @param result [String, Array<Content::Base, Hash>, nil] the tool call result
|
|
214
|
+
# @return [Items::FunctionCallOutput] the added function call output item
|
|
215
215
|
def add_function_call_output(call_id:, result:)
|
|
216
|
-
add_item(Items::
|
|
216
|
+
add_item(Items::FunctionCallOutput.new(call_id: call_id, output: result))
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
# Add a raw item to the conversation.
|
data/lib/prompt_builder.rb
CHANGED
|
@@ -44,14 +44,16 @@ module PromptBuilder
|
|
|
44
44
|
"data:#{content_type};base64,#{[data].pack("m0")}"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# Parse a data URL into its media type and base64-encoded data.
|
|
47
|
+
# Parse a data URL into its media type and base64-encoded data. Media type
|
|
48
|
+
# parameters (e.g. "data:text/plain;charset=utf-8;base64,...") are allowed
|
|
49
|
+
# and excluded from the returned media type.
|
|
48
50
|
#
|
|
49
51
|
# @param url [String, nil] a URL that may be a data URL
|
|
50
52
|
# @return [Array(String, String), nil] a two-element array of +[media_type, data]+
|
|
51
|
-
# or nil if the URL is not a data URL
|
|
53
|
+
# or nil if the URL is not a base64-encoded data URL
|
|
52
54
|
def parse_data_url(url)
|
|
53
55
|
return nil unless url
|
|
54
|
-
match = url.match(/\Adata:([
|
|
56
|
+
match = url.match(/\Adata:([^;,]+)(?:;[^,]*)?;base64,(.*)\z/m)
|
|
55
57
|
return nil unless match
|
|
56
58
|
[match[1], match[2]]
|
|
57
59
|
end
|