Skip to content

fix: show provider error and recover session when image input is rejected - #1

Open
Shalin-Shah-2002 wants to merge 3 commits into
devfrom
image-rejection-recovery
Open

Shalin-Shah-2002 wants to merge 3 commits into
devfrom
image-rejection-recovery

Conversation

@Shalin-Shah-2002

@Shalin-Shah-2002 Shalin-Shah-2002 commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Issue for this PR

Closes anomalyco#52042

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Sending an image to a text-only custom model broke the session in three ways. This fixes each one.

1. The real error was hidden (packages/llm/src/route/executor.ts). The response body was only added to the error message when it was 500 characters or less. OpenAI-compatible providers usually return longer JSON, so users only saw Provider request failed with HTTP 400. Long bodies now get their reason from error.message, error, message, or detail. The full redacted body is still kept on http.body.

2. Declared modalities were ignored in V2 sessions (session/runner/model.ts, llm.ts, to-llm-message.ts). modalities.input already reaches the catalog model, but SessionRunnerModel.resolve returned an @opencode-ai/llm Model that carries no capabilities, so the runner never checked them. resolve now also returns the declared input list. Unsupported attachments become the same ERROR: Cannot read "<file>" (this model does not support image input). Inform the user. note the V1 path uses. When the list is empty (unknown), attachments are sent as before. A text-only custom model can now set "modalities": { "input": ["text"] } and the image never reaches the provider.

3. The rejected image was replayed forever (session/runner/to-llm-message.ts). Every later turn, including drains after a restart, re-sent the image and failed again. When a user message's attachments are followed by an assistant turn that failed before any output with HTTP 400/413/415/422 on the same model, those attachments are replaced with a short note on replay. A different model still gets them, and 5xx or network errors never drop attachments. The check uses the HTTP <status> text from RequestExecutor because stored assistant errors are only { type: "unknown", message }. I can switch it to a structured error kind if preferred.

Not covered: failed messages still aren't shown in the UI, which the issue also mentions.

How did you verify your code works?

  • Unit tests for each fix in packages/llm/test/executor.test.ts and packages/core/test/session-runner-message.test.ts.
  • A runner test in packages/core/test/session-runner.test.ts: a model that declares only text gets a prompt with an image, and the provider request contains the note instead of the image.
  • Each new test fails without its fix and passes with it.
  • bun typecheck passes in packages/llm and packages/core. bun test passes in packages/llm. In packages/core, 1106 of 1108 tests pass. The 2 failures are the util.flock "unwritable lock roots" tests, which also fail on dev when run as root.

Screenshots / recordings

N/A (not a UI change)

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

🤖 Generated with Claude Code

https://claude.ai/code/session_018CFR7BogLxVEwTy74UxWUE

claude added 3 commits October 1, 2026 02:49
Provider error bodies over 500 characters were dropped from the error
message entirely, leaving only "Provider request failed with HTTP 400".
Extract the short reason from `error.message`, `error`, `message`, or
`detail` so users can see why the provider rejected the request.

Refs anomalyco#52042

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018CFR7BogLxVEwTy74UxWUE
When a provider rejects a request because of an attachment (for example
an image sent to a text-only custom model), V2 session history replayed
the same attachment on every later turn, so the session could never
recover. Replace attachments whose turn failed with an HTTP 4xx request
error before producing any output with a short text note when replaying
to the same model. Switching to a different model still sends them.

Refs anomalyco#52042

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018CFR7BogLxVEwTy74UxWUE
The V2 session runner sent every user attachment to the provider without
checking the catalog model's input modalities, so declaring
`modalities.input` for a custom model had no effect on V2 sessions.
Return the catalog's declared input modalities alongside the resolved
model and replace attachments the model does not support with the same
"does not support <modality> input" note the V1 path uses. Models with
unknown modalities keep sending attachments unchanged.

Refs anomalyco#52042

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018CFR7BogLxVEwTy74UxWUE
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Hey! Your PR title Handle unsupported attachments and input modalities in LLM messages doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@Shalin-Shah-2002 Shalin-Shah-2002 changed the title Handle unsupported attachments and input modalities in LLM messages fix: show provider error and recover session when image input is rejected Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Thanks for updating your PR! It now meets our contributing guidelines. 👍

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.

session: provider image rejection bricks session — generic 400 error, no recovery path

2 participants