Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit242097b

refactor(ai): consolidate 3 duplicate Anthropic client constructors

refactor(ai): consolidate 3 duplicate Anthropic client constructors

ai-review.ts, spec-ai.ts, and merge-resolver.ts each built their own
private Anthropic client (identical construction logic to ai-client.ts's
shared getAnthropic(), just copy-pasted independently — spec-ai.ts's
docstring literally says "cribbed from ai-review.ts"). Three places to
drift out of sync for zero benefit.

All three now import getAnthropic() from ai-client.ts. spec-ai.ts's
_resetClientForTests() test export is kept (existing tests import it
directly) but now delegates to ai-client.ts's __resetAnthropicClientForTests
instead of managing its own module-local client.

ai-review.ts's hardcoded REVIEW_MODEL = "claude-sonnet-4-6" is now
modelForTask("code-review") — same resulting model (code-review isn't
Haiku-allowlisted), but it now goes through the shared task-routing
policy and AI_FORCE_SONNET kill-switch like every other AI feature,
instead of being the one place that bypassed it.
ccantynz-alt committed on July 20, 2026Parent: c166384
3 files changed+1053242097b598732893735dc2b6905fa674fe3aada9
3 changed files+10−53
Modifiedsrc/lib/ai-review.ts+3−15View fileUnifiedSplit
55 * Reviews are posted as PR comments with isAiReview=true.
66 */
77
8import Anthropic from "@anthropic-ai/sdk";
98import { eq, and, like } from "drizzle-orm";
109import { db } from "../db";
1110import { pullRequests, prComments } from "../db/schema";
1211import { getRepoPath } from "../git/repository";
1312import { config } from "./config";
13import { getAnthropic, modelForTask } from "./ai-client";
1414import { recordAiCost, extractUsage } from "./ai-cost-tracker";
1515import {
1616 isTrioReviewEnabled,
4646/** Max bytes of diff we send to Claude. Matches reviewDiff's internal cap. */
4747const DIFF_BYTE_CAP = 100_000;
4848
49let _client: Anthropic | null = null;
50
51function getClient(): Anthropic {
52 if (!_client) {
53 if (!config.anthropicApiKey) {
54 throw new Error("ANTHROPIC_API_KEY is not set");
55 }
56 _client = new Anthropic({ apiKey: config.anthropicApiKey });
57 }
58 return _client;
59}
60
6149/**
6250 * Run AI code review on a PR diff.
6351 */
6957 headBranch: string,
7058 diffText: string
7159): Promise<ReviewResult> {
72 const client = getClient();
60 const client = getAnthropic();
7361
74 const REVIEW_MODEL = "claude-sonnet-4-6";
62 const REVIEW_MODEL = modelForTask("code-review");
7563 const message = await client.messages.create({
7664 model: REVIEW_MODEL,
7765 max_tokens: 4096,
Modifiedsrc/lib/merge-resolver.ts+2−15View fileUnifiedSplit
77 * 3. Applies the resolved content and completes the merge
88 */
99
10import Anthropic from "@anthropic-ai/sdk";
11import { config } from "./config";
10import { getAnthropic } from "./ai-client";
1211import { getRepoPath } from "../git/repository";
1312
1413interface ConflictFile {
2827 commitSha?: string;
2928}
3029
31let _client: Anthropic | null = null;
32
33function getClient(): Anthropic {
34 if (!_client) {
35 if (!config.anthropicApiKey) {
36 throw new Error("ANTHROPIC_API_KEY is not set");
37 }
38 _client = new Anthropic({ apiKey: config.anthropicApiKey });
39 }
40 return _client;
41}
42
4330async function exec(
4431 cmd: string[],
4532 opts?: { cwd?: string; env?: Record<string, string> }
175162 filePath: string,
176163 conflictContent: string
177164): Promise<ResolvedFile | null> {
178 const client = getClient();
165 const client = getAnthropic();
179166
180167 try {
181168 const message = await client.messages.create({
Modifiedsrc/lib/spec-ai.ts+5−23View fileUnifiedSplit
1717 * rest of the `ai-*` modules.
1818 */
1919
20import Anthropic from "@anthropic-ai/sdk";
2120import { config } from "./config";
21import { getAnthropic, __resetAnthropicClientForTests } from "./ai-client";
2222
2323// ---------------------------------------------------------------------------
2424// Public types
259259}
260260
261261// ---------------------------------------------------------------------------
262// Anthropic client (local to this module — matches ai-review.ts pattern)
262// Anthropic client — shared with the rest of the ai-* modules, see ai-client.ts
263263// ---------------------------------------------------------------------------
264264
265let _client: Anthropic | null = null;
266
267function getClient(): Anthropic {
268 if (!_client) {
269 _client = new Anthropic({ apiKey: config.anthropicApiKey });
270 }
271 return _client;
272}
273
274/**
275 * Drop the cached Anthropic client. Only intended for tests that need to
276 * swap `globalThis.fetch` between calls — the SDK captures `fetch` at
277 * client construction time, so reusing a client would pin the stubbed
278 * fetch from an earlier test.
279 *
280 * @internal
281 */
282export function _resetClientForTests(): void {
283 _client = null;
284}
265/** @deprecated kept so existing test imports keep working; delegates to ai-client.ts's reset. */
266export const _resetClientForTests = __resetAnthropicClientForTests;
285267
286268// ---------------------------------------------------------------------------
287269// Main entry point
318300
319301 let rawText: string;
320302 try {
321 const client = getClient();
303 const client = getAnthropic();
322304 const message = await client.messages.create({
323305 model,
324306 max_tokens: 4096,
325307