Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/@ant/model-provider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,10 @@ export { anthropicMessagesToOpenAI } from './shared/openaiConvertMessages.js'
export type { ConvertMessagesOptions } from './shared/openaiConvertMessages.js'
export { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from './shared/openaiConvertTools.js'
export { adaptOpenAIStreamToAnthropic } from './shared/openaiStreamAdapter.js'

// Codex provider utilities
export { normalizeCodexCallId, resolveCodexCallId, createCodexFallbackCallId } from './providers/codex/callIds.js'
export { resolveCodexModel, resolveCodexMaxTokens } from './providers/codex/modelMapping.js'
export { anthropicMessagesToCodexInput } from './providers/codex/convertMessages.js'
export type { CodexImageConversionOptions } from './providers/codex/convertMessages.js'
export { anthropicToolsToCodex } from './providers/codex/convertTools.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
import { resolveCodexModel } from '../modelMapping.js'

describe('resolveCodexModel', () => {
const originalEnv = {
CODEX_MODEL: process.env.CODEX_MODEL,
CODEX_DEFAULT_HAIKU_MODEL: process.env.CODEX_DEFAULT_HAIKU_MODEL,
CODEX_DEFAULT_SONNET_MODEL: process.env.CODEX_DEFAULT_SONNET_MODEL,
CODEX_DEFAULT_OPUS_MODEL: process.env.CODEX_DEFAULT_OPUS_MODEL,
}

beforeEach(() => {
delete process.env.CODEX_MODEL
delete process.env.CODEX_DEFAULT_HAIKU_MODEL
delete process.env.CODEX_DEFAULT_SONNET_MODEL
delete process.env.CODEX_DEFAULT_OPUS_MODEL
})

afterEach(() => {
Object.assign(process.env, originalEnv)
})
Comment on lines +5 to +21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Node.js / Bun coerce undefined to the string "undefined" when assigning to process.env, or does it delete the key?

💡 Result:

In both Node.js and Bun, assigning undefined to a key in process.env coerces it to the string "undefined", rather than deleting the key. To delete a key, use delete process.env[key]. For Node.js: - Official documentation explicitly states: "Assigning a property on process.env will implicitly convert the value to a string... env.test = undefined; console.log(env.test); // => 'undefined' Use delete to delete a property from process.env." This behavior is deprecated but maintained for compatibility. - Confirmed in GitHub issue #48875 and multiple Stack Overflow answers. For Bun: - Bun aims for Node.js compatibility on process.env. Issues show historical bugs with assignment and delete (e.g., #4383 fixed in 1.0.15, #6762 fixed in 1.0.15), but no evidence of differing behavior on undefined coercion. - Bun docs show programmatic assignment like process.env.FOO = "hello"; and TypeScript typing as string | undefined, consistent with Node.js string coercion. - Recent issues (up to 2026) focus on inlining/build behavior, not runtime assignment differing from Node.js.

Citations:


Fix afterEach to properly delete undefined env vars instead of coercing to the string "undefined".

When restoring environment variables, Object.assign(process.env, originalEnv) coerces undefined values to the string "undefined" instead of deleting the keys. This pollutes subsequent tests: after this suite runs, any test importing this module will see process.env.CODEX_MODEL === "undefined", causing resolveCodexModel(...) to return the string "undefined" for every call.

Explicitly delete keys whose original value was undefined:

🔧 Proposed fix
   afterEach(() => {
-    Object.assign(process.env, originalEnv)
+    for (const [key, value] of Object.entries(originalEnv)) {
+      if (value === undefined) {
+        delete process.env[key]
+      } else {
+        process.env[key] = value
+      }
+    }
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const originalEnv = {
CODEX_MODEL: process.env.CODEX_MODEL,
CODEX_DEFAULT_HAIKU_MODEL: process.env.CODEX_DEFAULT_HAIKU_MODEL,
CODEX_DEFAULT_SONNET_MODEL: process.env.CODEX_DEFAULT_SONNET_MODEL,
CODEX_DEFAULT_OPUS_MODEL: process.env.CODEX_DEFAULT_OPUS_MODEL,
}
beforeEach(() => {
delete process.env.CODEX_MODEL
delete process.env.CODEX_DEFAULT_HAIKU_MODEL
delete process.env.CODEX_DEFAULT_SONNET_MODEL
delete process.env.CODEX_DEFAULT_OPUS_MODEL
})
afterEach(() => {
Object.assign(process.env, originalEnv)
})
const originalEnv = {
CODEX_MODEL: process.env.CODEX_MODEL,
CODEX_DEFAULT_HAIKU_MODEL: process.env.CODEX_DEFAULT_HAIKU_MODEL,
CODEX_DEFAULT_SONNET_MODEL: process.env.CODEX_DEFAULT_SONNET_MODEL,
CODEX_DEFAULT_OPUS_MODEL: process.env.CODEX_DEFAULT_OPUS_MODEL,
}
beforeEach(() => {
delete process.env.CODEX_MODEL
delete process.env.CODEX_DEFAULT_HAIKU_MODEL
delete process.env.CODEX_DEFAULT_SONNET_MODEL
delete process.env.CODEX_DEFAULT_OPUS_MODEL
})
afterEach(() => {
for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/`@ant/model-provider/src/providers/codex/__tests__/modelMapping.test.ts
around lines 5 - 21, The afterEach restores environment by calling
Object.assign(process.env, originalEnv) which turns undefined into the string
"undefined"; change afterEach to iterate the keys of originalEnv (the variables
captured in originalEnv: CODEX_MODEL, CODEX_DEFAULT_HAIKU_MODEL,
CODEX_DEFAULT_SONNET_MODEL, CODEX_DEFAULT_OPUS_MODEL) and for each key do: if
originalEnv[key] === undefined then delete process.env[key] else set
process.env[key] = originalEnv[key]; keep beforeEach and originalEnv as-is and
apply this logic inside the afterEach block so tests don’t get polluted.


test('CODEX_MODEL env var overrides all', () => {
process.env.CODEX_MODEL = 'my-custom-model'
expect(resolveCodexModel('claude-sonnet-4-6')).toBe('my-custom-model')
})

test('CODEX_DEFAULT_SONNET_MODEL overrides default map', () => {
process.env.CODEX_DEFAULT_SONNET_MODEL = 'my-sonnet'
expect(resolveCodexModel('claude-sonnet-4-6')).toBe('my-sonnet')
})

test('CODEX_DEFAULT_HAIKU_MODEL overrides default map', () => {
process.env.CODEX_DEFAULT_HAIKU_MODEL = 'my-haiku'
expect(resolveCodexModel('claude-haiku-4-5-20251001')).toBe('my-haiku')
})

test('CODEX_DEFAULT_OPUS_MODEL overrides default map', () => {
process.env.CODEX_DEFAULT_OPUS_MODEL = 'my-opus'
expect(resolveCodexModel('claude-opus-4-6')).toBe('my-opus')
})

test('maps known sonnet model via DEFAULT_MODEL_MAP', () => {
expect(resolveCodexModel('claude-sonnet-4-6')).toBe('gpt-5.4-mini')
})

test('maps known haiku model via DEFAULT_MODEL_MAP', () => {
expect(resolveCodexModel('claude-haiku-4-5-20251001')).toBe('gpt-5.4-mini')
})

test('maps known opus model via DEFAULT_MODEL_MAP', () => {
expect(resolveCodexModel('claude-opus-4-6')).toBe('gpt-5.4')
})

test('maps legacy sonnet models', () => {
expect(resolveCodexModel('claude-sonnet-4-20250514')).toBe('gpt-5.4-mini')
expect(resolveCodexModel('claude-3-5-sonnet-20241022')).toBe('gpt-5.4-mini')
})

test('maps legacy haiku models', () => {
expect(resolveCodexModel('claude-3-5-haiku-20241022')).toBe('gpt-5.4-mini')
})

test('maps legacy opus models', () => {
expect(resolveCodexModel('claude-opus-4-20250514')).toBe('gpt-5.4')
expect(resolveCodexModel('claude-opus-4-5-20251101')).toBe('gpt-5.4')
})

test('uses family default for unrecognized haiku model', () => {
expect(resolveCodexModel('claude-haiku-99')).toBe('gpt-5.4-mini')
})

test('uses family default for unrecognized sonnet model', () => {
expect(resolveCodexModel('claude-sonnet-99')).toBe('gpt-5.4-mini')
})

test('uses family default for unrecognized opus model', () => {
expect(resolveCodexModel('claude-opus-99')).toBe('gpt-5.4')
})

test('passes through unknown model name without family', () => {
expect(resolveCodexModel('some-random-model')).toBe('some-random-model')
})

test('strips [1m] suffix', () => {
expect(resolveCodexModel('claude-sonnet-4-6[1m]')).toBe('gpt-5.4-mini')
})

test('CODEX_MODEL takes precedence over family-specific vars', () => {
process.env.CODEX_MODEL = 'global-override'
process.env.CODEX_DEFAULT_SONNET_MODEL = 'family-override'
expect(resolveCodexModel('claude-sonnet-4-6')).toBe('global-override')
})
})
31 changes: 31 additions & 0 deletions packages/@ant/model-provider/src/providers/codex/callIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createHash } from 'crypto'

const MAX_CODEX_CALL_ID_LENGTH = 96

export function normalizeCodexCallId(value: unknown): string | null {
if (typeof value !== 'string') {
return null
}

const sanitized = value
.trim()
.replace(/\s+/g, '_')
.replace(/[^A-Za-z0-9._:-]/g, '_')
.replace(/_+/g, '_')
.slice(0, MAX_CODEX_CALL_ID_LENGTH)

return sanitized.length > 0 ? sanitized : null
}

export function createCodexFallbackCallId(seed: string): string {
const hash = createHash('sha1')
.update(seed.length > 0 ? seed : 'codex-call')
.digest('hex')
.slice(0, 24)

return `call_${hash}`
}

export function resolveCodexCallId(value: unknown, seed: string): string {
return normalizeCodexCallId(value) ?? createCodexFallbackCallId(seed)
}
Loading
Loading