Skip to content

feat(vision): filter sidecar describers by real image-input capability - #1326

Merged
lidge-jun merged 4 commits into
devfrom
codex/260809-vision-eligibility-core
Aug 9, 2026
Merged

feat(vision): filter sidecar describers by real image-input capability#1326
lidge-jun merged 4 commits into
devfrom
codex/260809-vision-eligibility-core

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

The Vision sidecar model picker offered every model whose provider was openai or anthropic. That is simultaneously too wide — it lists models that cannot see — and too narrow, because it asks about a provider name instead of a capability. Users have been asking for more models; this is the foundation that lets the answer be "any model that can actually accept an image".

This layer adds src/vision/eligibility.ts and nothing else. It has no caller yet; the management API and the dashboard consume it in the PRs above.

Two rules make the predicate non-obvious, and both are load-bearing:

  • provider.noVisionModels inverts the modality signal. That list marks models the proxy describes images for, and applyProviderConfigHints deliberately adds "image" to their advertised inputModalities so the Codex app does not block the attachment client-side before the sidecar can run. A blind model therefore advertises image input. Membership in that list is a hard disqualifier, checked before the modality list it rewrote.
  • Unknown is not zero. Catalog rows routinely omit inputModalities entirely — the live /api/models response carries none for either openai or anthropic rows. So modelAcceptsImageInput is a tri-state, and undefined stays eligible. Collapsing it to false would have emptied the picker completely.

visionEligibleModelOptions is the suggestion list and is deliberately not the same set as the write gate that lands in the next PR: absence from it must never imply rejection. gpt-5.6-luna and claude-haiku-4-5 are guaranteed per enabled backend so a cold or failed catalog still offers a usable describer.

Verification

  • bun run typecheck — exit 0
  • bun test tests/vision-eligibility.test.ts — 11 pass, 0 fail
  • bun run test (full suite) — 10146 pass, 0 fail, 632 files
  • Red ablations proving the tests are not vacuous, each reverted afterwards:
    • removing the isVisionSidecarConsumer early return → 1 fail (the inversion guard is genuinely exercised)
    • replacing the metadata fallback with undefined → 1 fail (the fallback branch is genuinely exercised)

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.

Stack (merge bottom-up):

# PR Layer Review focus
3 dashboard vision card dashboard layout, non-localized effort values
2 management API server the 400 gate and the allowed-list payload
1 vision eligibility core ← you are here predicate the inversion case and the tri-state

Review this PR's diff only.

Summary by CodeRabbit

  • New Features

    • Added vision sidecar model eligibility filtering for OpenAI and Anthropic providers.
    • Model options now prioritize baseline models, exclude models already used by sidecars, and remove duplicates.
    • Added image-capability detection using available provider and model metadata.
    • Unknown model capabilities remain selectable when eligibility cannot be confirmed.
    • Added backend-aware routing for eligible vision models.
  • Documentation

    • Added a phased roadmap for management APIs, dashboard integration, validation, fallback behavior, and rollout verification.
  • Tests

    • Added coverage for modality detection, overrides, metadata fallbacks, baselines, deduplication, and backend routing.

Roadmap unit for replacing the vision sidecar's provider-name model list
(every openai + every anthropic row) with a real image-input capability
filter, an allowed-model list on the wire, and a compact delegation-style
card.

The load-bearing finding is that catalog inputModalities is not a truthful
capability signal: applyProviderConfigHints deliberately adds "image" to a
model listed in provider.noVisionModels, because Codex gates attachments
client-side and a text-only entry would block the image before the sidecar
could run. A blind model therefore advertises image input, so noVisionModels
membership is a hard disqualifier checked before the modality list it
rewrote.

Two audit rounds with an independent reviewer are recorded in 002; the
write gate rejects only models that can be PROVEN blind, never merely
unknown ones, so an operator can still point at a model the catalog has
never heard of.

No production code in this commit.
The vision sidecar picker offered every model whose provider was openai or
anthropic, which is both too wide and too narrow: it listed models that
cannot see, while hiding capability behind a provider name.

This adds the predicate that answers the real question. Two rules make it
non-obvious, and both are load-bearing:

- provider.noVisionModels marks models the proxy describes images FOR, and
  applyProviderConfigHints deliberately adds "image" to their advertised
  modalities so Codex does not block the attachment client-side. A blind
  model therefore advertises image input, so list membership is a hard
  disqualifier checked BEFORE the modality list it rewrote.
- Catalog rows routinely omit inputModalities (live /api/models carries none
  for openai or anthropic rows), so the predicate is a tri-state and unknown
  stays eligible. Collapsing undefined into false would empty the picker.

visionEligibleModelOptions is the suggestion list and is deliberately NOT
the write gate: absence from it must never imply rejection. gpt-5.6-luna and
claude-haiku-4-5 are guaranteed per enabled backend so a cold catalog still
offers a usable describer.

No caller yet; the management API and dashboard consume this in later
layers.

Plan: devlog/_plan/260809_vision_sidecar_model_filter/010
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c6adc003-fad2-422f-9bec-e9cfb7d7df00

📥 Commits

Reviewing files that changed from the base of the PR and between 55c236b and 0ac7552.

📒 Files selected for processing (2)
  • src/vision/eligibility.ts
  • tests/vision-eligibility.test.ts

📝 Walkthrough

Walkthrough

This PR documents a roadmap for vision sidecar model filtering and implements its eligibility foundation. It detects image capability, excludes configured sidecar consumers, routes supported backends, preserves baselines, generates stable options, re-exports the API, and adds focused tests.

Changes

Vision sidecar model filtering

Layer / File(s) Summary
Eligibility contract and rollout design
devlog/_plan/260809_vision_sidecar_model_filter/*
Documents capability research, audit findings, phased API and dashboard plans, validation rules, acceptance checks, and publication instructions.
Eligibility predicates and option generation
src/vision/eligibility.ts
Adds image-capability detection, noVisionModels exclusions, OpenAI and Anthropic routing, baseline models, deduplicated options, and unknown-capability eligibility.
Public exports and eligibility validation
src/vision/index.ts, tests/vision-eligibility.test.ts
Re-exports the eligibility API and tests fallbacks, exclusions, native models, baselines, deduplication, and backend filtering.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProviderConfig
  participant ModelCatalog
  participant Eligibility
  participant VisionPicker
  ProviderConfig->>ModelCatalog: provide provider and model metadata
  ModelCatalog->>Eligibility: provide candidate models
  Eligibility->>Eligibility: apply capability and executor checks
  Eligibility->>VisionPicker: return stable eligible options
Loading

Possibly related PRs

Suggested reviewers: wibias, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: filtering Vision sidecar models based on actual image-input capability.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/260809-vision-eligibility-core

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

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

Hygiene

Deterministic PR hygiene checks passed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@devlog/_plan/260809_vision_sidecar_model_filter/020_management_api_allowed_models.md`:
- Around line 62-64: In
devlog/_plan/260809_vision_sidecar_model_filter/020_management_api_allowed_models.md:62-64,
update the phase 2 backend fallback to return backends unchanged when no vision
executor is enabled; do not add baseline models. In
devlog/_plan/260809_vision_sidecar_model_filter/030_dashboard_vision_card.md:57-59,
make the phase 3 legacy fallback apply only when serverOptions is undefined,
treating an empty server result as authoritative. Add coverage for visionModels:
[] and configurations with no enabled vision executor.

In `@src/vision/eligibility.ts`:
- Around line 127-131: Update the baseline insertion loop in the vision
eligibility function to retain the resolved provider identity for each enabled
backend, then pass the baseline model through isVisionEligibleModel before
adding it to byValue. Ensure models listed in the provider’s noVisionModels are
excluded, and add a regression test covering a baseline rejected by that
provider configuration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 21b43281-081e-41bc-8b0d-34b870eb1750

📥 Commits

Reviewing files that changed from the base of the PR and between 3e5cd53 and 2f42ab3.

📒 Files selected for processing (10)
  • devlog/_plan/260809_vision_sidecar_model_filter/000_plan.md
  • devlog/_plan/260809_vision_sidecar_model_filter/001_capability_signal_inventory.md
  • devlog/_plan/260809_vision_sidecar_model_filter/002_audit_synthesis.md
  • devlog/_plan/260809_vision_sidecar_model_filter/010_vision_eligibility_core.md
  • devlog/_plan/260809_vision_sidecar_model_filter/020_management_api_allowed_models.md
  • devlog/_plan/260809_vision_sidecar_model_filter/030_dashboard_vision_card.md
  • devlog/_plan/260809_vision_sidecar_model_filter/040_stack_publication.md
  • src/vision/eligibility.ts
  • src/vision/index.ts
  • tests/vision-eligibility.test.ts

Comment on lines +62 to +64
// Neither side resolvable (fresh install, no login): fall back to both so the
// picker is populated rather than empty, matching the permissive-unknown rule.
return backends.length > 0 ? backends : ["openai", "anthropic"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not replace an unavailable backend set with selectable legacy models.

When no executor is available, phase 2 returns both baselines. This advertises models that no sidecar can dispatch. If phase 2 returns an authoritative empty list instead, phase 3 currently restores the unfiltered legacy list. This violates the documented requirement that picker options are reachable by an enabled executor.

  • devlog/_plan/260809_vision_sidecar_model_filter/020_management_api_allowed_models.md#L62-L64: Return backends unchanged when no backend is enabled. Do not add unavailable baseline options.
  • devlog/_plan/260809_vision_sidecar_model_filter/030_dashboard_vision_card.md#L57-L59: Use the legacy list only when serverOptions === undefined. Treat [] as an authoritative server result.

Add coverage for a server response with visionModels: [] and for a configuration with no enabled vision executor.

📍 Affects 2 files
  • devlog/_plan/260809_vision_sidecar_model_filter/020_management_api_allowed_models.md#L62-L64 (this comment)
  • devlog/_plan/260809_vision_sidecar_model_filter/030_dashboard_vision_card.md#L57-L59
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@devlog/_plan/260809_vision_sidecar_model_filter/020_management_api_allowed_models.md`
around lines 62 - 64, In
devlog/_plan/260809_vision_sidecar_model_filter/020_management_api_allowed_models.md:62-64,
update the phase 2 backend fallback to return backends unchanged when no vision
executor is enabled; do not add baseline models. In
devlog/_plan/260809_vision_sidecar_model_filter/030_dashboard_vision_card.md:57-59,
make the phase 3 legacy fallback apply only when serverOptions is undefined,
treating an empty server result as authoritative. Add coverage for visionModels:
[] and configurations with no enabled vision executor.

Comment thread src/vision/eligibility.ts
@github-actions
github-actions Bot marked this pull request as ready for review August 9, 2026 03:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f42ab3b45

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/vision/eligibility.ts Outdated
Comment on lines +63 to +64
const provider = config.providers?.[providerName];
return provider ? modelInList(provider.noVisionModels, modelId) : false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve no-vision classifications from the effective provider

When a persisted registry-managed provider predates a newer noVisionModels entry, this reads only the raw config and misses the classification that gatherRoutedModels and routedProviderConfig backfill from the registry. The catalog then supplies the blind row with its deliberately augmented inputModalities: ["text", "image"], so this function accepts exactly the sidecar consumer it is meant to reject. Resolve the provider through the same registry-enrichment path used by routing/catalog generation before checking noVisionModels.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

Comment thread src/vision/eligibility.ts Outdated
Comment on lines +99 to +100
const adapter = config.providers?.[candidate.provider]?.adapter;
if (adapter === "anthropic") return "anthropic";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict Anthropic options to the provider the sidecar uses

When Anthropic OAuth is enabled alongside another adapter: "anthropic" provider such as Umans, Xiaomi, or an API-key gateway, this classifies that other provider's unique model IDs as Anthropic sidecar options. The option carries only the bare model ID, while planVisionSidecar always sends it through the provider returned by findAnthropicVisionProvider, so selecting such an option sends an unsupported ID to the unrelated OAuth endpoint and image description fails. Restrict candidates to the actual selected OAuth provider, or retain provider identity through selection and dispatch.

Useful? React with 👍 / 👎.

Comment thread src/vision/eligibility.ts Outdated
Comment on lines +77 to +78
if (candidate.native || SUPPORTED_NATIVE_OPENAI_SLUGS.has(candidate.id)) {
return advertisesImageInput(nativeInputModalities(candidate.id)) ?? true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor row modalities for non-native slug collisions

When a non-native provider exposes a model whose bare ID happens to equal a supported native slug, such as a custom gpt-5.4-mini row explicitly declaring inputModalities: ["text"], the slug-set check treats it as native and returns OpenAI's pinned image capability before consulting the row. This allows a positively known text-only candidate through the filter; use native metadata only when candidate.native is true, or otherwise scope the slug fallback to the canonical native provider.

Useful? React with 👍 / 👎.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes because the current eligibility core can still publish describers that the runtime cannot safely execute.\n\nThe blocking cases are:\n\n1. Resolve the effective provider through the same registry-enrichment path used by routing before applying noVisionModels. A stale persisted provider can otherwise miss a newer registry exclusion and admit a known-blind model.\n2. Keep provider identity attached to every candidate. Treating every adapter: anthropic row as an Anthropic sidecar option lets models from Umans/Xiaomi/custom gateways get dispatched through the unrelated OAuth provider selected by the runtime.\n3. Use native capability metadata only for a genuinely native/canonical candidate. A custom row whose bare ID collides with a native slug must still honor its own text-only modalities.\n4. Baseline insertion must pass the same eligibility check, and no enabled vision executor must produce an authoritative empty set rather than selectable legacy baselines.\n\nPlease add regressions for registry-enriched no-vision classification, multiple Anthropic-compatible providers, a non-native native-slug collision, a rejected baseline, and no enabled executor. The direction is useful, but this head should not merge until these provider-identity boundaries are fixed.

…ider

Four review findings shared one root: the predicate judged a row without the
provider identity that actually governs it.

- Consumer membership now reads the registry-enriched provider, matching what
  the catalog does before it force-adds "image" to exactly those rows. A config
  persisted before the registry learned noVisionModels no longer offers a blind
  describer.
- Anthropic options are restricted to the provider that would really execute
  them. Dispatch goes through one OAuth provider, so a key-auth row speaking the
  same wire was offered and then sent somewhere it is unsupported.
- Baselines run through the same eligibility check instead of bypassing it, and
  are dropped only on an explicit consumer listing, never on a silent table.
- Native OpenAI metadata applies to a native row or the canonical provider only,
  so a routed row declaring ["text"] keeps its own answer.
…s rows

Audit follow-up. The previous fallback still offered a canonical anthropic row
when no executor name was supplied, so a key-auth provider — which
findAnthropicVisionProvider never returns — could be picked and then fail at
describe time. An option that cannot be dispatched is worse than a missing one.

The executor name is now required for an Anthropic catalog suggestion; the
side's baseline keeps the picker populated on its own. Baseline comments now say
what the code does: offered unless explicitly listed as a sidecar consumer.
@lidge-jun
lidge-jun merged commit eebd9d4 into dev Aug 9, 2026
41 of 43 checks passed
@Wibias
Wibias deleted the codex/260809-vision-eligibility-core branch August 9, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants