Skip to content

fix(api): treat max_tool_calls 0 and digit-zero as omit - #711

Open
seonghobae wants to merge 39 commits into
mainfrom
feat/max-tool-calls-zero-omit-http-honesty-20260817083221
Open

fix(api): treat max_tool_calls 0 and digit-zero as omit#711
seonghobae wants to merge 39 commits into
mainfrom
feat/max-tool-calls-zero-omit-http-honesty-20260817083221

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

max_tool_calls zero omit; 1152 unit green

seonghobae and others added 30 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.
SDK optional defaults serialize omitted tool.function description/parameters/strict
as JSON null. Accepting those keys without popping them is not omit-equivalent:
proxy_completion forwards the body and several providers reject null parameters.
Pop in place so passthrough matches omit; keep non-null wrong types on invalid_tools.
Also pop response_format.json_schema.strict null. Tip substrate from #614.
Local full unit: 989 passed.
Null flag values on allowed keys (include_usage / include_obfuscation) stay
omit-equivalent. Dropping nulls before the allow-list made {unknown: null}
look empty and silently omit — dishonest for buyers. Fail closed with
invalid_stream_options on chat, Completions, and Responses. Tip substrate
from #638. Local full unit: 996 passed.
…ix/mode/metadata)

Parallel tip #653 lacked later omit seams. Restore top_logprobs empty-string
omit, tool_calls arguments null→empty string, Responses instructions
blank omit, Completions whitespace suffix omit, mode strip, and metadata
null value key-omit. Fail-closed paths for nonzero/non-string remain.
#668 restored accept-path 200s but left omit-equivalent keys on the
proxied body. Write back empty tool_calls arguments, pop blank
Responses instructions, persist cleaned metadata, and hoist chat
logprobs/top_logprobs before tools passthrough so providers see the
omit-equivalent payload.

Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
str.isalnum() accepted Unicode letters and digits (café, 名前,
Arabic-Indic digits), so the documented [a-zA-Z0-9_-]{1,64} check still
forwarded illegal names and buyers saw an opaque provider 400.

Require name.isascii() on tool.function.name, tool_calls function.name,
message name, and response_format.json_schema.name (plus 64-char cap on
json_schema.name). HTTP honesty locks Unicode reject and legal keep on
chat and Responses. Request-body fuzz exercises both validators.

Re-lands parallel tip #685/#669 seams onto the highest #686 substrate.
The gateway rejected all non-empty text as invalid_text, so official SDK
defaults (text.format.type=text) and structured json_object/json_schema
never reached the provider. Accept the official plane, pop null/blank
optionals, fail-closed on verbosity and dual-plane text+response_format,
and require ASCII [a-zA-Z0-9_-]{1,64} on flat json_schema names.

Mock echo includes text so HTTP honesty locks forward shape. Re-lands
parallel tip #681 onto the #687 substrate.
OpenAI SDKs send truncation auto/disabled; this gateway has no multi-turn
conversation window to truncate, so those values are omit-equivalent.
Unknown truncation strings remain fail-closed with invalid_truncation.
Also align conversation-controls tests with official text.format text.
Pop null/blank description and null strict inside response_format.json_schema
before passthrough, and fail closed on unknown nested keys — parity with
Responses text.format structured-output honesty.
Fail closed when tool_choice=required is sent without a non-empty tools
array (chat + Responses), matching parallel_tool_calls=true honesty so
clients cannot mandate tool use with no tools declared.
Reject blank metadata keys on chat/Responses so cost and observability
surfaces never index empty labels; non-empty keys keep the existing
16-entry and length contracts.
…nals

SDK clients send service_tier as " AUTO " and routing optionals as null.
Casefold after strip so auto/default match; treat routing channel/priority
empty and latency_tolerant null as omit. flex/priority and non-boolean
latency_tolerant remain fail-closed.
…seed

JS SDKs often send 0/1 for booleans and integer seeds as strings. Accept
int 0/1 (not True/False via int subclass) for store, stream, and
parallel_tool_calls; parse digit strings for seed (Responses passthrough;
Completions still fail-closed after type check as unsupported).
Assistants-style tool_resources must not surface as opaque unknown_fields.
Accept the key for named invalid_tool_resources; JSON null and empty object
are treat-as-omit on chat, Completions, and Responses.
Coerce digit-string n and best_of (write back int) on Completions/chat/
Responses. Accept JS int 0/1 for echo, background, and logprobs via the
shared optional-bool helper; digit-string top_logprobs "0" is omit.
Tip-substrate from #697; local unit: 1091 passed.
Form/query JS SDKs send store/stream/parallel as \"true\"/\"false\" and
sampling knobs as digit strings. Extend optional bool coerce for those
strings and int/float digit-string coerce for max_tokens and sampling
penalties/temperature/top_p. Non-numeric strings remain fail-closed.
…ws omit

JS JSON often serializes integers as 1.0. Coerce whole-number floats (and
digit strings for Responses max_output_tokens) on n/best_of/seed/max_* paths.
Treat chat stop whitespace as omit-equivalent like empty string.
Tip substrate from #703. Local full unit: 1111 passed.
…probs 0 omit

Accept FLOAT/Float as float on embeddings; coerce Responses stream false/"0"
like chat; treat Completions logprobs 0/false string forms as omit no-ops.
Tip substrate from #704. Local full unit: 1117 passed.
SDK clients send padded/uppercase tool_choice and function_call control
strings. Strip and casefold so none/auto remain omit-equivalent without
tools and required matches with tools; non-control values stay fail-closed.
Buyer SDKs and form clients send padded/uppercase enum strings. Treat
reasoning_effort none (chat/completions), response_format.type and
text.format.type (text/json_object/json_schema), and modalities ["text"]
as case-insensitive omit-equivalent or accepted controls; write back
lowercased types/modalities. Fail-closed for non-none effort and non-text
modalities remains.
…ttribution values

SDK form/query clients send "true"/"false"/0/1 for include_orchestration_trace;
route them through _coerce_optional_bool (parity with stream/store). Known
attribution dimensions with null or blank values omit rather than stringify.
…ings

SDK form/query clients send "true"/"false"/0/1 for routing.latency_tolerant
and stream_options flags; route them through _coerce_optional_bool. Empty
response_format.type alone omits. Responses top_logprobs accepts digit strings
when logprobs is true.
…ons digits

SDK clients send ROUTE/CONDUCT/AUTO and padded aliases; normalize via
strip+casefold. Embeddings dimensions digit strings type-check then still
fail closed as unsupported (not applied).
…ons digits

Strip+lower orchestration mode aliases so SDK-padded ROUTE/Conduct match.
Coerce embeddings dimensions digit strings and whole floats before the
existing fail-closed unsupported reject (type-honest invalid path).
SDK clients send 0/"0" when no tool rounds are requested; treat as omit
(parity with top_logprobs 0). Nonzero values still fail closed as
unsupported; bool is not an integer count.
@seonghobae
seonghobae enabled auto-merge (squash) August 16, 2026 23:32
@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 173 files, which is 73 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: b09788d1-4dea-4a09-8d57-5c5852df3c0c

📥 Commits

Reviewing files that changed from the base of the PR and between 6841b71 and 9313d6d.

📒 Files selected for processing (173)
  • conductor/tracks.md
  • contextual_orchestrator/cost_ledger.py
  • contextual_orchestrator/orchestrator.py
  • contextual_orchestrator/server.py
  • docs/architecture.md
  • evidence/cycle_iot_bool_coerce_attr_omit.txt
  • evidence/cycle_max_tool_calls_zero_omit.txt
  • evidence/cycle_mode_casefold_dimensions_coerce.txt
  • evidence/cycle_mode_casefold_embeddings_dimensions_coerce.txt
  • evidence/cycle_routing_latency_stream_options_bool_coerce.txt
  • fuzz/targets.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_bool_01_seed_str_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_response_format_json_schema_omit_real_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_digit_n_bool01_echo_logprobs_http_honesty.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_encoding_stream_logprobs_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_bool_coerce_http_honesty.py
  • tests/test_include_orchestration_trace_null_noop_http_honesty.py
  • tests/test_int_float_max_output_stop_ws_http_honesty.py
  • tests/test_json_schema_name_charset_http_honesty.py
  • tests/test_ledger_execution_identity_http_honesty.py
  • tests/test_max_tool_calls_zero_omit_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_metadata_key_nonempty_http_honesty.py
  • tests/test_mode_casefold_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_reasoning_format_modalities_casefold_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_routing_latency_stream_options_bool_coerce_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_service_tier_routing_null_omit_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_stream_options_unknown_null_reject_http_honesty.py
  • tests/test_streaming.py
  • tests/test_string_bool_numeric_coerce_http_honesty.py
  • tests/test_tip_reland_sdk_omit_http_honesty.py
  • tests/test_tip_reland_sdk_omit_persist_http_honesty.py
  • tests/test_tool_choice_auto_without_tools_noop_http_honesty.py
  • tests/test_tool_choice_function_call_casefold_http_honesty.py
  • tests/test_tool_choice_required_requires_tools_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_function_name_charset_http_honesty.py
  • tests/test_tool_function_null_fields_pop_http_honesty.py
  • tests/test_tool_resources_named_reject_http_honesty.py
  • tests/test_tool_strict_null_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.

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