refactor(openai-shim): extract Codex dispatch (#2074)

* refactor(openai-shim): extract Codex dispatch

* test(openai-shim): cover Codex dispatch guards
This commit is contained in:
JATMN
2026-08-10 11:10:02 +08:00
committed by GitHub
parent 7fae0ffee0
commit 93dbc72cbd
5 changed files with 528 additions and 280 deletions
+1 -7
View File
@@ -8,16 +8,10 @@ import {
import type { LocalCommandCall } from '../../types/command.js'
import { logForDebugging } from '../../utils/debug.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { COPILOT_HEADERS } from '../../services/github/deviceFlow.js'
import { hydrateGithubModelsTokenFromSecureStorage } from '../../utils/githubModelsCredentials.js'
import { getMainLoopModel } from '../../utils/model/model.js'
const COPILOT_HEADERS: Record<string, string> = {
'User-Agent': 'GitHubCopilotChat/0.26.7',
'Editor-Version': 'vscode/1.99.3',
'Editor-Plugin-Version': 'copilot-chat/0.26.7',
'Copilot-Integration-Id': 'vscode-chat',
}
// Large system prompt (~6000 chars, ~1500 tokens) to cross the 1024-token cache threshold
const SYSTEM_PROMPT = [
'You are a coding assistant. Answer concisely.',
-122
View File
@@ -5878,53 +5878,6 @@ function makeCodexSseResponse(responseData: Record<string, unknown>): Response {
return makeSseResponse([`event: response.completed\ndata: ${data}\n\n`])
}
test('GitHub Copilot codex responses transport does not replay after a pre-header timeout', async () => {
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com'
process.env.OPENAI_API_KEY = 'test-token'
process.env.API_TIMEOUT_MS = '20'
let fetchCalls = 0
const requestUrls: string[] = []
globalThis.fetch = (async (input, init) => {
fetchCalls++
requestUrls.push(String(input))
return pendingFetchUntilAbort(init)
}) as unknown as FetchType
const safety = new AbortController()
const safetyTimer = setTimeout(() => safety.abort(), 500)
const client = createOpenAIShimClient({}) as OpenAIShimClient
let caught: unknown
try {
await waitForPromise(
client.beta.messages.create(
{
model: 'gpt-5',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 32,
stream: false,
},
{ signal: safety.signal },
),
750,
'GitHub codex responses timeout did not settle',
)
} catch (error) {
caught = error
} finally {
clearTimeout(safetyTimer)
}
expect(caught).toBeDefined()
const error = caught as Error & { constructor: { name: string } }
expect(error.constructor.name).toBe('APIConnectionError')
expect(isOpenAIRequestNonReplayable(error)).toBe(true)
expect(fetchCalls).toBe(1)
expect(requestUrls).toEqual([
'https://api.githubcopilot.com/responses',
])
})
test('GitHub Copilot responses fallback does not replay after a pre-header timeout', async () => {
process.env.CLAUDE_CODE_USE_GITHUB = '1'
@@ -6070,81 +6023,6 @@ test('GitHub Copilot responses fallback does not retry non-retryable HTTP failur
// openaiShim test extraction seam 187 start: GitHub Copilot 401 codex_responses retries with refreshed token
test('GitHub Copilot 401 codex_responses retries with refreshed token', async () => {
const realGithubModule = realGithubModelsCredentials
try {
const refreshSpy = mock(async () => {
process.env.GITHUB_TOKEN = 'refreshed-token'
process.env.OPENAI_API_KEY = 'refreshed-token'
return true
})
mock.module('../../utils/githubModelsCredentials.js', () => ({
...realGithubModule,
refreshCopilotTokenOn401: refreshSpy,
}))
let codexCallCount = 0
let firstAuth: string | undefined
let secondAuth: string | undefined
globalThis.fetch = ((_, init) => {
codexCallCount++
const headers = new Headers(init?.headers)
const apiKey = headers.get('authorization')?.replace(/^Bearer /, '')
if (codexCallCount === 1) {
firstAuth = apiKey
return Promise.resolve(new Response(JSON.stringify({ error: { message: 'token expired' } }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
}))
}
if (codexCallCount === 2) {
secondAuth = apiKey
return Promise.resolve(makeCodexSseResponse({
response: {
id: 'resp_test',
output: [{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] }],
model: 'gpt-5',
usage: { input_tokens: 10, output_tokens: 5 },
},
}))
}
throw new Error(`unexpected codex call #${codexCallCount}`)
}) as unknown as FetchType
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com'
process.env.OPENAI_API_KEY = 'initial-token'
process.env.GITHUB_TOKEN = 'initial-token'
const { createOpenAIShimClient: createClient } =
await importFreshOpenAIShim('copilot-401-retry-codex')
const client = createClient({}) as OpenAIShimClient
const response = await client.beta.messages.create({
model: 'gpt-5',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 32,
stream: false,
})
expect(refreshSpy).toHaveBeenCalledTimes(1)
expect(process.env.GITHUB_TOKEN).toBe('refreshed-token')
expect(process.env.OPENAI_API_KEY).toBe('refreshed-token')
expect(codexCallCount).toBe(2)
expect(firstAuth).toBe('initial-token')
expect(secondAuth).toBe('refreshed-token')
expect(response).toBeDefined()
expect((response as Record<string, unknown>).content).toBeDefined()
} finally {
mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule)
}
})
// openaiShim test extraction seam 193 end
+24 -151
View File
@@ -37,15 +37,12 @@
*/
import { APIError } from '@anthropic-ai/sdk'
import {
readCodexCredentialsAsync,
refreshCodexAccessTokenIfNeeded,
} from '../../utils/codexCredentials.js'
import { logForDebugging } from '../../utils/debug.js'
import { createStreamAbortError, getStreamIdleTimeoutMs, readWithIdleTimeout, StreamIdleTimeoutError } from './openaiShim/streamControl.js'
export { getStreamIdleTimeoutMs } from './openaiShim/streamControl.js'
import { isBareMode, isEnvTruthy } from '../../utils/envUtils.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { type OpenAIShimEffortLevel } from '../../utils/effort.js'
import { COPILOT_HEADERS } from '../github/deviceFlow.js'
import { resolveGeminiCredential } from '../../utils/geminiAuth.js'
import { hydrateGeminiAccessTokenFromSecureStorage } from '../../utils/geminiCredentials.js'
import {
@@ -64,10 +61,10 @@ import {
codexStreamToAnthropic,
collectCodexCompletedResponse,
convertCodexResponseToAnthropicMessage,
performCodexRequest,
type AnthropicStreamEvent,
type ShimCreateParams,
} from './codexShim.js'
import { dispatchCodexRequest } from './openaiShim/codexDispatch.js'
import { hydrateOpenAIShimCompatibilityEnv as hydrateRequestPlanningEnv } from './openaiShim/requestPlanner.js'
import { prepareOpenAIRequest } from './openaiShim/requestPreparation.js'
import {
@@ -92,10 +89,8 @@ export { getApiTimeoutMs } from './openaiShim/transport.js'
import { executeOpenAIRequest } from './openaiShim/requestExecutor.js'
import {
getLocalProviderRetryBaseUrls,
getGithubEndpointType,
isAzureStyleBaseUrl,
isLocalProviderUrl,
resolveRuntimeCodexCredentials,
resolveProviderRequest,
shouldAttemptLocalToollessRetry,
} from './providerConfig.js'
@@ -105,7 +100,7 @@ import {
classifyOpenAINetworkFailure,
markOpenAIRequestNonReplayable,
} from './openaiErrorClassification.js'
import { redactSecretValueForDisplay, type SecretValueSource } from '../../utils/providerProfile.js'
import { redactSecretValueForDisplay } from '../../utils/providerProfile.js'
import { logApiCallStart, logApiCallEnd } from '../../utils/requestLogging.js'
import {
createStreamState,
@@ -154,13 +149,6 @@ const GITHUB_429_MAX_DELAY_SEC = 32
const CREDENTIAL_POOL_COOLDOWN_MS = 30_000
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 90_000
const GEMINI_API_HOST = 'generativelanguage.googleapis.com'
const COPILOT_HEADERS: Record<string, string> = {
'User-Agent': 'GitHubCopilotChat/0.26.7',
'Editor-Version': 'vscode/1.99.3',
'Editor-Plugin-Version': 'copilot-chat/0.26.7',
'Copilot-Integration-Id': 'vscode-chat',
}
function isCopilotTokenExpiredError(text: string): boolean {
const lower = text.toLowerCase()
return lower.includes('token expired') || lower.includes('token has expired')
@@ -461,143 +449,28 @@ class OpenAIShimMessages {
options?: { signal?: AbortSignal; headers?: Record<string, string> },
requestProcessEnv: NodeJS.ProcessEnv = process.env,
): Promise<Response> {
const githubEndpointType = getGithubEndpointType(request.baseUrl)
const isGithubMode = isGithubModelsMode()
const isGithubCopilotEndpoint = isGithubMode && (githubEndpointType === 'copilot' || githubEndpointType === 'ghe')
const isGithubWithCodexTransport = isGithubCopilotEndpoint && request.transport === 'codex_responses'
if (isGithubWithCodexTransport) {
const apiTimeoutMs = getApiTimeoutMs()
const responsesUrl = `${request.baseUrl}/responses`
let didRefreshCopilotCodexToken = false
let refreshedCopilotCodexToken: string | undefined
for (let attempt = 0; attempt < 2; attempt++) {
const apiKey = refreshedCopilotCodexToken ?? this.providerOverride?.apiKey ?? process.env.OPENAI_API_KEY ?? ''
if (!apiKey) {
throw new Error(
'GitHub Copilot auth is required. Run /onboard-github to sign in.',
)
}
try {
try {
return await performCodexRequest({
request,
credentials: {
apiKey,
source: 'env',
},
params,
defaultHeaders: {
...this.defaultHeaders,
...filterAnthropicHeaders(options?.headers),
...COPILOT_HEADERS,
},
signal: options?.signal,
fetcher: (input, init) => {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url
return fetchWithHeadersDeadline(url, init ?? {}, {
callerSignal: options?.signal,
timeoutMs: apiTimeoutMs,
})
},
})
} catch (error) {
if (options?.signal?.aborted) {
throw preserveCallerAbortError(error, options.signal)
}
if (error instanceof ResponseHeadersTimeoutError) {
const failure = {
...classifyOpenAINetworkFailure(error, {
url: responsesUrl,
}),
retryable: false,
}
throw createClassifiedTransportError(
error,
responsesUrl,
request.resolvedModel,
failure,
)
}
throw error
}
} catch (error) {
if (
!didRefreshCopilotCodexToken &&
error instanceof APIError &&
error.status === 401
) {
if (
apiKey === (process.env.OPENAI_API_KEY ?? '') &&
isCopilotTokenExpiredError(error.message)
) {
didRefreshCopilotCodexToken = true
const refreshed = await refreshCopilotTokenOn401()
if (refreshed) {
const newApiKey = process.env.OPENAI_API_KEY?.trim() || ''
if (newApiKey && newApiKey !== apiKey) {
refreshedCopilotCodexToken = newApiKey
continue
}
}
}
}
throw error
}
}
}
if (request.transport === 'codex_responses' && !isGithubMode) {
const refreshResult = await refreshCodexAccessTokenIfNeeded().catch(
async error => {
logForDebugging(
`[codex] access token refresh failed before request: ${error instanceof Error ? error.message : String(error)}`,
{ level: 'warn' },
)
return {
refreshed: false,
credentials: await readCodexCredentialsAsync(),
const codexResponse = await dispatchCodexRequest({
request,
params,
requestOptions: options,
defaultHeaders: this.defaultHeaders,
providerOverrideApiKey: this.providerOverride?.apiKey,
dependencies: {
getApiTimeoutMs,
fetchWithHeadersDeadline,
preserveCallerAbortError,
isCopilotTokenExpiredError,
classifyResponseHeadersTimeout: (error, requestUrl, model) => {
if (!(error instanceof ResponseHeadersTimeoutError)) return undefined
const failure = {
...classifyOpenAINetworkFailure(error, { url: requestUrl }),
retryable: false,
}
return createClassifiedTransportError(error, requestUrl, model, failure)
},
)
const credentials = resolveRuntimeCodexCredentials({
storedCredentials: refreshResult.credentials,
})
if (!credentials.apiKey) {
const oauthHint = isBareMode() ? '' : ', choose Codex OAuth in /provider'
const authHint = credentials.authPath
? `${oauthHint} or place a Codex auth.json at ${credentials.authPath}`
: oauthHint
const safeModel =
redactSecretValueForDisplay(request.requestedModel, process.env as SecretValueSource) ??
'the requested model'
throw new Error(
`Codex auth is required for ${safeModel}. Set CODEX_API_KEY${authHint}.`,
)
}
if (!credentials.accountId) {
throw new Error(
'Codex auth is missing chatgpt_account_id. Re-login with Codex OAuth, the Codex CLI, or set CHATGPT_ACCOUNT_ID/CODEX_ACCOUNT_ID.',
)
}
return performCodexRequest({
request,
credentials,
params,
defaultHeaders: {
...this.defaultHeaders,
...filterAnthropicHeaders(options?.headers),
},
signal: options?.signal,
})
}
},
})
if (codexResponse) return codexResponse
return this._doOpenAIRequest(request, params, options, requestProcessEnv)
}
@@ -0,0 +1,289 @@
import { APIError } from '@anthropic-ai/sdk'
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
import {
acquireSharedMutationLock,
releaseSharedMutationLock,
} from '../../../test/sharedMutationLock.js'
import type { performCodexRequest } from '../codexShim.js'
import type { ResolvedProviderRequest } from '../providerConfig.js'
import {
dispatchCodexRequest,
type CodexDispatchDependencies,
} from './codexDispatch.js'
const originalEnv = {
CHATGPT_ACCOUNT_ID: process.env.CHATGPT_ACCOUNT_ID,
CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB,
CODEX_ACCOUNT_ID: process.env.CODEX_ACCOUNT_ID,
CODEX_API_KEY: process.env.CODEX_API_KEY,
CODEX_AUTH_JSON_PATH: process.env.CODEX_AUTH_JSON_PATH,
CODEX_HOME: process.env.CODEX_HOME,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
}
function restoreEnv(key: keyof typeof originalEnv): void {
const value = originalEnv[key]
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
beforeEach(async () => {
await acquireSharedMutationLock('openaiShim/codexDispatch.test.ts')
delete process.env.CHATGPT_ACCOUNT_ID
delete process.env.CLAUDE_CODE_USE_GITHUB
delete process.env.CODEX_ACCOUNT_ID
delete process.env.CODEX_API_KEY
delete process.env.CODEX_AUTH_JSON_PATH
delete process.env.CODEX_HOME
delete process.env.OPENAI_API_KEY
})
afterEach(() => {
for (const key of Object.keys(originalEnv) as Array<keyof typeof originalEnv>) {
restoreEnv(key)
}
releaseSharedMutationLock()
})
const params = {
model: 'gpt-5',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 32,
}
function request(
transport: ResolvedProviderRequest['transport'],
baseUrl = 'https://api.openai.com/v1',
): ResolvedProviderRequest {
return {
transport,
requestedModel: 'gpt-5',
resolvedModel: 'gpt-5',
baseUrl,
}
}
const dependencies: CodexDispatchDependencies = {
classifyResponseHeadersTimeout: () => undefined,
fetchWithHeadersDeadline: () => Promise.reject(new Error('not expected')),
getApiTimeoutMs: () => 1_000,
isCopilotTokenExpiredError: text => text.includes('expired'),
preserveCallerAbortError: error => error,
}
test('returns null when the ordinary OpenAI dispatcher owns the request', async () => {
const response = await dispatchCodexRequest({
request: request('chat_completions'),
params,
defaultHeaders: {},
dependencies,
operations: { isGithubModelsMode: () => false },
})
expect(response).toBeNull()
})
test('refreshes an expired GitHub Copilot token once before retrying', async () => {
process.env.OPENAI_API_KEY = 'initial-token'
const observedKeys: string[] = []
const perform = mock(async options => {
observedKeys.push(options.credentials.apiKey)
if (observedKeys.length === 1) {
throw APIError.generate(401, undefined, 'token expired', new Headers())
}
return new Response('ok')
}) as unknown as typeof performCodexRequest
const refresh = mock(async () => {
process.env.OPENAI_API_KEY = 'refreshed-token'
return true
})
const response = await dispatchCodexRequest({
request: request('codex_responses', 'https://api.githubcopilot.com'),
params,
defaultHeaders: {},
dependencies,
operations: {
isGithubModelsMode: () => true,
performCodexRequest: perform,
refreshCopilotTokenOn401: refresh,
},
})
expect(await response?.text()).toBe('ok')
expect(observedKeys).toEqual(['initial-token', 'refreshed-token'])
expect(refresh).toHaveBeenCalledTimes(1)
})
test('does not retry a Copilot request when token refresh fails', async () => {
process.env.OPENAI_API_KEY = 'initial-token'
const error = APIError.generate(401, undefined, 'token expired', new Headers())
const perform = mock(async () => {
throw error
}) as unknown as typeof performCodexRequest
const refresh = mock(async () => false)
await expect(dispatchCodexRequest({
request: request('codex_responses', 'https://api.githubcopilot.com'),
params,
defaultHeaders: {},
dependencies,
operations: {
isGithubModelsMode: () => true,
performCodexRequest: perform,
refreshCopilotTokenOn401: refresh,
},
})).rejects.toBe(error)
expect(perform).toHaveBeenCalledTimes(1)
expect(refresh).toHaveBeenCalledTimes(1)
})
test('does not retry a Copilot request when refresh returns the same token', async () => {
process.env.OPENAI_API_KEY = 'unchanged-token'
const error = APIError.generate(401, undefined, 'token expired', new Headers())
const perform = mock(async () => {
throw error
}) as unknown as typeof performCodexRequest
const refresh = mock(async () => true)
await expect(dispatchCodexRequest({
request: request('codex_responses', 'https://api.githubcopilot.com'),
params,
defaultHeaders: {},
dependencies,
operations: {
isGithubModelsMode: () => true,
performCodexRequest: perform,
refreshCopilotTokenOn401: refresh,
},
})).rejects.toBe(error)
expect(perform).toHaveBeenCalledTimes(1)
expect(refresh).toHaveBeenCalledTimes(1)
})
test('preserves a caller abort while dispatching a Copilot request', async () => {
process.env.OPENAI_API_KEY = 'test-token'
const controller = new AbortController()
const preserved = new Error('preserved caller abort')
controller.abort()
const requestError = new Error('request aborted')
const perform = mock(async () => {
throw requestError
}) as unknown as typeof performCodexRequest
const preserveCallerAbortError = mock(() => preserved)
await expect(dispatchCodexRequest({
request: request('codex_responses', 'https://api.githubcopilot.com'),
params,
requestOptions: { signal: controller.signal },
defaultHeaders: {},
dependencies: { ...dependencies, preserveCallerAbortError },
operations: {
isGithubModelsMode: () => true,
performCodexRequest: perform,
},
})).rejects.toBe(preserved)
expect(perform).toHaveBeenCalledTimes(1)
expect(preserveCallerAbortError).toHaveBeenCalledWith(requestError, controller.signal)
})
test('rejects a Copilot request without credentials before dispatch', async () => {
const perform = mock(async () => new Response('unexpected')) as unknown as typeof performCodexRequest
await expect(dispatchCodexRequest({
request: request('codex_responses', 'https://api.githubcopilot.com'),
params,
defaultHeaders: {},
dependencies,
operations: {
isGithubModelsMode: () => true,
performCodexRequest: perform,
},
})).rejects.toThrow('/onboard-github')
expect(perform).not.toHaveBeenCalled()
})
test('classifies a GitHub Copilot pre-header timeout without replaying', async () => {
process.env.OPENAI_API_KEY = 'test-token'
const timeout = new Error('headers timed out')
const classified = new Error('classified non-replayable timeout')
const fetchWithDeadline = mock(async () => {
throw timeout
})
const classify = mock((error: unknown) =>
error === timeout ? classified : undefined,
)
const perform = mock(async options =>
options.fetcher?.('https://api.githubcopilot.com/responses', {}),
) as unknown as typeof performCodexRequest
await expect(dispatchCodexRequest({
request: request('codex_responses', 'https://api.githubcopilot.com'),
params,
defaultHeaders: {},
dependencies: {
...dependencies,
classifyResponseHeadersTimeout: classify,
fetchWithHeadersDeadline: fetchWithDeadline,
},
operations: {
isGithubModelsMode: () => true,
performCodexRequest: perform,
},
})).rejects.toBe(classified)
expect(fetchWithDeadline).toHaveBeenCalledTimes(1)
expect(classify).toHaveBeenCalledTimes(1)
})
test('uses refreshed first-party Codex credentials for dispatch', async () => {
let observedCredentials: { apiKey: string; accountId?: string } | undefined
const perform = mock(async options => {
observedCredentials = options.credentials
return new Response('ok')
}) as unknown as typeof performCodexRequest
const response = await dispatchCodexRequest({
request: request('codex_responses', 'https://chatgpt.com/backend-api/codex'),
params,
defaultHeaders: {},
dependencies,
operations: {
isGithubModelsMode: () => false,
performCodexRequest: perform,
refreshCodexAccessTokenIfNeeded: async () => ({
refreshed: true,
credentials: {
accessToken: 'codex-access-token',
accountId: 'account-1',
},
}),
},
})
expect(await response?.text()).toBe('ok')
expect(observedCredentials).toMatchObject({
apiKey: 'codex-access-token',
accountId: 'account-1',
})
})
test('rejects first-party Codex credentials without an account id', async () => {
await expect(dispatchCodexRequest({
request: request('codex_responses', 'https://chatgpt.com/backend-api/codex'),
params,
defaultHeaders: {},
dependencies,
operations: {
isGithubModelsMode: () => false,
refreshCodexAccessTokenIfNeeded: async () => ({
refreshed: false,
credentials: { accessToken: 'codex-access-token' },
}),
},
})).rejects.toThrow('Codex auth is missing chatgpt_account_id')
})
@@ -0,0 +1,214 @@
import { APIError } from '@anthropic-ai/sdk'
import {
readCodexCredentialsAsync,
refreshCodexAccessTokenIfNeeded,
} from '../../../utils/codexCredentials.js'
import { logForDebugging } from '../../../utils/debug.js'
import { isBareMode } from '../../../utils/envUtils.js'
import { COPILOT_HEADERS } from '../../github/deviceFlow.js'
import { refreshCopilotTokenOn401 } from '../../../utils/githubModelsCredentials.js'
import {
performCodexRequest,
type ShimCreateParams,
} from '../codexShim.js'
import {
getGithubEndpointType,
resolveRuntimeCodexCredentials,
type ResolvedProviderRequest,
} from '../providerConfig.js'
import {
redactSecretValueForDisplay,
type SecretValueSource,
} from '../../../utils/providerProfile.js'
import {
filterAnthropicHeaders,
isGithubModelsMode,
} from './providerCompatibility.js'
type PerformCodexRequest = typeof performCodexRequest
type ResponseHeadersTimeoutClassification = Error | undefined
export type CodexDispatchDependencies = {
classifyResponseHeadersTimeout(
error: unknown,
requestUrl: string,
model: string,
): ResponseHeadersTimeoutClassification
fetchWithHeadersDeadline(
url: string,
init: RequestInit,
options: { callerSignal?: AbortSignal; timeoutMs: number },
): Promise<Response>
getApiTimeoutMs(): number
isCopilotTokenExpiredError(text: string): boolean
preserveCallerAbortError(error: unknown, callerSignal: AbortSignal): unknown
}
type CodexDispatchOperations = {
isGithubModelsMode: typeof isGithubModelsMode
performCodexRequest: PerformCodexRequest
readCodexCredentialsAsync: typeof readCodexCredentialsAsync
refreshCodexAccessTokenIfNeeded: typeof refreshCodexAccessTokenIfNeeded
refreshCopilotTokenOn401: typeof refreshCopilotTokenOn401
}
const defaultOperations: CodexDispatchOperations = {
isGithubModelsMode,
performCodexRequest,
readCodexCredentialsAsync,
refreshCodexAccessTokenIfNeeded,
refreshCopilotTokenOn401,
}
export async function dispatchCodexRequest(options: {
request: ResolvedProviderRequest
params: ShimCreateParams
requestOptions?: { signal?: AbortSignal; headers?: Record<string, string> }
defaultHeaders: Record<string, string>
providerOverrideApiKey?: string
dependencies: CodexDispatchDependencies
operations?: Partial<CodexDispatchOperations>
}): Promise<Response | null> {
const {
request,
params,
requestOptions,
defaultHeaders,
providerOverrideApiKey,
dependencies,
} = options
const operations = { ...defaultOperations, ...options.operations }
const githubEndpointType = getGithubEndpointType(request.baseUrl)
const isGithubMode = operations.isGithubModelsMode()
const isGithubCopilotEndpoint =
isGithubMode &&
(githubEndpointType === 'copilot' || githubEndpointType === 'ghe')
if (isGithubCopilotEndpoint && request.transport === 'codex_responses') {
const apiTimeoutMs = dependencies.getApiTimeoutMs()
const responsesUrl = `${request.baseUrl}/responses`
let didRefreshToken = false
let refreshedToken: string | undefined
for (let attempt = 0; attempt < 2; attempt++) {
const apiKey =
refreshedToken ??
providerOverrideApiKey ??
process.env.OPENAI_API_KEY ??
''
if (!apiKey) {
throw new Error(
'GitHub Copilot auth is required. Run /onboard-github to sign in.',
)
}
try {
try {
return await operations.performCodexRequest({
request,
credentials: { apiKey, source: 'env' },
params,
defaultHeaders: {
...defaultHeaders,
...filterAnthropicHeaders(requestOptions?.headers),
...COPILOT_HEADERS,
},
signal: requestOptions?.signal,
fetcher: (input, init) => {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url
return dependencies.fetchWithHeadersDeadline(url, init ?? {}, {
callerSignal: requestOptions?.signal,
timeoutMs: apiTimeoutMs,
})
},
})
} catch (error) {
if (requestOptions?.signal?.aborted) {
throw dependencies.preserveCallerAbortError(
error,
requestOptions.signal,
)
}
const timeoutError = dependencies.classifyResponseHeadersTimeout(
error,
responsesUrl,
request.resolvedModel,
)
if (timeoutError !== undefined) throw timeoutError
throw error
}
} catch (error) {
if (
!didRefreshToken &&
error instanceof APIError &&
error.status === 401 &&
apiKey === (process.env.OPENAI_API_KEY ?? '') &&
dependencies.isCopilotTokenExpiredError(error.message)
) {
didRefreshToken = true
if (await operations.refreshCopilotTokenOn401()) {
const newApiKey = process.env.OPENAI_API_KEY?.trim() || ''
if (newApiKey && newApiKey !== apiKey) {
refreshedToken = newApiKey
continue
}
}
}
throw error
}
}
}
if (request.transport !== 'codex_responses' || isGithubMode) return null
const refreshResult = await operations.refreshCodexAccessTokenIfNeeded().catch(
async error => {
logForDebugging(
`[codex] access token refresh failed before request: ${error instanceof Error ? error.message : String(error)}`,
{ level: 'warn' },
)
return {
refreshed: false,
credentials: await operations.readCodexCredentialsAsync(),
}
},
)
const credentials = resolveRuntimeCodexCredentials({
storedCredentials: refreshResult.credentials,
})
if (!credentials.apiKey) {
const oauthHint = isBareMode() ? '' : ', choose Codex OAuth in /provider'
const authHint = credentials.authPath
? `${oauthHint} or place a Codex auth.json at ${credentials.authPath}`
: oauthHint
const safeModel =
redactSecretValueForDisplay(
request.requestedModel,
process.env as SecretValueSource,
) ?? 'the requested model'
throw new Error(
`Codex auth is required for ${safeModel}. Set CODEX_API_KEY${authHint}.`,
)
}
if (!credentials.accountId) {
throw new Error(
'Codex auth is missing chatgpt_account_id. Re-login with Codex OAuth, the Codex CLI, or set CHATGPT_ACCOUNT_ID/CODEX_ACCOUNT_ID.',
)
}
return operations.performCodexRequest({
request,
credentials,
params,
defaultHeaders: {
...defaultHeaders,
...filterAnthropicHeaders(requestOptions?.headers),
},
signal: requestOptions?.signal,
})
}