Skip to content

[Provider] kimi: no requiresAdjacentResponsesToolResults → 400 when hook-injected developer context splits a tool call from its result #4726

Closed

Description

@yxr1995-maker

Client or integration

Codex App (desktop). Also reproduced with a direct HTTP client against the local proxy, so this is not client-specific.

Provider or upstream service

kimi — Kimi Code Plan (OAuth, authMode: "oauth"), used over the Responses wire (adapter: "openai-responses", baseUrl: https://api.kimi.com/coding/v1).

OpenCodex version

2.56.0 (npm @bitkyc08/opencodex@2.56.0), macOS (arm64), default provider kimi. Behaviour is identical on main as of this report: the only requiresAdjacentResponsesToolResults: true in the registry is DeepSeek's.

Endpoint or capability

/v1/responses → Kimi https://api.kimi.com/coding/v1/responses, tool calls (tool result adjacency).

Current behaviour

Kimi's Responses endpoint requires a tool result to follow its call immediately. When any other item sits in between — most commonly hook-injected developer context such as LSP diagnostics — Kimi rejects the whole request with HTTP 400:

Invalid request: an assistant message with 'tool_calls' must be followed by tool
messages responding to each 'tool_call_id'. The following tool_call_ids did not
have response messages: exec:0

OpenCodex already implements exactly this repair — normalizeResponsesToolResultAdjacency() in src/adapters/openai-responses/tool-output-recovery.ts — but it is gated per provider in src/adapters/openai-responses/passthrough.ts:

if (provider.requiresAdjacentResponsesToolResults === true) {
  outBody = normalizeResponsesToolResultAdjacency(outBody);
}

That flag is seeded only by the DeepSeek registry entry (src/providers/registry/entries-core.ts, the requiresAdjacentResponsesToolResults: true line whose comment cites "DeepSeek rejects a valid Codex continuation when hook-provided developer context splits a call from its result (#1292)" — the identical failure shape).

No Kimi entry carries it:

  • entries-core.ts → id: "kimi" (adapter: "openai-chat") — no adjacency flag;
  • entries-extended.ts → id: "kimi-code" (adapter: "openai-chat") — no adjacency flag;
  • there is no Kimi entry on the Responses wire at all, so a user who switches Kimi to adapter: "openai-responses" (the same hand-configuration pattern the registry comment at entries-extended.ts acknowledges for other providers) inherits no adjacency normalization and every affected session 400s permanently, because statelessResponses: true replays the broken history on every turn.

Workaround currently applied locally: "requiresAdjacentResponsesToolResults": true in the kimi provider block of config.json — this resolves it (see verification below).

Expected behaviour

The kimi provider should get the same adjacency normalization DeepSeek gets, i.e. the registry entry should set:

requiresAdjacentResponsesToolResults: true,

The flag is inert for chat-wire bodies (it is only read in the openai-responses adapter path), so it is safe on the current openai-chat entries and would also be correct if a Kimi Responses-wire entry is added later.

Minimal redacted request or reproduction

Sent to the local proxy (loopback bind, no token required). Tool declared as the client declares Codex code mode (custom/exec), which the routed path lowers to a function.

{
  "model": "kimi/k3-256k",
  "stream": false,
  "max_output_tokens": 16,
  "tools": [{
    "type": "custom",
    "name": "exec",
    "description": "code mode exec",
    "format": { "type": "grammar", "syntax": "lark", "definition": "start: /[\\s\\S]+/" }
  }],
  "input": [
    { "type": "message", "role": "user", "content": [{ "type": "input_text", "text": "reply with the single word ok" }] },
    { "type": "custom_tool_call", "call_id": "exec_900_ocxtest", "name": "exec", "input": "text('hi')" },
    { "type": "message", "role": "developer", "content": [{ "type": "input_text", "text": "[hook] LSP diagnostics: none" }] },
    { "type": "custom_tool_call_output", "call_id": "exec_900_ocxtest", "output": [{ "type": "input_text", "text": "hi" }] }
  ]
}
curl -s -o out.json -w '%{http_code}' -X POST http://127.0.0.1:10100/v1/responses \
  -H 'content-type: application/json' --data-binary @body.json

Deterministic contrast, same body except the two lines noted:

input shape before fix after flag
call → developer message → result 400 200
call → result (adjacent, no intervening item) 200 200
call → developer message (no result item at all) 200 200

So the variable is purely the intervening item.

Actual response or error

HTTP/1.1 400
{"error":{"code":"invalid_request_error","message":"Invalid request: an assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: exec:0", "type":"invalid_request_error"}}

Note on the id: the request above uses call_id: "exec_900_ocxtest", yet Kimi reports exec:0. The <tool-name>:<ordinal> form is Kimi's own label for the call, not something OpenCodex rewrote. Real sessions show the same form (exec:17, collaboration__spawn_agent:0), which may be worth knowing when triaging similar reports.

Upstream documentation

No public specification documents this adjacency requirement for the Kimi Responses endpoint — Kimi Code Plan's documented surface is the Chat endpoint (https://platform.kimi.com/docs/api/chat, as cited in the registry entry). The requirement is therefore evidenced by the reproduction above (the 400 comes from https://api.kimi.com/coding/v1/responses), and by the fact that the same shape already required a workaround for DeepSeek in this repo.

Suggested mapping or implementation notes

Additional context and attachments

Real-world trigger in Codex App: the desktop LSP hook inserts a developer message ("LSP diagnostics after editing …") between a code-mode exec call and its result. The session then 400s on every subsequent turn while the model is Kimi; switching the same thread to DeepSeek (which has the flag) resumes normally — consistent with the flag being the only difference.

Local verification after applying the flag to the kimi provider config on a stock 2.56.0 install (installed src/ is byte-identical to the published 2.56.0 tarball): the repro body above returns 200, with the tool result reordered adjacent to its call. Restart + /healthz clean.

Checks

  • I searched existing provider and compatibility issues.
  • The request and response were redacted.
  • The expected behaviour is based on an upstream specification or a concrete client requirement.

Activity

  1. coderabbitai commented on Sep 15, 2026

    @coderabbitai
    Contributor
    ⚠️ Possible Duplicate Issue(s)

    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. added
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    toolstool_calls, MCP, web-search / sidecar tools
    on Sep 15, 2026
  3. lidge-jun commented on Sep 15, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 75 / 80

    이슈 #4726은 Kimi를 Responses 와이어(adapter: "openai-responses")로 붙였을 때, 훅이 넣은 developer 메시지가 tool call과 tool result 사이에 끼어들면 업스트림이 HTTP 400을 내는 문제를 보고한다. 에러 문구는 an assistant message with 'tool_calls' must be followed by tool messages...이고, 재현 본문은 custom_tool_call → developer → custom_tool_call_output 순서다. 인접(call→result)이면 200, 사이 메시지가 있으면 400으로, 원인은 끼어든 항목뿐이라는 표까지 있다.

    지금 dev HEAD(45cfb04e9, package 2.57.0)를 보면, 이미 같은 수리 함수가 있다. normalizeResponsesToolResultAdjacency()는 src/adapters/openai-responses/tool-output-recovery.ts 약 421행에 있고, call/result를 한 배치로 모은 뒤 끼어든 non-tool 항목을 배치 뒤로 보낸다. 게이트는 src/adapters/openai-responses/passthrough.ts 약 272–273행이다. provider.requiresAdjacentResponsesToolResults === true일 때만 그 함수를 탄다.

    그 플래그를 켜 둔 레지스트리 항목은 지금 DeepSeek뿐이다. src/providers/registry/entries-core.ts 약 1063–1066행 주석이 바로 #1292(훅 developer가 call/result를 갈라 DeepSeek가 거절)를 가리킨다. 반면 id: "kimi"(같은 파일 약 411–441행, adapter: "openai-chat")와 id: "kimi-code"(src/providers/registry/entries-extended.ts 약 888–905행)에는 이 필드가 없다. 레지스트리에 Responses 전용 Kimi 엔트리도 없다. 그래서 사용자가 Kimi를 Responses로 손설정하면 인접 정규화가 한 번도 안 돌고, 깨진 히스토리를 매 턴 다시 보내는 환경에서는 세션이 계속 400에 막힌다.

    플래그 자체는 chat 와이어 본문에서는 읽히지 않는다(Responses 패스스루에서만 검사). 그래서 지금 chat 기본 엔트리에 requiresAdjacentResponsesToolResults: true를 심어도 chat 경로 동작은 그대로이고, 나중에 Responses 엔트리를 추가하거나 손설정으로 와이어를 바꾸면 그때부터 수리가 켜진다. 제보자가 config.json의 kimi 블록에 같은 플래그를 넣어 재현 본문이 400→200으로 바뀐 것도 현재 코드와 맞다. derive.ts/router.ts는 레지스트리 시드를 저장 설정이 비어 있을 때 채우는 경로가 이미 있다.

    CodeRabbit이 #1292 중복 후보로 붙였지만, #1292는 DeepSeek용 수리·플래그의 원조이고 이번 이슈는 그 수리를 Kimi에도 시드하라는 좁은 provider-compatibility 요청이다. 타입/config 분할 캠페인에 무효화될 경로가 아니다. 재현·우회·제안이 모두 분명해서 작은 레지스트리 PR 하나로 닫기 좋다.

    라인 411–441 (src/providers/registry/entries-core.ts id: "kimi") - requiresAdjacentResponsesToolResults가 없어 Responses 손설정 시 인접 정규화가 꺼진 채로 남는다
    라인 888–905 (src/providers/registry/entries-extended.ts id: "kimi-code") - API-key 형제 항목에도 같은 플래그가 없다
    라인 1063–1066 (entries-core.ts DeepSeek) - #1292와 같은 실패 모양인데 Kimi만 시드에서 빠져 있다
    라인 272–273 (src/adapters/openai-responses/passthrough.ts) - 수리 자체는 이미 있고, 프로바이더 플래그로만 막혀 있다
    normalizeResponsesToolResultAdjacency (tool-output-recovery.ts ~421) - call/result 쌍·병렬 배치 규칙이 이미 있으므로 Kimi도 같은 게이트를 쓰면 된다

    메인테이너의 판단이 필요한 지점

    • kimi/kimi-code 둘 다에 플래그를 심을지, OAuth kimi만 먼저 할지
    • 손설정 Responses 사용자만 고칠지, 레지스트리에 Kimi Responses 엔트리까지 새로 둘지(이슈 본문은 플래그 시드만으로도 충분하다고 함)
    • 장기적으로 wire/capability에서 플래그를 파생할지, 지금은 DeepSeek처럼 엔트리 시드로 둘지

    너의 추천
    이슈는 유효하다. kimi와 kimi-code 레지스트리 엔트리에 requiresAdjacentResponsesToolResults: true를 추가하고(주석에 이 이슈 번호와 #1292 동형임을 적기), 훅이 call/result를 가르는 Responses 픽스처로 회귀 테스트를 잠근 뒤 CI 그린이면 머지하면 된다. 임시 우회는 제보자처럼 config.json kimi 블록에 같은 플래그를 넣는 것. 중복으로 닫을 필요는 없고(#1292는 DeepSeek 원조), close-don't-rebase 해당 없음.

    이 댓글은 grok-bot이 작성했습니다

  4. added a commit that references this issue on Sep 16, 2026
  5. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    Owner

    Landed via #4770 at aa31ed4

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    landed-via-maintainerOriginal PR closed after landing via a maintainer merge trainproviderProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reportstoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions