Repository navigation
400 reasoning_text in thinking mode must be passed back — triggered by runtime-synthesized assistant turn after a background shell exits #5145
Description
Describe the bug
A session using a custom OpenAI-compatible provider with wireApi: "responses" (DeepSeek, model deepseek-flash) is killed by an HTTP 400 on the model call that immediately follows a runtime-synthesized, model-less assistant turn.
The trigger is the background-shell-completion flow:
- A command is backgrounded (short
initial_wait). - When it exits, the runtime emits a
<system_notification>(kind.type = "shell_completed"). - The runtime then injects a synthetic
assistant.messageto read the shell output. That message hasmodel: undefined, emptycontent, and — critically — noreasoningBlocks. - The next request to the provider is rejected because thinking mode requires the previous assistant turn's reasoning to be passed back.
Result: session.error, the session ends (was_interrupted: true, interruption_reason: "error").
The provider and credentials are fine — the same provider/model served 160+ successful model calls in the same session. It is specifically the model-less synthetic turn that breaks the reasoning passback invariant.
Affected version
copilot --version: 1.0.95-3 (dist:pkg/win32-arm64/1.0.95-3)- Host: Copilot app (
copilot-desktop-gh-2.98.0) - OS: Windows, ARM64
Steps to reproduce the behavior
- Configure a custom provider:
type:custombaseUrl:https://api.deepseek.comwireApi:responsesauthKind:api_key- model
deepseek-flash(DeepSeek-V4.1-Flash; thinking mode is enabled by default)
- In a session, start a long-running command so it moves to the background, e.g. a
powershellcall with a shortinitial_wait:
dotnet test --project <integration test project> --filter-class "*SelectiveOperationScaleTests" - Continue working while the shell runs. When it exits, observe the injected turn and the failure:
system.notification→<system_notification>Shell command "…" (shellId: 68) has exited with exit code 2.</system_notification>assistant.messagewithmodel: undefined,content: "", oneread_powershelltool call, noreasoningBlockstool.execution_completeforread_powershell- HTTP 400 on the next model call →
session.error
- See error: session terminates.
Exact sequence from events.jsonl:
22:24:02.368Z assistant.message "Let me start the heaviest scale test in the background while I update the documentation."
22:24:02.380Z tool.execution_start powershell { initial_wait: 12 } → backgrounded as shell 68
22:24:45.826Z system.notification kind.type="shell_completed", shellId=68, exitCode=2
22:24:45.827Z assistant.message messageId=2414e771-…, model=undefined, content="",
toolRequests=[read_powershell {shellId:"68", delay:0}],
reasoningBlocks=<absent>
22:24:45.833Z tool.execution_complete
22:24:48.944Z (model call fails)
22:24:48.976Z session.error 400 The `reasoning_text` in the thinking mode must be passed back to the API.
Raw error envelope and error context:
{"type":"hook.start","data":{"hookType":"errorOccurred","input":{
"error":{"name":"Error","message":"{\"message\":\"The `reasoning_text` in the thinking mode must be passed back to the API. (request_id: 3821e7d3-e76b-4688-8be6-4461e1d1e46b)\",\"type\":\"invalid_request_error\",\"param\":null,\"code\":\"invalid_request_error\"}"},
"errorContext":"model_call",
"recoverable":true}}}
{"type":"session.error","data":{
"errorType":"query",
"message":"400 The `reasoning_text` in the thinking mode must be passed back to the API. (request_id: 3821e7d3-e76b-4688-8be6-4461e1d1e46b)",
"statusCode":400}}The synthetic assistant turn (note the UUID-style toolCallId, versus the model's call_00_… style):
{"messageId":"2414e771-73dc-4bb2-be5b-8b1e537e8982","content":"",
"toolRequests":[{"toolCallId":"0666f48c-0b07-413f-ae92-aaf94b4ca60f","name":"read_powershell",
"arguments":{"shellId":"68","delay":0},"toolTitle":"Read shell output"}]}Expected behavior
The session should continue. Thinking-mode providers require the reasoning payload on assistant turns; the runtime should satisfy that invariant for every assistant turn it sends, including turns it synthesizes itself (or must otherwise exclude those turns from the reasoning-required history).
Additional context
Provider configuration (valid, working):
type: custom, authKind: api_key, baseUrl: https://api.deepseek.com, wireApi: responses, headers: {}
model: deepseek-flash (max_prompt_tokens 840000, max_output_tokens 128000)
reasoning efforts: none | low | high | max
DeepSeek documents both a Responses API and thinking mode (on by default) for deepseek-flash.
Why this points at the client, not the provider:
-
142 of 162
assistant.messageevents in the session carried correct reasoning blocks:reasoningBlocks.provider = "openai-responses", blocktype: "reasoning", content[{type:"reasoning_text", …}], plusencrypted_content. -
20 assistant turns had no reasoning block. 18 were ordinary model turns; 1 was the model-less synthetic turn above — and the failure landed on the very next model call.
-
Same provider/model serviced 160+ other calls in this session and all other sessions without this error.
-
Across 44 local sessions with this provider, there is exactly one
session.errorof this kind. It was also the session's onlysystem.notification. Correlation is 1:1. -
The injected turn is not a model output, so it cannot carry model reasoning; sending it as a plain assistant turn breaks the API contract for thinking mode.
Impact: the entire session is terminated mid-task, losing in-flight work. In this case the session was implementing changes across many files.
Workarounds used: avoid backgrounding commands (run them in the foreground) in thinking-mode sessions; set the model's reasoning effort to none to sidestep the passback requirement.
Suggested direction: when the runtime injects a synthetic assistant turn (e.g. read_powershell after shell_completed), either attach whatever reasoning payload the provider requires, or mark it so it is not presented to thinking-mode providers as a reasoning-bearing assistant turn (e.g. keep it out of the reasoning-required history, or fold the shell output into the following user/tool message).
Related observation (possibly a separate issue): the same provider also produced two genuine context-length 400s in an earlier session — you requested 1049487 tokens (921487 in the messages, 128000 in the completion) against a 1,048,576-token context — while max_prompt_tokens was configured as 840000. That suggests the app-side prompt budget does not reserve enough headroom for its own token estimation error. Happy to file separately if useful.
Activity