fix(api): accept official Responses text.format type=text - #666
fix(api): accept official Responses text.format type=text#666seonghobae wants to merge 15 commits into
Conversation
…closed otherwise Chat history: message-level audio and legacy function_call are null/empty omit no-ops; non-empty fail closed with named errors (including tools passthrough). Tip substrate from #577 assistant refusal/annotations honesty. Local full unit: 940 passed.
…ed otherwise OpenAI fine-tune style message weight is not applied on this gateway. Accept null/0/1 as honest no-ops; reject other types and values with invalid_message_weight. Tip substrate from #578. Local full unit: 943 passed.
…ion role Reject unsupported message keys with named unknown_message_fields (not silent strip or tools-passthrough smuggle). Reject legacy function role with invalid_message_role migration to tool. Tip substrate from #579. Local full unit: 947 passed.
OpenAI partial-assistant prefix flag is not applied on this gateway. null/false are honest no-ops; true and non-booleans fail closed with invalid_message_prefix. Tip substrate from #580. Local full unit: 950 passed.
…therwise Named invalid_max_tool_calls on /v1/chat/completions instead of opaque unknown_fields. Aligns with Responses max_tool_calls honesty; gateway has no multi-step tool loop.
…losed otherwise Legacy /v1/completions treated max_tool_calls as unknown_fields. Accept the key for named invalid_max_tool_calls (null/empty/whitespace omit-equivalent), matching chat/Responses honesty so SDKs get a clear migration path.
SDK clients often send include_usage/include_obfuscation as JSON null. Drop null flag values before validation so null (and null+false mixes) match omit / all-false no-ops on chat, Completions, and Responses. True flags remain fail-closed with invalid_stream_options.
…or Responses parallel true SDK optional defaults often send function.strict and json_schema.strict as null — treat as omit rather than type errors. Align Responses parallel_tool_calls=true with chat by requiring a non-empty tools array.
SDK optional defaults often send description and parameters as JSON null. Treat null as omit rather than type errors; non-null non-string/object values remain fail-closed with invalid_tools.
OpenAI-style tool descriptions are at most 1024 characters. Over-long descriptions fail closed with named invalid_tools so SDKs never believe a truncated description was accepted.
SDK optional participant name blanks ("" / whitespace) are omit-equivalent
like JSON null. Non-string, over-long, and invalid charset names remain
fail-closed with invalid_message_name.
…ll as omit SDK optional defaults often send top_logprobs as "" and tool_calls function.arguments as JSON null. Empty/whitespace top_logprobs matches null/0 omit on chat and Completions; null arguments normalizes to empty JSON-text string. Non-zero top_logprobs and non-string arguments stay fail-closed.
SDK optional defaults often send instructions as empty or blank. Match JSON null omit-equivalent behavior so blank instructions do not fail closed as invalid_instructions; non-string and oversized remain rejected.
SDK null/empty/whitespace instructions are now omit-real: the key is removed so /v1/responses does not forward a blank system prompt. HTTP tests lock the mock echo, and the OpenAPI/docs contract tells callers to send a non-empty string when they want instructions. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
OpenAI SDKs send text: {format: {type: "text"}} as the default output
control. Rejecting that official default as invalid_text contradicted
response_format type=text on the same surface. Forward the default;
keep fail-closed for other non-empty text objects.
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Important Review skippedToo many files! This PR contains 151 files, which is 51 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (151)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
seonghobae
left a comment
There was a problem hiding this comment.
Unique tip 2f420a9 — SOUND
Reviewed only git show 2f420a946c5ee00c18505ca5f2fcb3286e7541bc (10 files). Not the 151-file honesty stack vs main. Not a CodeRabbit result (CLI not installed in this environment). Do not merge this stack onto main. Do not treat this COMMENT as an approve.
1. Verdict
SOUND for the stated slice: accept exactly {format: {type: "text"}}, forward it, fail-closed on every other non-empty text object.
_is_official_responses_text_format is an exact key-set match (text keys == {format}, format keys == {type}, type == "text"). Extra keys, verbosity, json_object / json_schema via text.format, and unknown format types stay invalid_text. text is not in _ORCHESTRATION_ONLY_KEYS, so proxy_completion forwards it; mock echo now includes text.
Local: python3 tests/test_responses_text_format_http_honesty.py and test_responses_conversation_controls_http_honesty.py both printed ok.
2. Concrete bugs in this tip
No runtime accept/forward bug in the unique tip.
One copy defect on a unique-tip line: the new invalid_text string (server.py:1202) says “unless format.type is text”. That overstates the accept set — {format:{type:text}, verbosity:…} still 400s. Tighten the message; do not widen the accept set.
3. Residual honesty holes (follow-ups)
- Dual-plane (this tip introduces it). Parent rejected every non-empty
text, sotext+response_formatcould not both reach the provider. After this tip,text: {format:{type:text}}+response_format: {type: json_object}is HTTP 200 and mock echo contains both keys (probed). Same forresponse_format: {type: text}. Concurrent #657 (8fd294f, #646 substrate) fail-closes dual-plane. Residual on this #649 substrate — do not fold #657's structuredtext.format/ verbosity / null-pop work in here. - Extra keys /
verbosity/ structured types viatext.formatstay 400. Intentional. Residual buyer gap if an SDK later emitstext.verbosity(evennull) as a default sibling. ALLOWED_RESPONSES_KEYSstill commentstextas “accepted only to fail closed” (not in this tip’s hunk). Doc drift only.- OpenAPI
textisobject|nullwith no exact-shape schema. Description is honest; schema is loose.
4. Do tests lock HTTP 200 + echo.text equality?
Yes. tests/test_responses_text_format_http_honesty.py:85-86:
assert status == 200, body
assert body.get("echo", {}).get("text") == _OFFICIAL_TEXT_FORMATSibling test_http_responses_rejects_text_control now uses {format:{type:xml}} so the official default is no longer the reject fixture. No HTTP lock for extra keys, verbosity, or dual-plane (follow-up).
5. Security / KV / PII
None in this tip. No new os.getenv / env runtime reads. No secrets. Official text object has no user content; extra keys are rejected before forward. Mock echo.text is test-only (mock://). Dummy test bearer is local.
6. Inline comments (unique-tip lines only)
Posted on server.py:1196 (dual-plane), server.py:1202 (overstated error string), fuzz/targets.py:101 (fuzz seam is a no-op).
Independent non-author review still required. Prefer #666 over #657 for this narrow default-text slice on the newer substrate; #657 remains the broader text.format landing on the older substrate. Do not merge either stack onto main from this review.
| or (isinstance(text, str) and not text.strip()) | ||
| ): | ||
| pass | ||
| elif _is_official_responses_text_format(text): |
There was a problem hiding this comment.
This accept branch is the unique-tip behavior, and it is exact for {format: {type: "text"}}.
It also opens a dual-plane hole the parent did not have: text: {format: {type: "text"}} plus response_format: {type: "json_object"} returns HTTP 200 and the mock echo contains both keys (probed locally). Concurrent #657 fail-closes dual-plane; this tip does not. Residual follow-up on this #649 substrate — do not fold #657's json_object / json_schema / verbosity work in here.
| raise RequestError( | ||
| 400, | ||
| "invalid_text", | ||
| "text is not supported on /v1/responses unless format.type is text", |
There was a problem hiding this comment.
The new message says text is unsupported "unless format.type is text". That overstates the accept set. {format: {type: "text"}, verbosity: null|low} and {format: {type: "text", name: "x"}} still 400 invalid_text (intentional, and correct under the honesty rule). Buyers who read this string will think any format.type=text object is valid. Keep the fail-closed set; tighten the message to "unless text is exactly {format: {type: text}}".
| # non-empty text objects fail closed with invalid_text. | ||
| if "text" in body: | ||
| try: | ||
| server._validate_responses_conversation_controls(body) |
There was a problem hiding this comment.
This new seam only swallows RequestError. It does not assert that {format: {type: "text"}} is accepted, nor that extra keys / non-text types raise invalid_text. The HTTP test file locks the accept path; this fuzz block does not. Residual: assert the official object does not raise, or drop the comment that implies the invariant is locked here.
There was a problem hiding this comment.
Stale comment
Review: unique slice SOUND
2f420a9does what it claims. Official SDK defaulttext: {format: {type: "text"}}is forwarded; extra keys or non-textformat.typestill returninvalid_text. Mockecho.textlocks the forwarded object.response_formatremains the path forjson_object/json_schema.Prefer this head over #657 for the official default. Do not open a third
text.formatPR. Do not foldtext.formatonto #667 / #679 (those are the Responses SSE envelope stack). Do not merge the honesty stack ontomainwithout an independent non-author APPROVE.Residual (not a blocker for this slice): the helper requires exact key sets (
{format}/{type}). A later SDK that addsverbositywould still 400 — land that as its own tip when a real client sends it.Buyer next action: send the official default or omit
text; put structured output onresponse_format.Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Unique tip 2f420a9 is SOUND for the stated slice
Review is of 2f420a9 only. Do not merge this 151-file honesty stack onto main. Independent non-author APPROVE is still required. This is not an approve.
_is_official_responses_text_format is an exact key-set match. Official {format: {type: text}} is forwarded; extra keys, verbosity, and structured types stay invalid_text. HTTP honesty locks 200 + echo.text equality. No KV / PII / secret issues in this tip.
Residual on this tip (do not fold here)
- Dual-plane hole this accept branch opens. After
2f420a9,text: {format:{type:text}}+response_format: {type: json_object}is HTTP 200 and mock echo contains both keys. Parent rejected every non-emptytext, so this could not happen before. - The
invalid_textstring “unless format.type is text” overstates the accept set —{format:{type:text}, verbosity:…}still 400s. - Structured
text.formatjson_object/ flatjson_schemaremain 400 by design on this tip.
Landing
Prefer #666 over #657 for the narrow type=text default on the newer #649 substrate. #657 is the broader slice on the older #646 substrate — do not merge either stack onto main.
The dual-plane + structured-types follow-up is the next unique tip on this substrate (8b6cb23 on cursor/bc-ea607b03-3ad5-4672-8eb5-40fbce1c3f91-412e). Review that successor for json_object / flat json_schema, omit-real optionals, verbosity, and dual-plane fail-closed. Do not open a third type=text-default PR.
Reviewer next action
Review 2f420a9 only. Do not APPROVE the honesty stack as a main merge. If you need structured text.format or dual-plane closed, review the successor unique tip instead of widening this one.
Sent by Cursor Automation: Fix Issues
Pull request was closed
|
Superseded by tip substrate ≥ #691 (cumulative OpenAI/gateway honesty band + auto-merge tip). Closing to free product-gate runners (Full unit + Semgrep). |


Summary
Unique tip on #649 (
85617de). OpenAI SDKs sendtext: {format: {type: "text"}}as the default Responses output control. The gateway rejected that official default asinvalid_texteven thoughresponse_format: {type: "text"}already returns HTTP 200.This tip makes the official default real:
_is_official_responses_text_formataccepts exactly{format: {type: "text"}}and_validate_responses_conversation_controlsforwards it.textobjects still returninvalid_text(useresponse_formatforjson_object/json_schema).echoincludestextso HTTP tests lock equality for the official default.create_model_response,docs/rest_api_design.md, andCHANGELOG.mdtell callers what to send next.Review this commit only
2f420a9— do not merge this honesty stack ontomain. Parent unique tip is #649 / #635.Test plan
python3 tests/test_responses_text_format_http_honesty.pypython3 tests/test_responses_conversation_controls_http_honesty.pypython3 tests/test_responses_instructions_reasoning_http_honesty.pypython3 tests/test_api_contract.pypython3 tests/test_product_planning_contract.pypython3 tests/test_self_check.pypython3 tests/test_paper_contracts.pypython3 tests/test_conventions.pypython3 tests/test_openai_passthrough.pyBuyer next action
Point SDKs at
/v1/responses. Officialtext.format{type: text}is forwarded. If you seeinvalid_text, you sent a non-defaulttextcontrol — switch structured output toresponse_formator omittext. Do not retry the same unsupported object.