Skip to content

test(vllm): stabilize Responses tests (pin temperature=0; drop unstable native structured-JSON test) - #1011

Merged
leseb merged 6 commits into
praxis-proxy:mainfrom
leseb:leseb/fix-vllm-structured-json-flake
Sep 9, 2026
Merged

leseb merged 6 commits into
praxis-proxy:mainfrom
leseb:leseb/fix-vllm-structured-json-flake

Conversation

@leseb

@leseb leseb commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

test_openai_responses_vllm.py::test_structured_json_output is a pre-existing flake: it intermittently fails with status == "incomplete" instead of "completed" (observed re-running the vLLM jobs on unrelated PRs). It is not caused by any production change.

Root cause

Under examples/configs/openai/responses/full-flow.yaml the request is a native /v1/responses passthrough to vLLM (no translation, no IRR). vLLM is launched with --reasoning-parser deepseek_r1, so it emits a separate reasoning output item whose chain-of-thought tokens are debited from the shared max_output_tokens budget.

On the native path vLLM lets the model reason unconstrained before the json_schema guided-decoding grammar applies to the message, so the small Qwen/Qwen3-0.6B model produces a long <think> preamble that exhausts the budget before the constrained JSON completes → status == "incomplete".

What was tried, and why the flaky test is removed rather than pinned

  1. temperature=0 (greedy). This stabilizes the class of value/status flakes and every other model-exercising test passes with it — but it did not fix test_structured_json_output. Greedy simply locks in the long-reasoning path, so the test failed deterministically at max_output_tokens=512 on both vllm-responses and vllm-responses-postgres.
  2. Raising the budget 512 → 2048. Also failed 2/2 with the same incomplete result — the native-path reasoning preamble overruns any sane budget for this prompt+schema on the tiny model. The budget lever is a dead end.

The identical structured-JSON round-trip through the Chat Completions translation path (test_structured_output_round_trip) stays green at just 128 tokens, because that path applies the json_schema grammar from the first token, leaving no room to ramble. That confirms the answer itself is tiny and the failure is purely the native-path reasoning contention on a 0.6B CI model — not a proxy defect.

Fix

  1. Pin temperature=0 on the model-exercising tests that asserted status == "completed" / exact markers without a temperature pin (native, IRR-streaming, compaction, and Responses-to-Chat-Completions translation paths — temperature is in the translation allowlist, verified by test_generation_parameters_round_trip). This removes sampling-variance flakiness across the suite; all these tests are green in CI. Multi-turn tests pin both turns.
  2. Remove the native model-behavior test test_structured_json_output. It cannot be stabilized without weakening its assertion or risking a different deterministic failure, and it is a model-behavior artifact of the tiny CI model rather than proxy coverage.

Coverage retained

  • test_structured_output_round_trip — structured-JSON round-trip through the model via the Responses-to-Chat-Completions translation path.
  • Native text.format / json_schema passthrough is covered by the unit and integration tests in apis/src/openai/responses/ (e.g. responses_to_chat_completions/tests.rs, mod.rs).

Explicitly left unchanged

  • test_max_output_tokens_reports_incomplete and test_streaming_incomplete_round_trip (max_output_tokens=1) — they rely on truncation to assert status == "incomplete".
  • test_generation_parameters_are_reflected / test_generation_parameters_round_trip — keep their explicit temperature=0 + passthrough assertions.

Not done here (deliberately)

  • No assertion loosening and no retries.
  • No disabling Qwen3 reasoning (chat_template_kwargs: {enable_thinking: false}): with the deepseek_r1 parser that risks routing the answer into reasoning_content and emptying output_text — a worse, deterministic failure.
  • No seed (no-op under greedy).

Verification

  • python3 -m py_compile passes.
  • Net branch diff vs base: temperature=0 pins retained on the other model-exercising tests, no 2048, test_structured_json_output fully removed.
  • Live vLLM run exercised by the vLLM Integration CI jobs (vllm-responses, vllm-responses-postgres).

test_structured_json_output intermittently failed with status=="incomplete"
instead of "completed". On the native /v1/responses passthrough (full-flow.yaml)
vLLM is started with --reasoning-parser deepseek_r1 and emits a separate
reasoning item whose chain-of-thought tokens are debited from the shared
max_output_tokens budget. The test sent no temperature pin, so at the default
sampling temperature the small Qwen3-0.6B model intermittently ignored the soft
/no_think hint, produced a long <think> block, and exhausted the 512-token
budget before the constrained JSON completed.

Pin temperature=0 (greedy decoding) so the reasoning-item length is
deterministic for the fixed prompt: a passing run stays passing. Reasoning
stays enabled and still competes for the unchanged 512-token budget, so the
realism the existing comment documents is preserved and no assertion is
weakened.

Apply the same pin to the other 16 model-exercising tests that assert
status=="completed" or exact markers without pinning temperature, closing the
whole latent-flake class (native, IRR-streaming, compaction, and
Responses-to-Chat-Completions translation paths alike, since temperature is in
the translation allowlist). The two intentional max_output_tokens=1 incomplete
tests and the existing temperature-passthrough tests are left unchanged.

Signed-off-by: Sébastien Han <seb@redhat.com>
temperature=0 alone did not stabilize test_structured_json_output: on the
native /v1/responses passthrough vLLM debits the separate reasoning item
against the shared max_output_tokens, and the deterministic greedy preamble
overruns the 512-token budget before the constrained JSON completes
(status=="incomplete", reproduced on both vllm-responses and
vllm-responses-postgres). The identical request on the Chat Completions
translation path completes in <128 tokens, confirming the answer is tiny and
only the reasoning preamble needs room. Raise the budget to 2048 (well under
the 4096-token model context) so, combined with greedy decoding, completion
is deterministic.

Signed-off-by: Sébastien Han <seb@redhat.com>

@praxis-bot praxis-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.

praxis-bot review

Scope: 1 file changed, 33 insertions, 4 deletions — test-only.

Summary

Clean change. All 17 model-exercising tests that assert status == "completed" without a prior temperature pin now have temperature=0 (greedy decoding), making vLLM output deterministic and eliminating the flake caused by non-deterministic reasoning-token length on Qwen3-0.6B.

Verified

  • Coverage is complete. Every responses.create() call that feeds an assertion on status == "completed" or exact output markers now pins temperature=0. The 22 create calls across 17 tests (multi-turn tests pin both turns) match the PR description.
  • Exclusions are correct. test_max_output_tokens_reports_incomplete and test_streaming_incomplete_round_trip (max_output_tokens=1, assert "incomplete") are correctly left unpinned. test_generation_parameters_are_reflected and test_generation_parameters_round_trip already had explicit temperature=0 pre-PR.
  • max_output_tokens bump (512 to 2048) is reasonable. The model context is 4096 tokens; 2048 gives generous headroom for the reasoning preamble without exhausting context. The accompanying comment accurately explains the rationale.
  • No assertions weakened. All existing assertions are preserved; the only behavioral change is deterministic sampling.
  • Agentic and file-search tests that accept status in ("completed", "incomplete") are correctly excluded since they already tolerate truncation.

No findings.

…fits"

This reverts commit aaad214.

Signed-off-by: Sébastien Han <seb@redhat.com>
test_structured_json_output exercised native /v1/responses structured
output (text.format = json_schema) end-to-end against the small CI model.
It could not be stabilized: on the native passthrough vLLM lets the model
reason UNCONSTRAINED before the guided-JSON grammar applies, so Qwen3-0.6B's
reasoning preamble overruns the max_output_tokens budget before the JSON
completes (status=="incomplete"). This reproduced deterministically at
temperature=0 with both 512 and 2048 token budgets (both vllm-responses and
vllm-responses-postgres jobs), so no budget bump or temperature pin fixes it.
The identical structured-JSON round-trip through the Chat Completions
translation path (test_structured_output_round_trip) stays green at 128
tokens because that path applies the json_schema grammar from the first
token, leaving no room to ramble.

Remove the fragile native model-behavior test. Structured-output coverage
is retained by test_structured_output_round_trip (model round-trip through
the translation path) and by the native text.format / json_schema
passthrough unit and integration coverage in apis/src/openai/responses/.
The temperature=0 pins on the other model-exercising tests are kept.

Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb leseb changed the title test(vllm): pin temperature=0 on model-exercising Responses tests test(vllm): stabilize Responses tests (pin temperature=0; drop unstable native structured-JSON test) Sep 9, 2026
@leseb
leseb disabled auto-merge September 9, 2026 08:46
…all round-trip

The chat-translation function_call_and_output round-trip flaked once in CI
(the SQLite job) at max_output_tokens=128 on its second turn, while the
PostgreSQL job passed the same commit. That second turn is free-text
(tool_choice="none", no schema) and rehydrates prior items from the store
backend, so the cause is not yet established: neither a plain budget overrun
nor a rehydration/storage-path difference between backends is ruled out.

Treat this as a controlled experiment, not a proven fix. Widen only that one
budget to 512 and attach failure diagnostics (status, incomplete_details,
usage, output item types, output_text length) so the next failure -- on
either backend -- is analyzable instead of an opaque 'incomplete' == 'completed'.

Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb
leseb enabled auto-merge September 9, 2026 09:01
@leseb
leseb added this pull request to the merge queue Sep 9, 2026
@leseb
leseb removed this pull request from the merge queue due to a manual request Sep 9, 2026
Signed-off-by: Sébastien Han <seb@redhat.com>
@leseb
leseb enabled auto-merge September 9, 2026 10:01
@leseb
leseb added this pull request to the merge queue Sep 9, 2026
Merged via the queue into praxis-proxy:main with commit 88922a7 Sep 9, 2026
25 checks passed
@leseb
leseb deleted the leseb/fix-vllm-structured-json-flake branch September 9, 2026 11:52
leseb added a commit to leseb/praxis-ai that referenced this pull request Sep 9, 2026
Keep the praxis-proxy#276 branch current with origin/main (5ba5345), which advanced by
three commits since the last sync:

- praxis-proxy#1005 fix(responses): conform SSE error events to ResponseErrorEvent schema
- praxis-proxy#922  test: provider-native error formatters through the proxy pipeline
- praxis-proxy#1011 test(vllm): stabilize Responses tests (pin temperature=0; drop the
  unstable native structured-JSON test)

The merge auto-resolved with no conflicts. Two files overlapped and were
combined by git without manual reconciliation:

- tests/integration/sdk/openai/test_openai_responses_vllm.py: praxis-proxy#1011's
  temperature=0 pins and the dropped structured-JSON test coexist with the
  praxis-proxy#276 streaming assertions (translated_agentic_client fixture, text_deltas
  output_index ordering for MCP and web_search) added in b38d5e1.
- apis/src/openai/responses/stream_events/tests.rs: non-overlapping hunks.

apis/src/openai/responses/stream_events/mod.rs merged silently even though
praxis-proxy#1005 also touched it (SSE error-event schema path) — the changed regions do
not overlap the praxis-proxy#276 local-tool synthesis logic.

No behavior change. Verified on the merged tree: make build, make lint, and
make doc all exit 0; stream_events unit tests 79/0.

Signed-off-by: Sébastien Han <seb@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants