fix(vision-bridge): reroute auto/ prefix to vision model when images present (#7871)

* fix(vision-bridge): reroute auto/ prefix to vision model when images present

Rebuilt clean on release/v3.8.49 (the original branch forked from an old main
and dragged ~70 unrelated files of base drift). Reconciled with the newer
VibeProxy credential guards on the tip: the reroute now also fires for auto/
models, the keep-credentialed-model skip does not apply to auto (keeping auto
would land on a text-only candidate), and the reroute-target credential guard
is preserved.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(guardrails): compact image_url literals to fit the 800-line test cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: herjarsa <204746071+herjarsa@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Hernan Javier Ardila Sanchez
2026-07-20 20:56:54 +02:00
committed by GitHub
parent 74204911c7
commit 6fa748e321
2 changed files with 64 additions and 47 deletions

View File

@@ -196,37 +196,48 @@ export class VisionBridgeGuardrail extends BaseGuardrail {
return { block: false };
}
// 3b. Auto/ prefix skip guardrail (auto-combo resolver handles vision-capable model selection)
if (model === "auto" || model.startsWith("auto/")) {
return { block: false };
}
// 3b. Auto/ prefix — don't skip guardrail entirely. Images still need to be
// described or rerouted to a vision-capable model. The auto-combo resolver
// does NOT currently filter models by vision capability, so without the
// guardrail an image-bearing request assigned to a text-only model will
// fail upstream with "does not support images".
const isAuto = model === "auto" || model.startsWith("auto/");
const forceVisionBridge = isVisionBridgeForcedModel(model);
// Declare before the conditional so they're available to the rest of preCall
let forceVisionBridge = false;
let comboVisionBridgeDecision: ComboVisionBridgeDecision | undefined;
// 4. Check if model supports vision
const capabilities = getResolvedModelCapabilities(model);
const comboVisionBridgeDecision = forceVisionBridge
? "process"
: this.deps.checkModelHasComboMapping
? (await this.deps.checkModelHasComboMapping(model))
? "process"
: "skip"
: await getComboVisionBridgeDecision(model);
if (!isAuto) {
forceVisionBridge = isVisionBridgeForcedModel(model);
if (comboVisionBridgeDecision === "skip") {
return { block: false };
}
// 4. Check if model supports vision
const capabilities = getResolvedModelCapabilities(model);
comboVisionBridgeDecision = forceVisionBridge
? "process"
: this.deps.checkModelHasComboMapping
? (await this.deps.checkModelHasComboMapping(model))
? "process"
: "skip"
: await getComboVisionBridgeDecision(model);
if (capabilities.supportsVision === true && !forceVisionBridge) {
// The request model supports vision natively, but check if a
// model-combo mapping routes this model through a combo where
// some targets may NOT support vision. In that case, the vision
// bridge must process images so combo targets can describe them.
if (comboVisionBridgeDecision !== "process") {
if (comboVisionBridgeDecision === "skip") {
return { block: false };
}
// Combo mapping found — fall through to process images
if (capabilities?.supportsVision === true && !forceVisionBridge) {
// The request model supports vision natively, but check if a
// model-combo mapping routes this model through a combo where
// some targets may NOT support vision. In that case, the vision
// bridge must process images so combo targets can describe them.
if (comboVisionBridgeDecision !== "process") {
return { block: false };
}
// Combo mapping found — fall through to process images
}
}
// For auto models (isAuto=true), force remains false and combo decision
// remains undefined, which makes the reroute check on line ~189 treat it
// like a non-combo model — exactly what we want: reroute to a vision model.
// 5. Get body and check for messages
const body = payload as Record<string, unknown>;
@@ -266,12 +277,18 @@ export class VisionBridgeGuardrail extends BaseGuardrail {
// exists, which produced: HTTP log zai → Guardrail reroute → opencode-zen 401
// "Missing API key" while the combo UI still showed body=zai. Fall through to
// the image-describe path so the user's chosen model still answers.
if (comboVisionBridgeDecision === "not-combo" && !forceVisionBridge) {
//
// Reroute also fires for the auto/ prefix (isAuto): the auto-combo resolver
// does not filter candidates for vision capability, so an image-bearing
// request with model=auto would land on a text-only model (#7871). Keeping
// "auto" is never the answer there, so the keep-credentialed-model skip
// below does not apply to auto — only the reroute-target credential guard.
if ((comboVisionBridgeDecision === "not-combo" || isAuto) && !forceVisionBridge) {
const checkCreds =
this.deps.hasUsableCredentials ?? hasUsableCredentialsForModel;
const originalUsable = await checkCreds(model);
if (originalUsable === true) {
if (originalUsable === true && !isAuto) {
// Keep the credentialed model; describe images below if needed.
context.log?.debug?.(
"VISION_BRIDGE",

View File

@@ -107,10 +107,7 @@ test("VB-S05: passthroughs when visionBridgeEnabled is false", async () => {
role: "user",
content: [
{ type: "text", text: "What is in this image?" },
{
type: "image_url",
image_url: { url: "https://example.com/image.png" },
},
{ type: "image_url", image_url: { url: "https://example.com/image.png" } },
],
},
],
@@ -237,7 +234,7 @@ test("VB-S04: passthroughs when messages array is empty", async () => {
// ── VB-S12: Auto-prefix skip ────────────────────────────────────────────────
test("VB-S12: skips guardrail for auto/ prefix model (auto/vision)", async () => {
test("VB-S12: reroutes auto/ prefix model to vision model (auto/vision)", async () => {
const guardrail = createGuardrail();
const payload = createPayload({
@@ -247,10 +244,7 @@ test("VB-S12: skips guardrail for auto/ prefix model (auto/vision)", async () =>
role: "user",
content: [
{ type: "text", text: "What is in this image?" },
{
type: "image_url",
image_url: { url: "https://example.com/image.png" },
},
{ type: "image_url", image_url: { url: "https://example.com/image.png" } },
],
},
],
@@ -258,11 +252,17 @@ test("VB-S12: skips guardrail for auto/ prefix model (auto/vision)", async () =>
const result = await guardrail.preCall(payload, createContext({ model: "auto/vision" }));
assert.strictEqual(result.block, false);
assert.strictEqual(result.modifiedPayload, undefined, "auto/vision should passthrough");
assert.strictEqual(visionCallCount, 0, "should NOT call vision API for auto prefix");
assert.ok(result.modifiedPayload, "auto/vision should reroute to vision model");
assert.strictEqual(
result.modifiedPayload?.model,
"openai/gpt-4o-mini",
"should reroute to configured vision model"
);
assert.strictEqual(result.meta?.rerouted, true, "rerouted meta should be set");
assert.strictEqual(visionCallCount, 0, "should NOT call vision API (reroute, not describe)");
});
test("VB-S12b: skips guardrail for bare auto prefix", async () => {
test("VB-S12b: reroutes auto prefix to best vision model when images present", async () => {
const guardrail = createGuardrail();
const payload = createPayload({
@@ -272,10 +272,7 @@ test("VB-S12b: skips guardrail for bare auto prefix", async () => {
role: "user",
content: [
{ type: "text", text: "What is in this image?" },
{
type: "image_url",
image_url: { url: "https://example.com/image.png" },
},
{ type: "image_url", image_url: { url: "https://example.com/image.png" } },
],
},
],
@@ -283,7 +280,13 @@ test("VB-S12b: skips guardrail for bare auto prefix", async () => {
const result = await guardrail.preCall(payload, createContext({ model: "auto" }));
assert.strictEqual(result.block, false);
assert.strictEqual(result.modifiedPayload, undefined, "auto should passthrough");
assert.ok(result.modifiedPayload, "auto should reroute to vision model");
assert.strictEqual(
result.modifiedPayload?.model,
"openai/gpt-4o-mini",
"should reroute to configured vision model"
);
assert.strictEqual(result.meta?.rerouted, true, "rerouted meta should be set");
});
// ── VB-S01: Single image → reroute (individual non-vision model) ───────────
@@ -298,10 +301,7 @@ test("VB-S01: reroutes non-vision model with images to best vision model", async
role: "user",
content: [
{ type: "text", text: "What is in this image?" },
{
type: "image_url",
image_url: { url: "https://example.com/image.png" },
},
{ type: "image_url", image_url: { url: "https://example.com/image.png" } },
],
},
],