From 5c327db64c7cb411153e88780d9c5f578ddc63da Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 27 Jun 2026 10:49:13 -0700 Subject: [PATCH] fix(selfhost): route single-provider AI through the name-aware router (#1610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single-provider self-host (e.g. AI_PROVIDER=claude-code) failed every AI review with an opaque claude_code_exit_1. resolveAiReviewerPlan addresses the lone reviewer by provider name ({ model: "claude-code" }) — the same router address convention as the 2-provider plan — but createSelfHostAi returned the BARE provider for a single provider, bypassing routeProviders's name-stripping. So createClaudeCodeAi ran resolveModel(undefined, "claude-code", ...), which passes "claude-code" through verbatim (it only strips @cf/ ids), producing `claude --model claude-code` → 404 → exit 1, three attempts, then exhausted. Wrap a single provider in routeProviders too, so the provider-name address resolves to the provider's own default model. Regression test pins that a single-provider env.AI.run() sends the provider default, never the literal provider name as a model id. --- src/selfhost/ai.ts | 11 +++++++---- test/unit/selfhost-ai.test.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/selfhost/ai.ts b/src/selfhost/ai.ts index bba446bf46..284e7a2d5a 100644 --- a/src/selfhost/ai.ts +++ b/src/selfhost/ai.ts @@ -436,13 +436,16 @@ export function resolveRequiredCliProviders(env: Record): SelfHostAi | undefined { const providers = buildProviders(env); if (providers.length === 0) return undefined; - if (providers.length === 1) return providers[0]?.ai; return routeProviders(providers); } diff --git a/test/unit/selfhost-ai.test.ts b/test/unit/selfhost-ai.test.ts index 3781675a9a..52e1e2b391 100644 --- a/test/unit/selfhost-ai.test.ts +++ b/test/unit/selfhost-ai.test.ts @@ -206,6 +206,20 @@ describe("routeProviders (#dual-ai-combiner — address one provider by name for const ai = createSelfHostAi({ AI_PROVIDER: "anthropic,ollama", ANTHROPIC_API_KEY: "sk-ant", AI_BASE_URL: "http://o/v1" }); expect(typeof ai?.run).toBe("function"); }); + + it("createSelfHostAi routes a SINGLE provider through the router too — a name address yields the provider default, never `--model ` (#1610)", async () => { + // Regression (#1610): a single-provider self-host returned env.AI as the BARE provider, so the reviewer plan's + // name address ({ model: "openai-compatible" } — or "claude-code") reached it as a model id. `claude --model + // claude-code` 404'd and broke EVERY review. The router must strip the name to the provider's own default. + let sentModel = ""; + vi.stubGlobal("fetch", vi.fn(async (_u: string, init: { body: string }) => { + sentModel = (JSON.parse(init.body) as { model: string }).model; + return new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), { status: 200 }); + })); + const ai = createSelfHostAi({ AI_PROVIDER: "openai-compatible", AI_BASE_URL: "http://o/v1" }); + await ai?.run("openai-compatible", { prompt: "x" }); // the single-provider reviewer-plan address IS the provider name + expect(sentModel).toBe("llama3.1"); // resolveModel(undefined, "", "llama3.1") — NOT the literal "openai-compatible" + }); }); describe("resolveProviderNames + resolveAiReviewerPlan (#dual-ai-combiner)", () => {