fix(api): fail-closed unknown tool_calls entry and function fields - #584
fix(api): fail-closed unknown tool_calls entry and function fields#584seonghobae wants to merge 6 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.
Reject non-OpenAI keys on assistant tool_calls objects (named unknown_tool_call_fields / unknown_tool_call_function_fields). Allow optional non-negative index (null omit) for stream-assembled histories.
|
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 137 files, which is 37 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 (137)
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 |
There was a problem hiding this comment.
Stale comment
Review: validator is sound; do not merge this stack
The unique commit (
3ce1a50) is the right shape:_validate_chat_assistant_tool_callsalready runs before tools passthrough, so unknowntool_callsentry/function keys and invalidindexfail closed on both orchestration and passthrough paths.Do not merge this head. It is stacked on #582
1a196b0, which fail-opens messageweight/prefix/refusal/annotationswhentoolsis present (those checks live only in_validate_messagesafter the early-return).The entry-key checks plus a tools-path HTTP case are folded into #585 (
cbd0420). Prefer #585 over #582/#583/#584.Sent by Cursor Automation: fix all
There was a problem hiding this comment.
Stale comment
Do not merge #584 at
3ce1a50. Unknowntool_callsentry/function keys fail closed before passthrough, but this head inherits the #582 tools-path fail-open for weight/prefix/refusal/annotations and the #585 leftover (developer/ content /name). Repair is #586 (28ef9d2).Independent non-author APPROVE is still required on #586 before merge.
Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Do not merge 3ce1a50. The unique tool_calls entry/function-key commit is fail-closed on both the orchestration path and tools passthrough (live: extra entry key → 400 unknown_tool_call_fields; extra function key → 400 unknown_tool_call_function_fields; index null omit / non-negative int accept / bool reject). That part is the right shape.
This stacked head still returns 200 when tools is present together with unsupported message weight, prefix, refusal, or annotations. Those named rejects live only in _validate_messages, which runs after the tools/response_format early-return. proxy_completion then forwards the original messages verbatim, so a real provider can apply prefix/weight while this gateway claims those planes fail closed. Completions max_tool_calls is still opaque unknown_fields. Omit-equivalent max_tool_calls is not popped. Shared ModelClient sampling defaults are mutated per request on ThreadingHTTPServer.
Next action: land the mergeable repair at #586 (28ef9d2) — it hoists the remaining message planes before passthrough, name-rejects Completions max_tool_calls, strips omit-equivalent max_tool_calls, and isolates sampling knobs on threading.local. Do not merge #582–#585 at their current heads; they are substrates of the same stack.
Sent by Cursor Automation: Fix Issues
| @@ -733,36 +3598,226 @@ def do_POST(self) -> None: # noqa: N802 | |||
| return | |||
There was a problem hiding this comment.
Tools/response_format early-return happens before _validate_messages. Live at this head: tools + weight: 2 / prefix: true / nonempty refusal / nonempty annotations all return 200. Hoist those four checks next to _validate_chat_message_audio_function_call (already called above) so the passthrough path cannot smuggle them. #586 does this.
| "non-empty message function_call is not supported on /v1/chat/completions; " | ||
| "use tool_calls instead", | ||
| ) | ||
| if "weight" in message: |
There was a problem hiding this comment.
These weight / prefix / refusal / annotations rejects are correct on the orchestration path, but they never run when tools is present. That is the commercial-honesty hole: proxy_completion forwards the original messages, so a provider can apply a plane this gateway says it rejects. Move the checks into a pre-passthrough helper and add HTTP cases with nonempty tools.
| value = body.get("max_tool_calls") | ||
| # Explicit JSON null or empty/whitespace string is treat-as-omit. | ||
| if value is None or (isinstance(value, str) and not value.strip()): | ||
| return |
There was a problem hiding this comment.
Null / empty / whitespace returns without body.pop("max_tool_calls"). The key is not in _ORCHESTRATION_ONLY_KEYS, so proxy_completion forwards it. Pop on omit and assert absence on the tools path (mock echo must include the key).
| ALLOWED_EMBEDDINGS_KEYS = { | ||
| "model", "input", "encoding_format", "dimensions", "user", "metadata", "attribution", "routing", | ||
| } | ||
| ALLOWED_COMPLETIONS_KEYS = { |
There was a problem hiding this comment.
ALLOWED_COMPLETIONS_KEYS omits max_tool_calls, so /v1/completions still 400s as opaque unknown_fields. Add the key and call _validate_max_tool_calls(..., endpoint_path="/v1/completions") so Completions matches chat/Responses named invalid_max_tool_calls.
| try: | ||
| for index in (None, 0, 1): | ||
| call = _valid_call() | ||
| if index is not None or index is None: |
There was a problem hiding this comment.
if index is not None or index is None: is always true, so the loop never tests a missing index key. Split “omit key” vs “explicit null”, and add one nonempty-tools extra-key case so the pre-passthrough reject stays locked.


Summary
tool_callsentries: onlyid/type/functionplus optional streamindex; other keys fail closed (unknown_tool_call_fields).functionobject: onlyname/arguments; extra keys fail closed (unknown_tool_call_function_fields).index: null omit; non-negative int accepted; other types fail closed (invalid_tool_calls).Test plan
pytest tests/test_tool_calls_entry_keys_http_honesty.pypytest tests/test_chat_assistant_tool_calls_http_honesty.py(+ related)Product gates only: Full unit + Semgrep (Strix ignored). Independent non-author APPROVE still required.