Skip to content

fix: Responses API native content types in adapter - #19

Merged
callingmedic911 merged 7 commits into
mainfrom
aditya/fix-responses-api-content-types
Mar 11, 2026
Merged

fix: Responses API native content types in adapter#19
callingmedic911 merged 7 commits into
mainfrom
aditya/fix-responses-api-content-types

Conversation

@callingmedic911

@callingmedic911 callingmedic911 commented Mar 10, 2026

Copy link
Copy Markdown
Member

Summary

  • OpenAIResponsesAdapter now produces Responses API native content types (input_text, input_image, input_file) instead of inheriting Chat Completions types (text, image_url, file) from OpenAIAdapter
  • OpenAI rejects Chat Completions content block types when sent via the Responses API input parameter
  • Bumps version to 0.5.11

Note

Fix OpenAIResponsesAdapter to use Responses API native content types

  • Replaces Chat Completions content types (text, image_url, file) with Responses API native types (input_text, input_image, input_file) in adapters.py.
  • Adds a _map_responses_content static helper that converts internal content representations to the correct Responses API shapes, deriving filenames from slot_name and content_type for file uploads.
  • Risk: audio content and non-image MediaContentUrl items now raise ValueError instead of being passed through.

Macroscope summarized 4edac8f.

Summary by CodeRabbit

  • Bug Fixes

    • Content blocks now use native Responses-format types for text, images, and files (input_text, input_image, input_file), improving handling and correctness for image and base64 file media.
  • Tests

    • Updated test suite to validate the new Responses-format content and the expanded media handling, including image URLs and base64-encoded file cases.

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adapter mapping changed to emit OpenAI Responses API native content types (input_text, input_image, input_file); system-role messages are skipped and media content is normalized via a new mapping helper. Tests and changelog updated accordingly.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added note: openai_responses adapter now emits Responses API native content types.
Core Adapter Logic
src/freeplay/resources/adapters.py
Refactored OpenAIResponsesAdapter (now inheriting LLMAdapter) to skip system roles, wrap messages as message objects, handle media vs non-media uniformly, and added _map_responses_content to convert TextContent / MediaContentUrl / MediaContentBase64 into input_text, input_image, or input_file; raises on unsupported audio or non-image URLs.
Tests
tests/test_adapters.py
Updated expectations to use input_text/input_image and added/adjusted tests for base64 file/media mapping to input_file and input_image formats.

Sequence Diagram(s)

(omitted — change is a mapping/refactor within the adapter and does not add multi-component sequential flow requiring visualization)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • BrianNewsom

Poem

🐰 I hopped through bytes and mapped each part,
Turning text and images into Responses art.
input_text, input_image, input_file in a row,
I nibbled old shapes and taught them to glow.
A little rabbit dance for the adapter's new flow. 🥕

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: Responses API native content types in adapter' clearly and specifically describes the main change: the OpenAIResponsesAdapter now uses Responses API native content types instead of Chat Completions types.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch aditya/fix-responses-api-content-types

Comment @coderabbitai help to get the list of available commands and usage tips.

Content blocks now use input_text, input_image, input_file instead of
Chat Completions types (text, image_url, file) which OpenAI rejects.
@callingmedic911
callingmedic911 force-pushed the aditya/fix-responses-api-content-types branch from 6f3f72a to 21a2be0 Compare March 10, 2026 19:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/freeplay/resources/adapters.py (1)

385-395: Remove unnecessary isinstance check flagged by pyright.

After the preceding checks for TextContent, dict, content.type == "audio", and MediaContentUrl, the only remaining possibility is MediaContentBase64. The isinstance(content, MediaContentBase64) check is always true at this point.

♻️ Proposed refactor
-        if isinstance(content, MediaContentBase64):
-            if content.type == "file":
-                return {
-                    "type": "input_file",
-                    "filename": f"{content.slot_name}.{content.content_type.split('/')[-1]}",
-                    "file_data": f"data:{content.content_type};base64,{content.data}",
-                }
-            return {
-                "type": "input_image",
-                "image_url": f"data:{content.content_type};base64,{content.data}",
-            }
-        raise ValueError(f"Unexpected content type {type(content)}")
+        # At this point, content must be MediaContentBase64
+        if content.type == "file":
+            return {
+                "type": "input_file",
+                "filename": f"{content.slot_name}.{content.content_type.split('/')[-1]}",
+                "file_data": f"data:{content.content_type};base64,{content.data}",
+            }
+        return {
+            "type": "input_image",
+            "image_url": f"data:{content.content_type};base64,{content.data}",
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/freeplay/resources/adapters.py` around lines 385 - 395, The
isinstance(content, MediaContentBase64) guard is redundant because earlier
branches (TextContent, dict, content.type == "audio", MediaContentUrl) already
exclude other types; remove the isinstance(...) check and unnest its body so the
code directly handles the MediaContentBase64 case, keeping the existing branches
that return the same dictionaries (the "input_file" branch when content.type ==
"file" and the "input_image" branch otherwise) and preserving the filename and
data construction logic referencing content.slot_name and content.content_type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/freeplay/resources/adapters.py`:
- Around line 359-364: The conditional around appending to result is redundant:
both branches call result.append({"type": "message", **msg}), so remove the
if/else and perform a single append; in addition, if you intended to guard by
content's element types to satisfy the type checker, replace the untyped
all(...) comprehension with a typed check (e.g., annotate or cast content to
Sequence[Any] before the all(...) call, or narrow content's type to
Sequence[Mapping] earlier) so the TypeChecker knows what 'item' is—apply these
changes where content, result, and msg are used in this adapter function.
- Around line 373-374: The handler accepts a raw dict at runtime but the
function's type hint for the parameter content is declared as Union[TextContent,
MediaContentBase64, MediaContentUrl]; update the type signature to reflect the
passthrough by adding Dict[str, Any] (or Mapping[str, Any]) to the Union for the
content parameter, and keep the existing `if isinstance(content, dict): return
content` branch, or if dicts should be unreachable remove that branch and
validate/convert incoming dicts instead; target the function that declares the
content parameter and the `if isinstance(content, dict)` branch to make the
types consistent.

---

Nitpick comments:
In `@src/freeplay/resources/adapters.py`:
- Around line 385-395: The isinstance(content, MediaContentBase64) guard is
redundant because earlier branches (TextContent, dict, content.type == "audio",
MediaContentUrl) already exclude other types; remove the isinstance(...) check
and unnest its body so the code directly handles the MediaContentBase64 case,
keeping the existing branches that return the same dictionaries (the
"input_file" branch when content.type == "file" and the "input_image" branch
otherwise) and preserving the filename and data construction logic referencing
content.slot_name and content.content_type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21936091-1df0-4782-984a-d81b77d37b28

📥 Commits

Reviewing files that changed from the base of the PR and between b3d481e and 21a2be0.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/freeplay/resources/adapters.py
  • tests/test_adapters.py

Comment thread src/freeplay/resources/adapters.py Outdated
Comment thread src/freeplay/resources/adapters.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/freeplay/resources/adapters.py (1)

343-343: Consider using dict.get for cleaner key access.

Static analysis flags this as unnecessarily verbose.

♻️ Proposed refactor
-            if "has_media" in message and message["has_media"]:
+            if message.get("has_media"):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/freeplay/resources/adapters.py` at line 343, Replace the verbose key
check for the message dict with a get-based truthy check: in the block that
currently uses "if 'has_media' in message and message['has_media']:" (the code
handling the incoming message variable named message), use
message.get('has_media') to simplify and make the intent clearer; ensure the
truthiness semantics remain the same so falsy or missing values are handled
identically.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/freeplay/resources/adapters.py`:
- Around line 384-388: The return that builds the "input_image" dict is
currently unconditional and its closing brace is over-indented, making the
subsequent raise ValueError unreachable and causing Ruff to fail; fix this by
making the image branch conditional (e.g., if content is the expected image type
or matches content.content_type), ensure the closing brace alignment uses
8-space indentation to match the surrounding block, and move the raise
ValueError into the function's fallback path (after the conditional branches) so
it executes only when no handled content type matches; refer to the return that
produces {"type": "input_image", "image_url":
f"data:{content.content_type};base64,{content.data}"} and the raise
ValueError(f"Unexpected content type {type(content)}") to locate the lines to
change.

---

Nitpick comments:
In `@src/freeplay/resources/adapters.py`:
- Line 343: Replace the verbose key check for the message dict with a get-based
truthy check: in the block that currently uses "if 'has_media' in message and
message['has_media']:" (the code handling the incoming message variable named
message), use message.get('has_media') to simplify and make the intent clearer;
ensure the truthiness semantics remain the same so falsy or missing values are
handled identically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 764fe4c1-9d64-42b9-9509-94ac6fe13201

📥 Commits

Reviewing files that changed from the base of the PR and between 21a2be0 and ed18b0c.

📒 Files selected for processing (1)
  • src/freeplay/resources/adapters.py

Comment thread src/freeplay/resources/adapters.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/freeplay/resources/adapters.py (1)

343-343: Consider simplifying the key check with dict.get.

The pattern "has_media" in message and message["has_media"] can be simplified to message.get("has_media"), which returns None (falsy) if the key is missing.

♻️ Suggested simplification
-            if "has_media" in message and message["has_media"]:
+            if message.get("has_media"):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/freeplay/resources/adapters.py` at line 343, Replace the explicit key
membership check and indexing ("has_media" in message and message["has_media"])
with the simpler truthy lookup message.get("has_media") to achieve the same
behavior when the key is missing; locate the occurrence that uses the variable
message in src/freeplay/resources/adapters.py and update that conditional to use
message.get("has_media") so it remains falsy when absent or false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/freeplay/resources/adapters.py`:
- Around line 367-370: The code currently rejects audio inputs but allows video
passed as MediaContentBase64 to fall through and be treated as input_image;
update the same check that inspects content.type to also raise a ValueError for
"video" (e.g., if content.type == "audio" or content.type == "video": raise
ValueError("Video content is not supported by the Responses API") ), so any
MediaContentBase64 with content.type "video" is consistently rejected before it
reaches the input_image handling.

---

Nitpick comments:
In `@src/freeplay/resources/adapters.py`:
- Line 343: Replace the explicit key membership check and indexing ("has_media"
in message and message["has_media"]) with the simpler truthy lookup
message.get("has_media") to achieve the same behavior when the key is missing;
locate the occurrence that uses the variable message in
src/freeplay/resources/adapters.py and update that conditional to use
message.get("has_media") so it remains falsy when absent or false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b9a4effb-5d1c-4ec6-879e-a345471f30a6

📥 Commits

Reviewing files that changed from the base of the PR and between ed18b0c and 84426c0.

📒 Files selected for processing (1)
  • src/freeplay/resources/adapters.py

Comment thread src/freeplay/resources/adapters.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/freeplay/resources/adapters.py`:
- Around line 341-342: The loop is discarding valid system messages
(role_support declares "system") by using `if message["role"] == "system":
continue`; instead collect system-role content and merge it into the prompt
instructions pipeline so system instructions are preserved. Replace the
`continue` with logic that appends `message["content"]` (or the full `message`)
to the same top-level `instructions`/`system_instructions` variable used for
prompt assembly (or create `system_instructions` and later prepend/merge it into
`instructions` before request send), ensuring existing handling for `developer`
and other roles remains unchanged and any downstream code that reads
`instructions` receives the system text.
- Around line 355-356: OpenAIResponsesAdapter currently lists "tool" in
role_support but later serializes messages as {"type": "message", **msg} (see
the msg = copy.deepcopy(message) / result.append(...) block), which yields
invalid OpenAI Conversations API payloads; fix by removing "tool" from the
adapter's role_support OR add explicit handling for messages with role == "tool"
in the message serialization path (e.g., skip them, raise a clear error, or
convert them to the proper conversation item type) so that tool messages never
get emitted as {"type":"message","role":"tool",...}; update
OpenAIResponsesAdapter.role_support and/or the serialization before
result.append to handle tool messages accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f9b7f0d-0897-43ee-9ce9-ccd4bb1fcad7

📥 Commits

Reviewing files that changed from the base of the PR and between 84426c0 and 4edac8f.

📒 Files selected for processing (1)
  • src/freeplay/resources/adapters.py

Comment thread src/freeplay/resources/adapters.py
Comment on lines +355 to +356
msg = copy.deepcopy(message)
result.append({"type": "message", **msg})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/freeplay/resources/adapters.py

Repository: freeplayai/freeplay-python

Length of output: 108


🏁 Script executed:

cat -n src/freeplay/resources/adapters.py | sed -n '340,370p'

Repository: freeplayai/freeplay-python

Length of output: 1475


🏁 Script executed:

cat -n src/freeplay/resources/adapters.py | sed -n '1,100p'

Repository: freeplayai/freeplay-python

Length of output: 3623


🏁 Script executed:

cat -n src/freeplay/resources/adapters.py | sed -n '320,360p'

Repository: freeplayai/freeplay-python

Length of output: 1725


🏁 Script executed:

cat -n src/freeplay/resources/adapters.py | sed -n '200,320p'

Repository: freeplayai/freeplay-python

Length of output: 5279


🏁 Script executed:

rg "tool" src/freeplay/resources/adapters.py -i -B 3 -A 3

Repository: freeplayai/freeplay-python

Length of output: 410


🏁 Script executed:

rg "type.*message" src/freeplay/resources/ -i -B 2 -A 2

Repository: freeplayai/freeplay-python

Length of output: 748


🏁 Script executed:

git ls-files | grep -E "(test|spec)" | head -20

Repository: freeplayai/freeplay-python

Length of output: 770


🏁 Script executed:

find tests -name "*.py" -type f | xargs rg "OpenAIResponsesAdapter|tool" -B 3 -A 3

Repository: freeplayai/freeplay-python

Length of output: 35639


🏁 Script executed:

rg "class OpenAI" src/freeplay/resources/adapters.py -A 50 | head -80

Repository: freeplayai/freeplay-python

Length of output: 3023


🏁 Script executed:

cat -n tests/test_adapters.py | grep -A 30 "openai_responses"

Repository: freeplayai/freeplay-python

Length of output: 5007


🏁 Script executed:

cat -n tests/test_adapters.py | grep -B 5 -A 20 "test_openai_responses"

Repository: freeplayai/freeplay-python

Length of output: 4048


🏁 Script executed:

web_search
OpenAI Conversations API Responses create-item schema allowed roles message type

Repository: freeplayai/freeplay-python

Length of output: 165


🌐 Web query:

OpenAI Conversations API Responses message schema roles allowed types

💡 Result:

For the Responses API / Conversations-compat message items, the schema is:

  • Item shape: {"type":"message","role":..., "content":...} (with optional phase on assistant messages). Roles are used for instruction hierarchy. Allowed role values: user, assistant, system, developer. [1]
  • content: either a string or an array of content parts. [1]
  • Allowed input content-part types (in content[]):
    • input_text [2]
    • input_image [2]
    • input_file [3]
  • Common assistant output content-part type:
    • output_text [4]

Sources: [1] [2] [3] [4]


Remove tool from supported roles or implement separate handling for tool responses.

The OpenAIResponsesAdapter declares "tool" as a supported role (line 332), but the OpenAI Conversations API only accepts user, assistant, system, and developer roles in message items. Tool messages must be sent as separate conversation item types. If a tool message reaches lines 355-356, it will serialize as {"type": "message", "role": "tool", ...}, producing an invalid API payload.

Either remove "tool" from role_support or add logic to handle tool messages separately (e.g., skip them, raise an error, or transform them to the appropriate conversation item type for the API).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/freeplay/resources/adapters.py` around lines 355 - 356,
OpenAIResponsesAdapter currently lists "tool" in role_support but later
serializes messages as {"type": "message", **msg} (see the msg =
copy.deepcopy(message) / result.append(...) block), which yields invalid OpenAI
Conversations API payloads; fix by removing "tool" from the adapter's
role_support OR add explicit handling for messages with role == "tool" in the
message serialization path (e.g., skip them, raise a clear error, or convert
them to the proper conversation item type) so that tool messages never get
emitted as {"type":"message","role":"tool",...}; update
OpenAIResponsesAdapter.role_support and/or the serialization before
result.append to handle tool messages accordingly.

Comment thread src/freeplay/resources/adapters.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/freeplay/resources/adapters.py (1)

343-343: Simplify dict access pattern.

The key existence check followed by access can be replaced with dict.get() for cleaner code.

♻️ Proposed fix
-            if "has_media" in message and message["has_media"]:
+            if message.get("has_media"):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/freeplay/resources/adapters.py` at line 343, Replace the explicit
key-existence check for the incoming message dict with dict.get to simplify the
condition: change the if that currently reads checking "has_media" in message
and then indexing message["has_media"] to use message.get("has_media") (e.g.,
inside the function/method handling incoming messages where the local variable
message is used) so the condition is shorter and handles missing keys safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/freeplay/resources/adapters.py`:
- Line 343: Replace the explicit key-existence check for the incoming message
dict with dict.get to simplify the condition: change the if that currently reads
checking "has_media" in message and then indexing message["has_media"] to use
message.get("has_media") (e.g., inside the function/method handling incoming
messages where the local variable message is used) so the condition is shorter
and handles missing keys safely.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca2baada-49e5-4eb9-8be8-345852c9e383

📥 Commits

Reviewing files that changed from the base of the PR and between 4edac8f and 3485440.

📒 Files selected for processing (1)
  • src/freeplay/resources/adapters.py

@callingmedic911
callingmedic911 merged commit 4679259 into main Mar 11, 2026
5 checks passed
@callingmedic911
callingmedic911 deleted the aditya/fix-responses-api-content-types branch March 11, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants