Add OpenAI Responses API adapter and developer role support - #17
Conversation
OpenAI's Responses API uses a different message/tool/output schema shape
than Chat Completions. This adds a new flavor so get_formatted(flavor_name=
"openai_responses") returns correctly shaped data:
- OpenAIResponsesAdapter: subclass of OpenAIAdapter that strips system
messages (they go to the instructions param) and wraps each message as
a native Responses API item with {"type": "message", ...}
- Tool schema: flat format (no nested function wrapper)
- Output schema: transforms to {"format": {"type": "json_schema", ...}}
- Metadata fix: dataclasses.replace on prompt_info when flavor is overridden
- Flavor-to-provider mapping: openai_responses -> openai
- Makefile: run-% now sets REQUESTS_CA_BUNDLE via mkcert for local HTTPS
- Updated example to use the new flavor with proper recording format
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an "openai_responses" flavor: new OpenAIResponsesAdapter and tests, prompt formatting and provider-mapping updates to support OpenAI Responses tool/output schemas and message lists, an updated example using the Responses API, and a Makefile change setting REQUESTS_CA_BUNDLE for run targets. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client / Example
participant Prompt as PromptFormatter
participant Adapter as OpenAIResponsesAdapter
participant API as OpenAI Responses API
participant Recorder as Session Recorder
Client->>Prompt: build FormattedPrompt (template, inputs)
Prompt->>Adapter: formatted_prompt.all_messages()
Adapter->>API: send messages + response_params
API-->>Adapter: completion/result
Adapter->>Recorder: record session (all_messages, session_info, call_info)
Recorder-->>Client: completion_id
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
examples/openai_responses_api.py (1)
33-35:dict[str, Any]requires Python 3.9+; rest of codebase usesDict[str, Any].The library code consistently uses
Dictfromtyping. If the project's minimum Python version is < 3.9, this will fail at runtime. Since this is an example file it's lower risk, but worth aligning for consistency.♻️ Proposed fix
-response_params: dict[str, Any] = { +response_params: Dict[str, Any] = {And update the import on line 3:
-from typing import Any +from typing import Any, Dict🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/openai_responses_api.py` around lines 33 - 35, Change the type annotation for response_params from built-in generic syntax to the typing module alias used across the codebase: replace dict[str, Any] with Dict[str, Any], and ensure Dict and Any are imported from typing (update the import at top of examples/openai_responses_api.py to include Dict). Locate the response_params declaration and formatted_prompt.prompt_info.model_parameters usage to confirm the annotation change is applied consistently.src/freeplay/resources/prompts.py (1)
174-184: List-based branching is implicit — could misfire for non-Responses providers.The
isinstance(converted, list)check is used as a proxy for "this is a Responses API output." If any future provider'smodel_dump()orto_dict()returns a list (or if a user passes a list asnew_message), this branch will incorrectly wrapself._messageswith{"type": "message", ...}.Consider keying on the flavor (available via
self.prompt_info.flavor_name) instead of the shape of the converted output.♻️ Sketch
def all_messages(self, new_message: ProviderMessage) -> List[Dict[str, Any]]: converted = convert_provider_message_to_dict(new_message) - if not isinstance(converted, list): - return self._messages + [converted] - # Responses API: output is a list of typed items. Wrap input - # messages as OpenAI Responses message items so the full list - # uses a consistent format for the recording API. - wrapped: List[Dict[str, Any]] = [ - {"type": "message", **m} for m in self._messages - ] - return wrapped + converted + if self.prompt_info.flavor_name == "openai_responses": + wrapped: List[Dict[str, Any]] = [ + {"type": "message", **m} for m in self._messages + ] + if isinstance(converted, list): + return wrapped + converted + return wrapped + [converted] + if not isinstance(converted, list): + return self._messages + [converted] + return self._messages + converted🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/freeplay/resources/prompts.py` around lines 174 - 184, The branching in all_messages currently treats any list-shaped converted value as a Responses API output; change it to test the provider flavor via self.prompt_info.flavor_name (e.g., compare to the Responses flavor string used in your app) instead of using isinstance(converted, list). Specifically, in the all_messages method (and around convert_provider_message_to_dict usage), detect if self.prompt_info.flavor_name indicates the Responses API and only then wrap existing self._messages into {"type":"message", **m} items before concatenating with converted; otherwise, treat converted as a regular item (or list) and append/extend without wrapping. Ensure you still handle both single-dict and list converted values, but use flavor_name as the authoritative switch so non-Responses providers that happen to return lists are not mis-classified.Makefile (1)
29-30:mkcertis now a hard dependency forrun-%.If
mkcertis not installed, this target will fail for all examples, not just the ones that need HTTPS to a local server. Consider guarding this or documenting the requirement.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Makefile` around lines 29 - 30, The Makefile target run-% currently unconditionally calls mkcert to set REQUESTS_CA_BUNDLE which makes mkcert a hard dependency; modify the run-% target so it first checks for mkcert's presence (or document the requirement) and only sets REQUESTS_CA_BUNDLE="$$(mkcert -CAROOT)/rootCA.pem" when mkcert is available, otherwise fall back to running the example without setting REQUESTS_CA_BUNDLE or print a clear error instructing the user to install mkcert; update the run-% target and any associated docs to reflect this conditional behavior.
🤖 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 264-273: The to_llm_syntax override in OpenAIResponsesAdapter
calls super().to_llm_syntax() which is typed as Union[str, List[Dict[str,
Any]]]; narrow that union before treating elements as dicts: assign formatted =
super().to_llm_syntax(messages), then add an explicit runtime/type check (e.g.,
if isinstance(formatted, str): raise TypeError(...) or return an appropriate
conversion) and only iterate when formatted is a List[Dict[str, Any]] so the
unpack {"type": "message", **m} and m["role"] are valid; update the method
OpenAIResponsesAdapter.to_llm_syntax to perform this check and raise a clear
error for the impossible str branch.
---
Nitpick comments:
In `@examples/openai_responses_api.py`:
- Around line 33-35: Change the type annotation for response_params from
built-in generic syntax to the typing module alias used across the codebase:
replace dict[str, Any] with Dict[str, Any], and ensure Dict and Any are imported
from typing (update the import at top of examples/openai_responses_api.py to
include Dict). Locate the response_params declaration and
formatted_prompt.prompt_info.model_parameters usage to confirm the annotation
change is applied consistently.
In `@Makefile`:
- Around line 29-30: The Makefile target run-% currently unconditionally calls
mkcert to set REQUESTS_CA_BUNDLE which makes mkcert a hard dependency; modify
the run-% target so it first checks for mkcert's presence (or document the
requirement) and only sets REQUESTS_CA_BUNDLE="$$(mkcert -CAROOT)/rootCA.pem"
when mkcert is available, otherwise fall back to running the example without
setting REQUESTS_CA_BUNDLE or print a clear error instructing the user to
install mkcert; update the run-% target and any associated docs to reflect this
conditional behavior.
In `@src/freeplay/resources/prompts.py`:
- Around line 174-184: The branching in all_messages currently treats any
list-shaped converted value as a Responses API output; change it to test the
provider flavor via self.prompt_info.flavor_name (e.g., compare to the Responses
flavor string used in your app) instead of using isinstance(converted, list).
Specifically, in the all_messages method (and around
convert_provider_message_to_dict usage), detect if self.prompt_info.flavor_name
indicates the Responses API and only then wrap existing self._messages into
{"type":"message", **m} items before concatenating with converted; otherwise,
treat converted as a regular item (or list) and append/extend without wrapping.
Ensure you still handle both single-dict and list converted values, but use
flavor_name as the authoritative switch so non-Responses providers that happen
to return lists are not mis-classified.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
Makefileexamples/openai_responses_api.pysrc/freeplay/resources/adapters.pysrc/freeplay/resources/prompts.pytests/test_adapters.py
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
examples/openai_responses_api.py (2)
27-31: Gate verbose payload logging behind a debug flag.These prints can expose prompt/system/user content in logs. Prefer conditional debug logging instead of always printing.
Suggested diff
-print(f"Instructions (system): {formatted_prompt.system_content}") -print(f"Input messages: {formatted_prompt.llm_prompt}") -print(f"Tool schema: {formatted_prompt.tool_schema}") -print(f"Output schema: {formatted_prompt.formatted_output_schema}") +if os.environ.get("DEBUG_OPENAI_RESPONSES_EXAMPLE") == "1": + print(f"Instructions (system): {formatted_prompt.system_content}") + print(f"Input messages: {formatted_prompt.llm_prompt}") + print(f"Tool schema: {formatted_prompt.tool_schema}") + print(f"Output schema: {formatted_prompt.formatted_output_schema}")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/openai_responses_api.py` around lines 27 - 31, Replace the unconditional print statements that expose prompt content (formatted_prompt.system_content, formatted_prompt.llm_prompt, formatted_prompt.tool_schema, formatted_prompt.formatted_output_schema) with conditional debug-level logging: check a debug flag or logger.isEnabledFor(logging.DEBUG) and emit these values via logger.debug rather than print, so sensitive prompt/user content is only logged when debug is explicitly enabled.
20-25: Make the Responses flavor explicit in this example.Line 20 currently relies on prompt/template defaults. Since this script is specifically for Responses API, pass
flavor_name="openai_responses"so the payload shape is deterministic.Suggested diff
formatted_prompt = fp_client.prompts.get_formatted( project_id=project_id, template_name="my-openai-prompt", + flavor_name="openai_responses", environment="latest", variables=input_variables, )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/openai_responses_api.py` around lines 20 - 25, The example call to fp_client.prompts.get_formatted (assigning formatted_prompt) should explicitly set the Responses flavor so the returned payload shape is deterministic; update the get_formatted invocation to include flavor_name="openai_responses" alongside project_id, template_name, environment, and variables so the code always retrieves the OpenAI Responses-shaped prompt.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/openai_responses_api.py`:
- Around line 33-47: The splatted response_params may contain reserved keys
(e.g., "model") from formatted_prompt.prompt_info.model_parameters and will
conflict with explicit arguments to openai_client.responses.create; before
constructing response_params remove reserved keys such as "model" (and "input"
if present) from formatted_prompt.prompt_info.model_parameters, then merge the
cleaned dict into response_params so openai_client.responses.create(input=...,
model=..., **response_params) cannot receive duplicate keys.
---
Nitpick comments:
In `@examples/openai_responses_api.py`:
- Around line 27-31: Replace the unconditional print statements that expose
prompt content (formatted_prompt.system_content, formatted_prompt.llm_prompt,
formatted_prompt.tool_schema, formatted_prompt.formatted_output_schema) with
conditional debug-level logging: check a debug flag or
logger.isEnabledFor(logging.DEBUG) and emit these values via logger.debug rather
than print, so sensitive prompt/user content is only logged when debug is
explicitly enabled.
- Around line 20-25: The example call to fp_client.prompts.get_formatted
(assigning formatted_prompt) should explicitly set the Responses flavor so the
returned payload shape is deterministic; update the get_formatted invocation to
include flavor_name="openai_responses" alongside project_id, template_name,
environment, and variables so the code always retrieves the OpenAI
Responses-shaped prompt.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
examples/openai_responses_api.pysrc/freeplay/resources/adapters.py
🚧 Files skipped from review as they are similar to previous changes (1)
- src/freeplay/resources/adapters.py
Instead of re-wrapping raw messages with {"type": "message", ...} in
all_messages, use self._llm_prompt which the adapter already formatted
correctly. This avoids a dict spread ordering bug and keeps the adapter
as the single source of truth for message formatting.
There was a problem hiding this comment.
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/prompts.py`:
- Around line 275-277: Guard the "openai_responses" branch against a missing
json_schema: before building inner = output_schema.get("json_schema", {}), check
that "json_schema" exists and is non-empty on output_schema (referencing the
flavor_name == "openai_responses" branch, the output_schema variable, and
inner), and if it's missing produce a clear failure (e.g., raise a ValueError
with a descriptive message including flavor_name/output_schema) or return an
explicit fallback instead of returning an incomplete {"format": {"type":
"json_schema", **inner}} structure.
- Around line 175-183: The method mixes raw dict messages (self._messages) with
adapter-formatted messages (self._llm_prompt + converted list), causing mixed
payload schemas; fix by normalizing to a single schema: when converted is a
list, map/convert self._messages into the same adapter-formatted shape used by
self._llm_prompt before concatenation (i.e., produce an adapter-formatted base
from self._messages and then return that + converted); use the same conversion
logic or helper that produced _llm_prompt so the concatenation of
list(self._llm_prompt or []) and converted is consistent with items derived from
self._messages, referencing convert_provider_message_to_dict, self._messages,
self._llm_prompt, new_message and converted to locate the code.
| elif flavor_name == "openai_responses": | ||
| inner = output_schema.get("json_schema", {}) | ||
| return {"format": {"type": "json_schema", **inner}} |
There was a problem hiding this comment.
Add a guard for missing json_schema before formatting.
When json_schema is absent, this returns an incomplete structure and fails later in a less actionable place.
💡 Proposed fix
elif flavor_name == "openai_responses":
- inner = output_schema.get("json_schema", {})
+ inner = output_schema.get("json_schema")
+ if not inner:
+ raise FreeplayConfigurationError(
+ "Missing json_schema in output_schema for openai_responses flavor."
+ )
return {"format": {"type": "json_schema", **inner}}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/freeplay/resources/prompts.py` around lines 275 - 277, Guard the
"openai_responses" branch against a missing json_schema: before building inner =
output_schema.get("json_schema", {}), check that "json_schema" exists and is
non-empty on output_schema (referencing the flavor_name == "openai_responses"
branch, the output_schema variable, and inner), and if it's missing produce a
clear failure (e.g., raise a ValueError with a descriptive message including
flavor_name/output_schema) or return an explicit fallback instead of returning
an incomplete {"format": {"type": "json_schema", **inner}} structure.
Always prefer adapter-formatted messages (_llm_prompt) over raw messages for recording. Falls back to _messages for string-format adapters (Llama) where _llm_prompt is None.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/freeplay/resources/prompts.py (1)
276-278:⚠️ Potential issue | 🟡 MinorAdd explicit validation for missing
json_schema.At Line 277, defaulting to
{}can silently build{"format": {"type": "json_schema"}}, which fails later with a less actionable error. This should fail fast with a clear configuration error.💡 Proposed fix
- inner = output_schema.get("json_schema", {}) + inner = output_schema.get("json_schema") + if not inner: + raise FreeplayConfigurationError( + "Missing json_schema in output_schema for openai_responses flavor." + ) return {"format": {"type": "json_schema", **inner}}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/freeplay/resources/prompts.py` around lines 276 - 278, When handling flavor_name == "openai_responses" in the code that reads output_schema, stop defaulting json_schema to {} and instead validate that output_schema contains a non-empty "json_schema" key; in the branch where inner = output_schema.get("json_schema", {}), replace that with an explicit check (e.g., if "json_schema" not in output_schema or not output_schema["json_schema"]: raise ValueError("openai_responses requires a non-empty 'json_schema' in output_schema")) so the function fails fast with a clear error rather than returning {"format": {"type": "json_schema"}} and causing downstream failures.
🤖 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/prompts.py`:
- Around line 175-183: The code uses "self._llm_prompt or self._messages" which
treats an empty list as falsy and incorrectly falls back to raw messages; change
this to explicitly check for None (e.g., use "self._llm_prompt if
self._llm_prompt is not None else self._messages") when computing input_messages
so an intentionally empty _llm_prompt is respected; update the assignment that
builds input_messages (referencing _llm_prompt, _messages and
convert_provider_message_to_dict) to use that None check and preserve the
List[Dict[str, Any]] typing.
---
Duplicate comments:
In `@src/freeplay/resources/prompts.py`:
- Around line 276-278: When handling flavor_name == "openai_responses" in the
code that reads output_schema, stop defaulting json_schema to {} and instead
validate that output_schema contains a non-empty "json_schema" key; in the
branch where inner = output_schema.get("json_schema", {}), replace that with an
explicit check (e.g., if "json_schema" not in output_schema or not
output_schema["json_schema"]: raise ValueError("openai_responses requires a
non-empty 'json_schema' in output_schema")) so the function fails fast with a
clear error rather than returning {"format": {"type": "json_schema"}} and
causing downstream failures.
The developer role (higher-precedence system instructions from OpenAI) is now accepted in prompt templates. For openai_responses the role passes through natively; for all other flavors it is coerced to system in BoundPrompt.format() with a warning, so individual adapters need no changes.
The all_messages method now uses llm_prompt (adapter-formatted) which for Anthropic strips system messages. Tests were still expecting the old count that included system. Adjust to expect 3 instead of 4.
| if formatted_prompt.tool_schema: | ||
| response_params["tools"] = formatted_prompt.tool_schema | ||
| if formatted_prompt.formatted_output_schema: | ||
| response_params["text"] = formatted_prompt.formatted_output_schema |
There was a problem hiding this comment.
The output schema param is really called text?
| return output_schema | ||
| elif flavor_name == "openai_responses": | ||
| inner = output_schema.get("json_schema", {}) | ||
| return {"format": {"type": "json_schema", **inner}} |
There was a problem hiding this comment.
i'm not sure this handling is consistent with the other flavors -- they don't provide the top level json_schema annotation.. It is a bit different than how we handle tools.
| # Coerce developer → system for flavors that don't support the developer role. | ||
| # openai_responses supports developer natively; all others treat it as system. | ||
| messages = self.messages | ||
| if final_flavor != "openai_responses" and any( |
There was a problem hiding this comment.
Can this logic live in the adapter instead of in the general format?
Summary
OpenAIResponsesAdapter(openai_responsesflavor) so prompts are formatted for the Responses API ({"type": "message", ...}items, system stripped tosystem_content){"format": {"type": "json_schema", ...}}all_messages()now uses adapter-formattedllm_promptconsistently, falling back to raw messages for string adapters (Llama)developerrole support: passes through natively foropenai_responses, coerced tosystemwith a warning for all other flavorsBoundPrompt.format()usesdataclasses.replaceto reflect effective flavor inprompt_infowhen overriddenTest plan
make type-check: no new type issuesNote
Add OpenAI Responses API adapter and preserve developer role for the 'openai_responses' flavor in
OpenAIResponsesAdapter.to_llm_syntaxand prompt formattingIntroduce the
openai_responsesflavor with a new adapter that strips system messages and wraps items as Responses API messages; update prompt formatting to map tools/output to Responses API shapes and preserve thedeveloperrole for this flavor; update the example to call the Responses API; add tests for adapter behavior and role coercion; setREQUESTS_CA_BUNDLEformake run-%.📍Where to Start
Start with the
OpenAIResponsesAdapterand flavor switch in adapters.py, then review the formatting logic in prompts.py.Changes since #17 opened
RoleSupportdataclass andprepare_messagesfunction infreeplay.resources.adaptersmodule to centralize role validation and coercion [1892d6f]prepare_messagesintoBoundPrompt.formatmethod infreeplay.resources.promptsmodule to replace inline role coercion logic [1892d6f]freeplay.resources.adaptersmodule [1892d6f]BoundPrompt.__format_output_schemamethod infreeplay.resources.promptsmodule to return normalized schema directly for 'openai_responses' flavor [1892d6f]examples.openai_responses_api.pyscript to pass structured format specification for OpenAI Responses API [1892d6f]scripts/type-baseline/pyright-baseline.json[1892d6f]pyright-baseline.jsontype checking baseline file [0960253]openai_responsesflavor adapter for the OpenAI Responses API [74bee68]developerrole with coercion and mapping [74bee68]Macroscope summarized 164a993.