[codex] feat(ai plugin): support custom request options#534
Conversation
Add new fields `headers` and `extraBody` to AiOption interface to allow passing custom request headers and additional OpenAI-compatible request body parameters. Implement normalization logic for headers and extra body, filter out reserved keys from extra body, and apply them to both streaming and non-streaming AI API calls.
There was a problem hiding this comment.
Code Review
This pull request introduces support for custom headers and extra OpenAI-compatible request body fields (extraBody) in the AI plugin API. It adds validation and normalization helpers (isPlainObject, normalizeHeaders, and normalizeExtraBody) to process these inputs before passing them to the OpenAI client. The review feedback suggests improving the robustness of these helpers by implementing stricter plain object validation, handling null values safely, and preventing the stringification of null or undefined header values.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| private isPlainObject(value: unknown): value is Record<string, unknown> { | ||
| return Boolean(value && typeof value === 'object' && !Array.isArray(value)) | ||
| } | ||
|
|
||
| private normalizeHeaders(headers: unknown): Record<string, string> | undefined { | ||
| if (headers === undefined) return undefined | ||
| if (!this.isPlainObject(headers)) { | ||
| throw new Error('headers 必须是对象') | ||
| } | ||
|
|
||
| const normalized = Object.fromEntries( | ||
| Object.entries(headers).map(([key, value]) => [key, String(value)]) | ||
| ) | ||
| return Object.keys(normalized).length > 0 ? normalized : undefined | ||
| } | ||
|
|
||
| private normalizeExtraBody(extraBody: unknown): Record<string, unknown> | undefined { | ||
| if (extraBody === undefined) return undefined | ||
| if (!this.isPlainObject(extraBody)) { | ||
| throw new Error('extraBody 必须是对象') | ||
| } | ||
|
|
||
| const normalized = { ...extraBody } | ||
| for (const key of RESERVED_EXTRA_BODY_KEYS) { | ||
| delete normalized[key] | ||
| } | ||
| return Object.keys(normalized).length > 0 ? normalized : undefined | ||
| } |
There was a problem hiding this comment.
There are a few areas where robustness and correctness can be improved:
- Stricter Plain Object Validation: The current
isPlainObjectimplementation returnstruefor instances ofDate,RegExp,Map,Set, etc. Checking the prototype usingObject.getPrototypeOfensures only plain objects are accepted. - Defensive Programming (Null Safety): The current checks only handle
undefined. Ifnullis passed (which is common in JSON/IPC payloads), it will fail theisPlainObjectcheck and throw an error. Using== nullsafely handles bothnullandundefined. - Header Stringification Pitfall: Using
String(value)directly on object entries can convertnullorundefinedvalues to"null"or"undefined"strings, which will be sent as actual header values. It is safer to filter outnullandundefinedvalues before stringifying.
private isPlainObject(value: unknown): value is Record<string, unknown> {
if (typeof value !== 'object' || value === null) return false
const proto = Object.getPrototypeOf(value)
return proto === null || proto === Object.prototype
}
private normalizeHeaders(headers: unknown): Record<string, string> | undefined {
if (headers == null) return undefined
if (!this.isPlainObject(headers)) {
throw new Error('headers 必须是对象')
}
const normalized: Record<string, string> = {}
for (const [key, value] of Object.entries(headers)) {
if (value !== null && value !== undefined) {
normalized[key] = String(value)
}
}
return Object.keys(normalized).length > 0 ? normalized : undefined
}
private normalizeExtraBody(extraBody: unknown): Record<string, unknown> | undefined {
if (extraBody == null) return undefined
if (!this.isPlainObject(extraBody)) {
throw new Error('extraBody 必须是对象')
}
const normalized = { ...extraBody }
for (const key of RESERVED_EXTRA_BODY_KEYS) {
delete normalized[key]
}
return Object.keys(normalized).length > 0 ? normalized : undefined
}There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR extends the plugin AI API to allow callers to pass additional HTTP headers and OpenAI-compatible extra request body fields, forwarding them through the OpenAI SDK request.
Changes:
- Added
headersandextraBodytoAiOption. - Added normalization helpers to validate/normalize headers and extra body fields.
- Wired
headersandextraBodyinto both non-streaming and streaming chat completion requests.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private isPlainObject(value: unknown): value is Record<string, unknown> { | ||
| return Boolean(value && typeof value === 'object' && !Array.isArray(value)) | ||
| } | ||
|
|
||
| private normalizeHeaders(headers: unknown): Record<string, string> | undefined { | ||
| if (headers === undefined) return undefined | ||
| if (!this.isPlainObject(headers)) { | ||
| throw new Error('headers 必须是对象') | ||
| } | ||
|
|
||
| const normalized = Object.fromEntries( | ||
| Object.entries(headers).map(([key, value]) => [key, String(value)]) | ||
| ) |
| const client = this.createClient(modelConfig) | ||
| const openaiTools = option.tools?.length ? this.convertTools(option.tools) : undefined | ||
| const messages = [...option.messages] | ||
| const requestHeaders = this.normalizeHeaders(option.headers) |
| const response = await client.chat.completions.create( | ||
| { | ||
| ...extraBody, | ||
| model: modelConfig.id, | ||
| messages: this.convertMessages(messages), | ||
| ...(openaiTools?.length ? { tools: openaiTools } : {}) | ||
| }, | ||
| { signal: abortController.signal } | ||
| { | ||
| signal: abortController.signal, | ||
| ...(requestHeaders ? { headers: requestHeaders } : {}) | ||
| } |
| private normalizeExtraBody(extraBody: unknown): Record<string, unknown> | undefined { | ||
| if (extraBody === undefined) return undefined | ||
| if (!this.isPlainObject(extraBody)) { | ||
| throw new Error('extraBody 必须是对象') | ||
| } | ||
|
|
||
| const normalized = { ...extraBody } | ||
| for (const key of RESERVED_EXTRA_BODY_KEYS) { | ||
| delete normalized[key] | ||
| } | ||
| return Object.keys(normalized).length > 0 ? normalized : undefined | ||
| } |
Summary
Adds per-request AI options for plugin calls:
headersthrough to the OpenAI SDK request optionsextraBodyfields into chat completion request bodiesmodel,messages,tools,stream) protected fromextraBodyoverridesValidation
pnpm run typecheck:nodepnpm build:unpacknode _work/2026-06-08-ai-extra-body/verify-ai-extra-body-dev.jsgit -C _work/pr-ai-extra-body-submit diff --check upstream/main..HEAD