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>
This commit is contained in:
Matt Rubens 2025-05-01 23:30:57 -04:00 committed by GitHub
parent 2d8beb2ed0
commit a356d70669
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 312 additions and 58 deletions

View file

@ -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
}

View file

@ -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])

View file

@ -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.
*/

View file

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

View file

@ -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 })

View file

@ -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?:

View file

@ -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?:

View file

@ -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,

View file

@ -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>

View file

@ -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",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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>)",

View file

@ -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",

View file

@ -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 密钥",

View file

@ -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>)",