Skip to content

fix(api): accept official Responses text.format type=text - #666

Closed
seonghobae wants to merge 15 commits into
mainfrom
cursor/bc-75fc6ed0-8f07-4c8c-b0b5-8b097fc431b2-d034
Closed

fix(api): accept official Responses text.format type=text#666
seonghobae wants to merge 15 commits into
mainfrom
cursor/bc-75fc6ed0-8f07-4c8c-b0b5-8b097fc431b2-d034

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

Summary

Unique tip on #649 (85617de). OpenAI SDKs send text: {format: {type: "text"}} as the default Responses output control. The gateway rejected that official default as invalid_text even though response_format: {type: "text"} already returns HTTP 200.

This tip makes the official default real:

  • _is_official_responses_text_format accepts exactly {format: {type: "text"}} and _validate_responses_conversation_controls forwards it.
  • Other non-empty text objects still return invalid_text (use response_format for json_object / json_schema).
  • Mock echo includes text so HTTP tests lock equality for the official default.
  • OpenAPI create_model_response, docs/rest_api_design.md, and CHANGELOG.md tell callers what to send next.

Review this commit only

2f420a9 — do not merge this honesty stack onto main. Parent unique tip is #649 / #635.

Test plan

  • python3 tests/test_responses_text_format_http_honesty.py
  • python3 tests/test_responses_conversation_controls_http_honesty.py
  • python3 tests/test_responses_instructions_reasoning_http_honesty.py
  • python3 tests/test_api_contract.py
  • python3 tests/test_product_planning_contract.py
  • python3 tests/test_self_check.py
  • python3 tests/test_paper_contracts.py
  • python3 tests/test_conventions.py
  • python3 tests/test_openai_passthrough.py

Buyer next action

Point SDKs at /v1/responses. Official text.format {type: text} is forwarded. If you see invalid_text, you sent a non-default text control — switch structured output to response_format or omit text. Do not retry the same unsupported object.

seonghobae and others added 15 commits August 16, 2026 21:18
…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>
@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Too 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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c27f9aa-470d-404a-9a52-14bd1de84e5d

📥 Commits

Reviewing files that changed from the base of the PR and between 6841b71 and 2f420a9.

📒 Files selected for processing (151)
  • CHANGELOG.md
  • contextual_orchestrator/api_contract.py
  • contextual_orchestrator/cost_ledger.py
  • contextual_orchestrator/orchestrator.py
  • contextual_orchestrator/server.py
  • docs/architecture.md
  • docs/rest_api_design.md
  • fuzz/targets.py
  • tests/test_analytics_runtime.py
  • tests/test_api_contract.py
  • tests/test_assistant_refusal_annotations_http_honesty.py
  • tests/test_assistant_tool_calls_null_noop_http_honesty.py
  • tests/test_audio_websearch_reasoning_null_noop_http_honesty.py
  • tests/test_background_reasoning_reject_http_honesty.py
  • tests/test_batch_embeddings.py
  • tests/test_batch_embeddings_encoding_dimensions_http_honesty.py
  • tests/test_batch_embeddings_endpoint_http_honesty.py
  • tests/test_batch_embeddings_routing_http_honesty.py
  • tests/test_batch_embeddings_user_http_honesty.py
  • tests/test_budget_enforcement.py
  • tests/test_chat_assistant_tool_calls_http_honesty.py
  • tests/test_chat_attribution_routing_http_honesty.py
  • tests/test_chat_audio_web_search_reject_http_honesty.py
  • tests/test_chat_developer_multimodal_content_http_honesty.py
  • tests/test_chat_empty_user_system_content_http_honesty.py
  • tests/test_chat_include_orchestration_trace_http_honesty.py
  • tests/test_chat_include_reject_http_honesty.py
  • tests/test_chat_logit_bias_http_honesty.py
  • tests/test_chat_max_completion_tokens_http_honesty.py
  • tests/test_chat_max_tool_calls_http_honesty.py
  • tests/test_chat_message_name_http_honesty.py
  • tests/test_chat_modalities_http_honesty.py
  • tests/test_chat_n_gt1_http_honesty.py
  • tests/test_chat_openai_metadata_http_honesty.py
  • tests/test_chat_orchestration_mode_http_honesty.py
  • tests/test_chat_parallel_tool_calls_http_honesty.py
  • tests/test_chat_penalties_http_honesty.py
  • tests/test_chat_prediction_http_honesty.py
  • tests/test_chat_reasoning_effort_http_honesty.py
  • tests/test_chat_reasoning_object_reject_http_honesty.py
  • tests/test_chat_response_format_http_honesty.py
  • tests/test_chat_service_tier_http_honesty.py
  • tests/test_chat_store_http_honesty.py
  • tests/test_chat_stream_options_http_honesty.py
  • tests/test_chat_temperature_top_p_http_honesty.py
  • tests/test_chat_tool_call_id_http_honesty.py
  • tests/test_chat_tool_choice_functions_http_honesty.py
  • tests/test_chat_tools_shape_http_honesty.py
  • tests/test_chat_top_logprobs_http_honesty.py
  • tests/test_chat_unknown_fields_http_honesty.py
  • tests/test_commercial_readiness.py
  • tests/test_completions_chat_era_fields_reject_http_honesty.py
  • tests/test_completions_empty_tools_noop_http_honesty.py
  • tests/test_completions_include_reject_http_honesty.py
  • tests/test_completions_legacy_knobs_http_honesty.py
  • tests/test_completions_max_completion_tokens_http_honesty.py
  • tests/test_completions_max_tokens_http_honesty.py
  • tests/test_completions_max_tool_calls_http_honesty.py
  • tests/test_completions_metadata_service_tier_http_honesty.py
  • tests/test_completions_prompt_shape_http_honesty.py
  • tests/test_completions_response_format_audio_null_http_honesty.py
  • tests/test_completions_response_format_reject_http_honesty.py
  • tests/test_completions_sampling_knobs_http_honesty.py
  • tests/test_completions_seed_http_honesty.py
  • tests/test_completions_stop_http_honesty.py
  • tests/test_completions_store_http_honesty.py
  • tests/test_completions_stream_options_http_honesty.py
  • tests/test_completions_stream_reject_http_honesty.py
  • tests/test_completions_tool_choice_function_call_noop_http_honesty.py
  • tests/test_completions_tools_noop_extensions_http_honesty.py
  • tests/test_completions_tools_reject_http_honesty.py
  • tests/test_completions_top_logprobs_reject_http_honesty.py
  • tests/test_cost_review_server.py
  • tests/test_embeddings_blank_input_http_honesty.py
  • tests/test_embeddings_encoding_format_http_honesty.py
  • tests/test_embeddings_metadata_http_honesty.py
  • tests/test_embeddings_model_pool_http_honesty.py
  • tests/test_embeddings_null_optional_noop_http_honesty.py
  • tests/test_embeddings_routing_http_honesty.py
  • tests/test_embeddings_user_field_http_honesty.py
  • tests/test_empty_modalities_prediction_noop_http_honesty.py
  • tests/test_empty_stop_array_noop_http_honesty.py
  • tests/test_empty_stream_options_include_noop_http_honesty.py
  • tests/test_empty_string_controls_noop_http_honesty.py
  • tests/test_empty_string_encoding_tool_choice_endpoint_noop_http_honesty.py
  • tests/test_empty_string_numeric_controls_noop_http_honesty.py
  • tests/test_empty_string_reasoning_text_include_noop_http_honesty.py
  • tests/test_empty_string_stop_noop_http_honesty.py
  • tests/test_empty_tools_array_http_honesty.py
  • tests/test_function_call_reasoning_empty_noop_http_honesty.py
  • tests/test_functions_null_max_tool_calls_null_http_honesty.py
  • tests/test_include_orchestration_trace_null_noop_http_honesty.py
  • tests/test_ledger_execution_identity_http_honesty.py
  • tests/test_message_audio_function_call_http_honesty.py
  • tests/test_message_name_empty_omit_http_honesty.py
  • tests/test_message_name_null_noop_http_honesty.py
  • tests/test_message_prefix_http_honesty.py
  • tests/test_message_unknown_fields_http_honesty.py
  • tests/test_message_weight_http_honesty.py
  • tests/test_multimodal_content_parts_shape_http_honesty.py
  • tests/test_multimodal_message_content_http_honesty.py
  • tests/test_openai_models_listing_http.py
  • tests/test_openai_passthrough.py
  • tests/test_openai_sdk_control_fields_reject_http_honesty.py
  • tests/test_openai_user_field_http_honesty.py
  • tests/test_prediction_modalities_model_strip_http_honesty.py
  • tests/test_product_planning_contract.py
  • tests/test_prompt_cache_retention_reject_http_honesty.py
  • tests/test_reasoning_effort_none_store_stream_empty_noop_http_honesty.py
  • tests/test_responses_attribution_routing_http_honesty.py
  • tests/test_responses_conversation_controls_http_honesty.py
  • tests/test_responses_instructions_reasoning_http_honesty.py
  • tests/test_responses_logit_bias_logprobs_http_honesty.py
  • tests/test_responses_max_output_tokens_http_honesty.py
  • tests/test_responses_max_tokens_http_honesty.py
  • tests/test_responses_max_tool_calls_reject_http_honesty.py
  • tests/test_responses_metadata_http_honesty.py
  • tests/test_responses_modalities_prediction_http_honesty.py
  • tests/test_responses_model_required_http_honesty.py
  • tests/test_responses_n_http_honesty.py
  • tests/test_responses_parallel_tool_calls_http_honesty.py
  • tests/test_responses_penalties_http_honesty.py
  • tests/test_responses_response_format_http_honesty.py
  • tests/test_responses_seed_stop_http_honesty.py
  • tests/test_responses_service_tier_http_honesty.py
  • tests/test_responses_store_http_honesty.py
  • tests/test_responses_stream_options_http_honesty.py
  • tests/test_responses_stream_reject_http_honesty.py
  • tests/test_responses_temperature_top_p_http_honesty.py
  • tests/test_responses_text_format_http_honesty.py
  • tests/test_responses_tools_shape_http_honesty.py
  • tests/test_responses_user_field_http_honesty.py
  • tests/test_sales_readiness.py
  • tests/test_sdk_null_legacy_controls_noop_http_honesty.py
  • tests/test_sdk_null_object_optional_noop_http_honesty.py
  • tests/test_sdk_null_optional_noop_http_honesty.py
  • tests/test_security_hardening.py
  • tests/test_service_tier_encoding_format_strip_http_honesty.py
  • tests/test_stream_null_noop_http_honesty.py
  • tests/test_stream_options_false_tool_choice_empty_noop_http_honesty.py
  • tests/test_stream_options_null_flags_noop_http_honesty.py
  • tests/test_streaming.py
  • tests/test_tool_choice_auto_without_tools_noop_http_honesty.py
  • tests/test_tool_choice_strip_modalities_text_noop_http_honesty.py
  • tests/test_tool_description_length_http_honesty.py
  • tests/test_tool_description_parameters_null_noop_http_honesty.py
  • tests/test_tool_strict_null_noop_http_honesty.py
  • tests/test_top_logprobs_args_null_omit_http_honesty.py
  • tests/test_top_logprobs_zero_omit_noop_http_honesty.py
  • tests/test_true_streaming.py
  • tests/test_user_null_omit_noop_http_honesty.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

❤️ Share

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

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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, so text + response_format could 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 for response_format: {type: text}. Concurrent #657 (8fd294f, #646 substrate) fail-closes dual-plane. Residual on this #649 substrate — do not fold #657's structured text.format / verbosity / null-pop work in here.
  • Extra keys / verbosity / structured types via text.format stay 400. Intentional. Residual buyer gap if an SDK later emits text.verbosity (even null) as a default sibling.
  • ALLOWED_RESPONSES_KEYS still comments text as “accepted only to fail closed” (not in this tip’s hunk). Doc drift only.
  • OpenAPI text is object|null with 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_FORMAT

Sibling 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):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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}}".

Comment thread fuzz/targets.py
# non-empty text objects fail closed with invalid_text.
if "text" in body:
try:
server._validate_responses_conversation_controls(body)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@cursor cursor 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.

Stale comment

Review: unique slice SOUND

2f420a9 does what it claims. Official SDK default text: {format: {type: "text"}} is forwarded; extra keys or non-text format.type still return invalid_text. Mock echo.text locks the forwarded object. response_format remains the path for json_object / json_schema.

Prefer this head over #657 for the official default. Do not open a third text.format PR. Do not fold text.format onto #667 / #679 (those are the Responses SSE envelope stack). Do not merge the honesty stack onto main without an independent non-author APPROVE.

Residual (not a blocker for this slice): the helper requires exact key sets ({format} / {type}). A later SDK that adds verbosity would 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 on response_format.

View PR

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

@cursor cursor 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.

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-empty text, so this could not happen before.
  • The invalid_text string “unless format.type is text” overstates the accept set — {format:{type:text}, verbosity:…} still 400s.
  • Structured text.format json_object / flat json_schema remain 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.

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

@seonghobae

Copy link
Copy Markdown
Contributor Author

Superseded by tip substrate ≥ #691 (cumulative OpenAI/gateway honesty band + auto-merge tip). Closing to free product-gate runners (Full unit + Semgrep).

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