Repository navigation
feat(core): transcribe images for models without vision support - #54366
savasturkoglu1 wants to merge 6 commits into
Conversation
|
The following comment was made by an LLM, it may be inaccurate: |
There was a problem hiding this comment.
The idea is reasonable, but this needs a rebase and some rework before it can land. I ran the new test file (7 pass), the rest of the session-runner* tests (137 pass) and packages/core typecheck (clean). I couldn't run it end to end: that needs a real vision model on the same provider, and the simulated model can't provide one.
Public API: adds the optional experimental.vision_model config key (provider/model), which is backwards compatible. The internal SessionRunnerModel interface gains resolveInfo; nothing outside packages/core changes.
Concerns:
- Rebase onto the current runner. On latest
dev, requests go throughSessionModelRequest(packages/core/src/session/model-request.ts, called fromrunner/step.ts). It already replaces images the model can't read with anERROR: Cannot read "<file>" … Inform the user.text, so turns no longer fail with a provider error. TherunTurnAttempt/toLLMMessages(context, model)code this hooks into is gone. The bridge needs to run beforeunsupportedParts/boundImages. The transcription request should also go through that same request-building path, so it gets the session headers, user agent and pluginmodel.requesthooks. Right now it is a separatellm.streamcall that skips them. - Images go to another model by default. With no config set, the user's images are sent to an auto-picked model the user never chose, which may cost more. Those tokens also don't show up in the session's cost or token totals. Please make this opt-in through
experimental.vision_model, or at least record the usage. - Failures are retried on every step (see inline).
- Tests:
session-runner-vision.test.tsrelies on shared module-level state (let catalogModels,configuredVisionModel,transcribeChunks, a sharedrequestsarray) reset by hand in each case. Please build that state inside each test's layer instead, so the cases don't depend on running in order.
| Stream.filter(LLMEvent.is.textDelta), | ||
| Stream.map((event) => event.text), | ||
| Stream.mkString, | ||
| Effect.catch(() => Effect.succeed("")), |
There was a problem hiding this comment.
Errors are swallowed without a log, and an empty or failed result is never cached (line 77). If the vision model is broken or rate-limited, every image gets sent to it again on every step of the turn and on every later turn, each time costing a full request. Please log the failure, and remember failed keys for at least the rest of the turn. Also, this Map lives for the whole process and never drops entries.
| // Only the selected model's own provider is used. Sending the user's image | ||
| // to a different provider would be surprising, so no candidate means the | ||
| // images are left untouched and the provider error surfaces unchanged. | ||
| const candidates = (yield* deps.catalog.model.available()).filter( |
There was a problem hiding this comment.
With no experimental.vision_model set, this picks the newest vision model on the provider and sends the user's images to it automatically. That model can be much more expensive than the one the user selected, and its usage never reaches the session's cost or token totals. I think this should be opt-in (only when vision_model is configured), or its spend should at least be recorded on the session.
| const model = yield* models.resolve(session) | ||
| const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq) | ||
| const context = entries.map((entry) => entry.message) | ||
| const bridged = yield* vision.bridge({ session, model, messages: context }) |
There was a problem hiding this comment.
On latest dev this code no longer exists. Requests are built by SessionModelRequest (called from runner/step.ts), which already replaces images the model can't read via unsupportedParts. After rebasing, the bridge has to run before that step, or the images will already have been replaced with the error text.
|
Hi, quick context for reviewers: This ports #48334 to V2, as suggested when that PR was closed ("no longer taking PRs for V1, please port it over"). Before: in V2, prompt attachments are lowered into provider media parts unconditionally, so pasting a screenshot while a non-vision model is selected sends the image to the provider and fails the turn with a provider error. After: the image is transcribed by an active vision-capable model from the same provider (or This is scoped as a first slice of the runner's unchecked "attachment normalization" item — it doesn't attempt full materialization, and to keep the review small it reuses Happy to adjust the approach — e.g. preferring the catalog's |
9b3a09d to
4ffaf9f
Compare
|
Thanks for the review — rebased onto latest Runner seam. The bridge now runs in Opt-in. Nothing is transcribed unless Failures. A failed or empty transcription is logged ( Tests. PR description. Updated to the repository template.
|
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
4ffaf9f to
6b233dd
Compare
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
Issue for this PR
Closes #48332
Type of change
What does this PR do?
When the selected model cannot read images, V2 replaces each attached image with
ERROR: Cannot read "<file>" ... Information the user.This lets the user opt into transcribing those images instead, so they get usable context without switching models.Set
experimental.vision_modelto aprovider/modelthat accepts image input. Before each step builds its request, the runner replaces unreadable image attachments with asynthetictranscript message and drops those files from the prompt:It is opt-in: nothing is sent anywhere unless
vision_modelis configured, and provider usage is recorded on the session with sourcevision. Failed or empty transcriptions are logged and remembered so they are not retried on every later step; the original error text is still shown in that case. Transcription goes through the normal request path (SessionModelRequest.generate), so it carries the session headers, user agent, and model request hooks.Touches
packages/schema(config field, usage source),packages/core(newSessionVisionservice, runner hook), and regenerated client types.How did you verify your code works?
bun testinpackages/core/test/session-vision.test.tscovers: transcription and synthetic injection, no-op without config, no-op when the selected model reads images, no-op with no images, caching, failed transcription remembered, and empty transcript treated as failure. Also ran the fullpackages/coresuite andbun run checkfrom the root.Screenshots / recordings
Not a UI change.
Checklist