fix(api): fail-closed whitespace-only mode on tools passthrough - #629
fix(api): fail-closed whitespace-only mode on tools passthrough#629cursor[bot] wants to merge 18 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.
…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.
…age honesty Null include_usage/include_obfuscation stay omit-equivalent, but unknown stream_options keys no longer become no-ops just because their value is null. Weight, prefix, refusal, annotations, developer role, empty user/system content, and participant name now use the same named errors on the tools passthrough path as on orchestration. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Hoist stream, required model, stream_options, and temperature/top_p range checks before proxy_completion so a tools or response_format body cannot return a billed JSON completion when the SDK asked for SSE, or silently pick a pool model when model is omitted. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Call the orchestration message, max_tokens, attribution, and routing validators before proxy_completion so a tools or response_format body cannot bill a completion with no prompt, crash on a non-object entry, or silently drop unknown spend/routing keys. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Passthrough has no batch job plane. Reject routing.channel=batch and latency_tolerant=true before proxy_completion so a tool-calling body cannot bill a silent sync completion. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Hoist the orchestration request-knob validators before proxy_completion so a tools or response_format body cannot bill a completion that silently drops seed, stop, n>1, logprobs, logit_bias, or out-of-range penalties. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
stream_chat defaulted to 0.2 even after the HTTP path wrote default_temperature from the request. A streamed invoice summary at temperature=0.8 no longer silently falls back to 0.2. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
… proxy SDK optional defaults serialize omitted fields as JSON null. Accepting those keys without popping them is not omit-equivalent: proxy_completion forwards the body and several providers reject a null JSON Schema object. Pop the keys in place, keep non-null wrong types on invalid_tools, and assert omit-real via mock echo on chat and Responses. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Hoist mode/orchestration and include_orchestration_trace before proxy_completion. tools + mode=explode and include_orchestration_trace="yes" were 200 on #601. mode=conduct and include_orchestration_trace=true also fail closed — this path has no Conductor workflow or trusted-trace plane. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Reuse the orchestration or-chain so tools + mode=" " is invalid_mode instead of a billed omit. Add live HTTP cases for mode=route, orchestration_mode=explode, and response_format+conduct. 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. |
There was a problem hiding this comment.
Mixed aliases bill a completion on this head
Whitespace-only mode is correctly invalid_mode when it is the first truthy alias. Collapsing orchestration / orchestration_mode / mode into one or chain reintroduces a billed omit.
Live HTTP on this head (e3bffe0):
{"model": "mock-planner", "messages": [{"role": "user", "content": "look up invoice 4419"}], "tools": [{"type": "function", "function": {"name": "lookup_balance", "parameters": {"type": "object"}}}], "orchestration": "route", "mode": "conduct"}returns 200 with choices — a billed chat.completion. The same hide applies to orchestration=route plus mode: " ".
The orchestration path may use or to pick a single mode. Passthrough still has to reject every alias that asked for a workflow this path cannot run (Nielsen et al., 2025; Xu et al., 2025).
The defect is contextual_orchestrator/server.py _validate_chat_passthrough_orchestration_controls: body.get("orchestration") or body.get("orchestration_mode") or body.get("mode"). orchestration=route is truthy, so mode=conduct never reaches _validate_mode.
Buyer next action: omit mode or send auto/route. Do not send spaces, mode=conduct, or mixed orchestration=route plus mode=conduct.
Do not merge this head. Repair is per-key checks (JSON null/"" omit; whitespace and conduct fail closed on each key) plus live HTTP for the mixed body. Successor unique is #647. If the SSE tip is the landing stack, prefer #640 — it already keeps per-key checks. Do not merge #601 / #613 / #629 in parallel.
Sent by Cursor Automation: Fix Issues


Summary
Successor to #613 at
37f1224. Unique commite3bffe0.#613 hoisted mode/trace checks before
proxy_completion, but_validate_chat_passthrough_orchestration_controlsskipped whitespace-onlymode/orchestration/orchestration_mode(not candidate.strip()→ continue). The orchestration path usesbody.get("orchestration") or body.get("orchestration_mode") or body.get("mode") or "auto"and then_validate_mode. A space-only string is truthy there, so the same body is400 invalid_modewithout tools and a billedchat.completionwith tools.orchain so whitespace-only mode isinvalid_modeon passthrough.mode: " "→ 400; tools +mode=route→ 200/choices;orchestration_mode=explode;response_format+mode=conduct.mode=routealongsideauto/ omit.Buyer next action: omit
modeor sendauto/route; do not send whitespace-onlymode. Omitmode=conductandinclude_orchestration_trace=trueon tool-calling requests. Keep omittingseed,stop,n>1, andlogprobs.Do not merge #601 or #613 in parallel. Prefer this head. Merge only after Full unit + Semgrep are green and an independent non-author review lands.
Test plan
python3 tests/test_passthrough_mode_trace_http_honesty.pypython3 tests/test_chat_orchestration_mode_http_honesty.pypython3 tests/test_chat_include_orchestration_trace_http_honesty.pypython3 tests/test_include_orchestration_trace_null_noop_http_honesty.pypython3 tests/test_paper_contracts.pytest_self_check.pytest_conventions.pytest_api_contract.pytest_product_planning_contract.pyDocs
docs/rest_api_design.md,docs/architecture.mdrecord the whitespaceor-chain andmode=routeno-op.References