Skip to content

config: compatibility.supportsStore silently dropped from opencode.jsonc #52617

Open

Description

@gabrielferreira-cit

Summary

Model-level compatibility.supportsStore set in opencode.jsonc is silently ignored: the openai-compatible chat protocol keeps injecting "store": false into every request body, which strict gateways reject. A sibling key in the same block (maxTokensField) is honored, so the file is parsed — only some compatibility keys take effect.

Environment

  • opencode version: 2.0.21
  • OS: Linux 7.0.0-34-generic (linux x64)
  • Terminal: WarpTerminal, TERM=xterm-256color, COLORTERM=truecolor
  • Shell: /bin/zsh
  • Install/channel: latest
  • Active plugins: opencode-models-discovery@latest, superpowers@git+https://github.com/obra/superpowers.git, local rtk.ts

Reproduction

  1. Configure an openai-compatible provider pointing at a backend that rejects unknown body fields (e.g. a LiteLLM proxy routing Claude models to a Bedrock/azure_ai backend), with one model:
    "models": {
      "claude-sonnet-5-5": {
        "modelID": "claude-sonnet-5-5",
        "compatibility": { "supportsStore": false, "maxTokensField": "max_tokens" }
      }
    }
  2. Send any prompt with that model (or capture the outgoing request body with a local probe endpoint).
  3. Observe the request uses max_tokens (override honored) but still contains "store": false.

Expected Behavior

With compatibility.supportsStore: false, the store key should be omitted from the request body entirely.

Actual Behavior

"store": false is always sent unless the provider id/baseURL matches the built-in nonstandard-provider list. The gateway responds with litellm.UnsupportedParamsError: azure_ai does not support parameters: ['store'], making the model unusable. Requests without store succeed (verified directly against the gateway).

Additional Context

  • Inspecting the v2.0.21 binary, the config-file compatibility schema accepts reasoningField, requireReasoning, maxTokensField, requireFinishReason, requireAssistantAfterTool, supportsPromptCacheKey — but not supportsStore (nor supportsStrictMode, etc.), while the runtime LanguageModelCompatibility schema includes them. So the key is dropped at config load, never reaching detectSupportsStore's ?? request.model.compatibility?.supportsStore fallback.
  • Consistent across repeated runs; no failed to load plugin / config errors logged for the entry (it is silently skipped, matching the existing configuration normalization diagnostic ... action="skipped malformed recognized value" pattern for unknown keys).
  • Workaround attempted: an http.request session-hook plugin stripping store (works in isolation against a probe endpoint) — blocked by a separate reload bug filed alongside this one.

Activity

  1. github-actions commented on Oct 2, 2026

    @github-actions
    Contributor

    This issue appears to follow the bug-report template and includes substantive description, reproduction steps, and environment details.


    This issue may be related to existing reports about unsupported OpenAI-compatible request fields and compatibility controls. Please check:

  2. mperreir commented on Oct 5, 2026

    @mperreir

    Hello,
    Until this bug is fixed, here is a workaround using the plugin idea you proposed (it works correctly on my side).
    strip-store.ts

  3. added 2 commits that reference this issue on Oct 13, 2026
    8736a25
    4024e6e
  4. fihaaade commented on Oct 24, 2026

    @fihaaade
    Contributor

    This should be fixed on dev by #57239: Chat Completions now only sends store to OpenAI and Azure by default, so strict gateways such as LiteLLM no longer get "store": false. I think this can be closed once a release includes it.

  5. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed report. I reproduced this, and part of it is already fixed on dev.

    I pointed two openai-compatible providers at a local endpoint that records request bodies. Both used your model config (compatibility: { supportsStore: false, maxTokensField: "max_tokens" }). One baseURL looks like a LiteLLM gateway; the other contains .openai.azure.com, so OpenCode's built-in detection treats it as Azure OpenAI.

    • 2.0.21: every request for both providers contains "store": false, while max_tokens is used. This matches what you described.
    • Latest dev (891a020): the gateway-style provider no longer gets store. Since v2.1.0, store is only sent by default to OpenAI and Azure endpoints (fix(ai): only send store on chat completions for OpenAI and Azure #57239), so upgrading should fix the LiteLLM UnsupportedParamsError for you. The setting itself is still ignored, though: the Azure-looking provider still gets "store": false despite supportsStore: false. The config schema has no supportsStore field, so the key is dropped when the config loads, as you found.

    The script below reproduces it. Related:

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #52617: model-level compatibility.supportsStore in opencode.jsonc is
    # dropped at config load, so the openai-compatible chat protocol ignores it.
    # Usage: ./repro.sh            -> runs `opencode` from PATH
    #        OC_DEV=/path/to/repo ./repro.sh -> runs the CLI from a source checkout
    set -euo pipefail
    PORT=${PORT:-4517}
    WORK=$(mktemp -d /tmp/opencode/repro52617.XXXX)
    mkdir -p "$WORK/project" "$WORK/home"
    BODIES="$WORK/bodies.jsonl"
    
    # Probe endpoint: records each request body and answers with a minimal SSE stream.
    cat > "$WORK/probe.ts" <<EOF
    const out = Bun.file("$BODIES").writer()
    Bun.serve({ port: $PORT, async fetch(req) {
      const url = new URL(req.url)
      if (!url.pathname.endsWith("/chat/completions")) return new Response("{}", { headers: { "content-type": "application/json" } })
      const body = await req.json()
      out.write(JSON.stringify({ path: url.pathname, body }) + "\n"); out.flush()
      const chunk = (o: unknown) => "data: " + JSON.stringify(o) + "\n\n"
      const base = { id: "x", object: "chat.completion.chunk", created: 0, model: body.model }
      const sse = chunk({ ...base, choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }] })
        + chunk({ ...base, choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } })
        + "data: [DONE]\n\n"
      return new Response(sse, { headers: { "content-type": "text/event-stream" } })
    } })
    EOF
    bun "$WORK/probe.ts" & PROBE=$!
    trap 'kill $PROBE 2>/dev/null || true' EXIT
    sleep 1
    
    # "proxy": a LiteLLM-like gateway URL. "azproxy": URL containing ".openai.azure.com"
    # (in the path) so built-in detection says store IS supported; supportsStore:false
    # in config must override that.
    cat > "$WORK/project/opencode.jsonc" <<EOF
    {
      "providers": {
        "proxy": {
          "name": "Proxy",
          "package": "@opencode/ai/providers/openai-compatible",
          "settings": { "baseURL": "http://127.0.0.1:$PORT/v1", "apiKey": "x" },
          "models": { "claude-sonnet-5-5": { "modelID": "claude-sonnet-5-5",
            "compatibility": { "supportsStore": false, "maxTokensField": "max_tokens" } } }
        },
        "azproxy": {
          "name": "AzProxy",
          "package": "@opencode/ai/providers/openai-compatible",
          "settings": { "baseURL": "http://127.0.0.1:$PORT/.openai.azure.com/v1", "apiKey": "x" },
          "models": { "claude-sonnet-5-5": { "modelID": "claude-sonnet-5-5",
            "compatibility": { "supportsStore": false, "maxTokensField": "max_tokens" } } }
        }
      }
    }
    EOF
    
    run() {
      if [ -n "${OC_DEV:-}" ]; then
        # the standalone server is spawned as a child bun process; pass the same flags to it
        BUN_OPTIONS="--conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts" "$@"
      else opencode "$@"; fi
    }
    cd "$WORK/project"
    export XDG_CONFIG_HOME="$WORK/home/config" XDG_DATA_HOME="$WORK/home/data" XDG_STATE_HOME="$WORK/home/state" XDG_CACHE_HOME="$WORK/home/cache"
    echo "version: $(run --version)"
    for m in proxy/claude-sonnet-5-5 azproxy/claude-sonnet-5-5; do
      run run --standalone -m "$m" "say ok" >/dev/null 2>"$WORK/err.log" || { echo "run $m failed"; tail -20 "$WORK/err.log"; }
    done
    echo "--- captured request bodies (path, store present?, store value, max_tokens field) ---"
    bun -e '
    for (const l of (await Bun.file(process.argv[1]).text()).trim().split("\n").filter(Boolean)) {
      const { path, body } = JSON.parse(l)
      console.log(path, "| store:", "store" in body ? JSON.stringify(body.store) : "<omitted>",
        "| max_tokens:", body.max_tokens !== undefined, "| max_completion_tokens:", body.max_completion_tokens !== undefined)
    }' "$BODIES"

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions