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
11 changes: 7 additions & 4 deletions src/selfhost/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,16 @@ export function resolveRequiredCliProviders(env: Record<string, string | undefin
});
}

/** Select the self-host AI provider(s) from AI_PROVIDER. A comma-separated list of TWO+ providers is addressable
* by name for dual review (see `routeProviders`) and otherwise falls back through them in order; a single
* provider is used directly. Returns undefined when unconfigured or no provider has its credential. */
/** Select the self-host AI provider(s) from AI_PROVIDER and wrap them in the name-aware router. A comma-separated
* list of TWO+ providers is addressable by name for dual review (see `routeProviders`) and otherwise falls back
* through them in order; a SINGLE provider is wrapped the same way — NOT returned bare — so a reviewer-plan address
* that names the provider (`{ model: "claude-code" }`, the single-provider plan from `resolveAiReviewerPlan`)
* resolves to that provider's own default model instead of reaching it verbatim as `claude --model claude-code`
* (a 404 that broke every review on a single-provider self-host, #1610). Returns undefined when unconfigured or no
* provider has its credential. */
export function createSelfHostAi(env: Record<string, string | undefined>): SelfHostAi | undefined {
const providers = buildProviders(env);
if (providers.length === 0) return undefined;
if (providers.length === 1) return providers[0]?.ai;
return routeProviders(providers);
}

Expand Down
14 changes: 14 additions & 0 deletions test/unit/selfhost-ai.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <provider>` (#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)", () => {
Expand Down
Loading