-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: add custom requestHeaders
support for custom OAI and Azure models in BYOK
#1231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
d599be9
7c7a865
dc5e330
d4c61c1
1f03640
a81d291
7747145
5beba4f
11d40e0
e02f408
69ecaa0
a8e29d9
092f70e
f0c6dce
b4c06ce
bb9d3c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2955,6 +2955,13 @@ | |
"type": "boolean", | ||
"default": false, | ||
"description": "Whether the model supports thinking capabilities" | ||
}, | ||
"requestHeaders": { | ||
"type": "object", | ||
"description": "Additional HTTP headers to include with requests to this model. These reserved headers are not allowed and ignored if present: ['api-key', 'authorization', 'content-type', 'openai-intent', 'x-github-api-version', 'x-initiator', 'x-interaction-id', 'x-interaction-type', 'x-onbehalf-extension-id', 'x-request-id', 'x-vscode-user-agent-library-version']", | ||
"additionalProperties": { | ||
"type": "string" | ||
} | ||
} | ||
}, | ||
"required": [ | ||
|
@@ -3024,6 +3031,13 @@ | |
"type": "boolean", | ||
"default": false, | ||
"description": "Whether the model supports thinking capabilities" | ||
}, | ||
"requestHeaders": { | ||
"type": "object", | ||
"description": "Additional HTTP headers to include with requests to this model. These reserved headers are not allowed and ignored if present: ['api-key', 'authorization', 'content-type', 'openai-intent', 'x-github-api-version', 'x-initiator', 'x-interaction-id', 'x-interaction-type', 'x-onbehalf-extension-id', 'x-request-id', 'x-vscode-user-agent-library-version']", | ||
"additionalProperties": { | ||
"type": "string" | ||
} | ||
} | ||
}, | ||
"required": [ | ||
|
@@ -4267,4 +4281,4 @@ | |
"string_decoder": "npm:[email protected]", | ||
"node-gyp": "npm:[email protected]" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,52 @@ function hydrateBYOKErrorMessages(response: ChatResponse): ChatResponse { | |
} | ||
|
||
export class OpenAIEndpoint extends ChatEndpoint { | ||
// Reserved headers that cannot be overridden for security and functionality reasons | ||
private static readonly _reservedHeaders: ReadonlySet<string> = new Set([ | ||
// Authentication & Authorization | ||
'api-key', | ||
'authorization', | ||
'cookie', | ||
'set-cookie', | ||
// Content & Protocol | ||
'content-type', | ||
'content-length', | ||
'transfer-encoding', | ||
'host', | ||
// Routing & Proxying | ||
'proxy-authorization', | ||
'proxy-authenticate', | ||
'x-forwarded-for', | ||
'x-forwarded-host', | ||
'x-forwarded-proto', | ||
'forwarded', | ||
// Security & CORS | ||
'origin', | ||
'referer', | ||
'sec-fetch-site', | ||
'sec-fetch-mode', | ||
'sec-fetch-dest', | ||
// Application-specific | ||
'openai-intent', | ||
'x-github-api-version', | ||
'x-initiator', | ||
'x-interaction-id', | ||
'x-interaction-type', | ||
'x-onbehalf-extension-id', | ||
'x-request-id', | ||
'x-vscode-user-agent-library-version', | ||
'user-agent', | ||
]); | ||
|
||
// RFC 7230 compliant header name pattern: token characters only | ||
private static readonly _validHeaderNamePattern = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/; | ||
|
||
// Maximum limits to prevent abuse | ||
private static readonly _maxHeaderNameLength = 256; | ||
private static readonly _maxHeaderValueLength = 8192; | ||
private static readonly _maxCustomHeaderCount = 20; | ||
|
||
private readonly _customHeaders: Record<string, string>; | ||
constructor( | ||
protected readonly modelMetadata: IChatModelInformation, | ||
protected readonly _apiKey: string, | ||
|
@@ -58,7 +104,7 @@ export class OpenAIEndpoint extends ChatEndpoint { | |
@IInstantiationService protected instantiationService: IInstantiationService, | ||
@IConfigurationService configurationService: IConfigurationService, | ||
@IExperimentationService expService: IExperimentationService, | ||
@ILogService logService: ILogService | ||
@ILogService protected logService: ILogService | ||
) { | ||
super( | ||
modelMetadata, | ||
|
@@ -74,6 +120,87 @@ export class OpenAIEndpoint extends ChatEndpoint { | |
expService, | ||
logService | ||
); | ||
this._customHeaders = this._sanitizeCustomHeaders(modelMetadata.requestHeaders); | ||
} | ||
|
||
private _sanitizeCustomHeaders(headers: Readonly<Record<string, string>> | undefined): Record<string, string> { | ||
if (!headers) { | ||
return {}; | ||
} | ||
|
||
const entries = Object.entries(headers); | ||
|
||
if (entries.length > OpenAIEndpoint._maxCustomHeaderCount) { | ||
this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has ${entries.length} custom headers, exceeding limit of ${OpenAIEndpoint._maxCustomHeaderCount}. Only first ${OpenAIEndpoint._maxCustomHeaderCount} will be processed.`); | ||
} | ||
|
||
const sanitized: Record<string, string> = {}; | ||
let processedCount = 0; | ||
|
||
for (const [rawKey, rawValue] of entries) { | ||
if (processedCount >= OpenAIEndpoint._maxCustomHeaderCount) { | ||
break; | ||
} | ||
|
||
const key = rawKey.trim(); | ||
if (!key) { | ||
this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has empty header name, skipping.`); | ||
continue; | ||
} | ||
|
||
if (key.length > OpenAIEndpoint._maxHeaderNameLength) { | ||
this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has header name exceeding ${OpenAIEndpoint._maxHeaderNameLength} characters, skipping.`); | ||
continue; | ||
} | ||
|
||
if (!OpenAIEndpoint._validHeaderNamePattern.test(key)) { | ||
this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has invalid header name format: '${key}', Skipping.`); | ||
continue; | ||
} | ||
|
||
const lowerKey = key.toLowerCase(); | ||
if (OpenAIEndpoint._reservedHeaders.has(lowerKey)) { | ||
this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' attempted to override reserved header '${key}', skipping.`); | ||
continue; | ||
} | ||
|
||
const sanitizedValue = this._sanitizeHeaderValue(rawValue); | ||
if (sanitizedValue === undefined) { | ||
this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has invalid value for header '${key}': '${rawValue}', skipping.`); | ||
continue; | ||
} | ||
|
||
sanitized[key] = sanitizedValue; | ||
processedCount++; | ||
} | ||
|
||
return sanitized; | ||
} | ||
|
||
private _sanitizeHeaderValue(value: unknown): string | undefined { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we not sanitzing twice here? Both above and with this function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So _sanitizeHeaderValue() is a helper function called by _sanitizeCustomHeaders() during initialization, not a separate sanitization step. The headers are sanitized once during object construction and then reused safely throughout the object's lifetime. The separation is: _sanitizeCustomHeaders() handles the overall header collection logic (limits, reserved headers, key validation) while _sanitizeHeaderValue() focuses specifically on value sanitization (control characters, length limits, etc.)." |
||
if (typeof value !== 'string') { | ||
return undefined; | ||
} | ||
|
||
const trimmed = value.trim(); | ||
|
||
if (trimmed.length > OpenAIEndpoint._maxHeaderValueLength) { | ||
return undefined; | ||
} | ||
|
||
// Disallow control characters including CR, LF, and others (0x00-0x1F, 0x7F) | ||
// This prevents HTTP header injection and response splitting attacks | ||
if (/[\x00-\x1F\x7F]/.test(trimmed)) { | ||
return undefined; | ||
} | ||
|
||
// Additional check for potential Unicode issues | ||
// Reject headers with bidirectional override characters or zero-width characters | ||
if (/[\u200B-\u200D\u202A-\u202E\uFEFF]/.test(trimmed)) { | ||
return undefined; | ||
} | ||
|
||
return trimmed; | ||
} | ||
|
||
override createRequestBody(options: ICreateEndpointBodyOptions): IEndpointBody { | ||
|
@@ -149,6 +276,9 @@ export class OpenAIEndpoint extends ChatEndpoint { | |
} else { | ||
headers['Authorization'] = `Bearer ${this._apiKey}`; | ||
} | ||
for (const [key, value] of Object.entries(this._customHeaders)) { | ||
headers[key] = value; | ||
} | ||
return headers; | ||
} | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.