diff --git a/apps/cli/eslint.config.mjs b/apps/cli/eslint.config.mjs index 694bf73664..483b531cb8 100644 --- a/apps/cli/eslint.config.mjs +++ b/apps/cli/eslint.config.mjs @@ -1,4 +1,9 @@ import { config } from "@roo-code/config-eslint/base" +import { createProviderIdentifierConfig } from "@roo-code/config-eslint/provider-identifiers" +import { providerIdentifiers, retiredProviderIdentifiers } from "@roo-code/types/provider-identifiers" /** @type {import("eslint").Linter.Config} */ -export default [...config] +export default [ + ...config, + createProviderIdentifierConfig({ providerIdentifiers, retiredProviderIdentifiers }), +] diff --git a/apps/cli/src/agent/__tests__/extension-host.test.ts b/apps/cli/src/agent/__tests__/extension-host.test.ts index 0c42c88d5b..5e848e5a7e 100644 --- a/apps/cli/src/agent/__tests__/extension-host.test.ts +++ b/apps/cli/src/agent/__tests__/extension-host.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" // pnpm --filter @roo-code/cli test src/agent/__tests__/extension-host.test.ts import { EventEmitter } from "events" @@ -28,7 +29,7 @@ vi.mock("@/lib/storage/index.js", () => ({ */ function createTestHost({ mode = "code", - provider = "openrouter", + provider = providerIdentifiers.openrouter, model = "test-model", ...options }: Partial = {}): ExtensionHost { @@ -113,7 +114,7 @@ describe("ExtensionHost", () => { extensionPath: "/my/extension", user: null, apiKey: "test-key", - provider: "openrouter", + provider: providerIdentifiers.openrouter, model: "test-model", ephemeral: false, debug: false, diff --git a/apps/cli/src/lib/storage/__tests__/settings.test.ts b/apps/cli/src/lib/storage/__tests__/settings.test.ts index b2a3b18cb4..59b323ca60 100644 --- a/apps/cli/src/lib/storage/__tests__/settings.test.ts +++ b/apps/cli/src/lib/storage/__tests__/settings.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import fs from "fs/promises" import path from "path" @@ -58,7 +59,7 @@ describe("Settings Storage", () => { const settingsData = { onboardingProviderChoice: OnboardingProviderChoice.Byok, mode: "architect", - provider: "anthropic" as const, + provider: providerIdentifiers.anthropic, model: "claude-sonnet-4-20250514", reasoningEffort: "high" as const, } @@ -95,7 +96,7 @@ describe("Settings Storage", () => { it("should merge settings with existing ones", async () => { await saveSettings({ mode: "code" }) - await saveSettings({ provider: "openrouter" as const }) + await saveSettings({ provider: providerIdentifiers.openrouter }) const savedData = await fs.readFile(expectedSettingsFile, "utf-8") const settings = JSON.parse(savedData) @@ -107,7 +108,7 @@ describe("Settings Storage", () => { it("should save all default settings fields", async () => { await saveSettings({ mode: "architect", - provider: "anthropic" as const, + provider: providerIdentifiers.anthropic, model: "claude-opus-4.6", reasoningEffort: "medium" as const, consecutiveMistakeLimit: 5, @@ -155,7 +156,7 @@ describe("Settings Storage", () => { await saveSettings({ onboardingProviderChoice: OnboardingProviderChoice.Byok, mode: "architect", - provider: "gemini" as const, + provider: providerIdentifiers.gemini, }) await resetOnboarding() @@ -172,7 +173,7 @@ describe("Settings Storage", () => { // Test that all the settings that can be used as defaults are properly saved and loaded const defaultSettings = { mode: "debug", - provider: "openai-native" as const, + provider: providerIdentifiers.openaiNative, model: "gpt-4o", reasoningEffort: "low" as const, consecutiveMistakeLimit: 7, @@ -205,7 +206,7 @@ describe("Settings Storage", () => { it("should support all settings together including requireApproval", async () => { const allSettings = { mode: "architect", - provider: "anthropic" as const, + provider: providerIdentifiers.anthropic, model: "claude-sonnet-4-20250514", reasoningEffort: "high" as const, requireApproval: true, @@ -231,7 +232,7 @@ describe("Settings Storage", () => { it("should support all settings together including oneshot", async () => { const allSettings = { mode: "architect", - provider: "anthropic" as const, + provider: providerIdentifiers.anthropic, model: "claude-sonnet-4-20250514", reasoningEffort: "high" as const, consecutiveMistakeLimit: 9, diff --git a/apps/cli/src/types/__tests__/types.test.ts b/apps/cli/src/types/__tests__/types.test.ts index 5ed0c84016..538dbec72e 100644 --- a/apps/cli/src/types/__tests__/types.test.ts +++ b/apps/cli/src/types/__tests__/types.test.ts @@ -1,4 +1,4 @@ -import { providerIdentifiers } from "@roo-code/types" +import { providerIdentifiers, retiredProviderIdentifiers } from "@roo-code/types" import { isSupportedProvider, supportedProviders } from "../types.js" @@ -20,7 +20,7 @@ describe("isSupportedProvider", () => { }) it("returns false for 'roo' (retired provider)", () => { - expect(isSupportedProvider("roo")).toBe(false) + expect(isSupportedProvider(retiredProviderIdentifiers.roo)).toBe(false) }) it("returns false for unknown provider", () => { diff --git a/apps/cli/src/ui/__tests__/store.test.ts b/apps/cli/src/ui/__tests__/store.test.ts index 5b8b4fbf77..970d52aa0a 100644 --- a/apps/cli/src/ui/__tests__/store.test.ts +++ b/apps/cli/src/ui/__tests__/store.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import { RooCodeSettings } from "@roo-code/types" import { useCLIStore } from "../store.js" @@ -168,7 +169,7 @@ describe("useCLIStore", () => { }) it("should PRESERVE apiConfiguration", () => { - const config: RooCodeSettings = { apiProvider: "openai", apiModelId: "gpt-4" } + const config: RooCodeSettings = { apiProvider: providerIdentifiers.openai, apiModelId: "gpt-4" } useCLIStore .getState() diff --git a/apps/vscode-e2e/eslint.config.mjs b/apps/vscode-e2e/eslint.config.mjs index 222e2069b0..19145e60a9 100644 --- a/apps/vscode-e2e/eslint.config.mjs +++ b/apps/vscode-e2e/eslint.config.mjs @@ -1,4 +1,6 @@ import { config } from "@roo-code/config-eslint/base" +import { createProviderIdentifierConfig } from "@roo-code/config-eslint/provider-identifiers" +import { providerIdentifiers, retiredProviderIdentifiers } from "@roo-code/types/provider-identifiers" /** @type {import("eslint").Linter.Config} */ export default [ @@ -15,4 +17,5 @@ export default [ "@typescript-eslint/no-floating-promises": "error", }, }, + createProviderIdentifierConfig({ providerIdentifiers, retiredProviderIdentifiers }), ] diff --git a/apps/vscode-e2e/src/suite/anthropic-opus-4-7.test.ts b/apps/vscode-e2e/src/suite/anthropic-opus-4-7.test.ts index ac7e5e4f5f..24897db274 100644 --- a/apps/vscode-e2e/src/suite/anthropic-opus-4-7.test.ts +++ b/apps/vscode-e2e/src/suite/anthropic-opus-4-7.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import { createServer, type IncomingMessage, type ServerResponse } from "http" @@ -177,7 +178,7 @@ suite("Claude Opus 4.7 (Anthropic)", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), @@ -203,7 +204,7 @@ suite("Claude Opus 4.7 (Anthropic)", function () { // aimock handles /v1/messages natively and serves Anthropic-format SSE responses. // In record mode the real x-api-key is forwarded so aimock can proxy to api.anthropic.com. await api.setConfiguration({ - apiProvider: "anthropic" as const, + apiProvider: providerIdentifiers.anthropic, apiKey: aimockUrl && !isRecord ? "mock-key" : process.env.ANTHROPIC_API_KEY!, apiModelId: "claude-opus-4-7", enableReasoningEffort: reasoningEnabled, diff --git a/apps/vscode-e2e/src/suite/index.ts b/apps/vscode-e2e/src/suite/index.ts index a81c622ad6..0d53d65b2b 100644 --- a/apps/vscode-e2e/src/suite/index.ts +++ b/apps/vscode-e2e/src/suite/index.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as path from "path" import Mocha from "mocha" import { glob } from "glob" @@ -20,7 +21,7 @@ export async function run() { const isRecord = process.env.AIMOCK_RECORD === "true" await api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, // In record mode, forward the real key so aimock can proxy it to OpenRouter. // In replay mode, "mock-key" is sufficient — aimock never contacts the real API. openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, diff --git a/apps/vscode-e2e/src/suite/providers/bedrock.test.ts b/apps/vscode-e2e/src/suite/providers/bedrock.test.ts index 6a0c4907f7..882988306e 100644 --- a/apps/vscode-e2e/src/suite/providers/bedrock.test.ts +++ b/apps/vscode-e2e/src/suite/providers/bedrock.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import { startBedrockMockServer, type BedrockMockServer } from "../../bedrock-mock-server" @@ -26,7 +27,7 @@ suite("Bedrock provider", function () { if (!aimockUrl && BEDROCK_LIVE_E2E && AWS_BEARER_TOKEN_BEDROCK) { // Live mode — explicitly opted into real AWS credentials, no aimock intercepting traffic. await globalThis.api.setConfiguration({ - apiProvider: "bedrock" as const, + apiProvider: providerIdentifiers.bedrock, awsUseApiKey: true, awsApiKey: AWS_BEARER_TOKEN_BEDROCK, awsRegion: BEDROCK_REGION, @@ -38,7 +39,7 @@ suite("Bedrock provider", function () { // deserializer to drop the delta field (take() reads top-level only). mockServer = await startBedrockMockServer() await globalThis.api.setConfiguration({ - apiProvider: "bedrock" as const, + apiProvider: providerIdentifiers.bedrock, awsUseApiKey: true, awsApiKey: "mock-key", awsRegion: BEDROCK_REGION, @@ -56,7 +57,7 @@ suite("Bedrock provider", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), @@ -108,7 +109,7 @@ suite("Bedrock provider", function () { // formation end-to-end. if (!process.env.AIMOCK_URL && BEDROCK_LIVE_E2E && AWS_BEARER_TOKEN_BEDROCK) { await api.setConfiguration({ - apiProvider: "bedrock" as const, + apiProvider: providerIdentifiers.bedrock, awsUseApiKey: true, awsApiKey: AWS_BEARER_TOKEN_BEDROCK, awsRegion: BEDROCK_REGION, @@ -116,7 +117,7 @@ suite("Bedrock provider", function () { }) } else { await api.setConfiguration({ - apiProvider: "bedrock" as const, + apiProvider: providerIdentifiers.bedrock, awsUseApiKey: true, awsApiKey: "mock-key", awsRegion: BEDROCK_REGION, diff --git a/apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts b/apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts index 6391c79873..d5be2eba2f 100644 --- a/apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts +++ b/apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import * as fs from "fs/promises" import * as path from "path" @@ -211,7 +212,7 @@ async function runDeepSeekToolProbe( try { await api.setConfiguration({ - apiProvider: "deepseek" as const, + apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: aimockUrl && !isRecord ? "mock-key" : DEEPSEEK_API_KEY!, ...(aimockUrl && { deepSeekBaseUrl: `${aimockUrl}/v1` }), apiModelId: modelId, @@ -336,7 +337,7 @@ suite("DeepSeek V4 provider", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl ? isRecord ? (process.env.OPENROUTER_API_KEY ?? "mock-key") diff --git a/apps/vscode-e2e/src/suite/providers/gemini.test.ts b/apps/vscode-e2e/src/suite/providers/gemini.test.ts index 7355c1cefd..269fd68328 100644 --- a/apps/vscode-e2e/src/suite/providers/gemini.test.ts +++ b/apps/vscode-e2e/src/suite/providers/gemini.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import { RooCodeEventName, type ClineMessage } from "@roo-code/types" @@ -190,7 +191,7 @@ suite("Gemini provider", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), @@ -207,7 +208,7 @@ suite("Gemini provider", function () { const promptTag = `gemini-e2e:reasoning-${reasoningEffort}` await api.setConfiguration({ - apiProvider: "gemini" as const, + apiProvider: providerIdentifiers.gemini, geminiApiKey: aimockUrl && !isRecord ? "mock-key" : GEMINI_API_KEY!, apiModelId: GEMINI_MODEL_ID, enableReasoningEffort: reasoningEffort !== "disable", diff --git a/apps/vscode-e2e/src/suite/providers/openrouter.test.ts b/apps/vscode-e2e/src/suite/providers/openrouter.test.ts index 8b7e3d788f..0a31452067 100644 --- a/apps/vscode-e2e/src/suite/providers/openrouter.test.ts +++ b/apps/vscode-e2e/src/suite/providers/openrouter.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import { setDefaultSuiteTimeout } from "../test-utils" @@ -93,7 +94,7 @@ suite("OpenRouter provider", function () { restoreFetch = installOpenRouterRequestCapture(requests, baseUrl) await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : OPENROUTER_API_KEY!, openRouterModelId: "anthropic/claude-haiku-4-5", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), diff --git a/apps/vscode-e2e/src/suite/providers/xai.test.ts b/apps/vscode-e2e/src/suite/providers/xai.test.ts index 7403d71279..0cae6e5377 100644 --- a/apps/vscode-e2e/src/suite/providers/xai.test.ts +++ b/apps/vscode-e2e/src/suite/providers/xai.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import * as fs from "fs/promises" import * as path from "path" @@ -336,7 +337,7 @@ async function runXAIToolProbe( try { await api.setConfiguration({ - apiProvider: "xai" as const, + apiProvider: providerIdentifiers.xai, xaiApiKey: XAI_API_KEY ?? "mock-key", apiModelId: modelId, }) @@ -549,7 +550,7 @@ suite("xAI provider", function () { ) await globalThis.api.setConfiguration({ - apiProvider: "xai" as const, + apiProvider: providerIdentifiers.xai, xaiApiKey: XAI_API_KEY ?? "mock-key", apiModelId: XAI_MODEL_ID, }) @@ -569,7 +570,7 @@ suite("xAI provider", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), diff --git a/apps/vscode-e2e/src/suite/providers/zai.test.ts b/apps/vscode-e2e/src/suite/providers/zai.test.ts index d3603c370c..25a342e423 100644 --- a/apps/vscode-e2e/src/suite/providers/zai.test.ts +++ b/apps/vscode-e2e/src/suite/providers/zai.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import { RooCodeEventName, type ClineMessage } from "@roo-code/types" @@ -21,7 +22,7 @@ type ZAiRequestCapture = { maxTokens?: number } function getBaseZAiConfiguration() { return { - apiProvider: "zai" as const, + apiProvider: providerIdentifiers.zai, zaiApiKey: ZAI_API_KEY ?? "mock-key", zaiApiLine: "international_api" as const, modelMaxTokens: undefined, @@ -197,7 +198,7 @@ suite("Z.ai GLM provider", function () { await globalThis.api.upsertProfile( "default", { - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), diff --git a/apps/vscode-e2e/src/suite/subtasks.test.ts b/apps/vscode-e2e/src/suite/subtasks.test.ts index 9da2cec28f..857c8accc5 100644 --- a/apps/vscode-e2e/src/suite/subtasks.test.ts +++ b/apps/vscode-e2e/src/suite/subtasks.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import { RooCodeEventName, type ClineMessage } from "@roo-code/types" @@ -735,7 +736,7 @@ suite("Roo Code Subtasks", function () { const aimockUrl = process.env.AIMOCK_URL const parentProfile = { - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "mock-key", openRouterModelId: "openai/gpt-4.1", rateLimitSeconds: 0, diff --git a/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts b/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts index 36bd847416..622b998fac 100644 --- a/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts +++ b/apps/vscode-e2e/src/suite/tools/apply-diff.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import * as fs from "fs/promises" import * as path from "path" @@ -110,7 +111,7 @@ suite("Roo Code apply_diff Tool", function () { const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "anthropic/claude-sonnet-4.5", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), @@ -136,7 +137,7 @@ suite("Roo Code apply_diff Tool", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), diff --git a/apps/vscode-e2e/src/suite/tools/execute-command.test.ts b/apps/vscode-e2e/src/suite/tools/execute-command.test.ts index db09fba264..8b45c22d9f 100644 --- a/apps/vscode-e2e/src/suite/tools/execute-command.test.ts +++ b/apps/vscode-e2e/src/suite/tools/execute-command.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import * as fs from "fs/promises" import * as path from "path" @@ -25,7 +26,7 @@ suite("Roo Code execute_command Tool", function () { const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "anthropic/claude-sonnet-4.5", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), @@ -51,7 +52,7 @@ suite("Roo Code execute_command Tool", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), diff --git a/apps/vscode-e2e/src/suite/tools/terminal-profile.test.ts b/apps/vscode-e2e/src/suite/tools/terminal-profile.test.ts index a23bd8163c..63b26e7bfb 100644 --- a/apps/vscode-e2e/src/suite/tools/terminal-profile.test.ts +++ b/apps/vscode-e2e/src/suite/tools/terminal-profile.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" /** * Linux-only e2e smoke test for the VS Code terminal profile override. * @@ -41,7 +42,7 @@ suite("Terminal Profile", function () { const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "anthropic/claude-sonnet-4.5", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), @@ -97,7 +98,7 @@ suite("Terminal Profile", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), diff --git a/apps/vscode-e2e/src/suite/tools/write-to-file.test.ts b/apps/vscode-e2e/src/suite/tools/write-to-file.test.ts index 55cf80a483..695276c9e2 100644 --- a/apps/vscode-e2e/src/suite/tools/write-to-file.test.ts +++ b/apps/vscode-e2e/src/suite/tools/write-to-file.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" import * as assert from "assert" import * as fs from "fs/promises" import * as path from "path" @@ -25,7 +26,7 @@ suite("Roo Code write_to_file Tool", function () { const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "anthropic/claude-sonnet-4.5", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), @@ -51,7 +52,7 @@ suite("Roo Code write_to_file Tool", function () { const aimockUrl = process.env.AIMOCK_URL const isRecord = process.env.AIMOCK_RECORD === "true" await globalThis.api.setConfiguration({ - apiProvider: "openrouter" as const, + apiProvider: providerIdentifiers.openrouter, openRouterApiKey: aimockUrl && !isRecord ? "mock-key" : process.env.OPENROUTER_API_KEY!, openRouterModelId: "openai/gpt-4.1", ...(aimockUrl && { openRouterBaseUrl: `${aimockUrl}/v1` }), diff --git a/apps/vscode-e2e/src/visual/sceneController.ts b/apps/vscode-e2e/src/visual/sceneController.ts index 66da706045..a4372c95ae 100644 --- a/apps/vscode-e2e/src/visual/sceneController.ts +++ b/apps/vscode-e2e/src/visual/sceneController.ts @@ -2,7 +2,13 @@ import fs from "fs/promises" import * as vscode from "vscode" -import { RooCodeEventName, type ClineMessage, type RooCodeTestAPI, type WebviewThemeFixture } from "@roo-code/types" +import { + providerIdentifiers, + RooCodeEventName, + type ClineMessage, + type RooCodeTestAPI, + type WebviewThemeFixture, +} from "@roo-code/types" import { isCompletedAsk } from "../suite/utils" @@ -89,7 +95,7 @@ export async function run(): Promise { if (!prompt || !result) throw new Error("Chat prompt and result are required for the populated chat scene") await api.setConfiguration({ - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, lastShownAnnouncementId: api.getLatestAnnouncementId(), openRouterApiKey: "mock-key", openRouterModelId: "openai/gpt-4.1", diff --git a/packages/cloud/eslint.config.mjs b/packages/cloud/eslint.config.mjs index c603a68f12..7c0d5b2903 100644 --- a/packages/cloud/eslint.config.mjs +++ b/packages/cloud/eslint.config.mjs @@ -1,9 +1,12 @@ import { config } from "@roo-code/config-eslint/base" +import { createProviderIdentifierConfig } from "@roo-code/config-eslint/provider-identifiers" +import { providerIdentifiers, retiredProviderIdentifiers } from "@roo-code/types/provider-identifiers" import globals from "globals" /** @type {import("eslint").Linter.Config} */ export default [ ...config, + createProviderIdentifierConfig({ providerIdentifiers, retiredProviderIdentifiers }), { files: ["**/*.cjs"], languageOptions: { diff --git a/packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts b/packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts index b0486b971c..6d8e0035dd 100644 --- a/packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts +++ b/packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "@roo-code/types" // pnpm test src/__tests__/CloudSettingsService.parsing.test.ts import type { ExtensionContext } from "vscode" @@ -97,7 +98,7 @@ describe("CloudSettingsService - Response Parsing", () => { providerProfiles: { default: { id: "default", - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-3-opus-20240229", apiKey: "test-key", modelTemperature: 0.7, diff --git a/packages/config-eslint/package.json b/packages/config-eslint/package.json index 8c01525543..92f67536d9 100644 --- a/packages/config-eslint/package.json +++ b/packages/config-eslint/package.json @@ -5,7 +5,11 @@ "exports": { "./base": "./base.js", "./react": "./react.js", - "./next-js": "./next.js" + "./next-js": "./next.js", + "./provider-identifiers": "./provider-identifiers.js" + }, + "scripts": { + "test": "node --test provider-identifiers.test.js" }, "devDependencies": { "@eslint/js": "9.39.4", diff --git a/packages/config-eslint/provider-identifiers.js b/packages/config-eslint/provider-identifiers.js new file mode 100644 index 0000000000..6cc953fe08 --- /dev/null +++ b/packages/config-eslint/provider-identifiers.js @@ -0,0 +1,202 @@ +const typescriptExpressionWrappers = new Set([ + "ChainExpression", + "TSAsExpression", + "TSInstantiationExpression", + "TSNonNullExpression", + "TSSatisfiesExpression", + "TSTypeAssertion", +]) + +const useCanonicalMessageId = "useCanonical" + +function unwrapExpression(node) { + while (typescriptExpressionWrappers.has(node?.type)) { + node = node.expression + } + + return node +} + +function getMemberExpressionStaticName(node) { + if (!node.computed && node.property.type === "Identifier") { + return node.property.name + } + + if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") { + return node.property.value + } + + return undefined +} + +function getStaticName(node) { + node = unwrapExpression(node) + + switch (node?.type) { + case "Identifier": + case "PrivateIdentifier": + return node.name + case "MemberExpression": + return getMemberExpressionStaticName(node) + case "Literal": + return typeof node.value === "string" ? node.value : undefined + default: + return undefined + } +} + +function isProviderLike(node) { + return getStaticName(node)?.toLowerCase().includes("provider") ?? false +} + +function isCanonicalProviderRegistry(node) { + const name = getStaticName(node) + return name === "providerIdentifiers" || name === "retiredProviderIdentifiers" +} + +export function createProviderIdentifierConfig({ providerIdentifiers, retiredProviderIdentifiers }) { + const providerReplacementsByValue = new Map([ + ...Object.entries(providerIdentifiers).map(([member, value]) => [value, `providerIdentifiers.${member}`]), + ...Object.entries(retiredProviderIdentifiers).map(([member, value]) => [ + value, + `retiredProviderIdentifiers.${member}`, + ]), + ]) + + function getRawProvider(node) { + node = unwrapExpression(node) + + if (node?.type === "Literal" && typeof node.value === "string") { + const replacement = providerReplacementsByValue.get(node.value) + return replacement ? { replacement, value: node.value } : undefined + } + + if (node?.type === "TemplateLiteral" && node.expressions.length === 0) { + const value = node.quasis[0]?.value.cooked + const replacement = value ? providerReplacementsByValue.get(value) : undefined + return replacement ? { replacement, value } : undefined + } + + return undefined + } + + function getProviderMapDeclarator(objectExpression) { + let expression = objectExpression + while (typescriptExpressionWrappers.has(expression.parent?.type)) { + expression = expression.parent + } + + const declarator = expression.parent + return declarator?.type === "VariableDeclarator" && declarator.init === expression ? declarator : undefined + } + + const noRawProviderIdentifiers = { + meta: { + type: "problem", + docs: { description: "Require canonical provider identifiers in provider-like contexts" }, + schema: [], + messages: { + [useCanonicalMessageId]: 'Use {{replacement}} instead of the raw provider identifier "{{value}}".', + }, + }, + create(context) { + function reportIfRawProvider(node) { + const provider = getRawProvider(node) + if (provider) { + context.report({ node, messageId: useCanonicalMessageId, data: provider }) + } + } + + function reportIfRawProviderMapKey(node) { + if (node.computed || node.parent?.type !== "ObjectExpression") { + return + } + + const declarator = getProviderMapDeclarator(node.parent) + if (!declarator || !isProviderLike(declarator.id) || isCanonicalProviderRegistry(declarator.id)) { + return + } + + const value = getStaticName(node.key) + const replacement = value ? providerReplacementsByValue.get(value) : undefined + if (replacement) { + context.report({ + node: node.key, + messageId: useCanonicalMessageId, + data: { replacement, value }, + }) + } + } + + return { + Property(node) { + reportIfRawProviderMapKey(node) + if (isProviderLike(node.key)) { + reportIfRawProvider(node.value) + } + }, + PropertyDefinition(node) { + if (isProviderLike(node.key)) { + reportIfRawProvider(node.value) + } + }, + VariableDeclarator(node) { + if (isProviderLike(node.id)) { + reportIfRawProvider(node.init) + } + }, + AssignmentPattern(node) { + if (isProviderLike(node.left)) { + reportIfRawProvider(node.right) + } + }, + AssignmentExpression(node) { + if (isProviderLike(node.left)) { + reportIfRawProvider(node.right) + } + }, + BinaryExpression(node) { + if (!["===", "!==", "==", "!="].includes(node.operator)) { + return + } + + if (isProviderLike(node.left)) { + reportIfRawProvider(node.right) + } + if (isProviderLike(node.right)) { + reportIfRawProvider(node.left) + } + }, + SwitchStatement(node) { + if (isProviderLike(node.discriminant)) { + for (const switchCase of node.cases) { + reportIfRawProvider(switchCase.test) + } + } + }, + CallExpression(node) { + if (isProviderLike(node.callee)) { + for (const argument of node.arguments) { + reportIfRawProvider(argument) + } + } + }, + } + }, + } + + return { + files: ["**/*.ts", "**/*.tsx"], + ignores: ["**/fixtures/**"], + plugins: { + zoo: { + rules: { + "no-raw-provider-identifiers": noRawProviderIdentifiers, + }, + }, + }, + rules: { + "zoo/no-raw-provider-identifiers": "error", + }, + } +} diff --git a/packages/config-eslint/provider-identifiers.test.js b/packages/config-eslint/provider-identifiers.test.js new file mode 100644 index 0000000000..e95870bc01 --- /dev/null +++ b/packages/config-eslint/provider-identifiers.test.js @@ -0,0 +1,156 @@ +import { RuleTester } from "eslint" +import typescriptEslint from "typescript-eslint" + +import { createProviderIdentifierConfig } from "./provider-identifiers.js" + +const config = createProviderIdentifierConfig({ + providerIdentifiers: { + anthropic: "anthropic", + gemini: "gemini", + openaiNative: "openai-native", + openrouter: "openrouter", + poe: "poe", + vercelAiGateway: "vercel-ai-gateway", + }, + retiredProviderIdentifiers: { + groq: "groq", + roo: "roo", + }, +}) +const rule = config.plugins.zoo.rules["no-raw-provider-identifiers"] + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + parser: typescriptEslint.parser, + }, +}) + +ruleTester.run("no-raw-provider-identifiers provider map keys", rule, { + valid: [ + "const dynamicProviderExtras = { [providerIdentifiers.openrouter]: {} }", + "const response = { openrouter: externalPayload }", + 'const providerIdentifiers = { openrouter: "openrouter", vercelAiGateway: "vercel-ai-gateway" }', + ], + invalid: [ + { + code: 'const dynamicProviderExtras = { openrouter: {}, "vercel-ai-gateway": {} }', + errors: [ + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.openrouter", value: "openrouter" }, + }, + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.vercelAiGateway", value: "vercel-ai-gateway" }, + }, + ], + }, + ], +}) + +ruleTester.run("no-raw-provider-identifiers provider-like values", rule, { + valid: [ + "const apiProvider = retiredProviderIdentifiers.roo", + "const provider = retiredProviderIdentifiers.groq", + 'const config = { apiProvider: "external-provider" }', + "const config = { apiProvider: providerIdentifiers.poe }", + "const provider = providerIdentifiers.openrouter", + "getProviderServiceConfig(providerIdentifiers.gemini)", + 'if (config?.protocol === "gemini") {}', + 'const response = { protocol: "anthropic", format: "openrouter" }', + ], + invalid: [ + { + code: 'const apiProvider = "roo"', + errors: [ + { + messageId: "useCanonical", + data: { replacement: "retiredProviderIdentifiers.roo", value: "roo" }, + type: "Literal", + }, + ], + }, + { + code: "const persistedProvider = `groq`", + errors: [ + { + messageId: "useCanonical", + data: { replacement: "retiredProviderIdentifiers.groq", value: "groq" }, + type: "TemplateLiteral", + }, + ], + }, + { + code: 'const config = { apiProvider: "poe", imageProvider: `openrouter` }', + errors: [ + { messageId: "useCanonical", data: { replacement: "providerIdentifiers.poe", value: "poe" } }, + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.openrouter", value: "openrouter" }, + }, + ], + }, + { + code: 'class Service { #apiProvider = "gemini" }', + errors: [ + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.gemini", value: "gemini" }, + type: "Literal", + }, + ], + }, + { + code: 'config["apiProvider"] = "poe"; if (imageProvider === "openrouter") {}', + errors: [ + { messageId: "useCanonical", data: { replacement: "providerIdentifiers.poe", value: "poe" } }, + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.openrouter", value: "openrouter" }, + }, + ], + }, + { + code: 'switch (config.apiProvider) { case "poe": break; case providerIdentifiers.openrouter: break }', + errors: [{ messageId: "useCanonical", data: { replacement: "providerIdentifiers.poe", value: "poe" } }], + }, + { + code: 'function select(provider = "openrouter") { return provider }', + errors: [ + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.openrouter", value: "openrouter" }, + }, + ], + }, + { + code: 'getProviderServiceConfig("gemini")', + errors: [ + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.gemini", value: "gemini" }, + }, + ], + }, + { + code: '(getProviderServiceConfig)("gemini")', + errors: [ + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.gemini", value: "gemini" }, + }, + ], + }, + { + code: 'if (config?.apiProvider === "gemini") {}', + errors: [ + { + messageId: "useCanonical", + data: { replacement: "providerIdentifiers.gemini", value: "gemini" }, + }, + ], + }, + ], +}) diff --git a/packages/types/eslint.config.mjs b/packages/types/eslint.config.mjs index c603a68f12..dd57625dfa 100644 --- a/packages/types/eslint.config.mjs +++ b/packages/types/eslint.config.mjs @@ -1,9 +1,12 @@ import { config } from "@roo-code/config-eslint/base" +import { createProviderIdentifierConfig } from "@roo-code/config-eslint/provider-identifiers" +import { providerIdentifiers, retiredProviderIdentifiers } from "./src/provider-identifiers.ts" import globals from "globals" /** @type {import("eslint").Linter.Config} */ export default [ ...config, + createProviderIdentifierConfig({ providerIdentifiers, retiredProviderIdentifiers }), { files: ["**/*.cjs"], languageOptions: { diff --git a/packages/types/src/__tests__/kimi-code.test.ts b/packages/types/src/__tests__/kimi-code.test.ts index 3d8b9ac549..5b2696fab3 100644 --- a/packages/types/src/__tests__/kimi-code.test.ts +++ b/packages/types/src/__tests__/kimi-code.test.ts @@ -1,11 +1,15 @@ +import { providerIdentifiers } from "../provider-identifiers.js" import { SECRET_STATE_KEYS, dynamicProviders, + kimiCodeAuthMethodSchema, kimiCodeDefaultModelId, providerSettingsSchema, providerSettingsSchemaDiscriminated, } from "../index.js" +const [kimiCodeOAuthAuthMethod] = kimiCodeAuthMethodSchema.options + describe("Kimi Code provider types", () => { it("registers Kimi Code as a dynamic provider with a distinct secret", () => { expect(dynamicProviders).toContain("kimi-code") @@ -16,14 +20,19 @@ describe("Kimi Code provider types", () => { it("parses OAuth and API-key settings independently from Moonshot", () => { expect( providerSettingsSchemaDiscriminated.parse({ - apiProvider: "kimi-code", + apiProvider: providerIdentifiers.kimiCode, kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "kimi-key", apiModelId: kimiCodeDefaultModelId, }), ).toMatchObject({ kimiCodeApiKey: "kimi-key" }) - expect(providerSettingsSchema.parse({ apiProvider: "kimi-code", kimiCodeAuthMethod: "oauth" })).toMatchObject({ - kimiCodeAuthMethod: "oauth", + expect( + providerSettingsSchema.parse({ + apiProvider: providerIdentifiers.kimiCode, + kimiCodeAuthMethod: kimiCodeOAuthAuthMethod, + }), + ).toMatchObject({ + kimiCodeAuthMethod: kimiCodeOAuthAuthMethod, }) }) }) diff --git a/packages/types/src/__tests__/provider-model-id.test.ts b/packages/types/src/__tests__/provider-model-id.test.ts index 7c404c1cd4..a623b24b27 100644 --- a/packages/types/src/__tests__/provider-model-id.test.ts +++ b/packages/types/src/__tests__/provider-model-id.test.ts @@ -1,3 +1,4 @@ +import { retiredProviderIdentifiers } from "../provider-identifiers.js" import { getModelId, modelIdKeys, providerIdentifiers, type ProviderSettings } from "../index.js" const expectedModelIdKeys = [ @@ -75,7 +76,7 @@ describe("getModelId", () => { it("preserves legacy model ID precedence for retired providers", () => { const settings: ProviderSettings = { - apiProvider: "groq", + apiProvider: retiredProviderIdentifiers.groq, lmStudioDraftModelId: "draft-model", requestyModelId: "requesty-model", } diff --git a/packages/types/src/__tests__/telemetry.test.ts b/packages/types/src/__tests__/telemetry.test.ts index 29e6207794..6b8a9fc30c 100644 --- a/packages/types/src/__tests__/telemetry.test.ts +++ b/packages/types/src/__tests__/telemetry.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "../provider-identifiers.js" // pnpm --filter @roo-code/types test src/__tests__/telemetry.test.ts import { @@ -536,7 +537,7 @@ describe("telemetry error utilities", () => { consecutiveMistakeCount: 5, consecutiveMistakeLimit: 3, reason: "no_tools_used", - provider: "anthropic", + provider: providerIdentifiers.anthropic, modelId: "claude-3-sonnet-20240229", }) }) diff --git a/packages/types/src/image-generation.ts b/packages/types/src/image-generation.ts index 76bfca2552..a40d28073b 100644 --- a/packages/types/src/image-generation.ts +++ b/packages/types/src/image-generation.ts @@ -1,3 +1,5 @@ +import { providerIdentifiers } from "./provider-identifiers.js" + /** * Image generation model constants */ @@ -16,12 +18,28 @@ export interface ImageGenerationModel { export const IMAGE_GENERATION_MODELS: ImageGenerationModel[] = [ // OpenRouter models - { value: "google/gemini-2.5-flash-image", label: "Gemini 2.5 Flash Image", provider: "openrouter" }, - { value: "google/gemini-3-pro-image-preview", label: "Gemini 3 Pro Image Preview", provider: "openrouter" }, - { value: "openai/gpt-5-image", label: "GPT-5 Image", provider: "openrouter" }, - { value: "openai/gpt-5-image-mini", label: "GPT-5 Image Mini", provider: "openrouter" }, - { value: "black-forest-labs/flux.2-flex", label: "Black Forest Labs FLUX.2 Flex", provider: "openrouter" }, - { value: "black-forest-labs/flux.2-pro", label: "Black Forest Labs FLUX.2 Pro", provider: "openrouter" }, + { + value: "google/gemini-2.5-flash-image", + label: "Gemini 2.5 Flash Image", + provider: providerIdentifiers.openrouter, + }, + { + value: "google/gemini-3-pro-image-preview", + label: "Gemini 3 Pro Image Preview", + provider: providerIdentifiers.openrouter, + }, + { value: "openai/gpt-5-image", label: "GPT-5 Image", provider: providerIdentifiers.openrouter }, + { value: "openai/gpt-5-image-mini", label: "GPT-5 Image Mini", provider: providerIdentifiers.openrouter }, + { + value: "black-forest-labs/flux.2-flex", + label: "Black Forest Labs FLUX.2 Flex", + provider: providerIdentifiers.openrouter, + }, + { + value: "black-forest-labs/flux.2-pro", + label: "Black Forest Labs FLUX.2 Pro", + provider: providerIdentifiers.openrouter, + }, ] /** @@ -32,7 +50,7 @@ export const IMAGE_GENERATION_MODEL_IDS = IMAGE_GENERATION_MODELS.map((m) => m.v /** * Image generation provider type */ -export type ImageGenerationProvider = "openrouter" +export type ImageGenerationProvider = typeof providerIdentifiers.openrouter /** * Get the image generation provider with backwards compatibility @@ -44,5 +62,5 @@ export function getImageGenerationProvider( explicitProvider: ImageGenerationProvider | undefined, _hasExistingModel: boolean, ): ImageGenerationProvider { - return explicitProvider !== undefined ? explicitProvider : "openrouter" + return explicitProvider !== undefined ? explicitProvider : providerIdentifiers.openrouter } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7e603f528..556a8e107d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -658,9 +658,6 @@ importers: '@types/vscode': specifier: 1.100.0 version: 1.100.0 - '@typescript-eslint/parser': - specifier: 8.32.1 - version: 8.32.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@vitest/coverage-v8': specifier: 4.1.9 version: 4.1.9(vitest@4.1.9) @@ -3466,6 +3463,7 @@ packages: '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} diff --git a/src/__tests__/no-raw-provider-identifiers.spec.mjs b/src/__tests__/no-raw-provider-identifiers.spec.mjs deleted file mode 100644 index 46d981b791..0000000000 --- a/src/__tests__/no-raw-provider-identifiers.spec.mjs +++ /dev/null @@ -1,137 +0,0 @@ -import { Linter } from "eslint" -import typescriptParser from "@typescript-eslint/parser" -import { describe, expect, it } from "vitest" - -import { noRawProviderIdentifiers } from "../eslint-rules/no-raw-provider-identifiers.mjs" - -const linter = new Linter({ configType: "eslintrc" }) - -linter.defineRule("zoo/no-raw-provider-identifiers", noRawProviderIdentifiers) -linter.defineParser("@typescript-eslint/parser", typescriptParser) - -function lint(code) { - return linter.verify(code, { - parserOptions: { ecmaVersion: 2022, sourceType: "module" }, - rules: { "zoo/no-raw-provider-identifiers": "error" }, - }) -} - -function lintTypeScript(code) { - return linter.verify(code, { - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: 2022, - sourceType: "module", - warnOnUnsupportedTypeScriptVersion: false, - }, - rules: { "zoo/no-raw-provider-identifiers": "error" }, - }) -} - -describe("no-raw-provider-identifiers", () => { - it("rejects a canonical provider literal in an apiProvider property", () => { - const messages = lint('const config = { apiProvider: "poe" }') - - expect(messages).toHaveLength(1) - expect(messages[0]).toMatchObject({ - ruleId: "zoo/no-raw-provider-identifiers", - message: 'Use providerIdentifiers.poe instead of the raw provider identifier "poe".', - }) - }) - - it("allows a non-canonical literal and a canonical registry member", () => { - expect(lint('const config = { apiProvider: "external-provider" }')).toHaveLength(0) - expect(lint("const config = { apiProvider: providerIdentifiers.poe }")).toHaveLength(0) - }) - - it("matches provider-like property names and static template literals", () => { - const messages = lint('const config = { provider: "poe", imageProvider: `openrouter` }') - - expect(messages).toHaveLength(2) - }) - - it("allows an empty static template in a provider-like context", () => { - expect(lint("const config = { apiProvider: `` }")).toHaveLength(0) - }) - - it("rejects canonical literals in provider-like variable declarations", () => { - const messages = lint(` - const apiProvider = "poe" - let fallbackProvider = \`openrouter\` - const label = "poe" - `) - - expect(messages.map(({ message }) => message)).toEqual([ - 'Use providerIdentifiers.poe instead of the raw provider identifier "poe".', - 'Use providerIdentifiers.openrouter instead of the raw provider identifier "openrouter".', - ]) - }) - - it("rejects canonical provider literals wrapped in TypeScript expressions", () => { - const messages = lintTypeScript(` - const apiProvider = "poe" as ApiProvider - const fallbackProvider = "openrouter" satisfies ApiProvider - const imageProvider = "openai-native" - const nestedProvider = ("anthropic" as ApiProvider)! - `) - - expect(messages.map(({ message }) => message)).toEqual([ - 'Use providerIdentifiers.poe instead of the raw provider identifier "poe".', - 'Use providerIdentifiers.openrouter instead of the raw provider identifier "openrouter".', - 'Use providerIdentifiers.openaiNative instead of the raw provider identifier "openai-native".', - 'Use providerIdentifiers.anthropic instead of the raw provider identifier "anthropic".', - ]) - }) - - it("rejects canonical literals in provider-like class fields", () => { - const messages = lintTypeScript(` - class Settings { - apiProvider = "poe" - label = "openrouter" - } - `) - - expect(messages.map(({ message }) => message)).toEqual([ - 'Use providerIdentifiers.poe instead of the raw provider identifier "poe".', - ]) - }) - - it("rejects canonical literals in provider-like assignments and comparisons", () => { - const messages = lint(` - config["apiProvider"] = "poe" - if (imageProvider === "openrouter") {} - if ("openai-native" !== config.fallbackProvider) {} - `) - - expect(messages.map(({ message }) => message)).toEqual([ - 'Use providerIdentifiers.poe instead of the raw provider identifier "poe".', - 'Use providerIdentifiers.openrouter instead of the raw provider identifier "openrouter".', - 'Use providerIdentifiers.openaiNative instead of the raw provider identifier "openai-native".', - ]) - }) - - it("rejects canonical literals in provider-like switch cases", () => { - const messages = lint(` - switch (config.apiProvider) { - case "poe": break - case providerIdentifiers.openrouter: break - } - `) - - expect(messages).toHaveLength(1) - expect(messages[0].message).toContain("providerIdentifiers.poe") - }) - - it("does not report canonical values outside provider-like contexts", () => { - const messages = lint(` - const label = "poe" - const config = { protocol: "anthropic", format: "openai" } - config[dynamicKey] = "poe" - if (apiProtocol === "anthropic") {} - if (provider > "poe") {} - switch (format) { case "openai": break } - `) - - expect(messages).toHaveLength(0) - }) -}) diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index b7a0a9595c..ff408b3188 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -25,9 +25,9 @@ vi.mock("../../../api", async () => { const modelInfoFor = (config: { apiProvider?: string; apiModelId?: string }) => { const id = config?.apiModelId ?? "" switch (config?.apiProvider) { - case "zai": + case types.providerIdentifiers.zai: return zaiModels[id] ?? {} - case "anthropic": + case types.providerIdentifiers.anthropic: return anthropicModels[id] ?? {} default: return {} diff --git a/src/eslint-rules/no-raw-provider-identifiers.mjs b/src/eslint-rules/no-raw-provider-identifiers.mjs deleted file mode 100644 index 7520cc6414..0000000000 --- a/src/eslint-rules/no-raw-provider-identifiers.mjs +++ /dev/null @@ -1,122 +0,0 @@ -import { providerIdentifiers, retiredProviderIdentifiers } from "@roo-code/types/provider-identifiers" - -const providerReplacementsByValue = new Map([ - ...Object.entries(providerIdentifiers).map(([member, value]) => [value, `providerIdentifiers.${member}`]), - ...Object.entries(retiredProviderIdentifiers).map(([member, value]) => [ - value, - `retiredProviderIdentifiers.${member}`, - ]), -]) -const typescriptExpressionWrappers = new Set([ - "TSAsExpression", - "TSNonNullExpression", - "TSSatisfiesExpression", - "TSTypeAssertion", -]) - -function getStaticName(node) { - if (node?.type === "Identifier") { - return node.name - } - - if (node?.type === "MemberExpression") { - if (!node.computed && node.property.type === "Identifier") { - return node.property.name - } - - if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") { - return node.property.value - } - } - - if (node?.type === "Literal" && typeof node.value === "string") { - return node.value - } - - return undefined -} - -function isProviderLike(node) { - return getStaticName(node)?.toLowerCase().includes("provider") ?? false -} - -function getRawProvider(node) { - while (typescriptExpressionWrappers.has(node?.type)) { - node = node.expression - } - - if (node?.type === "Literal" && typeof node.value === "string") { - const replacement = providerReplacementsByValue.get(node.value) - return replacement ? { replacement, value: node.value } : undefined - } - - if (node?.type === "TemplateLiteral" && node.expressions.length === 0) { - const value = node.quasis[0]?.value.cooked - const replacement = value ? providerReplacementsByValue.get(value) : undefined - return replacement ? { replacement, value } : undefined - } - - return undefined -} - -export const noRawProviderIdentifiers = { - meta: { - type: "problem", - docs: { description: "Require canonical provider identifiers in provider-like contexts" }, - schema: [], - messages: { - useCanonical: - 'Use {{replacement}} instead of the raw provider identifier "{{value}}".', - }, - }, - create(context) { - function reportIfRawProvider(node) { - const provider = getRawProvider(node) - if (provider) { - context.report({ node, messageId: "useCanonical", data: provider }) - } - } - - return { - Property(node) { - if (isProviderLike(node.key)) { - reportIfRawProvider(node.value) - } - }, - PropertyDefinition(node) { - if (isProviderLike(node.key)) { - reportIfRawProvider(node.value) - } - }, - VariableDeclarator(node) { - if (isProviderLike(node.id)) { - reportIfRawProvider(node.init) - } - }, - AssignmentExpression(node) { - if (isProviderLike(node.left)) { - reportIfRawProvider(node.right) - } - }, - BinaryExpression(node) { - if (!["===", "!==", "==", "!="].includes(node.operator)) { - return - } - - if (isProviderLike(node.left)) { - reportIfRawProvider(node.right) - } - if (isProviderLike(node.right)) { - reportIfRawProvider(node.left) - } - }, - SwitchStatement(node) { - if (isProviderLike(node.discriminant)) { - for (const switchCase of node.cases) { - reportIfRawProvider(switchCase.test) - } - } - }, - } - }, -} diff --git a/src/eslint-rules/no-raw-provider-identifiers.test.mjs b/src/eslint-rules/no-raw-provider-identifiers.test.mjs deleted file mode 100644 index c4efaf90ff..0000000000 --- a/src/eslint-rules/no-raw-provider-identifiers.test.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { RuleTester } from "eslint" - -import { noRawProviderIdentifiers } from "./no-raw-provider-identifiers.mjs" - -const ruleTester = new RuleTester({ - languageOptions: { - ecmaVersion: 2022, - sourceType: "module", - }, -}) - -ruleTester.run("no-raw-provider-identifiers", noRawProviderIdentifiers, { - valid: [ - "const apiProvider = retiredProviderIdentifiers.roo", - "const provider = retiredProviderIdentifiers.groq", - ], - invalid: [ - { - code: 'const apiProvider = "roo"', - errors: [ - { - message: - 'Use retiredProviderIdentifiers.roo instead of the raw provider identifier "roo".', - type: "Literal", - }, - ], - }, - { - code: "const persistedProvider = `groq`", - errors: [ - { - message: - 'Use retiredProviderIdentifiers.groq instead of the raw provider identifier "groq".', - type: "TemplateLiteral", - }, - ], - }, - ], -}) diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index 36a56c5df2..0dbf5c7f79 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -1,10 +1,11 @@ import { config } from "@roo-code/config-eslint/base" - -import { noRawProviderIdentifiers } from "./eslint-rules/no-raw-provider-identifiers.mjs" +import { createProviderIdentifierConfig } from "@roo-code/config-eslint/provider-identifiers" +import { providerIdentifiers, retiredProviderIdentifiers } from "@roo-code/types/provider-identifiers" /** @type {import("eslint").Linter.Config} */ export default [ ...config, + createProviderIdentifierConfig({ providerIdentifiers, retiredProviderIdentifiers }), { rules: { "prefer-const": ["error", { destructuring: "all" }], @@ -33,22 +34,6 @@ export default [ "no-undef": "off", }, }, - { - files: ["**/*.ts", "**/*.tsx"], - ignores: [ - "**/fixtures/**", - ], - plugins: { - zoo: { - rules: { - "no-raw-provider-identifiers": noRawProviderIdentifiers, - }, - }, - }, - rules: { - "zoo/no-raw-provider-identifiers": "error", - }, - }, { // Ratchet: enforce no-floating-promises directory by directory. Each // directory is added here once its floating promises are resolved. diff --git a/src/package.json b/src/package.json index 094d7aed93..28c4184da5 100644 --- a/src/package.json +++ b/src/package.json @@ -529,7 +529,6 @@ }, "devDependencies": { "@ai-sdk/openai-compatible": "2.0.56", - "@typescript-eslint/parser": "8.32.1", "@roo-code/build": "workspace:^", "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", diff --git a/src/services/code-index/__tests__/config-manager.spec.ts b/src/services/code-index/__tests__/config-manager.spec.ts index 6a496b6809..49a004bb27 100644 --- a/src/services/code-index/__tests__/config-manager.spec.ts +++ b/src/services/code-index/__tests__/config-manager.spec.ts @@ -59,6 +59,26 @@ describe("CodeIndexConfigManager", () => { expect(configManager.isFeatureEnabled).toBe(false) expect(configManager.currentEmbedderProvider).toBe("openai") }) + + it("loads Bedrock as the embedder provider with its optional profile", () => { + mockContextProxy.getGlobalState.mockReturnValue({ + codebaseIndexEnabled: true, + codebaseIndexEmbedderProvider: providerIdentifiers.bedrock, + codebaseIndexEmbedderModelId: "amazon.titan-embed-text-v2:0", + codebaseIndexBedrockRegion: "eu-west-1", + codebaseIndexBedrockProfile: "development", + codebaseIndexQdrantUrl: "http://localhost:6333", + }) + + configManager = new CodeIndexConfigManager(mockContextProxy) + + expect(configManager.getConfig()).toMatchObject({ + embedderProvider: providerIdentifiers.bedrock, + modelId: "amazon.titan-embed-text-v2:0", + bedrockOptions: { region: "eu-west-1", profile: "development" }, + isConfigured: true, + }) + }) }) describe("isFeatureEnabled", () => { diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index dc6f72d34a..62b191a44c 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -117,7 +117,7 @@ export class CodeIndexConfigManager { this.embedderProvider = providerIdentifiers.mistral } else if (codebaseIndexEmbedderProvider === providerIdentifiers.vercelAiGateway) { this.embedderProvider = providerIdentifiers.vercelAiGateway - } else if ((codebaseIndexEmbedderProvider as string) === "bedrock") { + } else if ((codebaseIndexEmbedderProvider as string) === providerIdentifiers.bedrock) { this.embedderProvider = providerIdentifiers.bedrock } else if (codebaseIndexEmbedderProvider === providerIdentifiers.openrouter) { this.embedderProvider = providerIdentifiers.openrouter diff --git a/src/shared/api.ts b/src/shared/api.ts index 1787e15e88..d1cc21cad0 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -6,6 +6,7 @@ import { ANTHROPIC_DEFAULT_MAX_TOKENS, isDynamicProvider, isLocalProvider, + providerIdentifiers, } from "@roo-code/types" // ApiHandlerOptions @@ -176,21 +177,21 @@ type CommonFetchParams = { // If a new dynamic provider is added in packages/types, this will fail to compile // until a corresponding entry is added here. const dynamicProviderExtras = { - openrouter: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type - "vercel-ai-gateway": {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type - "zoo-gateway": {} as { apiKey?: string; baseUrl?: string }, - litellm: {} as { apiKey?: string; baseUrl: string }, - requesty: {} as { apiKey?: string; baseUrl?: string }, - unbound: {} as { apiKey?: string }, - ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type - lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type - poe: {} as { apiKey?: string; baseUrl?: string }, - deepseek: {} as { apiKey?: string; baseUrl?: string }, - moonshot: {} as { apiKey?: string; baseUrl?: string }, - "opencode-go": {} as { apiKey?: string }, - kenari: {} as { apiKey?: string }, - nanogpt: {} as { apiKey?: string }, - "kimi-code": {} as { apiKey?: string }, + [providerIdentifiers.openrouter]: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type + [providerIdentifiers.vercelAiGateway]: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type + [providerIdentifiers.zooGateway]: {} as { apiKey?: string; baseUrl?: string }, + [providerIdentifiers.litellm]: {} as { apiKey?: string; baseUrl: string }, + [providerIdentifiers.requesty]: {} as { apiKey?: string; baseUrl?: string }, + [providerIdentifiers.unbound]: {} as { apiKey?: string }, + [providerIdentifiers.ollama]: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type + [providerIdentifiers.lmstudio]: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type + [providerIdentifiers.poe]: {} as { apiKey?: string; baseUrl?: string }, + [providerIdentifiers.deepseek]: {} as { apiKey?: string; baseUrl?: string }, + [providerIdentifiers.moonshot]: {} as { apiKey?: string; baseUrl?: string }, + [providerIdentifiers.opencodeGo]: {} as { apiKey?: string }, + [providerIdentifiers.kenari]: {} as { apiKey?: string }, + [providerIdentifiers.nanogpt]: {} as { apiKey?: string }, + [providerIdentifiers.kimiCode]: {} as { apiKey?: string }, } as const satisfies Record // Build the dynamic options union from the map, intersected with CommonFetchParams diff --git a/webview-ui/eslint.config.mjs b/webview-ui/eslint.config.mjs index db76f49211..11a49e9b41 100644 --- a/webview-ui/eslint.config.mjs +++ b/webview-ui/eslint.config.mjs @@ -1,8 +1,11 @@ import { reactConfig } from "@roo-code/config-eslint/react" +import { createProviderIdentifierConfig } from "@roo-code/config-eslint/provider-identifiers" +import { providerIdentifiers, retiredProviderIdentifiers } from "@roo-code/types/provider-identifiers" /** @type {import("eslint").Linter.Config} */ export default [ ...reactConfig, + createProviderIdentifierConfig({ providerIdentifiers, retiredProviderIdentifiers }), { rules: { "@typescript-eslint/no-unused-vars": [ diff --git a/webview-ui/playwright/AppProviders.tsx b/webview-ui/playwright/AppProviders.tsx index a11b1e3977..f8a274cd9f 100644 --- a/webview-ui/playwright/AppProviders.tsx +++ b/webview-ui/playwright/AppProviders.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { I18nextProvider } from "react-i18next" +import { providerIdentifiers } from "@roo-code/types" import { ExtensionStateContextProvider } from "@/context/ExtensionStateContext" import { TranslationProvider } from "@/i18n/TranslationContext" @@ -23,7 +24,7 @@ const defaultInitialState: InitialState = { taskHistory: [], shouldShowAnnouncement: false, telemetrySetting: "enabled", - apiConfiguration: { apiProvider: "anthropic" }, + apiConfiguration: { apiProvider: providerIdentifiers.anthropic }, currentApiConfigName: "Default", listApiConfigMeta: [{ id: "default", name: "Default", modelId: "claude-sonnet" }], pinnedApiConfigs: {}, diff --git a/webview-ui/playwright/ExtensionStateContext.tsx b/webview-ui/playwright/ExtensionStateContext.tsx index 3aadd4bd8d..597c28cac5 100644 --- a/webview-ui/playwright/ExtensionStateContext.tsx +++ b/webview-ui/playwright/ExtensionStateContext.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext } from "react" +import { providerIdentifiers } from "@roo-code/types" const noop = () => undefined @@ -14,7 +15,7 @@ const defaultState = { currentApiConfigName: "Default", listApiConfigMeta: [{ id: "default", name: "Default", modelId: "claude-sonnet" }], pinnedApiConfigs: {}, - apiConfiguration: { apiProvider: "anthropic" }, + apiConfiguration: { apiProvider: providerIdentifiers.anthropic }, enterBehavior: "send", lockApiConfigAcrossModes: false, telemetrySetting: "enabled", diff --git a/webview-ui/playwright/roo-code-types.ts b/webview-ui/playwright/roo-code-types.ts index d07b4aaddc..5d0c0bbf15 100644 --- a/webview-ui/playwright/roo-code-types.ts +++ b/webview-ui/playwright/roo-code-types.ts @@ -15,6 +15,8 @@ export const CODEBASE_INDEX_DEFAULTS = { SEARCH_SCORE_STEP: 0.05, } as const +export { providerIdentifiers } from "../../packages/types/src/provider-identifiers" + export const TelemetryEventName = { MODE_SWITCH: "Mode Switched", MODE_SELECTOR_OPENED: "Mode Selector Opened", diff --git a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx index 3236510911..f5c2ba1062 100644 --- a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx +++ b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx @@ -1,6 +1,7 @@ // npm run test ContextWindowProgress.spec.tsx import { render, screen, fireEvent } from "@/utils/test-utils" +import { providerIdentifiers } from "@roo-code/types" import TaskHeader from "@src/components/chat/TaskHeader" @@ -17,7 +18,7 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ // Mock ExtensionStateContext since we use useExtensionState vi.mock("@src/context/ExtensionStateContext", () => ({ useExtensionState: vi.fn(() => ({ - apiConfiguration: { apiProvider: "openai" }, + apiConfiguration: { apiProvider: providerIdentifiers.openai }, currentTaskItem: { id: "test-id", number: 1, size: 1024 }, })), })) diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 5742be032c..d451836d1f 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -12,7 +12,12 @@ import { import * as ProgressPrimitive from "@radix-ui/react-progress" import { TriangleAlert } from "lucide-react" -import { type IndexingStatus, type EmbedderProvider, CODEBASE_INDEX_DEFAULTS } from "@roo-code/types" +import { + type IndexingStatus, + type EmbedderProvider, + CODEBASE_INDEX_DEFAULTS, + providerIdentifiers, +} from "@roo-code/types" import { vscode } from "@src/utils/vscode" import { useExtensionState } from "@src/context/ExtensionStateContext" @@ -83,8 +88,10 @@ interface LocalCodeIndexSettings { codebaseIndexOpenRouterSpecificProvider?: string } +type TranslationCallback = (key: string) => string + // Validation schema for codebase index settings -const createValidationSchema = (provider: EmbedderProvider, t: any) => { +export const createValidationSchema = (provider: EmbedderProvider, t: TranslationCallback) => { const baseSchema = z.object({ codebaseIndexEnabled: z.boolean(), codebaseIndexQdrantUrl: z @@ -95,7 +102,7 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { }) switch (provider) { - case "openai": + case providerIdentifiers.openai: return baseSchema.extend({ codeIndexOpenAiKey: z.string().min(1, t("settings:codeIndex.validation.openaiApiKeyRequired")), codebaseIndexEmbedderModelId: z @@ -103,7 +110,7 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) - case "ollama": + case providerIdentifiers.ollama: return baseSchema.extend({ codebaseIndexEmbedderBaseUrl: z .string() @@ -131,7 +138,7 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelDimensionRequired")), }) - case "gemini": + case providerIdentifiers.gemini: return baseSchema.extend({ codebaseIndexGeminiApiKey: z.string().min(1, t("settings:codeIndex.validation.geminiApiKeyRequired")), codebaseIndexEmbedderModelId: z @@ -139,7 +146,7 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) - case "mistral": + case providerIdentifiers.mistral: return baseSchema.extend({ codebaseIndexMistralApiKey: z.string().min(1, t("settings:codeIndex.validation.mistralApiKeyRequired")), codebaseIndexEmbedderModelId: z @@ -147,7 +154,7 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) - case "vercel-ai-gateway": + case providerIdentifiers.vercelAiGateway: return baseSchema.extend({ codebaseIndexVercelAiGatewayApiKey: z .string() @@ -157,7 +164,7 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) - case "bedrock": + case providerIdentifiers.bedrock: return baseSchema.extend({ codebaseIndexBedrockRegion: z.string().min(1, t("settings:codeIndex.validation.bedrockRegionRequired")), codebaseIndexBedrockProfile: z.string().optional(), @@ -166,7 +173,7 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) - case "openrouter": + case providerIdentifiers.openrouter: return baseSchema.extend({ codebaseIndexOpenRouterApiKey: z .string() @@ -218,7 +225,7 @@ export const CodeIndexPopover: React.FC = ({ const getDefaultSettings = (): LocalCodeIndexSettings => ({ codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "", - codebaseIndexEmbedderProvider: "openai", + codebaseIndexEmbedderProvider: providerIdentifiers.openai, codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "", codebaseIndexEmbedderModelDimension: undefined, @@ -593,13 +600,13 @@ export const CodeIndexPopover: React.FC = ({ // Fetch OpenRouter model providers for embedding model const { data: openRouterEmbeddingProviders } = useOpenRouterModelProviders( - currentSettings.codebaseIndexEmbedderProvider === "openrouter" + currentSettings.codebaseIndexEmbedderProvider === providerIdentifiers.openrouter ? currentSettings.codebaseIndexEmbedderModelId : undefined, undefined, { enabled: - currentSettings.codebaseIndexEmbedderProvider === "openrouter" && + currentSettings.codebaseIndexEmbedderProvider === providerIdentifiers.openrouter && !!currentSettings.codebaseIndexEmbedderModelId, }, ) @@ -720,7 +727,7 @@ export const CodeIndexPopover: React.FC = ({ // if the main API provider is also configured for Bedrock if ( value === "bedrock" && - apiConfiguration?.apiProvider === "bedrock" + apiConfiguration?.apiProvider === providerIdentifiers.bedrock ) { // Only populate if currently empty if ( @@ -781,7 +788,7 @@ export const CodeIndexPopover: React.FC = ({ {/* Provider-specific settings */} - {currentSettings.codebaseIndexEmbedderProvider === "openai" && ( + {currentSettings.codebaseIndexEmbedderProvider === providerIdentifiers.openai && ( <>