Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/fixes/10986-reasoning-only-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- fix(command-code): surface reasoning-only output as content when a model emits no text-delta (#10986)
5 changes: 3 additions & 2 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,10 @@
"src/sse/handlers/chatHelpers.ts": 1019,
"src/shared/middleware/chatBodyAdmission.ts": 1005,
"_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file).",
"open-sse/executors/commandCode.ts": 1038,
"open-sse/executors/commandCode.ts": 1059,
"_rebaseline_2026_08_21_10859_vision_bridge_catalog": "#10859 own growth (Vision Bridge fixes #10808/#10809): src/lib/modelCapabilities.ts 1006->1016 (+10, cmd/gpt-5.3-codex* text-only capability resolution) and open-sse/executors/commandCode.ts 988->1023 (+35, Command Code wire-model normalization for bare ids + reasoning field fallback for opencode-routed gateways). Cohesive bug fixes at the existing capability-resolution / executor chokepoints; not extractable mid-fix. Covered by tests/unit/model-capabilities-command-code-codex-textonly-10703.test.ts, tests/unit/command-code-vision.test.ts, tests/unit/opencode-mimo-reasoning-details-nonstream.test.ts. Pushed directly to release (own-session miss: the original rebaseline was made in a throwaway validation worktree and never landed on the PR branch or the release before merge).",
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts."
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts.",
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming)."
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
Expand Down
23 changes: 22 additions & 1 deletion open-sse/executors/commandCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,7 @@ function createStreamResponse(
const decoder = new TextDecoder();
let buffer = "";
let sentRole = false;
let sentContent = false;
let closed = false;
const state: AggregateState = {
content: "",
Expand Down Expand Up @@ -772,7 +773,10 @@ function createStreamResponse(
switch (event.type) {
case "text-delta": {
const text = stringValue(event.text) || "";
if (text) controller.enqueue(sse(chatCompletionChunk(id, model, { content: text })));
if (text) {
sentContent = true;
controller.enqueue(sse(chatCompletionChunk(id, model, { content: text })));
}
state.content += text;
break;
}
Expand Down Expand Up @@ -810,6 +814,12 @@ function createStreamResponse(
break;
case "finish": {
state.finishReason = mapFinishReason(event.finishReason);
// If the model only produced reasoning-delta events (no text-delta), the
// client-visible stream would otherwise end with no content. Emit one
// content delta carrying the accumulated reasoning text (#10986).
if (!sentContent && state.reasoning && state.toolCalls.length === 0) {
controller.enqueue(sse(chatCompletionChunk(id, model, { content: state.reasoning })));
}
controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason)));
// Emit a standards-compliant usage-only chunk (choices: []) before
// [DONE] when upstream reported usage. stream.ts's extractUsage
Expand Down Expand Up @@ -857,6 +867,9 @@ function createStreamResponse(
if (!closed) {
if (!sentRole)
controller.enqueue(sse(chatCompletionChunk(id, model, { role: "assistant" })));
if (!sentContent && state.reasoning && state.toolCalls.length === 0) {
controller.enqueue(sse(chatCompletionChunk(id, model, { content: state.reasoning })));
}
controller.enqueue(sse(chatCompletionChunk(id, model, {}, state.finishReason)));
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
Expand Down Expand Up @@ -947,6 +960,14 @@ async function createJsonResponse(
}

const message: JsonRecord = { role: "assistant", content: state.content };
// Some Command Code models emit the whole answer as reasoning-delta events and
// never a text-delta. When that leaves content empty, surface the reasoning text
// as content too (#10986) so OpenAI-compatible clients get a usable answer. Keep
// reasoning_content populated as well for reasoning-aware clients, and do not
// override content when real text OR tool calls are present.
if (!state.content && state.reasoning && state.toolCalls.length === 0) {
message.content = state.reasoning;
}
if (state.reasoning) message.reasoning_content = state.reasoning;
if (state.toolCalls.length > 0) message.tool_calls = state.toolCalls;

Expand Down
62 changes: 62 additions & 0 deletions tests/unit/command-code-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,68 @@ test("Command Code data: SSE lines aggregate into non-stream ChatCompletion JSON
});
});

test("Command Code reasoning-only output falls back to reasoning as content (non-stream)", async () => {
globalThis.fetch = async () =>
commandCodeStream(
[
{ type: "reasoning-delta", text: "The user wants 79874+93658. " },
{ type: "reasoning-delta", text: "That equals 173532." },
{
type: "finish",
finishReason: "stop",
totalUsage: { inputTokens: 20, outputTokens: 64, outputTokenDetails: { reasoningTokens: 61 } },
},
],
{ sse: true }
);

const { response } = await getExecutor("command-code").execute({
model: "meta/muse-spark-1.2-contributor",
stream: false,
credentials: { apiKey: "cc_test_key" },
body: { messages: [{ role: "user", content: "Calculate 79874+93658, and reply with the result only." }] },
});

const json = await response.json();
const message = json.choices[0].message;
// Regression #10986: when the model emits only reasoning-delta events (never a
// text-delta), content must fall back to the reasoning text instead of "" (which
// OpenAI-compatible clients treat as null/no answer).
assert.equal(message.content, "The user wants 79874+93658. That equals 173532.");
// reasoning_content must STAY populated for reasoning-aware clients.
assert.equal(message.reasoning_content, "The user wants 79874+93658. That equals 173532.");
});

test("Command Code reasoning-only output emits a content delta chunk when streaming", async () => {
globalThis.fetch = async () =>
commandCodeStream(
[
{ type: "reasoning-delta", text: "The result is 173532." },
{ type: "finish", finishReason: "stop" },
],
{ sse: true }
);

const { response } = await getExecutor("command-code").execute({
model: "meta/muse-spark-1.2-contributor",
stream: true,
credentials: { apiKey: "cc_test_key" },
body: { messages: [{ role: "user", content: "Calcular 79874+93658" }] },
});

const sse = await response.text();
assert.match(sse, /data: \[DONE\]/);
const chunks = parseSsePayloads(sse);
assert.equal(chunks[0].choices[0].delta.role, "assistant");
// Regression #10986: the reasoning-only stream must emit a content delta when it
// otherwise ends with no content. reasoning_content stays present too.
const contentDelta = chunks.find((c) => c.choices[0].delta.content !== undefined);
assert.equal(contentDelta.choices[0].delta.content, "The result is 173532.");
const reasoningDelta = chunks.find((c) => c.choices[0].delta.reasoning_content !== undefined);
assert.equal(reasoningDelta.choices[0].delta.reasoning_content, "The result is 173532.");
assert.equal(chunks.at(-1).choices[0].finish_reason, "stop");
});

test("Command Code executor surfaces upstream and streamed errors", async () => {
globalThis.fetch = async () =>
new Response("bad key", { status: 401, statusText: "Unauthorized" });
Expand Down
Loading