Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
})
Expand Down Expand Up @@ -262,6 +263,7 @@ export const PROVIDER_SETTINGS_KEYS = keysOf<ProviderSettings>()([
// Anthropic
"apiModelId",
"apiKey",
"anthropicApiKeyUseEnvVar",
"anthropicBaseUrl",
"anthropicUseAuthToken",
// Glama
Expand Down
33 changes: 33 additions & 0 deletions scripts/find-missing-translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
* --locale=<locale> Only check a specific locale (e.g. --locale=fr)
* --file=<file> Only check a specific file (e.g. --file=chat.json)
* --area=<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(
Expand All @@ -30,6 +33,8 @@
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
},
Expand All @@ -54,6 +59,7 @@
'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:
Expand Down Expand Up @@ -105,6 +111,27 @@
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

Check warning

Code scanning / CodeQL

Prototype-polluting function Medium

The property chain
here
is recursively assigned to
current
without guarding against prototype pollution.
} 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]
Expand Down Expand Up @@ -198,11 +225,17 @@
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"))
}
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,33 @@ export interface ApiHandler {
countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number>
}


/**
* 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)
Expand All @@ -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)
}
}
18 changes: 16 additions & 2 deletions webview-ui/src/components/settings/providers/Anthropic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<K extends keyof ProviderSettings, E>(
Expand All @@ -30,20 +31,33 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro
[setApiConfigurationField],
)

const handleUseApiKeyEnvVarChange = (checked: boolean) => {
setUseApiKeyEnvVar(checked)
setApiConfigurationField("anthropicApiKeyUseEnvVar", checked)
}

return (
<>
<VSCodeTextField
value={apiConfiguration?.apiKey || ""}
type="password"
onInput={handleInputChange("apiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
className="w-full"
disabled={useApiKeyEnvVar}
>
<label className="block font-medium mb-1">{t("settings:providers.anthropicApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.apiKey && (
<Checkbox
checked={useApiKeyEnvVar}
onChange={handleUseApiKeyEnvVarChange}
className="mb-2">
{t("settings:providers.apiKeyUseEnvVar", { name: "ANTHROPIC_API_KEY"})}
</Checkbox>
{(!(apiConfiguration?.apiKey || apiConfiguration?.anthropicApiKeyUseEnvVar)) && (
<VSCodeButtonLink href="https://console.anthropic.com/settings/keys" appearance="secondary">
{t("settings:providers.getAnthropicApiKey")}
</VSCodeButtonLink>
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/utils/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down