-
Notifications
You must be signed in to change notification settings - Fork 664
feat(management): publish vision describers and refuse the blind ones #1327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
cbcbbdd
feat(management): publish vision describers and refuse the blind ones
lidge-jun 9532f2f
fix(management): judge describers against the executor that would run…
lidge-jun 6196fc5
refactor(management): make the resolved vision executor an explicit a…
lidge-jun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /** | ||
| * The one place that decides which models the management API offers as vision | ||
| * describers, and which it refuses to persist. | ||
| * | ||
| * Two routes write a vision sidecar model — `PUT /api/sidecar-settings` and the | ||
| * `visionSidecar` override in `PUT /api/claude-code`. They share this module so | ||
| * the policy cannot drift: a gate on one route and a stale copy on the other is | ||
| * the same as no gate at all. | ||
| */ | ||
| import type { OcxConfig } from "../../types"; | ||
| import { findAnthropicVisionProvider, type AnthropicVisionProvider } from "../../vision"; | ||
| import { | ||
| modelAcceptsImageInput, | ||
| visionEligibleModelOptions, | ||
| type VisionCandidateModel, | ||
| type VisionModelOption, | ||
| type VisionSidecarBackend, | ||
| } from "../../vision/eligibility"; | ||
| import { listOpenAiForwardSidecarCandidates } from "../../providers/openai-sidecar"; | ||
| import { listManagementModelRows } from "./model-rows"; | ||
|
|
||
| /** | ||
| * Backends whose executor could actually run: openai forward, anthropic OAuth. | ||
| * | ||
| * `anthropicSidecar` is REQUIRED rather than defaulted. `findAnthropicVisionProvider` | ||
| * reads the OAuth account store from disk, and a default argument made every helper | ||
| * in this chain re-resolve it whenever a caller passed an explicit `undefined` — | ||
| * which is exactly the no-executor case. Passing it in keeps one read per request. | ||
| */ | ||
| export function enabledVisionBackends( | ||
| config: OcxConfig, | ||
| anthropicSidecar: AnthropicVisionProvider | undefined, | ||
| ): VisionSidecarBackend[] { | ||
| const backends: VisionSidecarBackend[] = []; | ||
| // The OpenAI describer needs a CANONICAL ChatGPT forward provider, not merely a | ||
| // provider keyed "openai" — same predicate the runtime sidecar resolver uses. | ||
| if (listOpenAiForwardSidecarCandidates(config).length > 0) backends.push("openai"); | ||
| if (anthropicSidecar) backends.push("anthropic"); | ||
| // 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"]; | ||
| } | ||
|
|
||
| /** Visible catalog rows in the shape the eligibility predicate consumes. */ | ||
| export async function visionCandidateRows(config: OcxConfig): Promise<VisionCandidateModel[]> { | ||
| let rows: Awaited<ReturnType<typeof listManagementModelRows>> = []; | ||
| // A catalog outage must not 500 the settings route nor reject a write; with [] | ||
| // the option list degrades to the baselines, which is the intended floor. | ||
| try { rows = await listManagementModelRows(config); } catch { rows = []; } | ||
| return rows.filter(row => row.disabled !== true).map(row => ({ | ||
| provider: row.provider, | ||
| id: row.id, | ||
| ...(row.inputModalities ? { inputModalities: row.inputModalities } : {}), | ||
| ...(row.native ? { native: true } : {}), | ||
| })); | ||
| } | ||
|
|
||
| export function visionModelOptionsFrom( | ||
| config: OcxConfig, | ||
| candidates: readonly VisionCandidateModel[], | ||
| anthropicSidecar: AnthropicVisionProvider | undefined, | ||
| ): VisionModelOption[] { | ||
| return visionEligibleModelOptions( | ||
| config, | ||
| candidates, | ||
| enabledVisionBackends(config, anthropicSidecar), | ||
| anthropicSidecar?.providerName, | ||
| ); | ||
| } | ||
|
|
||
| /** Convenience for read paths that have no candidate list in hand yet. */ | ||
| export async function visionModelOptionsFor( | ||
| config: OcxConfig, | ||
| anthropicSidecar: AnthropicVisionProvider | undefined, | ||
| ): Promise<VisionModelOption[]> { | ||
| return visionModelOptionsFrom(config, await visionCandidateRows(config), anthropicSidecar); | ||
| } | ||
|
|
||
| /** | ||
| * Can we PROVE the requested describer is blind? | ||
| * | ||
| * Only a positive `false` rejects. An id no source knows stays allowed, because | ||
| * the runtime never required catalog membership and an operator may be ahead of | ||
| * our tables. | ||
| * | ||
| * When no catalog row matches, the caller's `backend` is only a HINT, never the | ||
| * authority. Trusting it let a client launder a known-blind OpenAI model past the | ||
| * gate by claiming `backend: "anthropic"`, since the id is absent from the | ||
| * Anthropic table and absence reads as "unknown". Both families are therefore | ||
| * consulted and any positive text-only verdict wins. That is safe precisely | ||
| * because the two vendor tables share no bare model id, so they can never | ||
| * disagree about one. | ||
| */ | ||
| export function visionDescriberIsProvablyBlind( | ||
| config: OcxConfig, | ||
| requested: string, | ||
| candidates: readonly VisionCandidateModel[], | ||
| backendHint: VisionSidecarBackend | undefined, | ||
| ): boolean { | ||
| // Catalog rows can carry operator-authored modalities. A positive claim from one | ||
| // must not hide another row or canonical metadata that proves this id is blind. | ||
| if (candidates.some(candidate => candidate.id === requested | ||
| && modelAcceptsImageInput(config, candidate) === false)) return true; | ||
|
|
||
| const hinted: VisionSidecarBackend = backendHint === "anthropic" ? "anthropic" : "openai"; | ||
| const probed: VisionSidecarBackend[] = hinted === "anthropic" | ||
| ? ["anthropic", "openai"] | ||
| : ["openai", "anthropic"]; | ||
| return probed.some(provider => modelAcceptsImageInput(config, { provider, id: requested }) === false); | ||
| } | ||
|
|
||
| /** The 400 body both routes return, so the two errors cannot diverge either. */ | ||
| export function visionDescriberRejection( | ||
| field: "vision.model" | "visionSidecar.model", | ||
| requested: string, | ||
| config: OcxConfig, | ||
| candidates: readonly VisionCandidateModel[], | ||
| ): { error: string; allowed: string[] } { | ||
| return { | ||
| error: `${field} "${requested}" cannot describe images: it has no image input support, or it is a model the vision sidecar describes FOR.`, | ||
| // The rejection path is not hot, so it resolves the executor itself rather than | ||
| // making every 400 caller thread one through. | ||
| allowed: visionModelOptionsFrom(config, candidates, findAnthropicVisionProvider(config)).map(option => option.value), | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a configuration with an Anthropic OAuth provider plus another Anthropic-compatible provider, these candidates include models from both providers, and
visionEligibleModelOptionstreats everyadapter: "anthropic"row as reachable. However,planVisionSidecarexecutes only through the provider returned byfindAnthropicVisionProvider, 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 👍 / 👎.