fix: guard rawBaseUrl against the literal string "undefined" from env vars (#340)

On Windows, shells can set OPENAI_BASE_URL to the literal string
"undefined" when the variable is referenced without quotes while unset.
The nullish-coalescing operator (??) does not catch this because
"undefined" is a truthy string, causing resolveProviderRequest() to
treat it as a real base URL. This broke the Codex transport check:
(!rawBaseUrl && isCodexAlias(model)) evaluated as (false || true) = false
so the transport was incorrectly set to chat_completions (issue #336).

Fix: introduce asEnvUrl() which trims the value and rejects both empty
strings and the sentinel string "undefined". Use it for all three
rawBaseUrl sources (options.baseUrl, OPENAI_BASE_URL, OPENAI_API_BASE).

Tests: add three new cases to the 'Codex provider config' describe block
covering the empty-string, "undefined"-string, and options-override
scenarios. Also add beforeEach/afterEach guards so individual tests
cannot contaminate each other via env var state.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
JiayuWang(王嘉宇)
2026-04-04 22:37:59 +08:00
committed by GitHub
parent 0951c8bc59
commit e4cf810e14
2 changed files with 53 additions and 6 deletions

View File

@@ -1,4 +1,4 @@
import { afterEach, describe, expect, test } from 'bun:test'
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
@@ -46,6 +46,21 @@ async function collectStreamEventTypes(responseText: string): Promise<string[]>
}
describe('Codex provider config', () => {
const originalOpenaiBaseUrl = process.env.OPENAI_BASE_URL
const originalOpenaiApiBase = process.env.OPENAI_API_BASE
beforeEach(() => {
delete process.env.OPENAI_BASE_URL
delete process.env.OPENAI_API_BASE
})
afterEach(() => {
if (originalOpenaiBaseUrl === undefined) delete process.env.OPENAI_BASE_URL
else process.env.OPENAI_BASE_URL = originalOpenaiBaseUrl
if (originalOpenaiApiBase === undefined) delete process.env.OPENAI_API_BASE
else process.env.OPENAI_API_BASE = originalOpenaiApiBase
})
test('resolves codexplan alias to Codex transport with reasoning', () => {
const resolved = resolveProviderRequest({ model: 'codexplan' })
expect(resolved.transport).toBe('codex_responses')
@@ -53,6 +68,27 @@ describe('Codex provider config', () => {
expect(resolved.reasoning).toEqual({ effort: 'high' })
})
test('resolves codexplan to Codex transport even when OPENAI_BASE_URL is the string "undefined"', () => {
// On Windows, env vars can leak as the literal string "undefined" instead of
// the JS value undefined when not properly unset (issue #336).
process.env.OPENAI_BASE_URL = 'undefined'
const resolved = resolveProviderRequest({ model: 'codexplan' })
expect(resolved.transport).toBe('codex_responses')
})
test('resolves codexplan to Codex transport even when OPENAI_BASE_URL is an empty string', () => {
process.env.OPENAI_BASE_URL = ''
const resolved = resolveProviderRequest({ model: 'codexplan' })
expect(resolved.transport).toBe('codex_responses')
})
test('prefers explicit baseUrl option over env var', () => {
process.env.OPENAI_BASE_URL = 'https://example.com/v1'
const resolved = resolveProviderRequest({ model: 'codexplan', baseUrl: 'https://chatgpt.com/backend-api/codex' })
expect(resolved.transport).toBe('codex_responses')
expect(resolved.baseUrl).toBe('https://chatgpt.com/backend-api/codex')
})
test('loads Codex credentials from auth.json fallback', () => {
const authPath = createTempAuthJson({
tokens: {