Skip to content

fix(opencode): advertise per-model image capabilities in the exported config - #4293

Closed
colthreepv wants to merge 1 commit into
lidge-jun:devfrom
colthreepv:codex/opencode-model-capabilities
Closed

fix(opencode): advertise per-model image capabilities in the exported config#4293
colthreepv wants to merge 1 commit into
lidge-jun:devfrom
colthreepv:codex/opencode-model-capabilities

Conversation

@colthreepv

@colthreepv colthreepv commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

opencode reads per-model image support from the model entry itself and cannot consult models.dev for opencodex — the provider is not listed there — so its loader falls back to a hardcoded false for an entry that declares neither attachment nor modalities. The OpenCode export wrote only name and limit, which cataloged every routed model as text-only and made the TUI refuse an image paste client-side. No request reached the proxy, so the vision sidecar never ran either.

The affected set was not limited to genuinely blind models: native OpenAI slugs whose /api/models row reports ["text","image"] were blocked the same way, as were text-only models the sidecar covers.

The row's declared inputModalities now travels through OpencodeCatalogModel / opencodeCatalogFromProxyRows and is serialized as opencode's own per-model fields on both provider generations:

"gpt-5.6-luna": {
  "name": "gpt-5.6-luna (native)",
  "limit": { "context": 272000, "output": 32000 },
  "attachment": true,
  "modalities": { "input": ["text", "image"], "output": ["text"] }
}

Three decisions carry the shape of the change:

  • A row that declares nothing is left alone. An entry with no capability keys is already read as text-only by opencode, so the emitted bytes for an undeclared model are unchanged. The full-document goldens move only for models that declare something.
  • Values outside opencode's enum are dropped, and the row survives. opencode accepts text|audio|image|video|pdf (opencode.ai/config.json), wider than the internal text|image|audio vocabulary, so nothing the proxy can produce today is filtered. The guard exists because audio already took a whole config file down for Gajae, whose loader rejects the file over one out-of-enum value. Where Pi and Gajae drop a row they cannot represent, opencode keeps it without capability keys rather than receiving text it cannot read.
  • Both generations get the fields. attachment / modalities are declared by opencode's V1 model schema. Its V2 model schema expresses the same information as capabilities.{tools,input,output}, which V2 reaches by migrating the V1 block. Both exported blocks therefore carry these fields; native V2 consumption of the extra keys is not established.

This is the same class of defect already fixed for the Codex app in #344/#349 and already handled by the Hermes/Pi/Gajae exporters (#3146); the OpenCode serializer was missed.

One pre-existing defect is fixed along the way because the change moves the data: exportModelsFromProxyRows re-joined modalities from the raw /api/models rows keyed by namespaced with the first row winning, so a disabled or hidden duplicate could donate its modality list to the visible entry — the donation the availability and effort-ladder rules already refuse. Modalities now come from the same visibility-filtered catalog row as the model itself.

Out of scope, stated rather than silently widened. Custom rows take a different path through listManagementModelRows, which rebuilds the row from config.customModels and copies only the stored modalities, discarding the sidecar-added image that gatherRoutedModels appended at src/codex/catalog/provider-fetch.ts:2649. A custom model with stored inputModalities: ["text"] whose provider also lists it in noVisionModels therefore still reaches opencode as text-only. That projection affects every client export built from this management-row projection, not only this one, and belongs in its own change; the guide documents the row-level rule and names this case rather than claiming sidecar coverage unconditionally.

Upstream verification

The emitted field names, the five-value enum, and the attachment semantics come from opencode's published config.json and from the loader that reads them (packages/opencode/src/provider/provider.ts:1523), so the V1 contract is both documented and loadable. The V2 path was read, not run: packages/core/src/config.ts and packages/core/src/v1/config/migrate.ts show V2 reaching these capabilities by migrating the V1 block. Native V2 consumption of the extra keys is not established here, so a reviewer who needs a V2 guarantee should confirm it against the target version.

Verification

  • Which revision. The pushed head is 9ad205758, based on dev d9849942a. It has since been rebased locally onto dev cb7f96cbc, the tip that landed the omo client, which also changes src/clients/config-export.ts, its contracts.ts and tests/config/client-config-export.test.ts. The rebase applied with no conflicts and altered none of this change's own bytes (same 10 files, +275/-27), but the checks below ran on that local rebase rather than on 9ad205758; the difference is the base, and the extra base commits are not exercised by them.
  • bun run typecheck — clean (native TypeScript 7.0.2, 1423 files).
  • bun run privacy:scan — passed.
  • bun test tests/config/client-config-export.test.ts tests/providers/opencode-cli.test.ts tests/clients/client-export-modality-enum.test.ts tests/server/management-client-config-route.test.ts tests/test-layout-tooling.test.ts tests/test-layout.test.ts — 195 pass, 0 fail. No test file was added, so no layout manifest entry was needed.
  • The repository CI on the pushed head 9ad205758 is green: 24 checks succeeded and 2 were skipped by design (windows <shard>/6, macos control), including test 1/4 to 4/4, gates, storage policy, api usage, docker smoke, and the keyring and npm-global jobs on Ubuntu, Windows and macOS.
  • The local full suite is a complete run of 22913 tests in 1181 files in 755s on this workstation: 22767 pass / 139 skip / 7 fail / 1 error. The five named failures were reproduced individually on a clean dev worktree at 29d632ff2 rather than assumed environmental — pnpm generated shims > verifies POSIX shims point at the active package and > accepts a pnpm group alias when it resolves to the active package, test runner isolation > keeps the .NET known-folder lookup resolvable inside the sandbox, and the two bearer admission is not reused as a Cursor upstream credential cases, which time out at 5s. Two of the seven counted failures are tests/routing/routing-policy-pool-quota.test.ts and tests/routing/routing-profile.test.ts, whose workers crashed with exit code 3 under the 4-way parallel run; both pass in isolation on the rebased head (35 pass, 0 fail), and routing is untouched here. The tally's single 1 error is unnamed in the captured recap and is left unattributed rather than called pre-existing.
  • Refreshing that run. Re-running the whole suite on this workstation after the rebase twice exceeded the runner's own 900s cap, which terminated it with code: 124, so the figures above come from the complete pre-rebase run rather than a re-derivation on the rebased tree.

New coverage, all behavioral:

  • tests/providers/opencode-cli.test.ts — the launcher path, proxy rows to catalog to OPENCODE_CONFIG_CONTENT: a declared ["text","image"] row reaches both blocks, a text-only row is advertised text-only rather than omitted, an undeclared row carries no capability keys, a disabled row stays filtered.
  • tests/clients/client-export-modality-enum.test.ts — the enum boundary, in the file that already guards Pi, Gajae and Hermes: a live ["text","image","audio"] row is carried whole, ["audio"] stays audio-only, an out-of-enum value is dropped with the entry retained, and no model in a whole catalog carries a value opencode rejects.
  • tests/config/client-config-export.test.ts — Fast rows inherit the capabilities of the row they duplicate (a Fast user selects exactly that selector), each generation owns its modalities map, and a disabled duplicate cannot donate its modality list.
  • tests/server/management-client-config-route.test.ts — the /api/client-config golden for the route the dashboard download uses, updated to the new entry shape.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Docs: docs-site/src/content/docs/guides/opencode.md gains an "Images and attachments" section stating where the capability fields come from, that an undeclared row stays plain, that a discovered model picks up the sidecar image, and that a custom row is written from the modalities stored on it. The seven translated copies of that page (fr, ja, ko, ru, tr, zh-cn, zh-tw) are untouched and none of them makes a claim about attachments that this section contradicts.

No security-relevant surface is touched. The change consumes catalog metadata the proxy already reports at GET /api/models and filters it to opencode's allowed values; admission, credentials, and headers are unchanged.

Closes #4286

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

done with ds41flash help, reviewed by astra (limitations noted above).

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 11, 2026
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (2/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 2/4).

Review readiness checklist

  • ✅ All CI tests are green on my local testing.
  • ✅ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

2/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@colthreepv
colthreepv force-pushed the codex/opencode-model-capabilities branch from 4e543d4 to ab26dde Compare September 11, 2026 17:25
… config

The OpenCode export emitted only `name` and `limit` per model, so opencode computed
`capabilities.attachment` and `capabilities.input.image` as false for every opencodex model:
the provider is absent from models.dev, and opencode's loader falls back to a hardcoded false
for an entry that says nothing. Attachments were then refused client-side in the TUI before any
request reached the proxy — including native OpenAI slugs that /api/models reports as
["text","image"], and text-only models the vision sidecar covers.

Carry the catalog row's `inputModalities` through `OpencodeCatalogModel` and
`opencodeCatalogFromProxyRows`, then serialize them as opencode's own per-model fields
(`attachment`, `modalities`) as declared by opencode's published model schema. Both provider
generations get them, so the two spellings of one model list cannot disagree; the V2 model
schema expresses capabilities as `capabilities.{tools,input,output}` (which opencode fills by
migrating this same `modalities` field) and its loader decodes with
`onExcessProperty: "ignore"`. A row that declares nothing keeps the previous entry shape, which
opencode already treats as text-only.

Values outside opencode's enum (text|audio|image|video|pdf) are dropped rather than written
through, the way `audio` had to be for Pi and Gajae; a row left with nothing acceptable keeps
its entry without capability keys instead of being retyped as text.

`exportModelsFromProxyRows` no longer re-joins modalities by `namespaced` — the catalog entry
carries them now, from the same visibility-filtered row as the model itself, so a disabled
duplicate cannot donate them.

Closes lidge-jun#4286
@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 70 / 80

이 PR은 OpenCode로 내보내는 모델 목록에 이미지(첨부) 가능 여부를 제대로 적어 주는 수정입니다. 지금 dev의 OpenCode export는 모델마다 이름(name)과 한도(limit)만 넣고, opencode가 읽는 attachment / modalities 필드는 비워 둡니다. 그런데 provider id opencodex는 models.dev에 없어서, opencode 1.18.30은 그 필드를 전부 false로 취급합니다. 그래서 TUI에서 이미지 붙여넣기가 프록시까지 가기도 전에 막힙니다. 네이티브 OpenAI 비전 모델도, vision sidecar가 받기로 한 text-only 모델도 같은 벽을 만납니다. 이슈 #4286이 바로 그 버그입니다.

고치는 길은 짧고 분명합니다. /api/models가 이미 주는 inputModalitiesOpencodeProxyModelRowopencodeCatalogFromProxyRowsOpencodeCatalogModel로 이어 받고, opencodeModelCapabilities()가 opencode enum(text|audio|image|video|pdf)만 남긴 뒤 V1/V2 양쪽 provider 블록에 attachmentmodalities를 씁니다. 선언이 없는 행은 예전처럼 이름만 남기고, enum 밖 값만 있는 행은 행 자체는 살려 두고 capability만 빼서 Gajae처럼 파일 전체가 깨지지 않게 합니다. 예전에 exportModelsFromProxyRows가 raw 행을 namespaced로 다시 붙이면서 disabled 중복이 modality를 기증하던 구멍도, 카탈로그 엔트리가 직접 들고 가도록 막아 두었습니다. Fast 행도 원본 행의 capability를 물려받습니다.

방금 dev#4290(omo 클라이언트)이 들어와 src/clients/config-export.ts, contracts.ts, tests/config/client-config-export.test.ts가 같이 바뀌었습니다. 이 PR 브랜치는 그 세 파일과 overlap이 있어 rebase가 필요합니다. 기능 방향은 겹치지 않습니다(omo 등록 vs opencode modality 전파). 초안(draft)이고 readiness checklist가 아직 비어 있으며, 본문에서 밝힌 대로 custom model + sidecar 이미지 승격은 이번 범위 밖입니다. 테스트는 modality enum·launcher 경로·management golden·disabled 기증 방지를 새로 덮고, 문서 guides/opencode.md에 Images and attachments 절을 추가했습니다. 번역본 7개는 건드리지 않았고, 첨부 관련 거짓 주장이 없어서 이번엔 영어로만 가도 됩니다.

라인 - (브랜치 ab26ddeae, 현재 dev eb314c53a와 세 파일 overlap) - #4290 omo merge 이후 config-export.ts / contracts.ts / client-config-export.test.ts rebase 필요. GitHub mergeable은 MERGEABLE이지만 merge-tree상 changed-in-both.
src/clients/config-export/model-metadata.ts opencodeModelCapabilities - attachmentinput.some(v !== "text")로 잡음. audio/video/pdf만 있어도 true. opencode 게이트와 맞는지 한 번 더 확인할 가치는 있음(이미지 외 첨부도 열어 주는 의도인지).
src/clients/config-export.ts opencodeProviderBlocks - V1/V2 양쪽에 같은 capability를 쓰고 modalities는 deep-copy. 설계는 맞음. rebase 때 omo 블록/registry 근처를 덮어쓰지 않게 주의.
경로 docs-site/.../guides/opencode.md - 영문만 Images and attachments 추가. 번역본은 첨부 거짓 주장이 없어 당장 필수는 아님.
경로 custom/listManagementModelRows sidecar 승격 - 본문이 명시한 out-of-scope. 모든 client export에 공통인 별도 이슈로 남겨야 함.
체크리스트 / draft - readiness 네 칸 미체크, isDraft true. CI는 label/hygiene/enforce-target만 통과한 상태(본문 suite는 로컬 보고).

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

  • feat(clients): add omo as an export and integration client #4290 위로 rebase한 뒤 바로 리뷰 큐에 올릴지, draft checklist를 기다릴지
  • custom model + vision sidecar 이미지 승격을 후속 이슈로 당장 딸지, 이번 PR merge 뒤에 받을지
  • attachment: true를 non-text 전부로 둘지, image만으로 좁힐지(opencode 실제 게이트와의 정합)

너의 추천
#4286을 닫는 올바른 수정이다. #4290(omo) 위로 rebase하고 draft를 Ready로 올린 뒤 merge하세요. custom/sidecar 승격은 별도 PR로 남기면 됩니다.

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

@lidge-jun

Copy link
Copy Markdown
Owner

Landed via maintainer carry #4300 (exact semantics preserved; squash merge ad09340d79bae6db0a949f9a382c7574ecfc32e2 on dev). Closing as superseded by #4300.

@lidge-jun lidge-jun closed this Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants