Repository navigation
bug(session): finish_reason 'unknown' triggers an assistant-prefill continuation; SAP AI Core (and other prefill-forbidding providers) 400 after every successful turn #46415
Description
bug(session): finish_reason: unknown triggers an assistant-prefill continuation, so providers that forbid prefill (SAP AI Core) return 400 after every successful turn
Summary
With SAP AI Core (sap-ai-core, @jerome-benoit/sap-ai-provider-v2, e.g. anthropic--claude-4.8-opus), every turn renders the assistant response correctly and is then immediately followed by an extra error:
400 - LLM Module: This model does not support assistant message prefill. The conversation must end with a user message.
Error location: LLM Module
Root cause: the provider's finish reason normalizes to "unknown", which the run loop treats as "turn incomplete, keep going". The loop then re-issues a model request whose message array ends with the assistant message (an assistant prefill / continuation). SAP's gateway rejects prefill, so a spurious 400 appears after each otherwise-successful turn.
Evidence
Querying the local session DB for recent sap-ai-core assistant messages, finish is "unknown" on every completed turn:
finish histogram: { "unknown": 10, "None": 2 } // "None" == the earlier auth-error turns
model: anthropic--claude-4.8-opus, finish: "unknown", error: false, completed: true // x10
Server log confirms SAP runs through the AI-SDK path:
llm.native_unsupported_reason="provider is not openai, opencode, or anthropic"
Root cause (code path)
-
Finish-reason enum is narrow.
packages/llm/src/schema/ids.ts:39export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"])
Note it does not include the AI SDK's
"other". -
Unrecognized reasons collapse to
"unknown".packages/opencode/src/session/llm/ai-sdk.ts:22-24function finishReason(value: string | undefined): FinishReason { return Schema.is(FinishReason)(value) ? value : "unknown" }
@jerome-benoit/sap-ai-provider-v2reports a finish reason that is not one of the six literals (AI SDK"other"or a raw Claude stop reason), so opencode recordsfinish = "unknown". -
The run loop keeps looping on
"unknown".packages/opencode/src/session/prompt.ts:1111-1116if ( lastAssistant?.finish && !["tool-calls", "unknown"].includes(lastAssistant.finish) && !hasToolCalls && lastAssistant.parentID === lastUser.id ) { // exit loop break }
Because
"unknown"is excluded from the exit condition, a completed turn (text produced, no tool calls) does not exit;step++and the model is called again. -
The continuation request is an assistant prefill. With no new user message and no tool results appended, the message array sent on the next iteration ends with the assistant message. Native Anthropic/OpenAI accept this (prefill/continuation), but the SAP AI Core gateway forbids it and returns
400 ... must end with a user message.
Reproduction
- Connect
sap-ai-coreand selectanthropic--claude-4.8-opus. - Send any prompt (e.g. "what is 1+1").
- The answer streams correctly, then an extra
400 ... does not support assistant message prefillerror appears. - Inspect the assistant message:
finish === "unknown".
Suggested fixes
- opencode (robust): do not issue a prefill continuation when
finish === "unknown"but the assistant already produced content and there are no pending/unexecuted tool calls — treat it as terminal. Alternatively, gate prefill continuation on a provider capability flag, since many gateways forbid trailing-assistant requests. - Finish-reason mapping: map the AI SDK's
"other"explicitly (and, forsap-ai-provider-v2, map Claude'send_turn/stop_sequenceto"stop") so a clean turn exits the loop instead of collapsing to"unknown". - Provider package:
@jerome-benoit/sap-ai-provider-v2should emit a standard"stop"finish reason for completed Claude turns.
Environment
- opencode dev branch (package version 1.18.23)
- macOS (Apple Silicon), Bun runtime
- Provider
sap-ai-corevia@jerome-benoit/sap-ai-provider-v2, modelanthropic--claude-4.8-opus
Related
- bug(provider): credentials set via CLI/
/connectare not applied to a running server (SAP AI Core, GitHub Copilot), and SAP multi-line keys are stored truncated #46354 (SAP AI Core / GitHub Copilot credential handling) — separate subsystem (auth), filed separately.
Activity
github-actions commentedon Aug 31, 2026
This issue might be a duplicate of existing issues. Please check:
unknown.unknownfinish.unknownfor unrecognized values, which is the same mapping issue described here.lgarceau768 commentedon Sep 2, 2026
Same root cause on a second provider, and this time with the recorded
finishvalue rather than a histogram.opencode 1.18.26,
amazon-bedrock/anthropic.claude-opus-5— so not gateway-specific, and not a provider that misreports its finish reason.Last three messages of the session, out of
opencode.db:tool-callsunknownThe error on that last one:
The
unknownmessage matches your suggested predicate exactly:time.completedset, a 1192-charactertextpart, and no tool part — its parts arestep-start,reasoning,text,step-finish. Content produced, nothing pending, and the loop continued into a trailing-assistant request anyway.["tool-calls","unknown"].includes(...)is still the exit condition in 1.18.26, at three call sites.The user-visible symptom is different enough from yours to be worth noting: a long reply appears to stop mid-sentence, then the 400 lands. Nothing is retryable in-session.
One thing for prioritisation: on Bedrock there is no model choice that avoids this. Assistant prefill 400s across the current Claude family, so every Anthropic model in that catalogue hits the same path.
amelvil2-ford commentedon Sep 3, 2026
I haven't used this provider, but does this fix help you? #47079
Let me know how it goes
ton618to commentedon Sep 5, 2026
Thanks for the pointer @amelvil2-ford.
I don't think #47079 resolves this specific case, and for SAP AI Core / Bedrock it would trade the 400 for a per-turn hard error.
The difference is subtle:
FinishReasonenum, soai-sdk.tsnormalizes it to"unknown"— on a turn that produced a complete text response.For
sap-ai-coreandamazon-bedrockAnthropic models,"unknown"is the value on every successful turn (see the DB evidence above and @lgarceau768's Bedrock confirmation). #47079's default path treats anything outsidestop | tool-calls | length | error | content-filterasok: falseand throwsunrecognized finish reason "unknown". So out of the box it would turn every completed SAP/Bedrock turn into an error, and you'd need afinish.chunkplugin that flipsok = truefor"unknown"just to get back to a working state. Thefinish.chunkhook itself is a nice addition, but as a default it swaps one per-turn failure for another on these providers.The proximate cause is the loop-exit predicate
!["tool-calls", "unknown"].includes(finish)(still present at the three call sites @lgarceau768 noted). The narrow fix that avoids both the prefill 400 and a spurious error is to treat an"unknown"finish as terminal when the turn already produced text and has no pending tool calls, while keeping the existing continue-on-"unknown"behavior for empty/truncated turns (so #45315 stays covered). Concretely:I've been running exactly this as a local patch against 1.18.23 and it cleanly stops the prefill 400 on SAP
anthropic--claude-4.8-opuswith no error line and no regression in the prompt-loop tests. Mapping the provider's raw finish (end_turn/stop→stop) would be the more complete fix, but the predicate above is a safe minimal guard in the meantime.