Skip to content

Custom OpenAI-compatible model: /thinking offers high, which breaks llama.cpp Qwen3.8 template, and the failure is retried 5 times #53202

Open

Description

@monsieur-nerd

Description

With a custom OpenAI-compatible provider (llama-swap → llama.cpp llama-server --jinja) serving Qwen3.8, /thinking offers Default, low, medium and high, although the model config declares no variants. Default works. Picking high makes every request fail, because the model's chat template only accepts low, medium and xhigh:

Error: Jinja Exception: Unexpected reasoning effort high. Supported types are xhigh (default), medium, and low.

llama-server answers HTTP 500 in ~7 ms, and opencode retries the request 5 times (⚠ Retrying in 7s · attempt 5) before showing the error.

Two issues:

  1. Effort levels are offered for a custom model that declares none. high can never work here, and the backend's xhigh can't be selected.
  2. A deterministic error is retried. In packages/opencode/src/session/retry.ts, retryable() retries any status >= 500, even when the error is not marked retryable.

Suggested fix for 2: don't retry a 500 whose body carries a template / validation error (Jinja Exception, chat template, raise_exception). The check would go in retryable(), and in statusReason() in packages/llm/src/route/executor.ts, which maps every >= 500 to the always-retryable ProviderInternalReason. I can open a PR if this direction is OK.

Workaround: stay on Default, low or medium.

Related: #25041 (5xx always retried), #52464 (deterministic errors retried), #48183 (variants for custom OpenAI-compatible providers).

Plugins

None

OpenCode version

2.0.22

Steps to reproduce

  1. Serve a Qwen3.8 GGUF with llama-server --jinja (here behind llama-swap v255, llama.cpp b10989).
  2. Configure it as a custom provider, with no variants:
    "providers": {
      "llamaswap": {
        "package": "@opencode/ai/providers/openai-compatible",
        "settings": { "baseURL": "http://127.0.0.1:8080/v1" },
        "env": ["LLAMACPP_API_KEY"],
        "models": { "qwen-code": { "modelID": "qwen-code", "limit": { "context": 65536, "output": 4096 } } }
      }
    }
  3. Run /thinking and pick high.
  4. Send any prompt.

Screenshot and/or share link

Screenshots: /thinking variant list, and the retried error.

Image Image

Operating System

Ubuntu 24.04 on WSL 2 (Windows host)

Terminal

VS Code integrated terminal (WSL)

Activity

  1. added
    needs:complianceThis means the issue will auto-close after 2 hours.
    and removed
    needs:complianceThis means the issue will auto-close after 2 hours.
    on Oct 4, 2026
  2. github-actions commented on Oct 4, 2026

    @github-actions
    Contributor

    Thanks for updating the issue with the required description, reproduction steps, and system information!

  3. opencode-agent commented on Oct 5, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. Until we improve the defaults, you can work around the first issue by declaring the variants yourself.

    When a custom model has no variants, opencode falls back to a generic low / medium / high effort list for openai-compatible providers. As soon as you set variants on the model, that fallback is skipped and /thinking shows only the variants you define. Each variant's settings.reasoningEffort is sent as reasoning_effort, so you can match exactly what the Qwen3.8 template accepts:

    "providers": {
      "llamaswap": {
        "package": "@opencode/ai/providers/openai-compatible",
        "settings": { "baseURL": "http://127.0.0.1:8080/v1" },
        "env": ["LLAMACPP_API_KEY"],
        "models": {
          "qwen-code": {
            "modelID": "qwen-code",
            "limit": { "context": 65536, "output": 4096 },
            "variants": [
              { "id": "low", "settings": { "reasoningEffort": "low" } },
              { "id": "medium", "settings": { "reasoningEffort": "medium" } },
              { "id": "xhigh", "settings": { "reasoningEffort": "xhigh" } }
            ]
          }
        }
      }
    }

    Default still sends no effort, so the template's own default applies. A variant can also set body (merged into the request JSON) or headers if a backend needs something other than reasoning_effort, for example "body": { "chat_template_kwargs": { ... } }.

    We'll also look at making the built-in variant logic less presumptuous for custom OpenAI-compatible models, and at the retry behavior for deterministic 5xx errors.

  4. opencodejr commented on Oct 27, 2026

    @opencodejr

    Thanks for the detailed report and for offering a PR!

    As far as I can tell, both behaviours are how v2 currently works on purpose, so I'm treating this as an improvement request:

    1. Effort levels on custom models. If a custom OpenAI-compatible model sets neither variants nor capabilities.reasoning, OpenCode assumes it supports the generic low / medium / high efforts. You can replace that list with the efforts your template accepts, which also makes xhigh selectable:
      "models": {
        "qwen-code": {
          "modelID": "qwen-code",
          "limit": { "context": 65536, "output": 4096 },
          "capabilities": {
            "reasoning": { "supported": true, "effort": ["low", "medium", "xhigh"] }
          }
        }
      }
    2. Retrying the 500. In v2 the error is classified in packages/ai/src/provider-error.ts (packages/opencode/src/session/retry.ts isn't in the current repo). Every status >= 500 is treated as a temporary provider error and retried. Here llama.cpp sends a 500 for a request that can never succeed, so not retrying would mean recognising template errors in the response body. That's a reasonable idea, and maintainers will decide whether that kind of body check fits.

    Related issues:

  5. jokaperes commented on Nov 4, 2026

    @jokaperes

    Another affected provider: Refuseless, with OpenCode v2.2.5 on macOS. This also affects clients consuming model.list (observed in T3 Code).

    With a custom @opencode/ai/providers/openai-compatible provider at https://api.refuseless.com/v1, and neither variants nor capabilities.reasoning declared, opencode api model.list returns low, medium, high, each mapped to the same settings.reasoningEffort. Selecting Medium produced:

    reasoning_effort must be one of off (or none), low, high, or max.
    

    Affected model: deepseek-v4.1-flash-abliterated-free. Refuseless documents that Medium is rejected, and publishes per-model reasoning_efforts / reasoning_default: https://docs.refuseless.com/guides/reasoning and https://docs.refuseless.com/api/models.

    Confirmed the existing workaround on v2.2.5: adding this under the model's capabilities makes both a fresh standalone model.list and the active service after opencode reload return exactly Off/Low/High/Max:

    "reasoning": { "supported": true, "effort": ["off", "low", "high", "max"] }

    Verification covered catalog output, without a new inference request. The provider rejection above came from the original failed request. The fallback is still present in packages/core/src/config/plugin/provider.ts, with the generic effort list in packages/core/src/variant.ts. Please consider this provider when revisiting inferred reasoning support for custom models.

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