fix(api): fail-closed empty messages before tools passthrough - #594
fix(api): fail-closed empty messages before tools passthrough#594cursor[bot] wants to merge 11 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.
Validate weight/prefix/refusal/annotations before the tools early-return so SDK tool-calling bodies cannot smuggle unsupported message fields. Strip omit-equivalent max_tool_calls before provider passthrough and name-reject the field on /v1/completions. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Reject non-OpenAI keys on assistant tool_calls objects with named unknown_tool_call_fields / unknown_tool_call_function_fields. Accept optional non-negative index (null omit) for stream-assembled histories. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Developer role, empty user/system content, multimodal part shape, and participant name were only checked in _validate_messages, which the tools/response_format early-return skips. SDK tool-calling bodies could therefore proxy unsupported values. Run the same fail-closed checks before passthrough and cover the realistic tools+field HTTP cases. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Completions and chat applied temperature, top_p, penalties, and max_tokens by mutating shared ModelClient defaults. Concurrent ThreadingHTTPServer workers could observe another request's knobs. request_sampling stores overrides on threading.local and restores them on exit; chat/stream_chat read the calling thread first. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Tools/response_format still billed a completion when messages was [], omitted, null, or a non-list. Require the same non-empty array as _validate_messages before the early-return. stream_chat now reads the same thread-local sampling knobs as chat so route streaming cannot drop top_p or penalties. 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. |
Keep Fugu/TRINITY/Conductor titles for paper-contract search while recording APA 7th citations. Split the optional tool_calls index case so omit-key is actually tested instead of the previous tautology. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Review of unique commits vs #586 (
28ef9d2)Unique range:
7a16460(emptymessages+ sharedstream_chatknobs) and61f1887(APA titles + real omit-keytool_callsindex). Code-reviewer + live HTTP on this tip.Buyer next action
Send a non-empty
messagesarray on every/v1/chat/completionscall, including tool-calling SDK bodies. Empty, omitted, null, or non-listmessagesnow return 400invalid_messagebefore passthrough — the same rule as the orchestration path. Do not merge #582–#586 or #589; those heads still billed a completion with no prompt.Do not merge #594 until Full unit and Semgrep are green on
61f1887and an independent non-author APPROVE lands. This review is COMMENT, not APPROVE.Unique commits — closed
_require_chat_messages_array(server.py) is shared by_validate_chat_message_content_and_name(before thePASSTHROUGH_TRIGGER_KEYSreturn) and_validate_messages.- Live HTTP: tools +
[]/ omitted / null / non-list, andresponse_format+[], all 400invalid_message.stream_chatnow reads the same thread-localrequest_samplingknobs aschat(temperature,top_p, penalties,max_tokens).61f1887splits omit-key /index: null/0/1so the optionaltool_callsindex case is not a tautology. Fugu / TRINITY / Conductor titles stay indocs/architecture.mdwith APA 7th citations.No unique-commit Critical or Important defects.
Residual fail-open on this tip (next slice, not a #594 bug)
These still sit after the passthrough return / inside
proxy_completionand still return 200chat.completion:
- tools + omitted
model— silent worker select- tools +
stream=true— JSON body (proxy_completionforcesstream=false)- tools +
stream_options/ non-objectattribution/routingUntil that hoist lands: always send a pool
model; omitstreamor setfalseon tool-calling requests. Prefer a successor of this tip over #591/#593 (those are based on #586 and lack the empty-messagesgate).Checks
Full unit + Semgrep were still queued at review time. Strix is not a product gate. Independent non-author APPROVE remains required.
Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Review (#594 @ 61f1887)
Unique empty-messages work is sound. Do not merge this head.
_require_chat_messages_array runs before the tools/response_format early-return and on the orchestration path. Live HTTP on this SHA: tools + [] / omitted / null / non-list, and response_format + [], all return 400 invalid_message. stream_chat now shares _effective_request_sampling (top_p / penalties / max_tokens). The tool_calls index tautology is gone — omit-key is actually posted.
Local scripts printed ok: test_message_role_content_name_tools_passthrough_http_honesty.py, test_model_client_request_sampling_thread_isolation.py, test_tool_calls_entry_keys_http_honesty.py.
Remaining fail-open at this SHA (live HTTP 200 chat.completion)
These still sit after the passthrough return, so SDK tool-calling bodies can still bill:
| Body | Result at 61f1887 |
|---|---|
tools + stream: true |
JSON 200 (proxy_completion forces stream=false) |
tools + omitted / unknown model |
200 silent pool pick |
tools + stream_options.include_usage=true |
200 |
tools + temperature=99 / top_p=2 |
200 |
Next action
Prefer #600 (6f35df4) — this tip plus the request-level hoist. Send a non-empty messages array and a pool model; omit stream (or set false) on tool-calling requests.
Do not merge #582–#594 or #589. #591 has the hoist but not the stream_chat knob share. #592 is missing the empty-messages helper. Independent non-author APPROVE + Full unit / Semgrep still required on #600.
CodeRabbit CLI 0.7.3: coderabbit auth login --agent needs a browser callback here; this review is manual + live HTTP.
Sent by Cursor Automation: Fix Issues
| omitted, null, or non-list ``messages`` value must still fail closed so | ||
| SDK tool-calling bodies cannot bill a completion with no prompt. | ||
| """ | ||
| if not isinstance(messages, list) or not messages: |
There was a problem hiding this comment.
This shared gate is the right close for #586. Live HTTP on 61f1887: tools + [] / omitted / null / non-list → 400 invalid_message. Keep it. The remaining buyer hole is the early-return below, which still skips stream / model / stream_options / sampling range — that hoist is #600, not a change to this helper.
| @@ -733,36 +3638,211 @@ def do_POST(self) -> None: # noqa: N802 | |||
| return | |||
There was a problem hiding this comment.
Still fail-open at this SHA. Live probe: tools + stream: true / omitted model / stream_options.include_usage=true / temperature=99 all return 200 chat.completion because _normalize_chat_stream_flag, _validate_completions_model, _require_pool_model, and the sampling-range checks run only after this return.
Do not merge this head. Prefer #600, which hoists those checks before proxy_completion. Buyer next action on that tip: send a pool model and omit stream (or set false) on tool-calling bodies.
There was a problem hiding this comment.
Review (#594 @ 61f1887)
Unique empty-messages work is sound. Do not merge this head.
_require_chat_messages_array runs before the tools/response_format early-return. Live HTTP on this SHA: tools + [] / omitted / null / non-list, and response_format + [], all return 400 invalid_message. stream_chat shares _effective_request_sampling. The tool_calls index omit-key case is a real tautology fix.
Local scripts printed ok: test_message_role_content_name_tools_passthrough_http_honesty.py, test_model_client_request_sampling_thread_isolation.py, test_tool_calls_entry_keys_http_honesty.py.
Remaining fail-open at this SHA
Live HTTP still 200 chat.completion for tools + stream: true, omitted/unknown model, stream_options.include_usage=true, and out-of-range temperature / top_p.
Next action
Prefer #597 (dbfa0e5) as the honesty-stack tip (empty messages + stream/model/spend/batch-routing on the #592 substrate). Do not merge #582–#596, #594, #599, or #600 in parallel.
#600 (6f35df4) is a #594-lineage parallel that hoists stream/model/sampling and keeps the stream_chat knob share. #597 still drops those knobs on route streaming — cherry-pick _effective_request_sampling onto #597 rather than landing #600.
Buyer next action on #597: send a non-empty messages array of objects and a pool model; omit stream (or set false) on tool-calling requests; omit routing.channel=batch.
Independent non-author APPROVE + Full unit / Semgrep still required. This automation will not APPROVE.
CodeRabbit CLI 0.7.3: coderabbit auth login --agent needs a browser callback here; this review is manual + live HTTP.
Sent by Cursor Automation: Fix Issues
|
Superseded by #597 and the later #601 → #607 line. #597 explicitly includes this PR’s empty-message/streaming honesty behavior while adding the broader spend/routing validation; the successor chain carries that forward on the newer landing substrate. Keeping #594 open would duplicate the same request-boundary ownership. No checks, reviews, or approvals transfer. |


Summary
messagesnow fail closed before the passthrough early-return, with the sameinvalid_messageas the orchestration path._require_chat_messages_array) keeps both paths on one non-empty-array rule so SDK tool-calling bodies cannot bill a completion with no prompt.stream_chatnow reads the same thread-localrequest_samplingknobs aschat(temperature,top_p, penalties,max_tokens) so route streaming cannot drop overrides.[]/ omitted / null / non-list, andresponse_format+[]. Isolation tests coverstream_chatknob application.This branch is the mergeable repair of #586 (
28ef9d2) and therefore of #582/#583/#584/#585. Do not merge those heads — tools + empty/omittedmessagesstill returned 200 on #586, andstream_chatstill ignoredtop_p/ penalties.Test plan
python3 tests/test_message_role_content_name_tools_passthrough_http_honesty.pypython3 tests/test_model_client_request_sampling_thread_isolation.pypython3 tests/test_true_streaming.pytest_streaming.pytest_security_hardening.pypython3 tests/test_message_weight_http_honesty.pytest_message_prefix_http_honesty.pytest_assistant_refusal_annotations_http_honesty.pypython3 tests/test_chat_developer_multimodal_content_http_honesty.pytest_chat_message_name_http_honesty.pytest_tool_calls_entry_keys_http_honesty.pypython3 tests/test_completions_sampling_knobs_http_honesty.pypython3 tests/test_api_contract.pytest_conventions.pytest_openai_passthrough.pytest_paper_contracts.pytest_self_check.pyProduct gates only: Full unit + Semgrep (Strix ignored). Independent non-author APPROVE still required.