Skip to content

fix(guardrails): keep an explicit bare model id when its vision capability is unknown (#14003) - #14844

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.51from
aniruddhaadak80:fix/14003-bare-id-unknown-vision-no-reroute
Sep 29, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.51from
aniruddhaadak80:fix/14003-bare-id-unknown-vision-no-reroute

Conversation

@aniruddhaadak80

@aniruddhaadak80 aniruddhaadak80 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • VisionBridgeGuardrail.preCall() treated an unknown vision capability the same as a proven text-only one, so any bare wire id missing a MODEL_SPECS entry silently had the whole request rerouted to a different provider's model.
  • Unknown capability on a bare, non-combo model id now keeps the requested model and describes the images, mirroring the guardrail's existing "keep the credentialed model" path.
  • A successful whole-request reroute is now logged at warn with the original and substitute model ids, so a substitution can no longer happen silently.

Root cause. getResolvedModelCapabilities() returns supportsVision: boolean | null, where null means "no verdict" (resolveVisionCapability() falls through to return null when the static spec, registry, synced row and vision-model list all miss). preCall() only short-circuits to pass-through on supportsVision === true, so null and false were indistinguishable and every unknown bare id became reroute-eligible.

For a bare id this is a reliable signal, not an accident of the data: resolveCapabilityInput() yields provider: null, and both getRegistryModel() and getSyncedCapabilityForResolved() return early without a provider. A bare id can therefore only get a verdict from MODEL_SPECS or isVisionModelId(), so null means OmniRoute has no static knowledge of that wire id at all.

Why the credential guard did not save it. hasUsableCredentialsForModel("kimi-for-coding") does model.split("/")[0], so for a bare id the model name is treated as a provider id, matches no provider_connections rows, and returns a hard false instead of the null that would have failed open. preCall() only keeps the requested model when that check returns true, so the request was rerouted — exactly the reported kimi-for-coding -> command-code/moonshotai/Kimi-K2.6 substitution, with no error surfaced, billing on the wrong connection, and call_logs recording the substitute as intended.

Why it bites generally. Providers rename and upgrade wire models faster than the static spec table tracks, so any registry/autoSync-sourced model that starts accepting images without a spec entry gets silently substituted. This PR removes that failure mode for bare ids; the data fix for the current K2.8 rename is tracked separately in #14117 (different files, no overlap).

The gate is deliberately narrow — it requires all of: not auto/, not a forced model, no provider/ prefix in the model string, supportsVision === null, and a not-combo decision. Notes on each:

  • The prefix test uses the model string, not capabilities.provider. capabilities.provider is null for many provider-qualified ids whose model is absent from the registry (e.g. deepseek/deepseek-chat), so keying off it misclassifies them as bare.
  • Combos are excluded because a zero-vision combo is deliberately reroute-eligible (fix(guardrails): reroute zero-vision combos through the vision bridge #10415) and its capability is resolved from its targets, not from its own name.
  • A blanket "unknown never reroutes" rule would regress the documented auto/zero-vision reroute behaviour, so it is not proposed here.

Unchanged: provider-qualified ids, combos and their no-vision fallback, forced describe/reroute modes, the auto/ prefix, visionBridgeRerouteTextOnly, and every proven-vision pass-through.

Related Issues

Validation

This is a remote-only contribution (authored through the Git Data API, no local checkout), so the focused loop was not executed locally and no local results are claimed. The commands below are the exact focused loop; the full unit shards, Vitest, the coverage ratchet and the production build run in CI (#8329).

# focused test (this PR's new regression test)
DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm \
  --import ./open-sse/utils/setupPolyfill.ts \
  --import ./tests/_setup/isolateDataDir.ts \
  --test --test-force-exit \
  tests/unit/guardrails/vision-bridge-bare-id-unknown-vision-14003.test.ts

# closest existing suites that must stay green
DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm \
  --import ./open-sse/utils/setupPolyfill.ts \
  --import ./tests/_setup/isolateDataDir.ts \
  --test --test-force-exit \
  tests/unit/guardrails/visionBridge.test.ts \
  tests/unit/guardrails/vision-bridge-auto-reroute.test.ts \
  tests/unit/guardrails/visionBridge-combo-reroute.test.ts \
  tests/unit/guardrails/vision-bridge-native-skip.test.ts \
  tests/unit/guardrails/visionBridge12111Repro.test.ts \
  tests/unit/vision-bridge-policy-reroute-6640.test.ts \
  tests/unit/vision-bridge-mode.test.ts \
  tests/unit/vision-bridge-negative-cache.test.ts

# routing category gates (golden path)
npm run test:combo:matrix
npm run check:known-symbols

# lint, changelog fragment integrity, build
npm run lint
npm run check:changelog-integrity
npm run build
  • Change type: routing (Vision Bridge guardrail)
  • Focused tests and category gates from the golden path — not run locally (remote-only, no checkout); delegated to CI
  • npm run lint — not run locally; delegated to CI
  • Reconciled with the active release base - cut from release/v3.8.51 tip 05a5c15. The base has since advanced 21 commits (192 files); none of them touch src/lib/guardrails/visionBridge.ts, this PR's test file, or its changelog fragment (verified via the compare API), and the PR is MERGEABLE with no conflict. No rebase was pushed because it would only re-run the full matrix for an identical tree; happy to rebase if preferred.
  • Production-code changes include a new or updated automated test in this PR
  • SonarQube is temporarily opt-in while the private project has no quota; it is not a PR gate.

Pre-existing red baseline on this base branch (please read)

release/v3.8.51 is already red in the unit shards, independently of this PR. Verified against three recently merged PRs on the same base (#14758, #14796, #14525, #14188) — all four Unit Tests fast-path shards report failure on their own heads. Recurring pre-existing failures in shard 4/4:

  • tests/unit/chatcore-upstream-timeouts.test.ts
  • tests/unit/proxyfetch-upstream-status-capture.test.ts
  • occasionally tests/unit/i18n-glossary-consistency-check.test.ts, tests/unit/settings-i18n-keys.test.ts, tests/unit/pack-artifact-policy.test.ts

Fast Quality Gates fails on exactly the same two gates on baseline PR #14796 (mutation-test-coverage check:dashboard-typecheck) and on the other baselines for mutation-test-coverage, so neither is attributable to this change. typecheck:core and ts7-diagnostics-ratchet pass, and the ratchet reports 0 added diagnostics.

What CI actually verified on this PR (head d040735), no local claims: API Route Typecheck, Change Classification, Docs Gates, No new ESLint warnings, Vitest (fast-path), semgrep, semgrep-cloud-platform/scan, and Merge integrity (changelog + generated skills) all pass. Across the four unit shards, no vision-bridge or guardrail test fails, and this PR's new test file passes all 5 cases. The remaining shard failures are the pre-existing ones listed above plus usage-pending-sweep, combo-responses-sse-failure-fallback, source-scanner-guards, flush-empty-retry, ghe-copilot, and build/mcp-bundle-startup — none of which this PR touches.

Two earlier CI runs on this PR did surface real regressions from a first version of the gate, both now fixed:

  • vision-bridge-auto-reroute.test.ts (2 cases) — the first version keyed "bare" off capabilities.provider, which is null for deepseek/deepseek-chat, so a provider-qualified id was misclassified as bare. Its describe result also populated the module-level describe cache, making the following case observe a cache hit. Fixed by keying off the model string.
  • visionBridge-combo-reroute.test.ts ("zero-vision combo reroutes the whole request") — the first version suppressed the reroute for a bare combo name. Fixed by restricting the gate to not-combo.

Tests Added Or Updated

  • tests/unit/guardrails/vision-bridge-bare-id-unknown-vision-14003.test.ts (new, 5 cases):

    1. VB-14003-BARE — kimi-for-coding with unknown capability and a credential check that reports false (the bare-id blind spot) while the reroute target is healthy: the model must not be swapped, no rerouted meta is emitted, the image is described, and the skip is logged naming the original model. Fails without this fix.
    2. VB-14003-QUALIFIED — deepseek/deepseek-chat (provider-qualified, also unknown) still reroutes and now logs Whole-request vision reroute. Locks the string-prefix boundary and guards against over-broadening.
    3. VB-14003-QUALIFIED-LEAF — the same wire id under a provider prefix (kimi-coding/kimi-for-coding) still reroutes.
    4. VB-14003-BARE-SPEC — a bare id with a vision spec (kimi-k3) still short-circuits before any new logic runs.
    5. VB-14003-AUTO — the unknown-capability gate never fires for an auto/ model. Asserted as "the new gate does not suppress the auto path" rather than via the resulting target model, because the vision router's selection/negative caches are module-level and make the concrete substitute order-dependent; the end-to-end auto reroute is already covered by tests/unit/guardrails/vision-bridge-auto-reroute.test.ts.

    Each case uses a distinct image URL so the module-level describe cache cannot satisfy a later case without calling the bridge.

Coverage Notes

  • src/lib/guardrails/visionBridge.ts is production code under src/, covered by the test file above. The change adds one derived boolean and one warn log; the new branch is exercised by case 1, and case 2 proves it is narrow.

Reviewer Notes

  • No migrations, feature flags, or config changes. The gate keys off capability resolution only.
  • Behaviour change to be aware of: a bare model id with unknown vision capability that previously got rerouted now stays on the requested model and receives described images instead. If such a model is genuinely text-only, the describe path is still correct (raw images would fail upstream); the intended-win case is the reported one, where the model does accept images.
  • Not included here: the credential helper's own behaviour for bare ids (hasUsableCredentialsForModel returning a hard false for an id with no /). It is a real latent issue affecting other callers, so it is left for a separate PR.
  • Interaction with Fix/14003 kimi coding k28 vision #14117: that PR adds the kimi-k2.8-preview spec, which makes kimi-for-coding resolve to a known vision model and take the existing early pass-through. This PR removes the silent-substitution failure mode for the next bare id that appears without a spec entry. The two touch disjoint files (modelSpecs.ts + registry vs visionBridge.ts) and can merge in either order.
  • Overlapping PRs checked before opening: Fix/14003 kimi coding k28 vision #14117 (data fix for this issue), fix(egress-cascade, vision, nvidia, kimi, tls): 9 cherry-picked fixes #14061 (open cherry-pick touching modelSpecs.ts/visionModels.ts/the Kimi registry — data-side only, no guardrail file), fix(providers): advertise the Kimi Code CLI version 2.1.1 #14842 (Kimi Code CLI version, runtime.ts only). No guardrail-behaviour claim is open on this issue.

@aniruddhaadak80
aniruddhaadak80 force-pushed the fix/14003-bare-id-unknown-vision-no-reroute branch from 204a488 to b290dad Compare September 25, 2026 16:57
…ility is unknown (cheaper-inference#14003)

A model id with no provider prefix resolves its vision capability from
MODEL_SPECS and the shared vision-model list only, because the provider
registry and models.dev sync lookups inside getResolvedModelCapabilities
both require a parsed provider. When neither yields a verdict the
capability is `null` (unknown) rather than `false` (proven text-only), and
VisionBridgeGuardrail.preCall treated the two identically: it only
short-circuits to pass-through on `supportsVision === true`.

So any bare wire id missing a spec entry became reroute-eligible. With
`kimi-for-coding` the credential guard could not save it either, because
hasUsableCredentialsForModel splits on "/" and treats the model name as a
provider, finds no connection rows, and reports a hard `false` instead of
the `null` that would have failed open. The request was then whole-request
rerouted to `command-code/moonshotai/Kimi-K2.6` with no error surfaced: the
wrong model answered, billing followed the wrong connection, and
call_logs recorded the substitute as intended.

Unknown capability on a bare id now keeps the requested model and describes
the images, mirroring the existing "keep the credentialed model" path. A
successful whole-request reroute is also logged at `warn` with the original
and substitute model ids, so a substitution can no longer happen silently.

Provider-qualified ids are deliberately unchanged (their registry and synced
rows are authoritative, and reclassifying their unknown-vs-text-only
treatment is a separate policy decision), as are combos, forced modes, the
`auto/` prefix, and every proven-vision pass-through.

Refs cheaper-inference#14003
@aniruddhaadak80
aniruddhaadak80 force-pushed the fix/14003-bare-id-unknown-vision-no-reroute branch from b290dad to d040735 Compare September 25, 2026 17:15
@aniruddhaadak80

Copy link
Copy Markdown
Contributor Author

Re-verification pass (2026-09-26) — no code change, head is still d040735.

Base reconciliation (current numbers). release/v3.8.51 has advanced 13 commits since this PR's base snapshot 86870a82 (now 8f5b448). Verified via the compare API that none of them touch src/lib/guardrails/visionBridge.ts, tests/unit/guardrails/vision-bridge-bare-id-unknown-vision-14003.test.ts, or changelog.d/fixes/14003-bare-id-unknown-vision.md. GitHub reports mergeable: true / rebaseable: true with no conflict. I have not pushed a rebase, because it would re-run the full ~14-minute matrix for a functionally identical tree; happy to rebase on request.

This PR's regression tests pass in CI. In Unit Tests fast-path (1/4) all five new cases are green:

✔ VB-14003-BARE: keeps a bare model id with unknown vision capability instead of rerouting
✔ VB-14003-QUALIFIED: a provider-qualified id with unknown capability still reroutes
✔ VB-14003-QUALIFIED-LEAF: the same wire id under a provider prefix still reroutes
✔ VB-14003-BARE-SPEC: a bare id with a vision spec still skips the bridge entirely
✔ VB-14003-AUTO: the unknown-capability gate never fires for an auto/ model

Complete failing-test inventory on this head (so it is clear nothing here is vision-bridge related):

Shard Failing tests
1/4 i18n-glossary-consistency-check (2), proxyfetch-upstream-status-capture, usage-pending-sweep
2/4 combo-responses-sse-failure-fallback (4), source-scanner-guards
3/4 chatcore-upstream-timeouts, flush-empty-retry (4), ghe-copilot, pack-artifact-policy
4/4 build/mcp-bundle-startup, settings-i18n-keys

Zero guardrail or vision-bridge failures across all four shards.

Baseline confirmation for the two red jobs. Fast Quality Gates fails here on mutation-test-coverage + check:dashboard-typecheck. Both are pre-existing on this base, not attributable to this change:

Passing on this head: API Route Typecheck, Change Classification, Docs Gates (fast-path), No new ESLint warnings, Vitest (fast-path), semgrep, semgrep-cloud-platform/scan, and Merge integrity (changelog + generated skills). Every command listed in the PR description was cross-checked against package.json at this head and exists (lint, check:changelog-integrity, build, test:combo:matrix, check:known-symbols).

Still happy to rebase onto 8f5b448 if you would rather review it against the current tip.

@diegosouzapw
diegosouzapw merged commit 143c8a1 into cheaper-inference:release/v3.8.51 Sep 29, 2026
11 of 16 checks passed
diegosouzapw added a commit that referenced this pull request Sep 29, 2026
… 4) (#15109)

Release-captain base-red fix (v3.8.51 release PR #11442, unit shards 3-4): one production defect (proxyLogger pulled into every settings→proxies import and queried the DB at import time; helper extracted to src/lib/proxyLogHost.ts) and seven contract propagations from #14732, #15044, #15067, #13548, #14117×#14844, #12810. 74/74 across the seven files, 176/176 proxy-log neighbours, typecheck clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(providers): Vision Bridge reroutes bare kimi-for-coding image requests to another provider (no vision spec, credential guard blind to bare ids)

2 participants