Skip to content

fix(opencode): re-derive api_mode per target model on /model switch - #16888

Closed
Sanjays2402 wants to merge 2 commits into
NousResearch:mainfrom
Sanjays2402:fix/issue-16878
Closed

fix(opencode): re-derive api_mode per target model on /model switch#16888
Sanjays2402 wants to merge 2 commits into
NousResearch:mainfrom
Sanjays2402:fix/issue-16878

Conversation

@Sanjays2402

Copy link
Copy Markdown
Contributor

What

opencode-zen and opencode-go each serve both anthropic_messages models (e.g. minimax-m2.7) and chat_completions models (e.g. deepseek-v4-flash) behind a single base_url (https://opencode.ai/zen/go/v1). When the user starts a session with a minimax-m2.7 default and runs /model deepseek-v4-flash, the api_mode resolver in hermes_cli/runtime_provider.py honours the persisted model_cfg.api_mode (= anthropic_messages, set by the previous default) before checking the opencode model registry, so the deepseek turn inherits anthropic_messages, strips /v1 from base_url (the Anthropic SDK adds its own /v1/messages), and 404s.

Reported and root-caused in #16878.

Fix

Promote the opencode detection branch above the configured_mode check in both api_mode resolution paths:

  • _resolve_runtime_from_pool_entry — pool-backed providers (~line 262)
  • _resolve_api_key_runtime — API-key fallback (~line 1213)

Both branches now call opencode_model_api_mode(provider, effective_model) unconditionally for opencode-zen / opencode-go before considering any persisted api_mode, so the mode always reflects the model the user just switched to. Other providers retain the existing precedence (persisted api_mode > URL detection).

Tests

Existing suite passes:

$ pytest tests/hermes_cli/test_model_switch_opencode_anthropic.py
............                                                             [100%]
12 passed, 13 warnings in 4.31s

This covers both the minimax-m2.7 → deepseek-v4-flash direction (the failing case in the issue) and the reverse deepseek-v4-flash → minimax-m2.7, which stays correct because the same code path also re-derives the mode from the target model.

Diff

hermes_cli/runtime_provider.py | 28 +++++++++++++++++---------
1 file changed, 19 insertions(+), 9 deletions(-)

Fixes #16878

run_agent.py persisted assistant turns with the chain of thought under
the internal field 'reasoning' and only wrote the protocol-standard
'reasoning_content' when (a) the upstream SDK happened to expose it as
a top-level attribute, or (b) the current provider was DeepSeek and the
turn had tool_calls. Streaming-only providers (glm, MiniMax, gpt-5.x via
aigw, Anthropic via openai-compat shim, etc.) accumulate reasoning from
delta.reasoning_content into a local string but never set the SDK
attribute, so the persisted message is missing reasoning_content.

The bug is silent until the user later replays that history through a
DeepSeek-v4 / Kimi thinking model, which requires reasoning_content on
every replayed assistant turn:

    The reasoning_content in the thinking mode must be passed back to
    the API.

Read-side patches (NousResearch#15213, NousResearch#15741, NousResearch#15748, NousResearch#15353) each fix one build
path, but every new path that reads history from disk is a fresh place
the same 400 can resurface.

Normalize at write time:
- prefer the SDK-supplied reasoning_content when present (may carry
  structured data);
- otherwise fall back to the already-sanitized reasoning_text that was
  accumulated from streaming deltas;
- finally default to "" so non-thinking providers ignore the field
  harmlessly while DeepSeek/Kimi see a valid (empty) value.

The internal 'reasoning' alias is preserved for backward compatibility
with existing read paths and downstream consumers.

Refs NousResearch#16844
opencode-zen and opencode-go each serve both anthropic_messages
(e.g. minimax-m2.7) and chat_completions (e.g. deepseek-v4-flash)
models behind a single base_url. The api_mode resolver in
hermes_cli/runtime_provider.py honoured the persisted
model_cfg.api_mode (set by the previous default model) before checking
the opencode model registry, so /model deepseek-v4-flash from a session
whose default was minimax-m2.7 inherited 'anthropic_messages', stripped
'/v1' from base_url (the Anthropic SDK adds its own /v1/messages), and
404'd.

Promote the opencode detection branch above the configured_mode check
in both api_mode resolution paths:

- _resolve_runtime_from_pool_entry (pool-backed providers)
- _resolve_api_key_runtime          (api-key providers, fallback path)

Both branches now call opencode_model_api_mode(provider, effective_model)
unconditionally for opencode-zen/go before considering any persisted
api_mode, so the mode always reflects the model the user just switched
to.

Existing tests pass (12/12 in tests/hermes_cli/test_model_switch_opencode_anthropic.py).

Fixes NousResearch#16878
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Apr 28, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #16885 — same fix: promotes opencode-go/zen api_mode detection above configured_mode override in runtime_provider.py to fix stale anthropic_messages mode on /model switch. Both fix #16878.

teknium1 added a commit that referenced this pull request Apr 28, 2026
…e api_mode

PR #16888 swaps the opencode-zen/go resolver so that api_mode is always
re-derived from the effective model before the persisted api_mode is
consulted. That's the point of the fix — a stale anthropic_messages
from a previous minimax default must not survive a /model switch to a
chat_completions target (or vice versa) and strip /v1 from base_url.

The prior test asserted the opposite precedence — that a persisted
api_mode won over model-derived mode — and was added in #4508 to lock
in escape-hatch behavior. Under the new precedence that escape hatch
no longer exists for opencode (only for providers that genuinely
support both modes at a single endpoint — and for opencode the model
name is the unambiguous signal). Rename + invert the assertion to
document the intentional behavior change.

Refs #16878.
@teknium1

Copy link
Copy Markdown
Contributor

Salvaged via PR #16890 — the opencode fix (commit efda405) was cherry-picked onto current main with your authorship preserved via rebase-merge (commits b52cecc + 54e24f7). The stacked run_agent.py reasoning_content change was dropped from this PR so it can be evaluated separately against #16844.

One existing test (test_opencode_go_configured_api_mode_still_overrides_default, added in #4508) had to be inverted to lock in the new precedence — it previously asserted the opposite escape-hatch behavior.

Thanks @Sanjays2402 — the root-cause analysis in the PR body and issue #16878 was exactly right.

@teknium1 teknium1 closed this Apr 28, 2026
cluricaun28 referenced this pull request in cluricaun28/Logos Apr 28, 2026
…e api_mode

PR #16888 swaps the opencode-zen/go resolver so that api_mode is always
re-derived from the effective model before the persisted api_mode is
consulted. That's the point of the fix — a stale anthropic_messages
from a previous minimax default must not survive a /model switch to a
chat_completions target (or vice versa) and strip /v1 from base_url.

The prior test asserted the opposite precedence — that a persisted
api_mode won over model-derived mode — and was added in #4508 to lock
in escape-hatch behavior. Under the new precedence that escape hatch
no longer exists for opencode (only for providers that genuinely
support both modes at a single endpoint — and for opencode the model
name is the unambiguous signal). Rename + invert the assertion to
document the intentional behavior change.

Refs #16878.
donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
…e api_mode

PR NousResearch#16888 swaps the opencode-zen/go resolver so that api_mode is always
re-derived from the effective model before the persisted api_mode is
consulted. That's the point of the fix — a stale anthropic_messages
from a previous minimax default must not survive a /model switch to a
chat_completions target (or vice versa) and strip /v1 from base_url.

The prior test asserted the opposite precedence — that a persisted
api_mode won over model-derived mode — and was added in NousResearch#4508 to lock
in escape-hatch behavior. Under the new precedence that escape hatch
no longer exists for opencode (only for providers that genuinely
support both modes at a single endpoint — and for opencode the model
name is the unambiguous signal). Rename + invert the assertion to
document the intentional behavior change.

Refs NousResearch#16878.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…e api_mode

PR NousResearch#16888 swaps the opencode-zen/go resolver so that api_mode is always
re-derived from the effective model before the persisted api_mode is
consulted. That's the point of the fix — a stale anthropic_messages
from a previous minimax default must not survive a /model switch to a
chat_completions target (or vice versa) and strip /v1 from base_url.

The prior test asserted the opposite precedence — that a persisted
api_mode won over model-derived mode — and was added in NousResearch#4508 to lock
in escape-hatch behavior. Under the new precedence that escape hatch
no longer exists for opencode (only for providers that genuinely
support both modes at a single endpoint — and for opencode the model
name is the unambiguous signal). Rename + invert the assertion to
document the intentional behavior change.

Refs NousResearch#16878.
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
…e api_mode

PR NousResearch#16888 swaps the opencode-zen/go resolver so that api_mode is always
re-derived from the effective model before the persisted api_mode is
consulted. That's the point of the fix — a stale anthropic_messages
from a previous minimax default must not survive a /model switch to a
chat_completions target (or vice versa) and strip /v1 from base_url.

The prior test asserted the opposite precedence — that a persisted
api_mode won over model-derived mode — and was added in NousResearch#4508 to lock
in escape-hatch behavior. Under the new precedence that escape hatch
no longer exists for opencode (only for providers that genuinely
support both modes at a single endpoint — and for opencode the model
name is the unambiguous signal). Rename + invert the assertion to
document the intentional behavior change.

Refs NousResearch#16878.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…e api_mode

PR NousResearch#16888 swaps the opencode-zen/go resolver so that api_mode is always
re-derived from the effective model before the persisted api_mode is
consulted. That's the point of the fix — a stale anthropic_messages
from a previous minimax default must not survive a /model switch to a
chat_completions target (or vice versa) and strip /v1 from base_url.

The prior test asserted the opposite precedence — that a persisted
api_mode won over model-derived mode — and was added in NousResearch#4508 to lock
in escape-hatch behavior. Under the new precedence that escape hatch
no longer exists for opencode (only for providers that genuinely
support both modes at a single endpoint — and for opencode the model
name is the unambiguous signal). Rename + invert the assertion to
document the intentional behavior change.

Refs NousResearch#16878.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…e api_mode

PR NousResearch#16888 swaps the opencode-zen/go resolver so that api_mode is always
re-derived from the effective model before the persisted api_mode is
consulted. That's the point of the fix — a stale anthropic_messages
from a previous minimax default must not survive a /model switch to a
chat_completions target (or vice versa) and strip /v1 from base_url.

The prior test asserted the opposite precedence — that a persisted
api_mode won over model-derived mode — and was added in NousResearch#4508 to lock
in escape-hatch behavior. Under the new precedence that escape hatch
no longer exists for opencode (only for providers that genuinely
support both modes at a single endpoint — and for opencode the model
name is the unambiguous signal). Rename + invert the assertion to
document the intentional behavior change.

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: /v1 stripped from base_url when switching models via /model with opencode-go provider

3 participants