MoonshotAI / MoonshotAI/kimi-code

Session permanently bricked after provider rejects an image with a generic 400 ("Invalid request.") — media-strip recovery never engages

Open
#3,087 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
7.5k
Forks
1.2k
Avg merge
11h 53m
Merged PRs (30d)
350

Description

Summary

When a provider rejects a request containing an image with HTTP 400 and a generic error
message (Invalid request.), the media-strip recovery introduced in 0.23.5 / 0.24.0 does
not engage, because isImageFormatError only matches a fixed set of message patterns.
The turn fails as non-retryable, the image stays in the conversation history, and every
subsequent request — including after kimi -r and brand-new prompts — fails with the
same 400. The session is permanently unusable.

This contradicts the stated intent of the existing mechanism ("recover an already-stuck
session by dropping the offending image and retrying, so one such image can no longer
make every later request fail" — 0.23.5 changelog).

Environment

  • kimi-code 0.37.2 (native install), macOS arm64
  • Provider: custom openai-type provider (third-party OpenAI-compatible gateway),
    model kimi-k3, capabilities include image_in, thinking effort max

Steps to reproduce

  1. Configure an openai-type model that declares image_in, but whose backend rejects
    image input with a generic 400 body, e.g.
    {"error":{"code":"invalid_request","message":"Invalid request.","param":null,"type":"invalid_request_error"}}
  2. In a session, let the agent read an image with ReadMediaFile (two screenshots, in
    this case), or paste an image.
  3. The next request — the first one containing the image — fails:
    provider.api_error: 400 Invalid request.
  4. Send any further prompt, or resume with kimi -r <session>: every request fails with
    the same 400. The session never recovers.

Expected behavior

One rejected image must not brick a session. The client should detect the image-related
rejection and resend with the offending media stripped (as it already does for
unsupported image url-style errors), or at least offer a recovery path.

Actual behavior

Every turn fails with 400 Invalid request. (retryable: false), forever.

Session log excerpt:

2026-08-19T07:22:11.050Z WARN  llm request failed  turnStep=1.19 model=k3-flashflame errorName=APIStatusError errorMessage="400 Invalid request." statusCode=400
2026-08-19T07:28:40.978Z WARN  llm request failed  turnStep=2.1  model=k3-flashflame errorName=APIStatusError errorMessage="400 Invalid request." statusCode=400
2026-08-19T07:30:58.222Z WARN  llm request failed  turnStep=3.1  model=k3-flashflame errorName=APIStatusError errorMessage="400 Invalid request." statusCode=400
2026-08-19T07:40:37.260Z WARN  llm request failed  turnStep=4.1  model=k3-flashflame errorName=APIStatusError errorMessage="400 Invalid request." statusCode=400
2026-08-19T07:45:20.991Z WARN  llm request failed  turnStep=5.1  model=k3-flashflame errorName=APIStatusError errorMessage="400 Invalid request." statusCode=400

Root-cause analysis

I captured the provider-bound request with a local relay:

  • The client's request is spec-compliant: blobref: references are correctly resolved
    to valid data:image/png;base64,... payloads (valid PNG bytes), placed in a synthetic
    user message ("Attached media from tool result:"), with correct tool_call/tool_result
    pairing. The 400 comes from the provider — this route does not accept image input at
    all (controlled replays against the provider: identical request without images → 200;
    with any image, even a 1x1 PNG or a remote URL → 400).
  • The recovery path in
    packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts only strips media
    when isImageFormatError(raw) returns true
    (packages/agent-core-v2/src/kosong/contract/errors.ts). Its pattern list
    (unsupported image url|format|type, invalid image ..., etc.) does not match a bare
    Invalid request., so the strip-and-resend never happens and the error propagates as
    non-retryable.

Suggested fix: when a 400 is received and the projected request contains media parts,
attempt the media-strip resend even if the error message doesn't match the known patterns
(limited to one attempt), instead of failing the turn outright.

Secondary observation: kimi -p retries a deterministic 400 ten times

A kimi -p run against this bricked session issued 10 identical requests with exponential
backoff over ~3 minutes (captured via relay; all with X-Stainless-Retry-Count: 0, i.e.
agent-loop retries, not SDK retries). Interactive turns failed after a single attempt.
A 4xx invalid_request is deterministic and should not be retried.

Artifacts

I have a full debug session export (/export-debug-zip) and the captured
request/response pair; happy to share them privately if useful (they contain session
content, so I'd rather not attach them publicly).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts and trace its media recovery decision into packages/agent-core-v2/src/kosong/contract/errors.ts, including the existing image-error patterns. Confirm the behavior with a provider response like the documented generic 400; done means a media-bearing request has a bounded recovery path and deterministic invalid requests are not retried indefinitely.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.