Skip to content

fix(sse): append synthetic user turn for GLM-family upstreams (400 [1214]) - #11209

Merged
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.50from
linhdmn:fix/glm-empty-user-turn-1214
Aug 23, 2026
Merged

fix(sse): append synthetic user turn for GLM-family upstreams (400 [1214])#11209
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.50from
linhdmn:fix/glm-empty-user-turn-1214

Conversation

@linhdmn

@linhdmn linhdmn commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Problem

Claude Code sessions routed through OmniRoute to opencode-go intermittently fail with:

API Error: 400 [400]: Error from provider (Console Go):
Upstream request failed: [1214] The messages parameter is illegal. Please check the documentation.

The error text originates entirely from the upstream Z.AI/Zhipu console gateway — OmniRoute just forwards it.

Root cause (verified live against the upstream, 2026-08-23)

GLM-family gateways reject any messages array that contains no role:"user" turn:

Payload shape Result
system + assistant(tool_calls) + tool ❌ 400 [1214]
same + trailing user ✅ 200
assistant content:null / "" with a user present ✅ 200

Claude Code agent loops legitimately produce the failing shape: every inbound user turn carries only tool_result blocks (translated to role:"tool"), and context compression can evict the original prompt — leaving the translated payload as system + assistant* + tool* with zero user turns.

Fix

  1. translateRequest flags GLM-family upstreams (opencode-go, opencode-zen, glm/zhipu/z-ai model ids) with a transient _ensureUserTurn credential marker (same pattern as _copilotClient / _preserveCacheControl).
  2. claudeToOpenAIRequest appends a minimal synthetic user turn ({role:"user", content:"(continue)"}) when no user message survives conversion.

Appending at the end keeps every earlier byte identical for upstream prompt caches; non-GLM providers keep byte-identical request bodies.

Testing

  • New unit tests: tests/unit/claude-to-openai-glm-user-turn.test.ts (red→green; injection only when flagged, no duplicate when a user exists, unchanged body without the flag)
  • Focused suite: 24/24 translator unit tests pass
  • eslint clean on touched files; npm run typecheck:core clean
  • E2E: the previously-failing payload replayed through POST /v1/messages returns 200 with model output

⚠️ base-red inherited: #9985

…ouzapw#1214)

Z.AI/Zhipu console gateways (opencode-go, opencode-zen, glm-* targets)
reject any chat.completions payload whose messages array has no
role:"user" turn with 400 [1214] 'The messages parameter is illegal'.

Claude Code agent loops legitimately produce that shape: every inbound
user turn carries only tool_result blocks (translated to role:"tool")
and context compression can evict the original prompt, leaving the
translated payload as system + assistant* + tool* only.

translateRequest now flags GLM-family providers (_ensureUserTurn) and
claudeToOpenAIRequest appends a minimal synthetic user turn when none
survives conversion. Appending at the end keeps earlier bytes identical
for upstream prompt caches; non-GLM providers keep byte-identical bodies.

Verified live against the opencode-go upstream on 2026-08-23:
- system + assistant(tool_calls) + tool          -> 1214
- same + trailing user                            -> 200
- assistant content:null/"" with a user present   -> 200
@linhdmn
linhdmn requested a review from diegosouzapw as a code owner August 23, 2026 05:03
The new covering unit test was missing from the mutation-testing
tap.testFiles allowlist, failing the strict Fast Quality Gates check.
@diegosouzapw
diegosouzapw merged commit e66181b into diegosouzapw:release/v3.8.50 Aug 23, 2026
11 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants