Skip to content

fix(api): fail-closed unknown tool_calls entry and function fields - #584

Open
seonghobae wants to merge 6 commits into
mainfrom
feat/tool-calls-entry-keys-http-honesty-20260816235806
Open

fix(api): fail-closed unknown tool_calls entry and function fields#584
seonghobae wants to merge 6 commits into
mainfrom
feat/tool-calls-entry-keys-http-honesty-20260816235806

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

Summary

  • Assistant tool_calls entries: only id/type/function plus optional stream index; other keys fail closed (unknown_tool_call_fields).
  • function object: only name/arguments; extra keys fail closed (unknown_tool_call_function_fields).
  • Optional index: null omit; non-negative int accepted; other types fail closed (invalid_tool_calls).
  • Tip substrate from fix(api): treat chat max_tool_calls null/empty as omit; fail-closed otherwise #582 (chat max_tool_calls band).

Test plan

  • pytest tests/test_tool_calls_entry_keys_http_honesty.py
  • pytest tests/test_chat_assistant_tool_calls_http_honesty.py (+ related)
  • CI Full unit + Semgrep product gates

Product gates only: Full unit + Semgrep (Strix ignored). Independent non-author APPROVE still required.

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

@seonghobae
seonghobae enabled auto-merge (squash) August 16, 2026 14:59
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 003be8a6-56f4-4e46-ab9d-9f01ae96df28

📥 Commits

Reviewing files that changed from the base of the PR and between 6841b71 and 3ce1a50.

📒 Files selected for processing (137)
  • contextual_orchestrator/cost_ledger.py
  • contextual_orchestrator/orchestrator.py
  • contextual_orchestrator/server.py
  • tests/test_analytics_runtime.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_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_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_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_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_streaming.py
  • tests/test_tool_calls_entry_keys_http_honesty.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_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.

@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: validator is sound; do not merge this stack

The unique commit (3ce1a50) is the right shape: _validate_chat_assistant_tool_calls already runs before tools passthrough, so unknown tool_calls entry/function keys and invalid index fail closed on both orchestration and passthrough paths.

Do not merge this head. It is stacked on #582 1a196b0, which fail-opens message weight / prefix / refusal / annotations when tools is present (those checks live only in _validate_messages after the early-return).

The entry-key checks plus a tools-path HTTP case are folded into #585 (cbd0420). Prefer #585 over #582/#583/#584.

View PR

Open in Web View Automation 

Sent by Cursor Automation: fix all

@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

Do not merge #584 at 3ce1a50. Unknown tool_calls entry/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.

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.

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.

Open in Web View Automation 

Sent by Cursor Automation: Fix Issues

@@ -733,36 +3598,226 @@ def do_POST(self) -> None: # noqa: N802
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@opencode-agent
opencode-agent Bot disabled auto-merge August 16, 2026 15:22
@seonghobae
seonghobae enabled auto-merge (squash) August 16, 2026 15:29
@opencode-agent
opencode-agent Bot disabled auto-merge August 16, 2026 16:08
@seonghobae
seonghobae enabled auto-merge (squash) August 16, 2026 16:09
@opencode-agent
opencode-agent Bot disabled auto-merge August 17, 2026 02:22
@seonghobae
seonghobae enabled auto-merge (squash) August 17, 2026 07:17
@opencode-agent
opencode-agent Bot disabled auto-merge August 17, 2026 07:50
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.

1 participant