From f36c9abb4a3f5381d8d86eb135d9868de255c19f Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Fri, 28 Aug 2026 12:32:43 +0300 Subject: [PATCH 01/13] refactor(eslint): move provider identifier rule to shared config --- .../config-eslint/provider-identifiers.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename src/eslint-rules/no-raw-provider-identifiers.mjs => packages/config-eslint/provider-identifiers.js (96%) diff --git a/src/eslint-rules/no-raw-provider-identifiers.mjs b/packages/config-eslint/provider-identifiers.js similarity index 96% rename from src/eslint-rules/no-raw-provider-identifiers.mjs rename to packages/config-eslint/provider-identifiers.js index 7520cc6414..2078769b6a 100644 --- a/src/eslint-rules/no-raw-provider-identifiers.mjs +++ b/packages/config-eslint/provider-identifiers.js @@ -65,8 +65,7 @@ export const noRawProviderIdentifiers = { docs: { description: "Require canonical provider identifiers in provider-like contexts" }, schema: [], messages: { - useCanonical: - 'Use {{replacement}} instead of the raw provider identifier "{{value}}".', + useCanonical: 'Use {{replacement}} instead of the raw provider identifier "{{value}}".', }, }, create(context) { From 7f7287062a643ee1b82254cef332385e3a166da4 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Fri, 28 Aug 2026 12:33:05 +0300 Subject: [PATCH 02/13] refactor(eslint): share provider identifier config across packages --- packages/config-eslint/package.json | 6 +- .../config-eslint/provider-identifiers.js | 197 +++++++++++------- .../provider-identifiers.test.js | 42 ++++ packages/types/eslint.config.mjs | 3 + src/eslint.config.mjs | 21 +- webview-ui/eslint.config.mjs | 3 + 6 files changed, 181 insertions(+), 91 deletions(-) create mode 100644 packages/config-eslint/provider-identifiers.test.js 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 index 2078769b6a..6f22941cdd 100644 --- a/packages/config-eslint/provider-identifiers.js +++ b/packages/config-eslint/provider-identifiers.js @@ -1,12 +1,3 @@ -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", @@ -40,82 +31,144 @@ function isProviderLike(node) { return getStaticName(node)?.toLowerCase().includes("provider") ?? false } -function getRawProvider(node) { - while (typescriptExpressionWrappers.has(node?.type)) { - node = node.expression - } +function isCanonicalProviderRegistry(node) { + const name = getStaticName(node) + return name === "providerIdentifiers" || name === "retiredProviderIdentifiers" +} - if (node?.type === "Literal" && typeof node.value === "string") { - const replacement = providerReplacementsByValue.get(node.value) - return replacement ? { replacement, value: node.value } : undefined - } +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}`, + ]), + ]) - 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 - } + function getRawProvider(node) { + while (typescriptExpressionWrappers.has(node?.type)) { + node = node.expression + } - return undefined -} + if (node?.type === "Literal" && typeof node.value === "string") { + const replacement = providerReplacementsByValue.get(node.value) + return replacement ? { replacement, value: node.value } : 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 }) - } + 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 { - 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) - } + 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: { + useCanonical: 'Use {{replacement}} instead of the raw provider identifier "{{value}}".', }, - AssignmentExpression(node) { - if (isProviderLike(node.left)) { - reportIfRawProvider(node.right) + }, + create(context) { + function reportIfRawProvider(node) { + const provider = getRawProvider(node) + if (provider) { + context.report({ node, messageId: "useCanonical", data: provider }) } - }, - BinaryExpression(node) { - if (!["===", "!==", "==", "!="].includes(node.operator)) { + } + + function reportIfRawProviderMapKey(node) { + if (node.computed || node.parent?.type !== "ObjectExpression") { return } - if (isProviderLike(node.left)) { - reportIfRawProvider(node.right) + const declarator = getProviderMapDeclarator(node.parent) + if (!declarator || !isProviderLike(declarator.id) || isCanonicalProviderRegistry(declarator.id)) { + return } - if (isProviderLike(node.right)) { - reportIfRawProvider(node.left) + + const value = getStaticName(node.key) + const replacement = value ? providerReplacementsByValue.get(value) : undefined + if (replacement) { + context.report({ + node: node.key, + messageId: "useCanonical", + data: { replacement, value }, + }) } - }, - SwitchStatement(node) { - if (isProviderLike(node.discriminant)) { - for (const switchCase of node.cases) { - reportIfRawProvider(switchCase.test) + } + + 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) + } + }, + 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) + } + } + }, + } + }, + } + + 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..7b59ec0d5a --- /dev/null +++ b/packages/config-eslint/provider-identifiers.test.js @@ -0,0 +1,42 @@ +import { RuleTester } from "eslint" + +import { createProviderIdentifierConfig } from "./provider-identifiers.js" + +const config = createProviderIdentifierConfig({ + providerIdentifiers: { + openrouter: "openrouter", + vercelAiGateway: "vercel-ai-gateway", + }, + retiredProviderIdentifiers: {}, +}) +const rule = config.plugins.zoo.rules["no-raw-provider-identifiers"] + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, +}) + +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" }, + }, + ], + }, + ], +}) 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/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/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": [ From 3255a2812a36c441e7e928c4a13d44583dbfd4c9 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Fri, 28 Aug 2026 12:42:10 +0300 Subject: [PATCH 03/13] fix(eslint): migrate provider literals and shared rule tests --- .../provider-identifiers.test.js | 64 +++++++- .../types/src/__tests__/kimi-code.test.ts | 7 +- .../src/__tests__/provider-model-id.test.ts | 3 +- .../types/src/__tests__/telemetry.test.ts | 3 +- packages/types/src/image-generation.ts | 30 +++- pnpm-lock.yaml | 4 +- .../no-raw-provider-identifiers.spec.mjs | 137 ------------------ .../no-raw-provider-identifiers.test.mjs | 39 ----- src/package.json | 1 - src/shared/api.ts | 31 ++-- .../__tests__/ContextWindowProgress.spec.tsx | 3 +- .../src/components/chat/CodeIndexPopover.tsx | 45 +++--- .../chat/__tests__/ApiConfigSelector.spec.tsx | 3 +- .../ChatTextArea.lockApiConfig.spec.tsx | 3 +- .../chat/__tests__/ChatTextArea.spec.tsx | 25 ++-- .../CodeIndexPopover.auto-populate.spec.tsx | 17 ++- .../chat/__tests__/TaskHeader.spec.tsx | 5 +- .../settings/ImageGenerationSettings.tsx | 5 +- .../ApiOptions.provider-filtering.spec.tsx | 5 +- .../settings/__tests__/ApiOptions.spec.tsx | 37 ++--- .../__tests__/ModelPicker.deprecated.spec.tsx | 11 +- .../SettingsView.change-detection.spec.tsx | 27 ++-- .../SettingsView.unsaved-changes.spec.tsx | 15 +- .../__tests__/ThinkingBudget.spec.tsx | 11 +- .../providers/__tests__/Friendli.spec.tsx | 9 +- .../OpenAICompatible.visual.fixture.tsx | 3 +- .../providers/__tests__/OpenCodeGo.spec.tsx | 11 +- .../providers/__tests__/ZooGateway.spec.tsx | 11 +- .../__tests__/providerModelConfig.spec.ts | 18 +-- .../welcome/WelcomeViewProvider.tsx | 4 +- .../__tests__/WelcomeViewProvider.spec.tsx | 9 +- .../src/context/ExtensionStateContext.tsx | 3 +- .../__tests__/ExtensionStateContext.spec.tsx | 13 +- .../src/utils/__tests__/validate.spec.ts | 56 +++---- 34 files changed, 303 insertions(+), 365 deletions(-) delete mode 100644 src/__tests__/no-raw-provider-identifiers.spec.mjs delete mode 100644 src/eslint-rules/no-raw-provider-identifiers.test.mjs diff --git a/packages/config-eslint/provider-identifiers.test.js b/packages/config-eslint/provider-identifiers.test.js index 7b59ec0d5a..ecd1713be1 100644 --- a/packages/config-eslint/provider-identifiers.test.js +++ b/packages/config-eslint/provider-identifiers.test.js @@ -4,10 +4,16 @@ import { createProviderIdentifierConfig } from "./provider-identifiers.js" const config = createProviderIdentifierConfig({ providerIdentifiers: { + anthropic: "anthropic", + openaiNative: "openai-native", openrouter: "openrouter", + poe: "poe", vercelAiGateway: "vercel-ai-gateway", }, - retiredProviderIdentifiers: {}, + retiredProviderIdentifiers: { + groq: "groq", + roo: "roo", + }, }) const rule = config.plugins.zoo.rules["no-raw-provider-identifiers"] @@ -40,3 +46,59 @@ ruleTester.run("no-raw-provider-identifiers provider map keys", rule, { }, ], }) + +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 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: '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" } }], + }, + ], +}) diff --git a/packages/types/src/__tests__/kimi-code.test.ts b/packages/types/src/__tests__/kimi-code.test.ts index 3d8b9ac549..1a59ed3e12 100644 --- a/packages/types/src/__tests__/kimi-code.test.ts +++ b/packages/types/src/__tests__/kimi-code.test.ts @@ -1,3 +1,4 @@ +import { providerIdentifiers } from "../provider-identifiers.js" import { SECRET_STATE_KEYS, dynamicProviders, @@ -16,13 +17,15 @@ 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({ + expect( + providerSettingsSchema.parse({ apiProvider: providerIdentifiers.kimiCode, kimiCodeAuthMethod: "oauth" }), + ).toMatchObject({ kimiCodeAuthMethod: "oauth", }) }) 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..c7d946de7f 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, + }, ] /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a666502f4e..cee51dd474 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -671,9 +671,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) @@ -3479,6 +3476,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/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/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/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/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..732280532f 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" @@ -95,7 +100,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 +108,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 +136,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 +144,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 +152,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 +162,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 +171,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 +223,7 @@ export const CodeIndexPopover: React.FC = ({ const getDefaultSettings = (): LocalCodeIndexSettings => ({ codebaseIndexEnabled: true, codebaseIndexQdrantUrl: "", - codebaseIndexEmbedderProvider: "openai", + codebaseIndexEmbedderProvider: providerIdentifiers.openai, codebaseIndexEmbedderBaseUrl: "", codebaseIndexEmbedderModelId: "", codebaseIndexEmbedderModelDimension: undefined, @@ -593,13 +598,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 +725,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 +786,7 @@ export const CodeIndexPopover: React.FC = ({ {/* Provider-specific settings */} - {currentSettings.codebaseIndexEmbedderProvider === "openai" && ( + {currentSettings.codebaseIndexEmbedderProvider === providerIdentifiers.openai && ( <>