-
Notifications
You must be signed in to change notification settings - Fork 15.5k
feat: 新增 Codex 订阅支持 #370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
feat: 新增 Codex 订阅支持 #370
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
bc4a2f1
feat: 添加 ChatGPT OAuth 订阅登录流程
claude-code-best cd59a88
feat: 集成 ChatGPT OAuth 订阅登录到 /login UI
claude-code-best 13799b5
fix: 将 modelType 从 openai-responses 改为 codex 并注册枚举值
claude-code-best 4427a6c
feat: 注册 codex modelType 并添加 /provider codex 切换
claude-code-best d091dd8
feat: 添加 Codex 模型 provider 完整实现
claude-code-best 1058b7e
feat: 修复 Codex 模型映射并添加登录后模型配置面板
claude-code-best File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
packages/@ant/model-provider/src/providers/codex/__tests__/modelMapping.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| }) | ||
|
|
||
| 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
31
packages/@ant/model-provider/src/providers/codex/callIds.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Does Node.js / Bun coerceundefinedto 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
#48875and 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.,#4383fixed in 1.0.15,#6762fixed 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:
undefinedtoprocess.env.*sets a"undefined"string value nodejs/node#48875process.envdoes not overwrite value oven-sh/bun#6762delete process.env.XXXnot work oven-sh/bun#4383Fix
afterEachto properly delete undefined env vars instead of coercing to the string"undefined".When restoring environment variables,
Object.assign(process.env, originalEnv)coercesundefinedvalues to the string"undefined"instead of deleting the keys. This pollutes subsequent tests: after this suite runs, any test importing this module will seeprocess.env.CODEX_MODEL === "undefined", causingresolveCodexModel(...)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
🤖 Prompt for AI Agents