Skip to content

fix(onboard): accept reasoning-mode models in the inference smoke probe - #3356

Closed
latenighthackathon wants to merge 2 commits into
NVIDIA:mainfrom
latenighthackathon:fix/inference-validate-thinking-models
Closed

fix(onboard): accept reasoning-mode models in the inference smoke probe#3356
latenighthackathon wants to merge 2 commits into
NVIDIA:mainfrom
latenighthackathon:fix/inference-validate-thinking-models

Conversation

@latenighthackathon

@latenighthackathon latenighthackathon commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #3341. The Option-3 compatible-endpoint smoke probe sent max_tokens=32 and required choices[0].message.content to be a non-empty string. Thinking-mode models like Qwen3.6 in vLLM with --reasoning-parser qwen3 exhaust the entire 32-token budget on the reasoning chain and return content=null with finish_reason="length", even though the endpoint is healthy and the round-trip succeeded. Onboard then rejected the response and exited at step [7/8] Validating inference.

Related Issue

Closes #3341

Changes

Two changes in the embedded smoke script in src/lib/onboard.ts:

  • Bump max_tokens from 32 to 256 so short reasoning chains have room to finish before content starts. 256 is still a cheap probe (~$0.001 on most paid providers for a single "say PONG") but gives a thinking model enough headroom to emit a content token.
  • Treat a non-empty message.reasoning or message.reasoning_content as a valid smoke signal when content is null or empty. vLLM emits reasoning (per the qwen3 reasoning parser, observed in the Cannot use thinking models like Qwen 3.6 27B - Nemoclaw validation request token length too small. #3341 trace) and OpenAI o1-style endpoints emit reasoning_content; both indicate the endpoint round-tripped a chat completion. The probe is a liveness check, not a response-shape contract, so a reasoning-only response is enough to declare the model reachable. The success line becomes INFERENCE_SMOKE_OK (reasoning-only response, N chars) so the distinction is visible in onboard logs.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Verification

  • npx prek run --all-files passes for the staged files.
  • Two new test cases in test/onboard.test.ts (#3341): one asserts the bumped max_tokens is in the generated script, one asserts the reasoning-fallback branch is present.
  • npx vitest run -t 'smoke|reasoning|max_tokens' test/onboard.test.ts — 6/6 pass (4 existing smoke tests + the 2 new ones).
  • Embedded Python parser exercised against four sample payloads (normal content, reasoning-only Qwen3.6 shape, reasoning_content o1 shape, empty response). The first three pass with INFERENCE_SMOKE_OK, the empty payload fails as expected. Output:
[OK] normal content -> exit=0: INFERENCE_SMOKE_OK PONG
[OK] reasoning only (Qwen3.6 case) -> exit=0: INFERENCE_SMOKE_OK (reasoning-only response, 11 chars)
[OK] reasoning_content (o1 style) -> exit=0: INFERENCE_SMOKE_OK (reasoning-only response, 3 chars)
[FAIL] empty response (should fail) -> exit=1: FAIL: no content or reasoning
  • npm run build:cli clean.
  • No secrets, API keys, or credentials committed.
  • Tests added for new behavior.

Rebased on current upstream/main (commit eb15e55e).


Signed-off-by: latenighthackathon latenighthackathon@users.noreply.github.com

Summary by CodeRabbit

  • Tests

    • Added targeted tests for the endpoint smoke script to verify robust response handling and reasoning-mode signals.
  • Improvements

    • Increased token allocation for smoke probes.
    • Strengthened response validation to tolerate malformed or reasoning-only outputs and avoid crashes on empty or unexpected structures.

Review Change Stack

@copy-pr-bot

copy-pr-bot Bot commented May 11, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The smoke probe for compatible-endpoint validation now budgets higher token limits and tolerates reasoning-only responses by accepting non-empty reasoning fields. Response parsing is defensive against malformed choices arrays. Tests cover token budget, reasoning acceptance, and safe choices handling.

Changes

Thinking Model Compatibility

Layer / File(s) Summary
Smoke Probe Token Budget
src/lib/onboard/compatible-endpoint-smoke.ts
Embedded Python payload increases max_tokens from 32 to 256 with comments explaining this prevents reasoning-chain models from exhausting budget and returning empty/length-terminated content.
Response Validator Logic
src/lib/onboard/compatible-endpoint-smoke.ts
Validation logic rewrites to defensively check choices shape and presence of message dict, then accepts success when either message.content is a non-empty string or when message.reasoning/message.reasoning_content is non-empty; failure cases emit detailed error and exit non-zero.
Test Coverage
src/lib/onboard/compatible-endpoint-smoke.test.ts
Three new tests verify the increased max_tokens for reasoning models, confirm reasoning-only responses are accepted as valid smoke signals with fallback field lookup and Python string validation, and ensure safe handling of empty or malformed choices arrays without unsafe indexing.

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A thinking model needed time to dream,
With budget bloated to 256 tokens' beam,
When reasoning flows where content lies still,
Our validator accepts with humble will,
No crashes on choices malformed or bare—
Just graceful checks with defensive care! 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: modifying the inference smoke probe to accept reasoning-mode models by increasing max_tokens and tolerating reasoning-only responses.
Linked Issues check ✅ Passed The PR fully addresses the coding requirements in #3341: increases max_tokens from 32 to 256, validates choices shape defensively, and treats non-empty message.reasoning/reasoning_content as valid smoke signals.
Out of Scope Changes check ✅ Passed All changes in both files are directly scoped to addressing #3341: test additions verify the max_tokens bump and reasoning-fallback logic, implementation changes only modify the smoke probe for reasoning-model compatibility.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/onboard.ts`:
- Around line 2579-2593: The parsing code can crash or mis-detect valid
responses: fix by guarding access to choices and by selecting a string-only
reasoning field. First, ensure you safely extract message by verifying
data.get("choices") is a non-empty list and that choices[0] is a dict before
calling .get (replace the current data.get("choices", [{}])[0] usage with an
explicit check for a list and len>0, then assign message = choices[0] if it's a
dict). Second, change the reasoning selection so you don't hide a string
reasoning_content behind a truthy non-string reasoning: explicitly test
message.get("reasoning_content") and message.get("reasoning") for str and
non-empty (prefer reasoning_content if it's a non-empty str, else reasoning if
it's a non-empty str) before treating it as a valid liveness signal; also ensure
content is only accepted if it's a str and non-empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d750ee9d-f538-4a9f-8895-63cc18cc13a6

📥 Commits

Reviewing files that changed from the base of the PR and between f66fb05 and 2e75cfe.

📒 Files selected for processing (2)
  • src/lib/onboard.ts
  • test/onboard.test.ts

Comment thread src/lib/onboard.ts Outdated
latenighthackathon added a commit to latenighthackathon/NemoClaw that referenced this pull request May 11, 2026
Two edge cases CodeRabbit flagged on PR NVIDIA#3356 (NVIDIA#3341 follow-up):

1. `data.get("choices", [{}])[0]` defaults to `[{}]` only when `choices` is
   missing; it still raised `IndexError` when the endpoint returned
   `choices: []` (which some vLLM error paths do). Guard explicitly:
   require a non-empty list whose first element is a dict before
   indexing; emit a clear failure when not.

2. `message.get("reasoning") or message.get("reasoning_content")`
   short-circuits on any truthy value. If `reasoning` was a non-string
   truthy value (e.g. `{}` or `[]` from a malformed provider response),
   the `or` would return it and a valid string `reasoning_content` was
   never seen. Iterate over both candidates and pick the first
   non-empty string, mirroring the `content` check shape.

Adds a third test case asserting the choices-empty guard pattern is
present in the generated script. The Python parser was exercised
against eight payloads (normal, reasoning-only, reasoning_content,
empty `choices`, missing `choices`, non-string `reasoning` masking
valid `reasoning_content`, non-dict `choices[0]`, empty payload) and
behaves as expected: three pass with a smoke-OK line, five fail
cleanly with a descriptive error.

Signed-off-by: latenighthackathon <support@latenighthackathon.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/onboard.test.ts (1)

706-722: ⚡ Quick win

Tighten parser-hardening assertions to match the stated fix scope

At Line 706 and Line 714, the new tests check key branches, but they don’t explicitly lock in the “non-empty string only” and “non-dict choice entry” guards described in the PR notes. Adding those assertions would prevent silent regressions in parser validation behavior.

Suggested test additions
   it("accepts a reasoning-only response as a valid smoke signal for thinking models (`#3341`)", () => {
     const script = buildCompatibleEndpointSandboxSmokeScript("Qwen/Qwen3.6-27B");

     assert.match(script, /message\.get\("reasoning"\)/);
     assert.match(script, /message\.get\("reasoning_content"\)/);
+    // Ensure fallback only accepts non-empty string reasoning payloads.
+    assert.match(script, /isinstance\([^,]+,\s*str\)/);
+    assert.match(script, /\.strip\(\)/);
     assert.match(script, /reasoning-only response/);
   });

   it("guards against empty or malformed choices arrays in the smoke parser (`#3341`)", () => {
     const script = buildCompatibleEndpointSandboxSmokeScript("Qwen/Qwen3.6-27B");

     assert.match(script, /not isinstance\(choices, list\) or not choices/);
+    // Ensure first choice shape is validated before message access.
+    assert.match(script, /not isinstance\([^,]+,\s*dict\)/);
     assert.doesNotMatch(script, /data\.get\("choices", \[\{\}\]\)\[0\]/);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/onboard.test.ts` around lines 706 - 722, Update the two smoke-parser
tests that use buildCompatibleEndpointSandboxSmokeScript to also assert the
tightened guards: for the reasoning-only branch assert the parser checks that
reasoning is a non-empty string (e.g., match a pattern like 'not
isinstance\\(reasoning, str\\) or not reasoning' or similar) and for the choices
guard assert there is an explicit check that each choice is a dict (e.g., match
'not all\\(isinstance\\(c, dict\\) for c in choices\\)' or 'not
isinstance\\(choice, dict\\)') so the tests lock in both the “non-empty string
only” and “non-dict choice entry” validations in the smoke parser used by
buildCompatibleEndpointSandboxSmokeScript.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/onboard.test.ts`:
- Around line 706-722: Update the two smoke-parser tests that use
buildCompatibleEndpointSandboxSmokeScript to also assert the tightened guards:
for the reasoning-only branch assert the parser checks that reasoning is a
non-empty string (e.g., match a pattern like 'not isinstance\\(reasoning, str\\)
or not reasoning' or similar) and for the choices guard assert there is an
explicit check that each choice is a dict (e.g., match 'not
all\\(isinstance\\(c, dict\\) for c in choices\\)' or 'not isinstance\\(choice,
dict\\)') so the tests lock in both the “non-empty string only” and “non-dict
choice entry” validations in the smoke parser used by
buildCompatibleEndpointSandboxSmokeScript.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5d4a7388-e7a1-42de-bf37-c233676eca29

📥 Commits

Reviewing files that changed from the base of the PR and between 2e75cfe and 3952f6c.

📒 Files selected for processing (2)
  • src/lib/onboard.ts
  • test/onboard.test.ts

@wscurran

Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this detailed PR to fix the onboard issue with thinking models like Qwen3.6. This change aims to improve the inference smoke probe by bumping max_tokens to 256 and treating non-empty message.reasoning or message.reasoning_content as a valid signal.


Related open issues:

latenighthackathon added a commit to latenighthackathon/NemoClaw that referenced this pull request May 11, 2026
…guards

CodeRabbit nit on PR NVIDIA#3356: the existing smoke-parser tests asserted the
high-level branches (reasoning fallback present, choices-list guard
present) but did not lock in the two finer guarantees the prior fixup
explicitly added:

- The reasoning fallback only treats a non-empty STRING reasoning
  payload as a liveness signal (the prior `or`-chain regression where
  a truthy non-string `reasoning` masked a valid string
  `reasoning_content`). Add `assert.match(script, /isinstance(value,
  str) and value.strip()/)` to the reasoning-only test so a future
  refactor that drops the isinstance check fails CI.

- The choices guard validates `choices[0]` is a dict before calling
  `.get("message", {})`, which is what catches the
  `choices=["str"]` / `choices=[null]` shapes from misbehaving
  providers. Add `assert.match(script, /not isinstance(choices[0],
  dict)/)` so the same regression in the choice-shape path is caught.

Source-only test, no behavior change. The PR NVIDIA#3356 source already
satisfies both assertions; this commit just makes the test surface
match the prose in the original commit message.

Signed-off-by: latenighthackathon <support@latenighthackathon.com>
@cv cv closed this May 12, 2026
@cv cv reopened this May 12, 2026
@latenighthackathon
latenighthackathon force-pushed the fix/inference-validate-thinking-models branch from 12c9526 to 9218417 Compare May 12, 2026 19:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/onboard/compatible-endpoint-smoke.test.ts`:
- Around line 57-59: The negative assertion in the test currently checks for the
exact string '"max_tokens": 32,' which misses variants without the trailing
comma; update the assertion that uses the variable script in
compatible-endpoint-smoke.test.ts (the expect(script).not... line) to use a
regex-based negative match that rejects any occurrence of "max_tokens" followed
by optional whitespace, a colon, optional whitespace, and the value 32 (so it
fails for both '"max_tokens": 32,' and '"max_tokens": 32'), leaving the positive
check for 256 intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 3422c17a-424f-4fc0-a50c-3b43e524700c

📥 Commits

Reviewing files that changed from the base of the PR and between 12c9526 and 9218417.

📒 Files selected for processing (2)
  • src/lib/onboard/compatible-endpoint-smoke.test.ts
  • src/lib/onboard/compatible-endpoint-smoke.ts

Comment thread src/lib/onboard/compatible-endpoint-smoke.test.ts
latenighthackathon added a commit to latenighthackathon/NemoClaw that referenced this pull request May 12, 2026
CodeRabbit nit on PR NVIDIA#3356: the negative assertion checked the literal
substring `"max_tokens": 32,` which only catches the comma-trailed
form. A regression to `"max_tokens": 32` (no trailing comma — e.g. if
it were the last field) would slip past the guard. Switch to a regex
that anchors on the value with `\b` so both forms fail the test.

Signed-off-by: latenighthackathon <support@latenighthackathon.com>
Closes NVIDIA#3341. The Option-3 compatible-endpoint smoke probe sent
max_tokens=32 and required choices[0].message.content to be a non-empty
string. Thinking-mode models like Qwen3.6 in vLLM with
--reasoning-parser exhaust the entire 32-token budget on the reasoning
chain and return content=null with finish_reason=length, even though
the endpoint is healthy and the round-trip succeeded. Validation then
rejected the response and onboard exited without the user having a way
to use the model.

Three changes in the embedded smoke script (now in
`src/lib/onboard/compatible-endpoint-smoke.ts` after the NVIDIA#3297 extract):

1. Bump max_tokens 32 -> 256 so short reasoning chains have room to
   finish before content starts. 256 is still a cheap probe (~$0.001
   on most paid providers for a single "say PONG") but gives a
   thinking model enough headroom to land a content token.

2. Treat a non-empty message.reasoning or message.reasoning_content as
   a valid smoke signal when content is null/empty. vLLM emits
   "reasoning" (per the qwen3 reasoning parser, observed in the NVIDIA#3341
   trace) and OpenAI o1-style endpoints emit "reasoning_content";
   both indicate the endpoint round-tripped a chat completion. Use a
   next() over both candidates so a truthy non-string value in one
   field cannot mask a valid string in the other.

3. Harden the parser against empty or malformed `choices` arrays:
   the previous `data.get("choices", [{}])[0]` defaulted only when
   `choices` was missing; it raised IndexError on `choices: []` and
   AttributeError on `choices: ["str"]`. Validate that choices is a
   non-empty list whose first element is a dict before indexing, and
   coerce a missing message to an empty dict.

Three new test cases in `compatible-endpoint-smoke.test.ts` assert the
bumped max_tokens, the reasoning fallback shape (including the
isinstance+strip guard), and the choices-array hardening.

Signed-off-by: latenighthackathon <support@latenighthackathon.com>
CodeRabbit nit on PR NVIDIA#3356: the negative assertion checked the literal
substring `"max_tokens": 32,` which only catches the comma-trailed
form. A regression to `"max_tokens": 32` (no trailing comma — e.g. if
it were the last field) would slip past the guard. Switch to a regex
that anchors on the value with `\b` so both forms fail the test.

Signed-off-by: latenighthackathon <support@latenighthackathon.com>
@latenighthackathon
latenighthackathon force-pushed the fix/inference-validate-thinking-models branch from cccfa80 to f1e6bf4 Compare May 14, 2026 01:47
@latenighthackathon

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by #3514, which landed 2026-05-13 and addresses the same root cause #3341.

#3514 takes a more thorough approach: it distinguishes route/config failures from model-budget failures, retries with a larger token budget (256 → 1024) when reasoning_content exhausts the initial budget, and adds an executable regression test for the MiniMax-shaped reasoning-only response. The compatible-endpoint smoke probe was also refactored in #3297 since this PR opened, so a rebase would have to be substantially reworked anyway.

Thanks for the review attention here. Cheers!

@latenighthackathon
latenighthackathon deleted the fix/inference-validate-thinking-models branch May 18, 2026 05:10
@wscurran wscurran added area: inference Inference routing, serving, model selection, or outputs bug-fix PR fixes a bug or regression feature PR adds or expands user-visible functionality and removed fix feature PR adds or expands user-visible functionality labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: inference Inference routing, serving, model selection, or outputs bug-fix PR fixes a bug or regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot use thinking models like Qwen 3.6 27B - Nemoclaw validation request token length too small.

3 participants