DeepSeek rejects a request whose assistant history omits reasoning_content
once the conversation is in thinking mode:
HTTP 400: The `reasoning_content` in the thinking mode must be passed
back to the API.
A thinking-mode turn that only emits tool calls produces no reasoning of
its own, so Message.Thinking is empty and the wire field was dropped. The
next request then mixes assistant messages with and without the field and
upstream fails. Only kimi_coding had an empty-string fallback.
Detect thinking mode from the conversation itself: when any assistant
message carries reasoning, every replayed assistant message must carry the
field, empty string included. Narrower than matching on model name, which
would also inject the key for non-thinking models such as deepseek-chat.
Reproduces from a run where iteration 1 returned reasoning plus an edit
call, the edit failed, iteration 2 returned a bare read_file call, and
iteration 3 was rejected.
Upstream returns HTTP 400 `thinking is enabled but reasoning_content
is missing in assistant tool call message at index N` when an
assistant message with tool_calls is replayed in history without a
reasoning_content field. Kimi has server-side thinking enabled by
default for kimi-k2-turbo-preview, so the field is required even when
goclaw doesn't have captured reasoning content to send (e.g. the model
emitted a tool_call without any thinking, or the stream chunk that
carried it was lost).
The existing branch already gates on
openAIWireAssistantReasoningContent(model) (kimi/deepseek/o-series)
and emits the field only when Thinking != "". Extend so kimi_coding
also emits an empty string when Thinking is unset — satisfies Kimi's
"must be present" check without inventing reasoning content. Other
providers in the allowlist keep today's behavior: omit when empty.
Three new tests:
- kimi_coding always carries reasoning_content on assistant
- kimi_coding preserves real Thinking content when set
- non-kimi providers (deepseek) do NOT inject empty reasoning_content
Reference: NousResearch/hermes-agent plugins/model-providers/kimi-coding
documents the same upstream behavior (thinking enabled by default,
reasoning_content roundtrip required).
Moonshot's Kimi Coding endpoint is OpenAI-compatible on the wire but
has two non-standard rules:
1. Every request must carry `User-Agent: claude-code/0.1.0` — without
it the upstream rejects the call outright.
2. `temperature` is locked to the server default; passing any other
value returns HTTP 400 `invalid temperature: only 1 is allowed for
this model`.
Rather than special-case either, this commit generalises both:
- WithExtraHeaders on OpenAIProvider — static headers attached to
every outgoing request. Reusable by any future provider that needs
pinned identity headers; mirrored in adapter_openai.ToRequest so
callers using the adapter path see the same shape.
- The existing skipTemp branch in openai_request.go gets a
provider_type check — kimi_coding joins o1/o3/o4/gpt-5-mini in
omitting `temperature` from the request body.
Provider wiring:
- store.ProviderKimiCoding constant + ValidProviderTypes entry +
KimiCoding{DefaultAPIBase,DefaultModel,RequiredUserAgent}.
- case store.ProviderKimiCoding in both registration switches
(cmd/gateway_providers.go and internal/http/providers.go).
- UI dropdown entry with the API base pre-filled.
5 unit tests cover: real outgoing header injection, adapter-path
header mirroring, empty-map WithExtraHeaders no-op, kimi_coding
strips temperature, and the negative control (other providers still
forward temperature).
Admin flow: Providers → Add → "Kimi Coding (Moonshot)" → paste API
key → save.