Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/lib/onboard/compatible-endpoint-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,38 @@ describe("compatible endpoint sandbox smoke helpers", () => {
expect(script).toContain("MODEL='provider/model'\\'''");
});

it("budgets enough max_tokens for reasoning-mode models (#3341)", () => {
const script = buildCompatibleEndpointSandboxSmokeScript("Qwen/Qwen3.6-27B");

expect(script).toContain('"max_tokens": 256');
// Regex (not substring) so a regression to `"max_tokens": 32` without the
// trailing comma also fails the test, per CR review on PR #3356.
expect(script).not.toMatch(/"max_tokens":\s*32\b/);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

// Fallback lookup walks both `reasoning` and `reasoning_content` and only
// accepts non-empty STRING payloads, so a truthy non-string value in one
// field cannot mask a valid string in the other.
expect(script).toContain('message.get("reasoning")');
expect(script).toContain('message.get("reasoning_content")');
expect(script).toContain("reasoning-only response");
expect(script).toMatch(/isinstance\(value,\s*str\)\s+and\s+value\.strip\(\)/);
});

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

// The parser must verify choices is a non-empty list of dicts before
// indexing, instead of relying on data.get("choices", [{}])[0] which
// crashed on choices=[] with IndexError.
expect(script).toContain("not isinstance(choices, list) or not choices");
expect(script).toContain("not isinstance(choices[0], dict)");
expect(script).not.toContain('data.get("choices", [{}])[0]');
});

it("wraps the script as a base64 decoded temporary shell command", () => {
const command = buildCompatibleEndpointSandboxSmokeCommand("nvidia/model");

Expand Down
45 changes: 36 additions & 9 deletions src/lib/onboard/compatible-endpoint-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,17 @@ python3 - "$MODEL" >"$payload_file" <<'PYPAYLOAD'
import json
import sys

# max_tokens=256 covers short reasoning-chain models (e.g. Qwen3.6 in thinking
# mode via vLLM with --reasoning-parser) that would otherwise spend their entire
# budget on the reasoning field and return content=null with finish_reason=length
# during the smoke probe. See GH #3341.
model = sys.argv[1]
print(json.dumps({
"model": model,
"messages": [
{"role": "user", "content": "Reply with exactly: PONG"}
],
"max_tokens": 32,
"max_tokens": 256,
}))
PYPAYLOAD

Expand Down Expand Up @@ -121,16 +125,39 @@ except Exception as exc:
print("inference.local returned non-JSON response: %s; body=%s" % (exc, body), file=sys.stderr)
sys.exit(1)

content = (
data.get("choices", [{}])[0]
.get("message", {})
.get("content")
)
if not isinstance(content, str) or not content.strip():
print("inference.local response did not contain choices[0].message.content: %s" % json.dumps(data)[:1000], file=sys.stderr)
choices = data.get("choices")
if not isinstance(choices, list) or not choices or not isinstance(choices[0], dict):
print("inference.local response did not contain a choices[0] dict: %s" % json.dumps(data)[:1000], file=sys.stderr)
sys.exit(1)
message = choices[0].get("message", {})
if not isinstance(message, dict):
message = {}

content = message.get("content")
if isinstance(content, str) and content.strip():
print("INFERENCE_SMOKE_OK " + content.strip()[:200])
sys.exit(0)

# Some reasoning-mode models (e.g. Qwen3.6 via vLLM --reasoning-parser, OpenAI
# o1-style endpoints) return content=null with the response carried under
# "reasoning" or "reasoning_content". Treat that as a valid liveness signal for
# the smoke probe: the endpoint round-tripped a chat completion, just under a
# different field name. Pick the first non-empty string from either field so a
# non-string value in one does not mask a valid string in the other. See GH #3341.
reasoning_text = next(
(
value.strip()
for value in (message.get("reasoning"), message.get("reasoning_content"))
if isinstance(value, str) and value.strip()
),
None,
)
if reasoning_text:
print("INFERENCE_SMOKE_OK (reasoning-only response, %d chars)" % len(reasoning_text))
sys.exit(0)

print("INFERENCE_SMOKE_OK " + content.strip()[:200])
print("inference.local response did not contain choices[0].message.content: %s" % json.dumps(data)[:1000], file=sys.stderr)
sys.exit(1)
PYRESP
`.trim();
}
Expand Down