Просмотр исходного кода

Customizable headers for the OpenAI-compatible provider (#3056)

* Customizable headers for the OpenAI-compatible provider

* PR feedback

* Fix migration

* Update webview-ui/src/components/settings/ApiOptions.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Matt Rubens 8 месяцев назад
Родитель
Сommit
a356d70669

+ 13 - 15
src/api/providers/openai.ts

@@ -35,12 +35,17 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 		const urlHost = this._getUrlHost(this.options.openAiBaseUrl)
 		const isAzureOpenAi = urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure
 
+		const headers = {
+			...DEFAULT_HEADERS,
+			...(this.options.openAiHeaders || {}),
+		}
+
 		if (isAzureAiInference) {
 			// Azure AI Inference Service (e.g., for DeepSeek) uses a different path structure
 			this.client = new OpenAI({
 				baseURL,
 				apiKey,
-				defaultHeaders: DEFAULT_HEADERS,
+				defaultHeaders: headers,
 				defaultQuery: { "api-version": this.options.azureApiVersion || "2024-05-01-preview" },
 			})
 		} else if (isAzureOpenAi) {
@@ -50,19 +55,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 				baseURL,
 				apiKey,
 				apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
-				defaultHeaders: {
-					...DEFAULT_HEADERS,
-					...(this.options.openAiHostHeader ? { Host: this.options.openAiHostHeader } : {}),
-				},
+				defaultHeaders: headers,
 			})
 		} else {
 			this.client = new OpenAI({
 				baseURL,
 				apiKey,
-				defaultHeaders: {
-					...DEFAULT_HEADERS,
-					...(this.options.openAiHostHeader ? { Host: this.options.openAiHostHeader } : {}),
-				},
+				defaultHeaders: headers,
 			})
 		}
 	}
@@ -361,7 +360,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 	}
 }
 
-export async function getOpenAiModels(baseUrl?: string, apiKey?: string, hostHeader?: string) {
+export async function getOpenAiModels(baseUrl?: string, apiKey?: string, openAiHeaders?: Record<string, string>) {
 	try {
 		if (!baseUrl) {
 			return []
@@ -372,16 +371,15 @@ export async function getOpenAiModels(baseUrl?: string, apiKey?: string, hostHea
 		}
 
 		const config: Record<string, any> = {}
-		const headers: Record<string, string> = {}
+		const headers: Record<string, string> = {
+			...DEFAULT_HEADERS,
+			...(openAiHeaders || {}),
+		}
 
 		if (apiKey) {
 			headers["Authorization"] = `Bearer ${apiKey}`
 		}
 
-		if (hostHeader) {
-			headers["Host"] = hostHeader
-		}
-
 		if (Object.keys(headers).length > 0) {
 			config["headers"] = headers
 		}

+ 10 - 0
src/core/config/ContextProxy.ts

@@ -192,6 +192,16 @@ export class ContextProxy {
 		// If a value is not present in the new configuration, then it is assumed
 		// that the setting's value should be `undefined` and therefore we
 		// need to remove it from the state cache if it exists.
+
+		// Ensure openAiHeaders is always an object even when empty
+		// This is critical for proper serialization/deserialization through IPC
+		if (values.openAiHeaders !== undefined) {
+			// Check if it's empty or null
+			if (!values.openAiHeaders || Object.keys(values.openAiHeaders).length === 0) {
+				values.openAiHeaders = {}
+			}
+		}
+
 		await this.setValues({
 			...PROVIDER_SETTINGS_KEYS.filter((key) => !isSecretStateKey(key))
 				.filter((key) => !!this.stateCache[key])

+ 33 - 0
src/core/config/ProviderSettingsManager.ts

@@ -17,6 +17,7 @@ export const providerProfilesSchema = z.object({
 		.object({
 			rateLimitSecondsMigrated: z.boolean().optional(),
 			diffSettingsMigrated: z.boolean().optional(),
+			openAiHeadersMigrated: z.boolean().optional(),
 		})
 		.optional(),
 })
@@ -38,6 +39,7 @@ export class ProviderSettingsManager {
 		migrations: {
 			rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
 			diffSettingsMigrated: true, // Mark as migrated on fresh installs
+			openAiHeadersMigrated: true, // Mark as migrated on fresh installs
 		},
 	}
 
@@ -90,6 +92,7 @@ export class ProviderSettingsManager {
 					providerProfiles.migrations = {
 						rateLimitSecondsMigrated: false,
 						diffSettingsMigrated: false,
+						openAiHeadersMigrated: false,
 					} // Initialize with default values
 					isDirty = true
 				}
@@ -106,6 +109,12 @@ export class ProviderSettingsManager {
 					isDirty = true
 				}
 
+				if (!providerProfiles.migrations.openAiHeadersMigrated) {
+					await this.migrateOpenAiHeaders(providerProfiles)
+					providerProfiles.migrations.openAiHeadersMigrated = true
+					isDirty = true
+				}
+
 				if (isDirty) {
 					await this.store(providerProfiles)
 				}
@@ -175,6 +184,30 @@ export class ProviderSettingsManager {
 		}
 	}
 
+	private async migrateOpenAiHeaders(providerProfiles: ProviderProfiles) {
+		try {
+			for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
+				// Use type assertion to access the deprecated property safely
+				const configAny = apiConfig as any
+
+				// Check if openAiHostHeader exists but openAiHeaders doesn't
+				if (
+					configAny.openAiHostHeader &&
+					(!apiConfig.openAiHeaders || Object.keys(apiConfig.openAiHeaders || {}).length === 0)
+				) {
+					// Create the headers object with the Host value
+					apiConfig.openAiHeaders = { Host: configAny.openAiHostHeader }
+
+					// Delete the old property to prevent re-migration
+					// This prevents the header from reappearing after deletion
+					configAny.openAiHostHeader = undefined
+				}
+			}
+		} catch (error) {
+			console.error(`[MigrateOpenAiHeaders] Failed to migrate OpenAI headers:`, error)
+		}
+	}
+
 	/**
 	 * List all available configs with metadata.
 	 */

+ 1 - 0
src/core/config/__tests__/ProviderSettingsManager.test.ts

@@ -56,6 +56,7 @@ describe("ProviderSettingsManager", () => {
 					migrations: {
 						rateLimitSecondsMigrated: true,
 						diffSettingsMigrated: true,
+						openAiHeadersMigrated: true,
 					},
 				}),
 			)

+ 1 - 1
src/core/webview/webviewMessageHandler.ts

@@ -310,7 +310,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 				const openAiModels = await getOpenAiModels(
 					message?.values?.baseUrl,
 					message?.values?.apiKey,
-					message?.values?.hostHeader,
+					message?.values?.openAiHeaders,
 				)
 
 				provider.postMessageToWebview({ type: "openAiModels", openAiModels })

+ 6 - 1
src/exports/roo-code.d.ts

@@ -50,7 +50,6 @@ type ProviderSettings = {
 	vertexRegion?: string | undefined
 	openAiBaseUrl?: string | undefined
 	openAiApiKey?: string | undefined
-	openAiHostHeader?: string | undefined
 	openAiLegacyFormat?: boolean | undefined
 	openAiR1FormatEnabled?: boolean | undefined
 	openAiModelId?: string | undefined
@@ -88,6 +87,12 @@ type ProviderSettings = {
 	azureApiVersion?: string | undefined
 	openAiStreamingEnabled?: boolean | undefined
 	enableReasoningEffort?: boolean | undefined
+	openAiHostHeader?: string | undefined
+	openAiHeaders?:
+		| {
+				[x: string]: string
+		  }
+		| undefined
 	ollamaModelId?: string | undefined
 	ollamaBaseUrl?: string | undefined
 	vsCodeLmModelSelector?:

+ 6 - 1
src/exports/types.ts

@@ -51,7 +51,6 @@ type ProviderSettings = {
 	vertexRegion?: string | undefined
 	openAiBaseUrl?: string | undefined
 	openAiApiKey?: string | undefined
-	openAiHostHeader?: string | undefined
 	openAiLegacyFormat?: boolean | undefined
 	openAiR1FormatEnabled?: boolean | undefined
 	openAiModelId?: string | undefined
@@ -89,6 +88,12 @@ type ProviderSettings = {
 	azureApiVersion?: string | undefined
 	openAiStreamingEnabled?: boolean | undefined
 	enableReasoningEffort?: boolean | undefined
+	openAiHostHeader?: string | undefined
+	openAiHeaders?:
+		| {
+				[x: string]: string
+		  }
+		| undefined
 	ollamaModelId?: string | undefined
 	ollamaBaseUrl?: string | undefined
 	vsCodeLmModelSelector?:

+ 4 - 2
src/schemas/index.ts

@@ -370,7 +370,6 @@ export const providerSettingsSchema = z.object({
 	// OpenAI
 	openAiBaseUrl: z.string().optional(),
 	openAiApiKey: z.string().optional(),
-	openAiHostHeader: z.string().optional(),
 	openAiLegacyFormat: z.boolean().optional(),
 	openAiR1FormatEnabled: z.boolean().optional(),
 	openAiModelId: z.string().optional(),
@@ -379,6 +378,8 @@ export const providerSettingsSchema = z.object({
 	azureApiVersion: z.string().optional(),
 	openAiStreamingEnabled: z.boolean().optional(),
 	enableReasoningEffort: z.boolean().optional(),
+	openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration
+	openAiHeaders: z.record(z.string(), z.string()).optional(),
 	// Ollama
 	ollamaModelId: z.string().optional(),
 	ollamaBaseUrl: z.string().optional(),
@@ -470,7 +471,6 @@ const providerSettingsRecord: ProviderSettingsRecord = {
 	// OpenAI
 	openAiBaseUrl: undefined,
 	openAiApiKey: undefined,
-	openAiHostHeader: undefined,
 	openAiLegacyFormat: undefined,
 	openAiR1FormatEnabled: undefined,
 	openAiModelId: undefined,
@@ -479,6 +479,8 @@ const providerSettingsRecord: ProviderSettingsRecord = {
 	azureApiVersion: undefined,
 	openAiStreamingEnabled: undefined,
 	enableReasoningEffort: undefined,
+	openAiHostHeader: undefined, // Keep temporarily for backward compatibility during migration
+	openAiHeaders: undefined,
 	// Ollama
 	ollamaModelId: undefined,
 	ollamaBaseUrl: undefined,

+ 126 - 22
webview-ui/src/components/settings/ApiOptions.tsx

@@ -3,7 +3,13 @@ import { useDebounce, useEvent } from "react-use"
 import { Trans } from "react-i18next"
 import { LanguageModelChatSelector } from "vscode"
 import { Checkbox } from "vscrui"
-import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import {
+	VSCodeButton,
+	VSCodeLink,
+	VSCodeRadio,
+	VSCodeRadioGroup,
+	VSCodeTextField,
+} from "@vscode/webview-ui-toolkit/react"
 import { ExternalLinkIcon } from "@radix-ui/react-icons"
 
 import { ReasoningEffort as ReasoningEffortType } from "@roo/schemas"
@@ -74,17 +80,92 @@ const ApiOptions = ({
 
 	const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)
 
+	const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
+		const headers = apiConfiguration?.openAiHeaders || {}
+		return Object.entries(headers)
+	})
+
+	// Effect to synchronize internal customHeaders state with prop changes
+	useEffect(() => {
+		const propHeaders = apiConfiguration?.openAiHeaders || {}
+		if (JSON.stringify(customHeaders) !== JSON.stringify(Object.entries(propHeaders))) setCustomHeaders(Object.entries(propHeaders))
+	}, [apiConfiguration?.openAiHeaders])
+
 	const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
 	const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState(
 		!!apiConfiguration?.openAiNativeBaseUrl,
 	)
 	const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
 	const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
-	const [openAiHostHeaderSelected, setOpenAiHostHeaderSelected] = useState(!!apiConfiguration?.openAiHostHeader)
 	const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat)
 	const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState(
 		!!apiConfiguration?.googleGeminiBaseUrl,
 	)
+
+	const handleAddCustomHeader = useCallback(() => {
+		// Only update the local state to show the new row in the UI
+		setCustomHeaders((prev) => [...prev, ["", ""]])
+		// Do not update the main configuration yet, wait for user input
+	}, [])
+
+	const handleUpdateHeaderKey = useCallback((index: number, newKey: string) => {
+		setCustomHeaders((prev) => {
+			const updated = [...prev]
+			if (updated[index]) {
+				updated[index] = [newKey, updated[index][1]]
+			}
+			return updated
+		})
+	}, [])
+
+	const handleUpdateHeaderValue = useCallback((index: number, newValue: string) => {
+		setCustomHeaders((prev) => {
+			const updated = [...prev]
+			if (updated[index]) {
+				updated[index] = [updated[index][0], newValue]
+			}
+			return updated
+		})
+	}, [])
+
+	const handleRemoveCustomHeader = useCallback((index: number) => {
+		setCustomHeaders((prev) => prev.filter((_, i) => i !== index))
+	}, [])
+
+	// Helper to convert array of tuples to object (filtering out empty keys)
+	const convertHeadersToObject = (headers: [string, string][]): Record<string, string> => {
+		const result: Record<string, string> = {}
+
+		// Process each header tuple
+		for (const [key, value] of headers) {
+			const trimmedKey = key.trim()
+
+			// Skip empty keys
+			if (!trimmedKey) continue
+
+			// For duplicates, the last one in the array wins
+			// This matches how HTTP headers work in general
+			result[trimmedKey] = value.trim()
+		}
+
+		return result
+	}
+
+	// Debounced effect to update the main configuration when local customHeaders state stabilizes
+	useDebounce(
+		() => {
+			const currentConfigHeaders = apiConfiguration?.openAiHeaders || {}
+			const newHeadersObject = convertHeadersToObject(customHeaders)
+
+			// Only update if the processed object is different from the current config
+			if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(newHeadersObject)) {
+				setApiConfigurationField("openAiHeaders", newHeadersObject)
+			}
+		},
+		300,
+		[customHeaders, apiConfiguration?.openAiHeaders, setApiConfigurationField],
+	)
+
 	const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
 	const noTransform = <T,>(value: T) => value
 
@@ -121,12 +202,15 @@ const ApiOptions = ({
 	useDebounce(
 		() => {
 			if (selectedProvider === "openai") {
+				// Use our custom headers state to build the headers object
+				const headerObject = convertHeadersToObject(customHeaders)
 				vscode.postMessage({
 					type: "requestOpenAiModels",
 					values: {
 						baseUrl: apiConfiguration?.openAiBaseUrl,
 						apiKey: apiConfiguration?.openAiApiKey,
-						hostHeader: apiConfiguration?.openAiHostHeader,
+						customHeaders: {}, // Reserved for any additional headers
+						openAiHeaders: headerObject,
 					},
 				})
 			} else if (selectedProvider === "ollama") {
@@ -145,6 +229,7 @@ const ApiOptions = ({
 			apiConfiguration?.openAiApiKey,
 			apiConfiguration?.ollamaBaseUrl,
 			apiConfiguration?.lmStudioBaseUrl,
+			customHeaders,
 		],
 	)
 
@@ -854,25 +939,44 @@ const ApiOptions = ({
 						)}
 					</div>
 
-					<div>
-						<Checkbox
-							checked={openAiHostHeaderSelected}
-							onChange={(checked: boolean) => {
-								setOpenAiHostHeaderSelected(checked)
-
-								if (!checked) {
-									setApiConfigurationField("openAiHostHeader", "")
-								}
-							}}>
-							{t("settings:providers.useHostHeader")}
-						</Checkbox>
-						{openAiHostHeaderSelected && (
-							<VSCodeTextField
-								value={apiConfiguration?.openAiHostHeader || ""}
-								onInput={handleInputChange("openAiHostHeader")}
-								placeholder="custom-api-hostname.example.com"
-								className="w-full mt-1"
-							/>
+					{/* Custom Headers UI */}
+					<div className="mb-4">
+						<div className="flex justify-between items-center mb-2">
+							<label className="block font-medium">{t("settings:providers.customHeaders")}</label>
+							<VSCodeButton
+								appearance="icon"
+								title={t("settings:common.add")}
+								onClick={handleAddCustomHeader}>
+								<span className="codicon codicon-add"></span>
+							</VSCodeButton>
+						</div>
+						{!customHeaders.length ? (
+							<div className="text-sm text-vscode-descriptionForeground">
+								{t("settings:providers.noCustomHeaders")}
+							</div>
+						) : (
+							customHeaders.map(([key, value], index) => (
+								<div key={index} className="flex items-center mb-2">
+									<VSCodeTextField
+										value={key}
+										className="flex-1 mr-2"
+										placeholder={t("settings:providers.headerName")}
+										onInput={(e: any) => handleUpdateHeaderKey(index, e.target.value)}
+									/>
+									<VSCodeTextField
+										value={value}
+										className="flex-1 mr-2"
+										placeholder={t("settings:providers.headerValue")}
+										onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)}
+									/>
+									<VSCodeButton
+										appearance="icon"
+										title={t("settings:common.remove")}
+										onClick={() => handleRemoveCustomHeader(index)}>
+										<span className="codicon codicon-trash"></span>
+									</VSCodeButton>
+								</div>
+							))
 						)}
 					</div>
 

+ 7 - 1
webview-ui/src/i18n/locales/ca/settings.json

@@ -4,7 +4,9 @@
 		"done": "Fet",
 		"cancel": "Cancel·lar",
 		"reset": "Restablir",
-		"select": "Seleccionar"
+		"select": "Seleccionar",
+		"add": "Afegir capçalera",
+		"remove": "Eliminar"
 	},
 	"header": {
 		"title": "Configuració",
@@ -107,6 +109,10 @@
 		"useCustomBaseUrl": "Utilitzar URL base personalitzada",
 		"useHostHeader": "Utilitzar capçalera Host personalitzada",
 		"useLegacyFormat": "Utilitzar el format d'API OpenAI antic",
+		"customHeaders": "Capçaleres personalitzades",
+		"headerName": "Nom de la capçalera",
+		"headerValue": "Valor de la capçalera",
+		"noCustomHeaders": "No hi ha capçaleres personalitzades definides. Feu clic al botó + per afegir-ne una.",
 		"openRouterTransformsText": "Comprimir prompts i cadenes de missatges a la mida del context (<a>Transformacions d'OpenRouter</a>)",
 		"model": "Model",
 		"getOpenRouterApiKey": "Obtenir clau API d'OpenRouter",

+ 7 - 1
webview-ui/src/i18n/locales/de/settings.json

@@ -4,7 +4,9 @@
 		"done": "Fertig",
 		"cancel": "Abbrechen",
 		"reset": "Zurücksetzen",
-		"select": "Auswählen"
+		"select": "Auswählen",
+		"add": "Header hinzufügen",
+		"remove": "Entfernen"
 	},
 	"header": {
 		"title": "Einstellungen",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Benutzerdefinierte Basis-URL verwenden",
 		"useHostHeader": "Benutzerdefinierten Host-Header verwenden",
 		"useLegacyFormat": "Altes OpenAI API-Format verwenden",
+		"customHeaders": "Benutzerdefinierte Headers",
+		"headerName": "Header-Name",
+		"headerValue": "Header-Wert",
+		"noCustomHeaders": "Keine benutzerdefinierten Headers definiert. Klicke auf die + Schaltfläche, um einen hinzuzufügen.",
 		"requestyApiKey": "Requesty API-Schlüssel",
 		"getRequestyApiKey": "Requesty API-Schlüssel erhalten",
 		"openRouterTransformsText": "Prompts und Nachrichtenketten auf Kontextgröße komprimieren (<a>OpenRouter Transformationen</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/en/settings.json

@@ -4,7 +4,9 @@
 		"done": "Done",
 		"cancel": "Cancel",
 		"reset": "Reset",
-		"select": "Select"
+		"select": "Select",
+		"add": "Add Header",
+		"remove": "Remove"
 	},
 	"header": {
 		"title": "Settings",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Use custom base URL",
 		"useHostHeader": "Use custom Host header",
 		"useLegacyFormat": "Use legacy OpenAI API format",
+		"customHeaders": "Custom Headers",
+		"headerName": "Header name",
+		"headerValue": "Header value",
+		"noCustomHeaders": "No custom headers defined. Click the + button to add one.",
 		"requestyApiKey": "Requesty API Key",
 		"getRequestyApiKey": "Get Requesty API Key",
 		"openRouterTransformsText": "Compress prompts and message chains to the context size (<a>OpenRouter Transforms</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/es/settings.json

@@ -4,7 +4,9 @@
 		"done": "Hecho",
 		"cancel": "Cancelar",
 		"reset": "Restablecer",
-		"select": "Seleccionar"
+		"select": "Seleccionar",
+		"add": "Añadir encabezado",
+		"remove": "Eliminar"
 	},
 	"header": {
 		"title": "Configuración",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Usar URL base personalizada",
 		"useHostHeader": "Usar encabezado Host personalizado",
 		"useLegacyFormat": "Usar formato API de OpenAI heredado",
+		"customHeaders": "Encabezados personalizados",
+		"headerName": "Nombre del encabezado",
+		"headerValue": "Valor del encabezado",
+		"noCustomHeaders": "No hay encabezados personalizados definidos. Haga clic en el botón + para añadir uno.",
 		"requestyApiKey": "Clave API de Requesty",
 		"getRequestyApiKey": "Obtener clave API de Requesty",
 		"openRouterTransformsText": "Comprimir prompts y cadenas de mensajes al tamaño del contexto (<a>Transformaciones de OpenRouter</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/fr/settings.json

@@ -4,7 +4,9 @@
 		"done": "Terminé",
 		"cancel": "Annuler",
 		"reset": "Réinitialiser",
-		"select": "Sélectionner"
+		"select": "Sélectionner",
+		"add": "Ajouter un en-tête",
+		"remove": "Supprimer"
 	},
 	"header": {
 		"title": "Paramètres",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Utiliser une URL de base personnalisée",
 		"useHostHeader": "Utiliser un en-tête Host personnalisé",
 		"useLegacyFormat": "Utiliser le format API OpenAI hérité",
+		"customHeaders": "En-têtes personnalisés",
+		"headerName": "Nom de l'en-tête",
+		"headerValue": "Valeur de l'en-tête",
+		"noCustomHeaders": "Aucun en-tête personnalisé défini. Cliquez sur le bouton + pour en ajouter un.",
 		"requestyApiKey": "Clé API Requesty",
 		"getRequestyApiKey": "Obtenir la clé API Requesty",
 		"openRouterTransformsText": "Compresser les prompts et chaînes de messages à la taille du contexte (<a>Transformations OpenRouter</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/hi/settings.json

@@ -4,7 +4,9 @@
 		"done": "पूर्ण",
 		"cancel": "रद्द करें",
 		"reset": "रीसेट करें",
-		"select": "चुनें"
+		"select": "चुनें",
+		"add": "हेडर जोड़ें",
+		"remove": "हटाएं"
 	},
 	"header": {
 		"title": "सेटिंग्स",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "कस्टम बेस URL का उपयोग करें",
 		"useHostHeader": "कस्टम होस्ट हेडर का उपयोग करें",
 		"useLegacyFormat": "पुराने OpenAI API प्रारूप का उपयोग करें",
+		"customHeaders": "कस्टम हेडर्स",
+		"headerName": "हेडर नाम",
+		"headerValue": "हेडर मूल्य",
+		"noCustomHeaders": "कोई कस्टम हेडर परिभाषित नहीं है। एक जोड़ने के लिए + बटन पर क्लिक करें।",
 		"requestyApiKey": "Requesty API कुंजी",
 		"getRequestyApiKey": "Requesty API कुंजी प्राप्त करें",
 		"openRouterTransformsText": "संदर्भ आकार के लिए प्रॉम्प्ट और संदेश श्रृंखलाओं को संपीड़ित करें (<a>OpenRouter ट्रांसफॉर्म</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/it/settings.json

@@ -4,7 +4,9 @@
 		"done": "Fatto",
 		"cancel": "Annulla",
 		"reset": "Ripristina",
-		"select": "Seleziona"
+		"select": "Seleziona",
+		"add": "Aggiungi intestazione",
+		"remove": "Rimuovi"
 	},
 	"header": {
 		"title": "Impostazioni",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Usa URL base personalizzato",
 		"useHostHeader": "Usa intestazione Host personalizzata",
 		"useLegacyFormat": "Usa formato API OpenAI legacy",
+		"customHeaders": "Intestazioni personalizzate",
+		"headerName": "Nome intestazione",
+		"headerValue": "Valore intestazione",
+		"noCustomHeaders": "Nessuna intestazione personalizzata definita. Fai clic sul pulsante + per aggiungerne una.",
 		"requestyApiKey": "Chiave API Requesty",
 		"getRequestyApiKey": "Ottieni chiave API Requesty",
 		"openRouterTransformsText": "Comprimi prompt e catene di messaggi alla dimensione del contesto (<a>Trasformazioni OpenRouter</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/ja/settings.json

@@ -4,7 +4,9 @@
 		"done": "完了",
 		"cancel": "キャンセル",
 		"reset": "リセット",
-		"select": "選択"
+		"select": "選択",
+		"add": "ヘッダーを追加",
+		"remove": "削除"
 	},
 	"header": {
 		"title": "設定",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "カスタムベースURLを使用",
 		"useHostHeader": "カスタムHostヘッダーを使用",
 		"useLegacyFormat": "レガシーOpenAI API形式を使用",
+		"customHeaders": "カスタムヘッダー",
+		"headerName": "ヘッダー名",
+		"headerValue": "ヘッダー値",
+		"noCustomHeaders": "カスタムヘッダーが定義されていません。+ ボタンをクリックして追加してください。",
 		"requestyApiKey": "Requesty APIキー",
 		"getRequestyApiKey": "Requesty APIキーを取得",
 		"openRouterTransformsText": "プロンプトとメッセージチェーンをコンテキストサイズに圧縮 (<a>OpenRouter Transforms</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/ko/settings.json

@@ -4,7 +4,9 @@
 		"done": "완료",
 		"cancel": "취소",
 		"reset": "초기화",
-		"select": "선택"
+		"select": "선택",
+		"add": "헤더 추가",
+		"remove": "삭제"
 	},
 	"header": {
 		"title": "설정",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "사용자 정의 기본 URL 사용",
 		"useHostHeader": "사용자 정의 Host 헤더 사용",
 		"useLegacyFormat": "레거시 OpenAI API 형식 사용",
+		"customHeaders": "사용자 정의 헤더",
+		"headerName": "헤더 이름",
+		"headerValue": "헤더 값",
+		"noCustomHeaders": "정의된 사용자 정의 헤더가 없습니다. + 버튼을 클릭하여 추가하세요.",
 		"requestyApiKey": "Requesty API 키",
 		"getRequestyApiKey": "Requesty API 키 받기",
 		"openRouterTransformsText": "프롬프트와 메시지 체인을 컨텍스트 크기로 압축 (<a>OpenRouter Transforms</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/pl/settings.json

@@ -4,7 +4,9 @@
 		"done": "Gotowe",
 		"cancel": "Anuluj",
 		"reset": "Resetuj",
-		"select": "Wybierz"
+		"select": "Wybierz",
+		"add": "Dodaj nagłówek",
+		"remove": "Usuń"
 	},
 	"header": {
 		"title": "Ustawienia",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Użyj niestandardowego URL bazowego",
 		"useHostHeader": "Użyj niestandardowego nagłówka Host",
 		"useLegacyFormat": "Użyj starszego formatu API OpenAI",
+		"customHeaders": "Niestandardowe nagłówki",
+		"headerName": "Nazwa nagłówka",
+		"headerValue": "Wartość nagłówka",
+		"noCustomHeaders": "Brak zdefiniowanych niestandardowych nagłówków. Kliknij przycisk +, aby dodać.",
 		"requestyApiKey": "Klucz API Requesty",
 		"getRequestyApiKey": "Uzyskaj klucz API Requesty",
 		"openRouterTransformsText": "Kompresuj podpowiedzi i łańcuchy wiadomości do rozmiaru kontekstu (<a>Transformacje OpenRouter</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -4,7 +4,9 @@
 		"done": "Concluído",
 		"cancel": "Cancelar",
 		"reset": "Redefinir",
-		"select": "Selecionar"
+		"select": "Selecionar",
+		"add": "Adicionar cabeçalho",
+		"remove": "Remover"
 	},
 	"header": {
 		"title": "Configurações",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Usar URL base personalizado",
 		"useHostHeader": "Usar cabeçalho Host personalizado",
 		"useLegacyFormat": "Usar formato de API OpenAI legado",
+		"customHeaders": "Cabeçalhos personalizados",
+		"headerName": "Nome do cabeçalho",
+		"headerValue": "Valor do cabeçalho",
+		"noCustomHeaders": "Nenhum cabeçalho personalizado definido. Clique no botão + para adicionar um.",
 		"requestyApiKey": "Chave de API Requesty",
 		"getRequestyApiKey": "Obter chave de API Requesty",
 		"openRouterTransformsText": "Comprimir prompts e cadeias de mensagens para o tamanho do contexto (<a>Transformações OpenRouter</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/ru/settings.json

@@ -4,7 +4,9 @@
 		"done": "Готово",
 		"cancel": "Отмена",
 		"reset": "Сбросить",
-		"select": "Выбрать"
+		"select": "Выбрать",
+		"add": "Добавить заголовок",
+		"remove": "Удалить"
 	},
 	"header": {
 		"title": "Настройки",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Использовать пользовательский базовый URL",
 		"useHostHeader": "Использовать пользовательский Host-заголовок",
 		"useLegacyFormat": "Использовать устаревший формат OpenAI API",
+		"customHeaders": "Пользовательские заголовки",
+		"headerName": "Имя заголовка",
+		"headerValue": "Значение заголовка",
+		"noCustomHeaders": "Пользовательские заголовки не определены. Нажмите кнопку +, чтобы добавить.",
 		"requestyApiKey": "Requesty API-ключ",
 		"getRequestyApiKey": "Получить Requesty API-ключ",
 		"openRouterTransformsText": "Сжимать подсказки и цепочки сообщений до размера контекста (<a>OpenRouter Transforms</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/tr/settings.json

@@ -4,7 +4,9 @@
 		"done": "Tamamlandı",
 		"cancel": "İptal",
 		"reset": "Sıfırla",
-		"select": "Seç"
+		"select": "Seç",
+		"add": "Başlık Ekle",
+		"remove": "Kaldır"
 	},
 	"header": {
 		"title": "Ayarlar",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Özel temel URL kullan",
 		"useHostHeader": "Özel Host başlığı kullan",
 		"useLegacyFormat": "Eski OpenAI API formatını kullan",
+		"customHeaders": "Özel Başlıklar",
+		"headerName": "Başlık adı",
+		"headerValue": "Başlık değeri",
+		"noCustomHeaders": "Tanımlanmış özel başlık yok. Eklemek için + düğmesine tıklayın.",
 		"requestyApiKey": "Requesty API Anahtarı",
 		"getRequestyApiKey": "Requesty API Anahtarı Al",
 		"openRouterTransformsText": "İstem ve mesaj zincirlerini bağlam boyutuna sıkıştır (<a>OpenRouter Dönüşümleri</a>)",

+ 7 - 1
webview-ui/src/i18n/locales/vi/settings.json

@@ -4,7 +4,9 @@
 		"done": "Hoàn thành",
 		"cancel": "Hủy",
 		"reset": "Đặt lại",
-		"select": "Chọn"
+		"select": "Chọn",
+		"add": "Thêm tiêu đề",
+		"remove": "Xóa"
 	},
 	"header": {
 		"title": "Cài đặt",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "Sử dụng URL cơ sở tùy chỉnh",
 		"useHostHeader": "Sử dụng tiêu đề Host tùy chỉnh",
 		"useLegacyFormat": "Sử dụng định dạng API OpenAI cũ",
+		"customHeaders": "Tiêu đề tùy chỉnh",
+		"headerName": "Tên tiêu đề",
+		"headerValue": "Giá trị tiêu đề",
+		"noCustomHeaders": "Chưa có tiêu đề tùy chỉnh nào được định nghĩa. Nhấp vào nút + để thêm.",
 		"requestyApiKey": "Khóa API Requesty",
 		"getRequestyApiKey": "Lấy khóa API Requesty",
 		"anthropicApiKey": "Khóa API Anthropic",

+ 7 - 1
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -4,7 +4,9 @@
 		"done": "完成",
 		"cancel": "取消",
 		"reset": "恢复默认设置",
-		"select": "选择"
+		"select": "选择",
+		"add": "添加标头",
+		"remove": "移除"
 	},
 	"header": {
 		"title": "设置",
@@ -109,6 +111,10 @@
 		"useCustomBaseUrl": "使用自定义基础 URL",
 		"useHostHeader": "使用自定义 Host 标头",
 		"useLegacyFormat": "使用传统 OpenAI API 格式",
+		"customHeaders": "自定义标头",
+		"headerName": "标头名称",
+		"headerValue": "标头值",
+		"noCustomHeaders": "暂无自定义标头。点击 + 按钮添加。",
 		"glamaApiKey": "Glama API 密钥",
 		"getGlamaApiKey": "获取 Glama API 密钥",
 		"requestyApiKey": "Requesty API 密钥",

+ 7 - 1
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -4,7 +4,9 @@
 		"done": "完成",
 		"cancel": "取消",
 		"reset": "重設",
-		"select": "選擇"
+		"select": "選擇",
+		"add": "新增標頭",
+		"remove": "移除"
 	},
 	"header": {
 		"title": "設定",
@@ -111,6 +113,10 @@
 		"useCustomBaseUrl": "使用自訂基礎 URL",
 		"useHostHeader": "使用自訂 Host 標頭",
 		"useLegacyFormat": "使用舊版 OpenAI API 格式",
+		"customHeaders": "自訂標頭",
+		"headerName": "標頭名稱",
+		"headerValue": "標頭值",
+		"noCustomHeaders": "尚未定義自訂標頭。點擊 + 按鈕以新增。",
 		"requestyApiKey": "Requesty API 金鑰",
 		"getRequestyApiKey": "取得 Requesty API 金鑰",
 		"openRouterTransformsText": "將提示和訊息鏈壓縮到上下文大小 (<a>OpenRouter 轉換</a>)",