Skip to content

Commit f95082a

Browse files
committed
refactor: delegate provider checking to AIService in getPreferredNameModel
- Changed getPreferredNameModel to async, calls aiService.createModel() to test if models are available instead of duplicating API key logic - Removes dependency on Config, avoiding duplication of provider-specific env var checking (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, etc.) - Updated tests to use mock AIService instead of mock Config - Single source of truth for provider availability in AIService
1 parent 24f3e2a commit f95082a

File tree

3 files changed

+35
-60
lines changed

3 files changed

+35
-60
lines changed

src/node/orpc/router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export const router = (authToken?: string) => {
174174
.input(schemas.nameGeneration.generate.input)
175175
.output(schemas.nameGeneration.generate.output)
176176
.handler(async ({ context, input }) => {
177-
const model = getPreferredNameModel(context.config);
177+
const model = await getPreferredNameModel(context.aiService);
178178
if (!model) {
179179
return {
180180
success: false,
Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,38 @@
11
import { describe, it, expect } from "bun:test";
22
import { getPreferredNameModel } from "./workspaceTitleGenerator";
3-
import type { Config } from "@/node/config";
3+
import type { AIService } from "./aiService";
4+
import { getKnownModel } from "@/common/constants/knownModels";
5+
6+
// Helper to create a mock AIService that succeeds for specific models
7+
function createMockAIService(availableModels: string[]): AIService {
8+
return {
9+
createModel: async (modelString: string) => {
10+
if (availableModels.includes(modelString)) {
11+
return { success: true, data: {} as never };
12+
}
13+
return { success: false, error: { type: "api_key_not_found", provider: "test" } };
14+
},
15+
} as unknown as AIService;
16+
}
417

518
describe("workspaceTitleGenerator", () => {
6-
it("getPreferredNameModel returns null when no providers configured", () => {
7-
// Save and clear env vars
8-
const savedAnthropicKey = process.env.ANTHROPIC_API_KEY;
9-
const savedAnthropicToken = process.env.ANTHROPIC_AUTH_TOKEN;
10-
delete process.env.ANTHROPIC_API_KEY;
11-
delete process.env.ANTHROPIC_AUTH_TOKEN;
19+
const HAIKU_ID = getKnownModel("HAIKU").id;
20+
const GPT_MINI_ID = getKnownModel("GPT_MINI").id;
1221

13-
try {
14-
const mockConfig = {
15-
loadProvidersConfig: () => null,
16-
} as unknown as Config;
17-
18-
expect(getPreferredNameModel(mockConfig)).toBeNull();
19-
} finally {
20-
// Restore env vars
21-
if (savedAnthropicKey) process.env.ANTHROPIC_API_KEY = savedAnthropicKey;
22-
if (savedAnthropicToken) process.env.ANTHROPIC_AUTH_TOKEN = savedAnthropicToken;
23-
}
22+
it("getPreferredNameModel returns null when no models available", async () => {
23+
const aiService = createMockAIService([]);
24+
expect(await getPreferredNameModel(aiService)).toBeNull();
2425
});
2526

26-
it("getPreferredNameModel prefers anthropic when configured", () => {
27-
const mockConfig = {
28-
loadProvidersConfig: () => ({
29-
anthropic: { apiKey: "test-key" },
30-
}),
31-
} as unknown as Config;
32-
33-
const model = getPreferredNameModel(mockConfig);
34-
expect(model).toContain("anthropic");
27+
it("getPreferredNameModel prefers Haiku when available", async () => {
28+
const aiService = createMockAIService([HAIKU_ID, GPT_MINI_ID]);
29+
const model = await getPreferredNameModel(aiService);
30+
expect(model).toBe(HAIKU_ID);
3531
});
3632

37-
it("getPreferredNameModel falls back to openai when anthropic not configured", () => {
38-
// Save and clear env vars
39-
const savedAnthropicKey = process.env.ANTHROPIC_API_KEY;
40-
const savedAnthropicToken = process.env.ANTHROPIC_AUTH_TOKEN;
41-
delete process.env.ANTHROPIC_API_KEY;
42-
delete process.env.ANTHROPIC_AUTH_TOKEN;
43-
44-
try {
45-
const mockConfig = {
46-
loadProvidersConfig: () => ({
47-
openai: { apiKey: "test-key" },
48-
}),
49-
} as unknown as Config;
50-
51-
const model = getPreferredNameModel(mockConfig);
52-
expect(model).toContain("openai");
53-
} finally {
54-
// Restore env vars
55-
if (savedAnthropicKey) process.env.ANTHROPIC_API_KEY = savedAnthropicKey;
56-
if (savedAnthropicToken) process.env.ANTHROPIC_AUTH_TOKEN = savedAnthropicToken;
57-
}
33+
it("getPreferredNameModel falls back to GPT Mini when Haiku unavailable", async () => {
34+
const aiService = createMockAIService([GPT_MINI_ID]);
35+
const model = await getPreferredNameModel(aiService);
36+
expect(model).toBe(GPT_MINI_ID);
5837
});
5938
});

src/node/services/workspaceTitleGenerator.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { generateObject } from "ai";
22
import { z } from "zod";
33
import type { AIService } from "./aiService";
4-
import type { Config } from "@/node/config";
54
import { log } from "./log";
65
import type { Result } from "@/common/types/result";
76
import { Ok, Err } from "@/common/types/result";
@@ -21,20 +20,17 @@ const workspaceNameSchema = z.object({
2120
});
2221

2322
/**
24-
* Get the preferred model for name generation based on configured providers.
23+
* Get the preferred model for name generation by testing which models the AIService
24+
* can actually create. This delegates credential checking to AIService, avoiding
25+
* duplication of provider-specific API key logic.
2526
*/
26-
export function getPreferredNameModel(config: Config): string | null {
27-
const providersConfig = config.loadProvidersConfig();
27+
export async function getPreferredNameModel(aiService: AIService): Promise<string | null> {
2828
for (const modelId of PREFERRED_MODELS) {
29-
const provider = modelId.split(":")[0];
30-
const providerConfig = providersConfig?.[provider];
31-
const hasKey = providerConfig
32-
? !!(providerConfig as { apiKey?: string }).apiKey
33-
: provider === "anthropic" &&
34-
!!(process.env.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_AUTH_TOKEN);
35-
if (hasKey) {
29+
const result = await aiService.createModel(modelId);
30+
if (result.success) {
3631
return modelId;
3732
}
33+
// If it's an API key error, try the next model; other errors are also skipped
3834
}
3935
return null;
4036
}

0 commit comments

Comments
 (0)