Skip to content

Commit c359ab0

Browse files
committed
🤖 refactor: centralize provider registry to prevent desync bugs
Problem: When adding OpenRouter, had to manually update PROVIDERS_LIST in ipcMain.ts. This created a class of bugs where a provider could be implemented in aiService but forgotten in the UI providers list. Solution: - Created src/constants/providers.ts with SUPPORTED_PROVIDERS constant - Single source of truth for all provider names - ipcMain.ts now uses [...SUPPORTED_PROVIDERS] instead of hardcoded list - Added runtime check in aiService.ts to validate provider is supported - Added unit tests for provider registry validation Benefits: - Adding a new provider only requires updating SUPPORTED_PROVIDERS - Runtime check prevents silent failures if handler not implemented - Type-safe with ProviderName type and isValidProvider() guard - Impossible to have provider in list but not in implementation Addresses Codex P1 comment about exposing OpenRouter in providers list.
1 parent e09d1ef commit c359ab0

File tree

4 files changed

+87
-3
lines changed

4 files changed

+87
-3
lines changed

src/constants/providers.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Test that SUPPORTED_PROVIDERS stays in sync
3+
*/
4+
5+
import { describe, test, expect } from "bun:test";
6+
import { SUPPORTED_PROVIDERS, isValidProvider } from "./providers";
7+
8+
describe("Provider Registry", () => {
9+
test("SUPPORTED_PROVIDERS includes all expected providers", () => {
10+
const expected = ["anthropic", "openai", "ollama", "openrouter"];
11+
expect(SUPPORTED_PROVIDERS).toEqual(expected);
12+
});
13+
14+
test("isValidProvider correctly identifies valid providers", () => {
15+
expect(isValidProvider("anthropic")).toBe(true);
16+
expect(isValidProvider("openai")).toBe(true);
17+
expect(isValidProvider("ollama")).toBe(true);
18+
expect(isValidProvider("openrouter")).toBe(true);
19+
});
20+
21+
test("isValidProvider rejects invalid providers", () => {
22+
expect(isValidProvider("invalid")).toBe(false);
23+
expect(isValidProvider("")).toBe(false);
24+
expect(isValidProvider("gpt-4")).toBe(false);
25+
});
26+
});

src/constants/providers.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Centralized provider registry
3+
*
4+
* All supported AI providers must be listed here. This prevents bugs where
5+
* a new provider is added to aiService but forgotten in PROVIDERS_LIST.
6+
*
7+
* When adding a new provider:
8+
* 1. Add the provider name to this array
9+
* 2. Implement provider handling in aiService.ts getModel()
10+
* 3. The test in aiService will fail if not all providers are handled
11+
*/
12+
export const SUPPORTED_PROVIDERS = [
13+
"anthropic",
14+
"openai",
15+
"ollama",
16+
"openrouter",
17+
] as const;
18+
19+
/**
20+
* Union type of all supported provider names
21+
*/
22+
export type ProviderName = (typeof SUPPORTED_PROVIDERS)[number];
23+
24+
/**
25+
* Type guard to check if a string is a valid provider name
26+
*/
27+
export function isValidProvider(provider: string): provider is ProviderName {
28+
return SUPPORTED_PROVIDERS.includes(provider as ProviderName);
29+
}
30+
31+
/**
32+
* Assert exhaustiveness at compile-time for switch/if-else chains
33+
*
34+
* Usage:
35+
* ```ts
36+
* if (provider === 'anthropic') { ... }
37+
* else if (provider === 'openai') { ... }
38+
* else if (provider === 'ollama') { ... }
39+
* else if (provider === 'openrouter') { ... }
40+
* else {
41+
* assertExhaustive(provider); // TypeScript error if a case is missing
42+
* }
43+
* ```
44+
*/
45+
export function assertExhaustive(value: never): never {
46+
throw new Error(`Unhandled provider case: ${value}`);
47+
}

src/services/aiService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { sanitizeToolInputs } from "@/utils/messages/sanitizeToolInput";
77
import type { Result } from "@/types/result";
88
import { Ok, Err } from "@/types/result";
99
import type { WorkspaceMetadata } from "@/types/workspace";
10+
import { SUPPORTED_PROVIDERS, type ProviderName } from "@/constants/providers";
1011

1112
import type { CmuxMessage, CmuxTextPart } from "@/types/message";
1213
import { createCmuxMessage } from "@/types/message";
@@ -261,6 +262,15 @@ export class AIService extends EventEmitter {
261262
});
262263
}
263264

265+
// Check if provider is supported (prevents silent failures when adding to SUPPORTED_PROVIDERS
266+
// but forgetting to implement handler below)
267+
if (!SUPPORTED_PROVIDERS.includes(providerName as ProviderName)) {
268+
return Err({
269+
type: "provider_not_supported",
270+
provider: providerName,
271+
});
272+
}
273+
264274
// Load providers configuration - the ONLY source of truth
265275
const providersConfig = this.config.loadProvidersConfig();
266276
let providerConfig = providersConfig?.[providerName] ?? {};

src/services/ipcMain.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { log } from "@/services/log";
1414
import { countTokens, countTokensBatch } from "@/utils/main/tokenizer";
1515
import { calculateTokenStats } from "@/utils/tokens/tokenStatsCalculator";
1616
import { IPC_CHANNELS, getChatChannel } from "@/constants/ipc-constants";
17+
import { SUPPORTED_PROVIDERS } from "@/constants/providers";
1718
import type { SendMessageError } from "@/types/errors";
1819
import type { SendMessageOptions, DeleteMessage } from "@/types/ipc";
1920
import { Ok, Err } from "@/types/result";
@@ -1120,9 +1121,9 @@ export class IpcMain {
11201121

11211122
ipcMain.handle(IPC_CHANNELS.PROVIDERS_LIST, () => {
11221123
try {
1123-
// Return all supported providers, not just configured ones
1124-
// This matches the providers defined in the registry
1125-
return ["anthropic", "openai"];
1124+
// Return all supported providers from centralized registry
1125+
// This automatically stays in sync as new providers are added
1126+
return [...SUPPORTED_PROVIDERS];
11261127
} catch (error) {
11271128
log.error("Failed to list providers:", error);
11281129
return [];

0 commit comments

Comments
 (0)