Skip to content

Commit a356d70

Browse files
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>
1 parent 2d8beb2 commit a356d70

File tree

25 files changed

+312
-58
lines changed

25 files changed

+312
-58
lines changed

src/api/providers/openai.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,17 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
3535
const urlHost = this._getUrlHost(this.options.openAiBaseUrl)
3636
const isAzureOpenAi = urlHost === "azure.com" || urlHost.endsWith(".azure.com") || options.openAiUseAzure
3737

38+
const headers = {
39+
...DEFAULT_HEADERS,
40+
...(this.options.openAiHeaders || {}),
41+
}
42+
3843
if (isAzureAiInference) {
3944
// Azure AI Inference Service (e.g., for DeepSeek) uses a different path structure
4045
this.client = new OpenAI({
4146
baseURL,
4247
apiKey,
43-
defaultHeaders: DEFAULT_HEADERS,
48+
defaultHeaders: headers,
4449
defaultQuery: { "api-version": this.options.azureApiVersion || "2024-05-01-preview" },
4550
})
4651
} else if (isAzureOpenAi) {
@@ -50,19 +55,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
5055
baseURL,
5156
apiKey,
5257
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
53-
defaultHeaders: {
54-
...DEFAULT_HEADERS,
55-
...(this.options.openAiHostHeader ? { Host: this.options.openAiHostHeader } : {}),
56-
},
58+
defaultHeaders: headers,
5759
})
5860
} else {
5961
this.client = new OpenAI({
6062
baseURL,
6163
apiKey,
62-
defaultHeaders: {
63-
...DEFAULT_HEADERS,
64-
...(this.options.openAiHostHeader ? { Host: this.options.openAiHostHeader } : {}),
65-
},
64+
defaultHeaders: headers,
6665
})
6766
}
6867
}
@@ -361,7 +360,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
361360
}
362361
}
363362

364-
export async function getOpenAiModels(baseUrl?: string, apiKey?: string, hostHeader?: string) {
363+
export async function getOpenAiModels(baseUrl?: string, apiKey?: string, openAiHeaders?: Record<string, string>) {
365364
try {
366365
if (!baseUrl) {
367366
return []
@@ -372,16 +371,15 @@ export async function getOpenAiModels(baseUrl?: string, apiKey?: string, hostHea
372371
}
373372

374373
const config: Record<string, any> = {}
375-
const headers: Record<string, string> = {}
374+
const headers: Record<string, string> = {
375+
...DEFAULT_HEADERS,
376+
...(openAiHeaders || {}),
377+
}
376378

377379
if (apiKey) {
378380
headers["Authorization"] = `Bearer ${apiKey}`
379381
}
380382

381-
if (hostHeader) {
382-
headers["Host"] = hostHeader
383-
}
384-
385383
if (Object.keys(headers).length > 0) {
386384
config["headers"] = headers
387385
}

src/core/config/ContextProxy.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ export class ContextProxy {
192192
// If a value is not present in the new configuration, then it is assumed
193193
// that the setting's value should be `undefined` and therefore we
194194
// need to remove it from the state cache if it exists.
195+
196+
// Ensure openAiHeaders is always an object even when empty
197+
// This is critical for proper serialization/deserialization through IPC
198+
if (values.openAiHeaders !== undefined) {
199+
// Check if it's empty or null
200+
if (!values.openAiHeaders || Object.keys(values.openAiHeaders).length === 0) {
201+
values.openAiHeaders = {}
202+
}
203+
}
204+
195205
await this.setValues({
196206
...PROVIDER_SETTINGS_KEYS.filter((key) => !isSecretStateKey(key))
197207
.filter((key) => !!this.stateCache[key])

src/core/config/ProviderSettingsManager.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const providerProfilesSchema = z.object({
1717
.object({
1818
rateLimitSecondsMigrated: z.boolean().optional(),
1919
diffSettingsMigrated: z.boolean().optional(),
20+
openAiHeadersMigrated: z.boolean().optional(),
2021
})
2122
.optional(),
2223
})
@@ -38,6 +39,7 @@ export class ProviderSettingsManager {
3839
migrations: {
3940
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
4041
diffSettingsMigrated: true, // Mark as migrated on fresh installs
42+
openAiHeadersMigrated: true, // Mark as migrated on fresh installs
4143
},
4244
}
4345

@@ -90,6 +92,7 @@ export class ProviderSettingsManager {
9092
providerProfiles.migrations = {
9193
rateLimitSecondsMigrated: false,
9294
diffSettingsMigrated: false,
95+
openAiHeadersMigrated: false,
9396
} // Initialize with default values
9497
isDirty = true
9598
}
@@ -106,6 +109,12 @@ export class ProviderSettingsManager {
106109
isDirty = true
107110
}
108111

112+
if (!providerProfiles.migrations.openAiHeadersMigrated) {
113+
await this.migrateOpenAiHeaders(providerProfiles)
114+
providerProfiles.migrations.openAiHeadersMigrated = true
115+
isDirty = true
116+
}
117+
109118
if (isDirty) {
110119
await this.store(providerProfiles)
111120
}
@@ -175,6 +184,30 @@ export class ProviderSettingsManager {
175184
}
176185
}
177186

187+
private async migrateOpenAiHeaders(providerProfiles: ProviderProfiles) {
188+
try {
189+
for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
190+
// Use type assertion to access the deprecated property safely
191+
const configAny = apiConfig as any
192+
193+
// Check if openAiHostHeader exists but openAiHeaders doesn't
194+
if (
195+
configAny.openAiHostHeader &&
196+
(!apiConfig.openAiHeaders || Object.keys(apiConfig.openAiHeaders || {}).length === 0)
197+
) {
198+
// Create the headers object with the Host value
199+
apiConfig.openAiHeaders = { Host: configAny.openAiHostHeader }
200+
201+
// Delete the old property to prevent re-migration
202+
// This prevents the header from reappearing after deletion
203+
configAny.openAiHostHeader = undefined
204+
}
205+
}
206+
} catch (error) {
207+
console.error(`[MigrateOpenAiHeaders] Failed to migrate OpenAI headers:`, error)
208+
}
209+
}
210+
178211
/**
179212
* List all available configs with metadata.
180213
*/

src/core/config/__tests__/ProviderSettingsManager.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ describe("ProviderSettingsManager", () => {
5656
migrations: {
5757
rateLimitSecondsMigrated: true,
5858
diffSettingsMigrated: true,
59+
openAiHeadersMigrated: true,
5960
},
6061
}),
6162
)

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
310310
const openAiModels = await getOpenAiModels(
311311
message?.values?.baseUrl,
312312
message?.values?.apiKey,
313-
message?.values?.hostHeader,
313+
message?.values?.openAiHeaders,
314314
)
315315

316316
provider.postMessageToWebview({ type: "openAiModels", openAiModels })

src/exports/roo-code.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ type ProviderSettings = {
5050
vertexRegion?: string | undefined
5151
openAiBaseUrl?: string | undefined
5252
openAiApiKey?: string | undefined
53-
openAiHostHeader?: string | undefined
5453
openAiLegacyFormat?: boolean | undefined
5554
openAiR1FormatEnabled?: boolean | undefined
5655
openAiModelId?: string | undefined
@@ -88,6 +87,12 @@ type ProviderSettings = {
8887
azureApiVersion?: string | undefined
8988
openAiStreamingEnabled?: boolean | undefined
9089
enableReasoningEffort?: boolean | undefined
90+
openAiHostHeader?: string | undefined
91+
openAiHeaders?:
92+
| {
93+
[x: string]: string
94+
}
95+
| undefined
9196
ollamaModelId?: string | undefined
9297
ollamaBaseUrl?: string | undefined
9398
vsCodeLmModelSelector?:

src/exports/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ type ProviderSettings = {
5151
vertexRegion?: string | undefined
5252
openAiBaseUrl?: string | undefined
5353
openAiApiKey?: string | undefined
54-
openAiHostHeader?: string | undefined
5554
openAiLegacyFormat?: boolean | undefined
5655
openAiR1FormatEnabled?: boolean | undefined
5756
openAiModelId?: string | undefined
@@ -89,6 +88,12 @@ type ProviderSettings = {
8988
azureApiVersion?: string | undefined
9089
openAiStreamingEnabled?: boolean | undefined
9190
enableReasoningEffort?: boolean | undefined
91+
openAiHostHeader?: string | undefined
92+
openAiHeaders?:
93+
| {
94+
[x: string]: string
95+
}
96+
| undefined
9297
ollamaModelId?: string | undefined
9398
ollamaBaseUrl?: string | undefined
9499
vsCodeLmModelSelector?:

src/schemas/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,6 @@ export const providerSettingsSchema = z.object({
370370
// OpenAI
371371
openAiBaseUrl: z.string().optional(),
372372
openAiApiKey: z.string().optional(),
373-
openAiHostHeader: z.string().optional(),
374373
openAiLegacyFormat: z.boolean().optional(),
375374
openAiR1FormatEnabled: z.boolean().optional(),
376375
openAiModelId: z.string().optional(),
@@ -379,6 +378,8 @@ export const providerSettingsSchema = z.object({
379378
azureApiVersion: z.string().optional(),
380379
openAiStreamingEnabled: z.boolean().optional(),
381380
enableReasoningEffort: z.boolean().optional(),
381+
openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration
382+
openAiHeaders: z.record(z.string(), z.string()).optional(),
382383
// Ollama
383384
ollamaModelId: z.string().optional(),
384385
ollamaBaseUrl: z.string().optional(),
@@ -470,7 +471,6 @@ const providerSettingsRecord: ProviderSettingsRecord = {
470471
// OpenAI
471472
openAiBaseUrl: undefined,
472473
openAiApiKey: undefined,
473-
openAiHostHeader: undefined,
474474
openAiLegacyFormat: undefined,
475475
openAiR1FormatEnabled: undefined,
476476
openAiModelId: undefined,
@@ -479,6 +479,8 @@ const providerSettingsRecord: ProviderSettingsRecord = {
479479
azureApiVersion: undefined,
480480
openAiStreamingEnabled: undefined,
481481
enableReasoningEffort: undefined,
482+
openAiHostHeader: undefined, // Keep temporarily for backward compatibility during migration
483+
openAiHeaders: undefined,
482484
// Ollama
483485
ollamaModelId: undefined,
484486
ollamaBaseUrl: undefined,

0 commit comments

Comments
 (0)