Add OpenAI responses mode and custom auth headers (#906)

* Add OpenAI profile responses and custom auth header support

* Fix knowledge graph config reference in query loop

* Address OpenAI profile review edge cases

* Remove unused getGlobalConfig import

Delete an unused import of getGlobalConfig from src/query.ts. This cleans up dead code and avoids unused-import lint warnings; no functional behavior changes.

* Address follow-up OpenAI profile review comments

* Refine OpenAI responses auth review fixes

* Fix custom auth header default scheme
This commit is contained in:
TechBrewBoss
2026-04-26 07:24:03 -05:00
committed by GitHub
parent a3e728a114
commit 6dedffe5ff
14 changed files with 938 additions and 107 deletions

View File

@@ -11,6 +11,10 @@
* Environment variables:
* CLAUDE_CODE_USE_OPENAI=1 — enable this provider
* OPENAI_API_KEY=sk-... — API key (optional for local models)
* OPENAI_AUTH_HEADER=api-key — optional custom auth header name
* OPENAI_AUTH_HEADER_VALUE=... — optional custom auth header value
* OPENAI_AUTH_SCHEME=bearer|raw — auth scheme for Authorization/custom header handling
* OPENAI_API_FORMAT=chat_completions|responses — request format for compatible APIs
* OPENAI_BASE_URL=http://... — base URL (default: https://api.openai.com/v1)
* OPENAI_MODEL=gpt-4o — default model override
* CODEX_API_KEY / ~/.codex/auth.json — Codex auth for codexplan/codexspark
@@ -74,6 +78,7 @@ import { createStreamState, processStreamChunk, getStreamStats } from '../../uti
type SecretValueSource = Partial<{
OPENAI_API_KEY: string
OPENAI_AUTH_HEADER_VALUE: string
CODEX_API_KEY: string
GEMINI_API_KEY: string
GOOGLE_API_KEY: string
@@ -1350,7 +1355,11 @@ class OpenAIShimMessages {
if (params.stream) {
const isResponsesStream = response.url?.includes('/responses')
return new OpenAIShimStream(
(request.transport === 'codex_responses' || isResponsesStream)
(
request.transport === 'codex_responses' ||
request.transport === 'responses' ||
isResponsesStream
)
? codexStreamToAnthropic(response, request.resolvedModel, options?.signal)
: openaiStreamToAnthropic(response, request.resolvedModel, options?.signal),
)
@@ -1365,7 +1374,11 @@ class OpenAIShimMessages {
}
const isResponsesNonStream = response.url?.includes('/responses')
if (isResponsesNonStream || (request.transport === 'chat_completions' && isGithubModelsMode())) {
if (
request.transport === 'responses' ||
isResponsesNonStream ||
(request.transport === 'chat_completions' && isGithubModelsMode())
) {
const contentType = response.headers.get('content-type') ?? ''
if (contentType.includes('application/json')) {
const parsed = await response.json() as Record<string, unknown>
@@ -1644,6 +1657,65 @@ class OpenAIShimMessages {
}
}
let omitResponsesTools = false
const buildResponsesBody = (): Record<string, unknown> => {
const responsesBody: Record<string, unknown> = {
model: request.resolvedModel,
input: convertAnthropicMessagesToResponsesInput(
params.messages as Array<{
role?: string
message?: { role?: string; content?: unknown }
content?: unknown
}>,
),
stream: params.stream ?? false,
store: false,
}
if (isMistral || isGeminiMode() || isMoonshot || isDeepSeek || isZai) {
delete responsesBody.store
}
if (!Array.isArray(responsesBody.input) || responsesBody.input.length === 0) {
responsesBody.input = [
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '' }],
},
]
}
const systemText = convertSystemPrompt(params.system)
if (systemText) {
responsesBody.instructions = systemText
}
if (body.max_tokens !== undefined) {
responsesBody.max_output_tokens = body.max_tokens
} else if (body.max_completion_tokens !== undefined) {
responsesBody.max_output_tokens = body.max_completion_tokens
}
if (params.temperature !== undefined) responsesBody.temperature = params.temperature
if (params.top_p !== undefined) responsesBody.top_p = params.top_p
if (!omitResponsesTools && params.tools && params.tools.length > 0) {
const convertedTools = convertToolsToResponsesTools(
params.tools as Array<{
name?: string
description?: string
input_schema?: Record<string, unknown>
}>,
)
if (convertedTools.length > 0) {
responsesBody.tools = convertedTools
}
}
return responsesBody
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...this.defaultHeaders,
@@ -1656,6 +1728,15 @@ class OpenAIShimMessages {
this.providerOverride?.apiKey ??
process.env.OPENAI_API_KEY ??
(isMiniMax ? process.env.MINIMAX_API_KEY : '')
const configuredAuthHeaderValue = process.env.OPENAI_AUTH_HEADER_VALUE?.trim()
const customAuthHeader = process.env.OPENAI_AUTH_HEADER?.trim()
const hasCustomAuthHeader = Boolean(
customAuthHeader &&
/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(customAuthHeader),
)
const authValue = hasCustomAuthHeader
? configuredAuthHeaderValue || apiKey
: apiKey
// Detect Azure endpoints by hostname (not raw URL) to prevent bypass via
// path segments like https://evil.com/cognitiveservices.azure.com/
let isAzure = false
@@ -1670,15 +1751,27 @@ class OpenAIShimMessages {
isBankr = request.baseUrl.toLowerCase().includes('bankr')
} catch { /* malformed URL — not Bankr */ }
if (apiKey) {
if (isAzure) {
if (authValue) {
if (hasCustomAuthHeader && customAuthHeader) {
const defaultCustomAuthScheme =
customAuthHeader.toLowerCase() === 'authorization' ? 'bearer' : 'raw'
const customAuthScheme =
process.env.OPENAI_AUTH_SCHEME === 'raw' ||
process.env.OPENAI_AUTH_SCHEME === 'bearer'
? process.env.OPENAI_AUTH_SCHEME
: defaultCustomAuthScheme
headers[customAuthHeader] =
customAuthScheme === 'bearer'
? `Bearer ${authValue}`
: authValue
} else if (isAzure) {
// Azure uses api-key header instead of Bearer token
headers['api-key'] = apiKey
headers['api-key'] = authValue
} else if (isBankr) {
// Bankr uses X-API-Key header instead of Bearer token
headers['X-API-Key'] = apiKey
headers['X-API-Key'] = authValue
} else {
headers.Authorization = `Bearer ${apiKey}`
headers.Authorization = `Bearer ${authValue}`
}
} else if (isGemini) {
const geminiCredential = await resolveGeminiCredential(process.env)
@@ -1725,8 +1818,13 @@ class OpenAIShimMessages {
? getLocalProviderRetryBaseUrls(request.baseUrl)
: []
const buildRequestUrl = (baseUrl: string): string =>
request.transport === 'responses'
? `${baseUrl}/responses`
: buildChatCompletionsUrl(baseUrl)
let activeBaseUrl = request.baseUrl
let chatCompletionsUrl = buildChatCompletionsUrl(activeBaseUrl)
let requestUrl = buildRequestUrl(activeBaseUrl)
const attemptedLocalBaseUrls = new Set<string>([activeBaseUrl])
let didRetryWithoutTools = false
@@ -1738,13 +1836,13 @@ class OpenAIShimMessages {
continue
}
const previousUrl = chatCompletionsUrl
const previousUrl = requestUrl
attemptedLocalBaseUrls.add(candidateBaseUrl)
activeBaseUrl = candidateBaseUrl
chatCompletionsUrl = buildChatCompletionsUrl(activeBaseUrl)
requestUrl = buildRequestUrl(activeBaseUrl)
logForDebugging(
`[OpenAIShim] self-heal retry reason=${reason} method=POST from=${redactUrlForDiagnostics(previousUrl)} to=${redactUrlForDiagnostics(chatCompletionsUrl)} model=${request.resolvedModel}`,
`[OpenAIShim] self-heal retry reason=${reason} method=POST from=${redactUrlForDiagnostics(previousUrl)} to=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`,
{ level: 'warn' },
)
@@ -1754,10 +1852,14 @@ class OpenAIShimMessages {
return false
}
let serializedBody = JSON.stringify(body)
let serializedBody = JSON.stringify(
request.transport === 'responses' ? buildResponsesBody() : body,
)
const refreshSerializedBody = (): void => {
serializedBody = JSON.stringify(body)
serializedBody = JSON.stringify(
request.transport === 'responses' ? buildResponsesBody() : body,
)
}
const buildFetchInit = () => ({
@@ -1852,7 +1954,7 @@ class OpenAIShimMessages {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
response = await fetchWithProxyRetry(
chatCompletionsUrl,
requestUrl,
buildFetchInit(),
)
} catch (error) {
@@ -1871,7 +1973,7 @@ class OpenAIShimMessages {
}
const failure = classifyOpenAINetworkFailure(error, {
url: chatCompletionsUrl,
url: requestUrl,
})
if (
@@ -1882,7 +1984,7 @@ class OpenAIShimMessages {
continue
}
throwClassifiedTransportError(error, chatCompletionsUrl, failure)
throwClassifiedTransportError(error, requestUrl, failure)
}
if (response.ok) {
@@ -1927,50 +2029,7 @@ class OpenAIShimMessages {
if (isGithub && response.status === 400) {
if (errorBody.includes('/chat/completions') || errorBody.includes('not accessible')) {
const responsesUrl = `${request.baseUrl}/responses`
const responsesBody: Record<string, unknown> = {
model: request.resolvedModel,
input: convertAnthropicMessagesToResponsesInput(
params.messages as Array<{
role?: string
message?: { role?: string; content?: unknown }
content?: unknown
}>,
),
stream: params.stream ?? false,
store: false,
}
if (!Array.isArray(responsesBody.input) || responsesBody.input.length === 0) {
responsesBody.input = [
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '' }],
},
]
}
const systemText = convertSystemPrompt(params.system)
if (systemText) {
responsesBody.instructions = systemText
}
if (body.max_tokens !== undefined) {
responsesBody.max_output_tokens = body.max_tokens
}
if (params.tools && params.tools.length > 0) {
const convertedTools = convertToolsToResponsesTools(
params.tools as Array<{
name?: string
description?: string
input_schema?: Record<string, unknown>
}>,
)
if (convertedTools.length > 0) {
responsesBody.tools = convertedTools
}
}
const responsesBody = buildResponsesBody()
let responsesResponse: Response
try {
@@ -2020,8 +2079,9 @@ class OpenAIShimMessages {
}
const hasToolsPayload =
Array.isArray(body.tools) &&
body.tools.length > 0
request.transport === 'responses'
? Array.isArray(params.tools) && params.tools.length > 0
: Array.isArray(body.tools) && body.tools.length > 0
if (
!didRetryWithoutTools &&
@@ -2034,10 +2094,11 @@ class OpenAIShimMessages {
didRetryWithoutTools = true
delete body.tools
delete body.tool_choice
omitResponsesTools = true
refreshSerializedBody()
logForDebugging(
`[OpenAIShim] self-heal retry reason=tool_call_incompatible mode=toolless method=POST url=${redactUrlForDiagnostics(chatCompletionsUrl)} model=${request.resolvedModel}`,
`[OpenAIShim] self-heal retry reason=tool_call_incompatible mode=toolless method=POST url=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`,
{ level: 'warn' },
)
continue
@@ -2050,7 +2111,7 @@ class OpenAIShimMessages {
errorBody,
errorResponse,
response.headers as unknown as Headers,
chatCompletionsUrl,
requestUrl,
rateHint,
failure,
)