Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8255,10 +8255,11 @@ export async function runVisualVisionForAdvisory(
model: args.settings.aiReviewModel ?? storedVisionKey.model,
}
: null;
// Self-host local vision (#4335): a dedicated ollama+VLM binding lets vision run WITHOUT a maintainer
// BYOK key -- see evaluateVisualVisionGate's header for why only an HTTP-capable provider (BYOK or this)
// can see the screenshots at all.
const selfHostVisionAvailable = Boolean(env.AI_VISION);
// Self-host local vision (#4335) still consumes operator resources, so mirror the AI-spend gate used by
// the other self-host review paths: confirmed contributors only unless the repo explicitly opts in to all
// authors. BYOK remains checked above because it also requires a confirmed contributor-owned repo key.
const selfHostVisionAllowed = args.confirmedContributor || args.settings.aiReviewAllAuthors;
const selfHostVisionAvailable = selfHostVisionAllowed && Boolean(env.AI_VISION);
const visionGate = evaluateVisualVisionGate({
routes: args.routes,
reputationSignal: visionReputation.signal,
Expand Down
42 changes: 42 additions & 0 deletions test/unit/visual-vision-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,48 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)",
]);
});

it("does not let an unconfirmed contributor spend self-host vision resources unless all-authors is enabled", async () => {
const runMock = vi.fn(async () => ({ response: findingsResponse([{ path: "/app", body: "should not run" }]) }));
const env = byokEnv();
(env as unknown as { AI_VISION: unknown }).AI_VISION = { run: runMock };
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const adv = findingsHolder();
await runVisualVisionForAdvisory(env, {
repoFullName,
pr,
author: "alice",
confirmedContributor: false,
settings: byokSettings({ aiReviewByok: false, aiReviewAllAuthors: false }),
advisory: adv,
routes: selfHostVisionRoutes(),
});
expect(runMock).not.toHaveBeenCalled();
expect(fetchMock).not.toHaveBeenCalled();
expect(adv.findings).toEqual([]);
});

it("allows self-host vision for an unconfirmed contributor when all-authors is explicitly enabled", async () => {
const runMock = vi.fn(async () => ({
response: findingsResponse([{ path: "/app", body: "All-authors opt-in covers this self-host call." }]),
}));
const env = byokEnv();
(env as unknown as { AI_VISION: unknown }).AI_VISION = { run: runMock };
stubShots();
const adv = findingsHolder();
await runVisualVisionForAdvisory(env, {
repoFullName,
pr,
author: "alice",
confirmedContributor: false,
settings: byokSettings({ aiReviewByok: false, aiReviewAllAuthors: true }),
advisory: adv,
routes: selfHostVisionRoutes(),
});
expect(runMock).toHaveBeenCalledTimes(1);
expect(adv.findings[0]).toMatchObject({ detail: "All-authors opt-in covers this self-host call." });
});

it("prefers a configured BYOK key over env.AI_VISION when both are available", async () => {
const env = byokEnv();
await upsertRepositoryAiKey(env, { repoFullName, provider: "anthropic", key: "sk-ant-vision-key", model: null });
Expand Down