Skip to content

400 reasoning_text in thinking mode must be passed back — triggered by runtime-synthesized assistant turn after a background shell exits #5145

Open

Description

@eruvalca

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:

  1. A command is backgrounded (short initial_wait).
  2. When it exits, the runtime emits a <system_notification> (kind.type = "shell_completed").
  3. The runtime then injects a synthetic assistant.message to read the shell output. That message has model: undefined, empty content, and — critically — no reasoningBlocks.
  4. 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

  1. Configure a custom provider:
    • type: custom
    • baseUrl: https://api.deepseek.com
    • wireApi: responses
    • authKind: api_key
    • model deepseek-flash (DeepSeek-V4.1-Flash; thinking mode is enabled by default)
  2. In a session, start a long-running command so it moves to the background, e.g. a powershell call with a short initial_wait:
    dotnet test --project <integration test project> --filter-class "*SelectiveOperationScaleTests"
  3. 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.message with model: undefined, content: "", one read_powershell tool call, no reasoningBlocks
    • tool.execution_complete for read_powershell
    • HTTP 400 on the next model call → session.error
  4. 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.message events in the session carried correct reasoning blocks: reasoningBlocks.provider = "openai-responses", block type: "reasoning", content [{type:"reasoning_text", …}], plus encrypted_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.error of this kind. It was also the session's only system.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

  1. added
    area:modelsModel selection, availability, switching, rate limits, and model-specific behavior
    area:toolsBuilt-in tools: file editing, shell, search, LSP, git, and tool call behavior
    on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:modelsModel selection, availability, switching, rate limits, and model-specific behaviorarea:toolsBuilt-in tools: file editing, shell, search, LSP, git, and tool call behavior

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions