Skip to content

feat(management): publish vision describers and refuse the blind ones - #1327

Merged
lidge-jun merged 3 commits into
devfrom
codex/260809-vision-sidecar-api
Aug 9, 2026
Merged

feat(management): publish vision describers and refuse the blind ones#1327
lidge-jun merged 3 commits into
devfrom
codex/260809-vision-sidecar-api

Conversation

@lidge-jun

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

Copy link
Copy Markdown
Owner

Summary

Builds on #1326. GET and PUT /api/sidecar-settings now carry a visionModels list, and both routes that can set a vision describer refuse a model that can be proven blind.

Without this layer the picker filter is cosmetic: anything holding the admin token could still write a text-only model into visionSidecar.model, and the sidecar would spend real tokens producing nothing.

Three decisions worth review attention:

  • The gate rejects only a PROVEN-blind model. An id no source knows stays allowed, because the runtime never required catalog membership and an operator may be ahead of our tables. tests/vision-reasoning-contract.test.ts already pins custom-vision at HTTP 200 and this change keeps it there. The suggestion list and the write gate are deliberately different sets.
  • The caller's backend is a hint, never the authority. Trusting it opened a laundering path: o3-mini is text-only in the OpenAI metadata table but absent from the Anthropic one, so a client could claim backend: "anthropic" and have a known-blind model read as merely unknown. Measured before the fix: {model: "o3-mini"} → 400, {model: "o3-mini", backend: "openai"} → 400, {model: "o3-mini", backend: "anthropic"}200 and persisted. Both families are now consulted and any positive text-only verdict wins, which is unambiguous because the two vendor tables share zero bare model ids.
  • One policy module serves both routes. A gate on /api/sidecar-settings with a private copy inside the Claude Code override is the same as no gate once the two drift, so vision-sidecar-options.ts owns the rule and both callers import it. The Claude Code dashboard control stays freeform by design — it is a text input with suggestions, not a constrained picker.

The catalog is read once per request and reused for the rejection body, so a 400 does not cost two provider fetches.

Verification

  • bun run typecheck — exit 0
  • bun test tests/sidecar-settings-vision-filter.test.ts — 11 pass, 0 fail
  • bun run test (full suite) — 10146 pass, 0 fail, 632 files
  • Activation evidence for the gate:
    • red ablation forcing the branch to if (false) → 1 fail, so the 400 path is genuinely covered
    • live three-variant probe through handleManagementAPI, before vs after via git stash: 200/200/200 with the model persisted → 400/400/400 with nothing persisted
    • saveConfigPreservingClaudeCode spy shows 0 calls on the reject path, so rejection precedes persistence
    • false-positive guard: webSearchSidecar still accepts o3-mini with 200, so the gate did not leak into the wrong field
  • docs-site/src/content/docs/guides/sidecars.md updated (SOT sync); translated locales deliberately left alone.

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 ← you are here server the 400 gate and the allowed-list payload
1 #1326 predicate the inversion case and the tri-state

Depends on #1326. Review this PR's diff only.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 729b2a8d-352d-4523-a082-42058167668f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 added the enhancement New feature or request label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@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.

@github-actions
github-actions Bot marked this pull request as draft August 9, 2026 03:11
@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: b640fe42aa

ℹ️ 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 on lines +81 to +82
const row = candidates.find(candidate => candidate.id === requested);
if (row) return modelAcceptsImageInput(config, row) === 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 Keep probing after an unrelated bare-ID match

When any catalog or custom-provider row shares the requested bare ID, this early return skips the OpenAI/Anthropic probes below. For example, a custom provider row for o3-mini advertising inputModalities: ["text", "image"] makes the management routes accept and persist o3-mini, even though the OpenAI metadata proves that the model sent by the OpenAI sidecar is text-only. Check all matching rows and the actual execution-backend metadata, rejecting when any authoritative verdict proves the requested describer blind.

Useful? React with 👍 / 👎.

Comment on lines +40 to +45
return rows.filter(row => row.disabled !== true).map(row => ({
provider: row.provider,
id: row.id,
...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
...(row.native ? { native: 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.

P1 Badge Limit suggestions to the provider the sidecar executes

In a configuration with an Anthropic OAuth provider plus another Anthropic-compatible provider, these candidates include models from both providers, and visionEligibleModelOptions treats every adapter: "anthropic" row as reachable. However, planVisionSidecar executes only through the provider returned by findAnthropicVisionProvider, so selecting an image-capable private model exposed by the other provider sends that bare ID to the OAuth provider and fails upstream. Build the option list from the actual executable provider's rows rather than every management catalog row.

Useful? React with 👍 / 👎.

Comment thread src/server/management/config-routes.ts Outdated
Comment on lines +395 to +396
if (!visionModels.some(option => option.value === visionModel)) {
visionModels.unshift({ value: visionModel, label: visionModel, backend: vs.backend ?? "openai" });

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 Resolve the backend-specific default before grandfathering it

When visionSidecar.backend is anthropic and no model override is stored, the runtime uses claude-sonnet-5, but this route computes visionModel as gpt-5.4-mini and now grandfathers that value as an Anthropic option. The API therefore publishes a selectable model that is neither configured nor the effective runtime default, and a client that submits the selected option will persist an OpenAI model for the Anthropic executor. Resolve the same backend-specific default as planVisionSidecar, or grandfather only an explicitly stored model; the identical post-PUT block needs the same correction.

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 management API still validates and publishes options against catalog-wide bare IDs instead of the backend/provider that will actually execute the sidecar.\n\nA colliding row can make the validator return early before authoritative OpenAI/Anthropic metadata is checked. Likewise, models from any Anthropic-compatible provider are suggested even though runtime dispatch uses only the provider selected by findAnthropicVisionProvider. Finally, the grandfathered default is currently gpt-5.4-mini even when the selected backend is Anthropic, whose runtime default is claude-sonnet-5.\n\nPlease carry the exact execution provider/backend into option construction and validation, evaluate all relevant authoritative matches rather than returning on an unrelated bare-ID row, and resolve the same backend-specific default as planVisionSidecar. Add focused tests for bare-ID collisions, multiple Anthropic-compatible providers, and an Anthropic backend with no explicit model. This stacked head is not merge-ready until those cases are green.

GET and PUT /api/sidecar-settings now carry a visionModels list, and both
routes that can set a vision describer refuse a model we can prove cannot
see. The GUI filter alone would have been cosmetic: anything holding the
admin token could still write a blind model into visionSidecar.model.

The gate rejects only a PROVEN-blind model. An id no source knows stays
allowed, because the runtime never required catalog membership and an
operator may be ahead of our tables; tests/vision-reasoning-contract.ts
pins custom-vision at 200 and this change keeps it there.

The caller's backend is a hint, never the authority. Trusting it opened a
laundering path: o3-mini is text-only in the OpenAI table but absent from
the Anthropic one, so a client could claim backend anthropic and have a
blind model read as merely unknown. Both families are now consulted and any
positive text-only verdict wins, which is unambiguous because the two
vendor tables share no bare model id.

One policy module serves both routes. A gate on /api/sidecar-settings with
a private copy in the Claude Code override is the same as no gate once the
two drift.

Plan: devlog/_plan/260809_vision_sidecar_model_filter/020
… them

Three corrections, one theme: the API answered from a projection of the config
rather than from what the runtime would actually do.

- The write gate no longer stops at the first catalog row matching the id. An
  operator-authored row declaring image input could hide the canonical table
  that proves the same id blind, so o3-mini could be persisted as a describer.
  Every matching row and both backend probes are consulted; any positive
  blind verdict rejects, and an id no source knows is still accepted.
- GET and the post-PUT response shared a copied block that defaulted to
  gpt-5.4-mini even under an anthropic backend, whose runtime default is
  claude-sonnet-5. Both now call one helper built on resolveEffectiveVisionModel,
  which planVisionSidecar also uses, so the report cannot drift from the run.
- Anthropic catalog options are scoped to the OAuth provider the runtime would
  dispatch through, resolved once per request and threaded into the filter.
…rgument

Audit follow-up. Defaulting the parameter to findAnthropicVisionProvider(config)
read like sugar, but an explicit undefined argument — precisely the no-executor
case — re-triggered the default in each helper, so one response could read the
OAuth account store four times. The threaded parameter is now required, and only
the top-level callers resolve it.
@lidge-jun
lidge-jun force-pushed the codex/260809-vision-sidecar-api branch from b640fe4 to 6196fc5 Compare August 9, 2026 05:23
@lidge-jun
lidge-jun changed the base branch from codex/260809-vision-eligibility-core to dev August 9, 2026 05:30
@lidge-jun
lidge-jun merged commit d4758bc into dev Aug 9, 2026
41 of 43 checks passed
@Wibias
Wibias deleted the codex/260809-vision-sidecar-api 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