diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 7076361ea5..8e6fe029af 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -73,6 +73,7 @@ const apiModelIdProviderModelSchema = baseProviderSettingsSchema.extend({ const anthropicSchema = apiModelIdProviderModelSchema.extend({ apiKey: z.string().optional(), + anthropicApiKeyUseEnvVar: z.boolean().optional(), anthropicBaseUrl: z.string().optional(), anthropicUseAuthToken: z.boolean().optional(), }) @@ -262,6 +263,7 @@ export const PROVIDER_SETTINGS_KEYS = keysOf()([ // Anthropic "apiModelId", "apiKey", + "anthropicApiKeyUseEnvVar", "anthropicBaseUrl", "anthropicUseAuthToken", // Glama diff --git a/scripts/find-missing-translations.js b/scripts/find-missing-translations.js index 9277d935ba..f953f71a96 100755 --- a/scripts/find-missing-translations.js +++ b/scripts/find-missing-translations.js @@ -8,11 +8,14 @@ * --locale= Only check a specific locale (e.g. --locale=fr) * --file= Only check a specific file (e.g. --file=chat.json) * --area= Only check a specific area (core, webview, or both) + * --create-missing Creates missing keys in the other locales with the EN value * --help Show this help message */ +const { set } = require("@dotenvx/dotenvx") const fs = require("fs") const path = require("path") +let createMissing = false // Process command line arguments const args = process.argv.slice(2).reduce( @@ -30,6 +33,8 @@ const args = process.argv.slice(2).reduce( console.error(`Error: Invalid area '${acc.area}'. Must be 'core', 'webview', or 'both'.`) process.exit(1) } + } else if (arg.startsWith("--create-missing")) { + createMissing = true } return acc }, @@ -54,6 +59,7 @@ Options: 'core' = Backend (src/i18n/locales) 'webview' = Frontend UI (webview-ui/src/i18n/locales) 'both' = Check both areas (default) + --create-missing Creates missing keys in the other locales with the EN value --help Show this help message Output: @@ -105,6 +111,27 @@ function getValueAtPath(obj, path) { return current } +// Set value at a dotted path in an object +function setValueAtPath(obj, path, value) { + const parts = path.split(".") + let current = obj + + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + + // If it's the last part, set the value + if (i === parts.length - 1) { + current[part] = value + } else { + // If the key doesn't exist or isn't an object, create an empty object + if (current[part] === undefined || typeof current[part] !== "object") { + current[part] = {} + } + current = current[part] + } + } +} + // Function to check translations for a specific area function checkAreaTranslations(area) { const LOCALES_DIR = LOCALES_DIRS[area] @@ -198,11 +225,17 @@ function checkAreaTranslations(area) { key, englishValue, }) + if (createMissing === true) { + setValueAtPath(localeContent, key, englishValue) // Set the missing key in the locale content + } } } if (missingKeys.length > 0) { missingTranslations[locale][name] = missingKeys + if (createMissing === true) { + fs.writeFileSync(localeFilePath, JSON.stringify(localeContent, null, "\t")) + } } } } diff --git a/src/api/index.ts b/src/api/index.ts index 8b09bf4cf9..96ecfa1d08 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -58,15 +58,33 @@ export interface ApiHandler { countTokens(content: Array): Promise } + +/** + * Read an environment variable value, returning a default value if not set. If neither key nor default is set, + * returns undefined + * @param key The environment variable key + * @param defaultValue The default value to return if the variable is not set + * @returns The value of the environment variable or the default value + */ +export function getEnvVar(key: string | undefined, defaultValue?: string | undefined): string | undefined { + if (key === undefined) { + return defaultValue + } + return process.env[key as string] ?? defaultValue +} + export function buildApiHandler(configuration: ProviderSettings): ApiHandler { const { apiProvider, ...options } = configuration switch (apiProvider) { case "anthropic": + options.apiKey = getEnvVar("ANTHROPIC_API_KEY", options.apiKey) return new AnthropicHandler(options) case "glama": + options.glamaApiKey = getEnvVar("GLAMA_API_KEY", options.glamaApiKey) return new GlamaHandler(options) case "openrouter": + options.openRouterApiKey = getEnvVar("OPEN_ROUTER_API_KEY", options.openRouterApiKey) return new OpenRouterHandler(options) case "bedrock": return new AwsBedrockHandler(options) @@ -75,38 +93,53 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { ? new AnthropicVertexHandler(options) : new VertexHandler(options) case "openai": + options.openAiApiKey = getEnvVar("OPEN_AI_API_KEY", options.openAiApiKey) return new OpenAiHandler(options) case "ollama": return new OllamaHandler(options) case "lmstudio": return new LmStudioHandler(options) case "gemini": + options.geminiApiKey = getEnvVar("GEMINI_API_KEY", options.geminiApiKey) return new GeminiHandler(options) case "openai-native": + options.openAiNativeApiKey = getEnvVar( + "OPEN_AI_NATIVE_API_KEY", + options.openAiNativeApiKey + ) return new OpenAiNativeHandler(options) case "deepseek": + options.deepSeekApiKey = getEnvVar("DEEP_SEEK_API_KEY", options.deepSeekApiKey) return new DeepSeekHandler(options) case "vscode-lm": return new VsCodeLmHandler(options) case "mistral": + options.mistralApiKey = getEnvVar("MISTRAL_API_KEY", options.mistralApiKey) return new MistralHandler(options) case "unbound": + options.unboundApiKey = getEnvVar("UNBOUND_API_KEY", options.unboundApiKey) return new UnboundHandler(options) case "requesty": + options.requestyApiKey = getEnvVar("REQUESTY_API_KEY", options.requestyApiKey) return new RequestyHandler(options) case "human-relay": return new HumanRelayHandler() case "fake-ai": return new FakeAIHandler(options) case "xai": + options.xaiApiKey = getEnvVar("XAI_API_KEY", options.xaiApiKey) return new XAIHandler(options) case "groq": + options.groqApiKey = getEnvVar("GROQ_API_KEY", options.groqApiKey) return new GroqHandler(options) case "chutes": + options.chutesApiKey = getEnvVar("CHUTES_API_KEY", options.chutesApiKey) return new ChutesHandler(options) case "litellm": + options.litellmApiKey = getEnvVar("LITELLM_API_KEY", options.litellmApiKey) return new LiteLLMHandler(options) default: + options.apiKey = getEnvVar("ANTROPIC_API_KEY", options.apiKey) return new AnthropicHandler(options) } } diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx index f340e73f72..c1ad9b9e96 100644 --- a/webview-ui/src/components/settings/providers/Anthropic.tsx +++ b/webview-ui/src/components/settings/providers/Anthropic.tsx @@ -18,6 +18,7 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro const { t } = useAppTranslation() const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) + const [useApiKeyEnvVar, setUseApiKeyEnvVar] = useState(!!apiConfiguration?.anthropicApiKeyUseEnvVar) const handleInputChange = useCallback( ( @@ -30,6 +31,11 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro [setApiConfigurationField], ) + const handleUseApiKeyEnvVarChange = (checked: boolean) => { + setUseApiKeyEnvVar(checked) + setApiConfigurationField("anthropicApiKeyUseEnvVar", checked) + } + return ( <> + className="w-full" + disabled={useApiKeyEnvVar} + >
{t("settings:providers.apiKeyStorageNotice")}
- {!apiConfiguration?.apiKey && ( + + {t("settings:providers.apiKeyUseEnvVar", { name: "ANTHROPIC_API_KEY"})} + + {(!(apiConfiguration?.apiKey || apiConfiguration?.anthropicApiKeyUseEnvVar)) && ( {t("settings:providers.getAnthropicApiKey")} diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 69b7590c0f..8e973c745d 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -32,7 +32,7 @@ export function validateApiConfiguration(apiConfiguration: ProviderSettings): st } break case "anthropic": - if (!apiConfiguration.apiKey) { + if (!(apiConfiguration.apiKey || apiConfiguration.anthropicApiKeyUseEnvVar)) { return i18next.t("settings:validation.apiKey") } break