Skip to content

tool_choice: 'required' incompatible with thinking-enabled models when using structured output #15226

Closed as not planned

Description

@bralca

Description

When using format: { type: "json_schema" } with the prompt_async API, OpenCode unconditionally sets toolChoice: "required" for the internal StructuredOutput tool. For reasoning models like Kimi K2.5
that have thinking automatically enabled, the downstream provider rejects this with:

tool_choice 'required' is incompatible with thinking enabled

This affects OpenCode Go (opencode-go). The same model + structured output works via OpenCode Zen (opencode), suggesting Zen reconciles the conflict server-side.

OpenCode Version

1.2.14

Plugins

No response

OpenCode version

No response

Steps to reproduce

  1. Start an OpenCode server with an opencode-go API key
  2. Create a session
  3. Send prompt_async with json_schema format targeting a reasoning model:

curl -X POST http://127.0.0.1:/session/<session_id>/prompt_async
-H "Authorization: Bearer "
-H "Content-Type: application/json"
-d '{
"parts": [{"type":"text","text":"Describe the directory. Return JSON."}],
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"description": {"type":"string"},
"files": {"type":"array","items":{"type":"string"}}
},
"required": ["description","files"]
},
"retryCount": 2
},
"model": {"providerID":"opencode-go","modelID":"kimi-k2.5"}
}'

  1. SSE /event endpoint fires session.error immediately:

{
"error": {
"message": "Provider returned error",
"code": 400,
"metadata": {
"raw": "{"error":{"message":"tool_choice 'required' is incompatible with thinking enabled"}}",
"provider_name": "Moonshot AI"
}
}
}

Expected Behavior

Structured output should work with all models including reasoning/thinking models.

Root Cause

Tracing through the v1.2.14 source:

  1. SessionPrompt.loop() creates a StructuredOutput tool when format.type === "json_schema"
  2. Unconditionally sets toolChoice: "required":
    toolChoice: format4.type === "json_schema" ? "required" : undefined
  3. Separately, ProviderTransform.options() enables thinking for reasoning models:
    result["thinking"] = { type: "enabled", budgetTokens: ... }
  4. Both tool_choice: "required" and thinking params are sent together to the downstream provider, which rejects the combination.
    Affected Models

Any model with reasoning: true accessed via providers that enforce thinking + tool_choice incompatibility. Confirmed: kimi-k2.5 (Moonshot AI) via opencode-go.

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Feb 26, 2026
  2. github-actions commented on Feb 26, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or related to existing issues. Please check:

  3. lucad87 commented on Mar 18, 2026

    @lucad87

    I had the same issue using the version 1.2.27 of opencode with Qwen3.5-9B as model.

    It seems that I found a workaround specifying the following prompt for an agent in opencode.json:

    "prompt": "If a tool or a command returns null, none, or empty values, do not assume it is a failure. Instead, analyze the situation and make a decision on how to proceed based on the context. Consider alternative approaches, gather more information, or adjust your strategy as needed to achieve the best outcome."

    in opencode.json I also have the following configuration:

    "reasoning": true,
    "tool_call": true,
    "interleaved": true
  4. alex-wesolowski-cb commented on Mar 24, 2026

    @alex-wesolowski-cb

    We're running into this issue as well when setting format as well as an anthropic variant with thinking: true for the model config. Is there a solution that allows structured output and thinking for anthropic models?

  5. assigned and unassigned on Apr 26, 2026
  6. removed
    bugSomething isn't working
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on May 3, 2026
  7. nickwang0808 commented on May 4, 2026

    @nickwang0808

    ran into the same issue as well

  8. dineshgadge commented on May 27, 2026

    @dineshgadge

    @kitlangton
    Running into the same issue with deepseek-v4-flash-free when format option is given while calling from opencode sdk.

    {
      message: "Error from provider (DeepSeek): Thinking mode does not support this tool_choice",
      statusCode: 400,
      isRetryable: false,
      responseHeaders: {
        "cf-placement": "remote-ORD",
        "cf-ray": "a0237c8998767f72-IAD",
        connection: "keep-alive",
        "content-length": "178",
        "content-type": "application/octet-stream",
        date: "Wed, 27 May 2026 08:00:33 GMT",
        server: "cloudflare",
      },
      responseBody: "{\"error\":{\"message\":\"Error from provider (DeepSeek): Thinking mode does not support this tool_choice\",\"type\":\"invalid_request_error\",\"param\":null,\"code\":\"invalid_request_error\"}}",
      metadata: {
        url: "https://opencode.ai/zen/v1/chat/completions",
      },
    }

    I am calling it like this

    import { createOpencodeClient } from "@opencode-ai/sdk"
    import { z } from "zod"
    
    const oc = createOpencodeClient({
      baseUrl: `http://localhost:${process.env.OPENCODE_SERVER_PORT}`,
      responseStyle: "data",
    })
    
    const main = async () => {
      const response = await oc.session.prompt({
        query: { directory: folderLocation },
        path: { id: sessionId },
        body: {
          model: { providerID: "opencode", modelID: "deepseek-v4-flash-free" },
          agent: "build",
          tools: { question: false },
          parts: [{ type: "text", text: ["my prompt here"] }],
          // @ts-expect-error - format is not supported in the opencode SDK types, but it is supported in the API
          format: { type: "json_schema", schema: z.toJSONSchema(MySchema) }
        },
      })
    }
    
    main()

    This even messes up the OpenCode Desktop app when that particular session is clicked. You'll have to delete the session or affected messages from sqlite for the app to function again.

  9. serejaris commented on May 27, 2026

    @serejaris

    I opened #29565 for this request-shape conflict.

    The regression covers the structured-output + thinking path where the final provider payload used to force tool_choice: "required"; the fix relaxes that to auto for thinking/reasoning provider surfaces before the runtime sends the request. That should cover the DeepSeek/OpenCode SDK case reported above as well, since it goes through the same structured-output request preparation path.

  10. github-actions commented on Jul 27, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

  11. iceteaSA commented on Sep 2, 2026

    @iceteaSA

    Refiled as #46735, per the auto-close note asking for a new issue.

    The new report adds a live reproduction with controls: claude-fable-5-1 returns 400 on {"type":"any"} and {"type":"tool"} while accepting auto/none, and claude-fable-5 accepts all four — so it is model-gated, and the failure is attributable to tool_choice rather than to request shape. Also an end-to-end capture showing opencode's required arriving on the wire as {"type":"any"}.

    The cross-provider reports in this thread (@bralca on Kimi K2.5, @lucad87 on Qwen3.5-9B, @alex-wesolowski-cb on Anthropic with thinking, @dineshgadge on DeepSeek v4 flash) are cited there as evidence this is one class rather than four separate provider quirks. @serejaris, your #29565 is credited as the narrower fix; the new issue argues for the provider's native structured output as the default since it avoids the forced choice entirely, with your compatibleToolChoice shape as the alternative.

    @rscambler — you asked here whether #29565 could be reopened and got no answer. It cannot be reopened by a non-maintainer, so #46735 is the path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions