fix(codex): harden routed catalog capability sync

This commit is contained in:
Long Li
2026-08-26 12:46:31 +09:00
parent 195b219707
commit 2abce65031
13 changed files with 677 additions and 42 deletions
@@ -4087,11 +4087,17 @@ const syncPreviewCredentials = computed(() => {
const baseUrl = isCNPlatform.value && apiProtocol.value === 'adaptive'
? adaptiveBaseUrls.value.chat_completions.trim() || apiKeyBaseUrl.value.trim()
: apiKeyBaseUrl.value.trim()
const modelMapping = buildModelMappingObject(
modelRestrictionMode.value,
allowedModels.value,
modelMappings.value
)
return {
platform: form.platform,
type: form.type,
base_url: baseUrl || undefined,
api_key: apiKeyValue.value
api_key: apiKeyValue.value,
...(modelMapping ? { model_mapping: modelMapping } : {})
}
})
@@ -4987,7 +4993,14 @@ const submitCreateAccount = async (payload: CreateAccountRequest) => {
submitting.value = true
try {
const account = await adminAPI.accounts.create(withAntigravityConfirmFlag(payload))
if (upstreamModelsPreviewed.value) {
const modelMapping = payload.credentials.model_mapping
const hasConcreteMappedTarget = payload.type === 'apikey' &&
typeof modelMapping === 'object' &&
modelMapping !== null &&
Object.values(modelMapping).some((target) =>
typeof target === 'string' && target.trim() !== '' && !target.includes('*')
)
if (upstreamModelsPreviewed.value || hasConcreteMappedTarget) {
try {
const result = await adminAPI.accounts.syncUpstreamModels(account.id)
if (result.warnings?.some(warning => warning.code === 'upstream_model_metadata_incomplete')) {
@@ -126,7 +126,11 @@ const ModelWhitelistSelectorStub = defineComponent({
syncCredentials: Object,
},
emits: ['update:modelValue', 'upstream-synced'],
template: '<button type="button" data-testid="model-whitelist-selector" @click="$emit(\'upstream-synced\')">models</button>',
template: `<button
type="button"
data-testid="model-whitelist-selector"
@click="$emit('update:modelValue', ['public-glm']); $emit('upstream-synced')"
>models</button>`,
})
function mountModal(groups: any[] = []) {
@@ -257,6 +261,38 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
expect(syncUpstreamModelsMock).toHaveBeenCalledWith(42)
})
it('includes the current concrete model mapping in preview credentials', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'OpenAI')
await selectButtonByText(wrapper, 'API Key')
await wrapper.get('form#create-account-form input[type="password"]').setValue('test-api-key')
await wrapper.get('[data-testid="model-whitelist-selector"]').trigger('click')
await flushPromises()
expect(wrapper.getComponent(ModelWhitelistSelectorStub).props('syncCredentials')).toMatchObject({
model_mapping: { 'public-glm': 'public-glm' }
})
})
it('runs formal capability sync after creating an account with explicit mappings', async () => {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'OpenAI')
await selectButtonByText(wrapper, 'API Key')
await wrapper.get('form#create-account-form input[type="text"]').setValue('Mapped account')
await wrapper.get('form#create-account-form input[type="password"]').setValue('test-api-key')
await selectButtonByText(wrapper, 'admin.accounts.modelMapping')
await selectButtonByText(wrapper, 'admin.accounts.addMapping')
await wrapper.get('input[placeholder="admin.accounts.requestModel"]').setValue('public-glm')
await wrapper.get('input[placeholder="admin.accounts.actualModel"]').setValue('glm-5.3')
await wrapper.get('form#create-account-form').trigger('submit.prevent')
await flushPromises()
expect(createAccountMock.mock.calls[0]?.[0]?.credentials?.model_mapping).toEqual({
'public-glm': 'glm-5.3'
})
expect(syncUpstreamModelsMock).toHaveBeenCalledWith(42)
})
it('warns when post-create capability metadata remains incomplete', async () => {
syncUpstreamModelsMock.mockResolvedValue({
models: ['x-preview-f-free'],
+59 -7
View File
@@ -315,8 +315,8 @@ let codexModelManifestRequestID = 0
const showCodexModelCatalog = computed(() =>
props.show &&
((props.platform === 'openai' && (activeClientTab.value === 'codex' || activeClientTab.value === 'codex-ws')) ||
((props.platform === 'grok' || props.platform === 'deepseek' || props.platform === 'composite') && activeClientTab.value === 'codex'))
(activeClientTab.value === 'codex' ||
(props.platform === 'openai' && activeClientTab.value === 'codex-ws'))
)
const codexModelCatalogPath = computed(() => {
@@ -451,12 +451,14 @@ const clientTabs = computed((): TabConfig[] => {
case 'gemini':
return [
{ id: 'gemini', label: t('keys.useKeyModal.cliTabs.geminiCli'), icon: SparkleIcon },
{ id: 'codex', label: t('keys.useKeyModal.cliTabs.codexCli'), icon: TerminalIcon },
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon }
]
case 'antigravity':
return [
{ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon },
{ id: 'gemini', label: t('keys.useKeyModal.cliTabs.geminiCli'), icon: SparkleIcon },
{ id: 'codex', label: t('keys.useKeyModal.cliTabs.codexCli'), icon: TerminalIcon },
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon }
]
case 'grok':
@@ -476,6 +478,7 @@ const clientTabs = computed((): TabConfig[] => {
default:
return [
{ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon },
{ id: 'codex', label: t('keys.useKeyModal.cliTabs.codexCli'), icon: TerminalIcon },
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon }
]
}
@@ -510,6 +513,13 @@ const currentTabs = computed(() => {
})
const platformDescription = computed(() => {
if (activeClientTab.value === 'codex' &&
props.platform !== 'openai' &&
props.platform !== 'grok' &&
props.platform !== 'deepseek' &&
props.platform !== 'composite') {
return t('keys.useKeyModal.routedCodex.description')
}
switch (props.platform) {
case 'openai':
if (activeClientTab.value === 'claude') {
@@ -542,6 +552,13 @@ const platformDescription = computed(() => {
})
const platformNote = computed(() => {
if (activeClientTab.value === 'codex' &&
props.platform !== 'openai' &&
props.platform !== 'grok' &&
props.platform !== 'deepseek' &&
props.platform !== 'composite') {
return t('keys.useKeyModal.routedCodex.note')
}
switch (props.platform) {
case 'openai':
if (activeClientTab.value === 'claude') {
@@ -715,8 +732,14 @@ const currentFiles = computed((): FileConfig[] => {
}
return generateOpenAIFiles(baseUrl, apiKey)
case 'gemini':
if (activeClientTab.value === 'codex') {
return generateRoutedCodexFiles(apiBase, apiKey, 'gemini')
}
return [generateGeminiCliContent(baseUrl, apiKey)]
case 'antigravity':
if (activeClientTab.value === 'codex') {
return generateRoutedCodexFiles(apiBase, apiKey, 'antigravity')
}
if (activeClientTab.value === 'gemini') {
return [generateGeminiCliContent(`${baseUrl}/antigravity`, apiKey)]
}
@@ -740,6 +763,9 @@ const currentFiles = computed((): FileConfig[] => {
}
return generateAnthropicFiles(baseRoot, apiKey)
default:
if (activeClientTab.value === 'codex' && props.platform) {
return generateRoutedCodexFiles(apiBase, apiKey, props.platform)
}
return generateAnthropicFiles(baseUrl, apiKey)
}
})
@@ -1176,13 +1202,35 @@ supports_websockets = false
function generateRoutedCodexFiles(
baseUrl: string,
apiKey: string,
platform: 'deepseek' | 'composite'
platform: GroupPlatform
): FileConfig[] {
const isWindows = activeTab.value === 'windows'
const configDir = isWindows ? '%userprofile%\\.codex' : '~/.codex'
const preferredModel = platform === 'deepseek' ? 'deepseek-v4-pro' : 'gpt-5.5'
const preferredModels: Partial<Record<GroupPlatform, string>> = {
openai: 'gpt-5.5',
anthropic: 'claude-sonnet-4-6',
gemini: 'gemini-2.5-pro',
antigravity: 'claude-sonnet-4-6',
grok: 'grok-4.5',
kimi: 'kimi-k2.5',
zhipu: 'glm-4.7',
deepseek: 'deepseek-v4-pro',
composite: 'gpt-5.5'
}
const preferredModel = preferredModels[platform] || ''
const model = selectCodexCatalogModel(preferredModel)
const label = platform === 'deepseek' ? 'DeepSeek' : 'Composite'
const labels: Record<GroupPlatform, string> = {
anthropic: 'Anthropic',
openai: 'OpenAI',
gemini: 'Gemini',
antigravity: 'Antigravity',
grok: 'Grok',
kimi: 'Kimi',
zhipu: 'Zhipu',
deepseek: 'DeepSeek',
composite: 'Composite'
}
const label = labels[platform]
const envContent = isWindows
? `$env:SUB2API_API_KEY="${apiKey}"`
: `export SUB2API_API_KEY="${apiKey}"`
@@ -1195,7 +1243,7 @@ disable_response_storage = true
model_catalog_json = "${escapeTomlBasicString(codexModelCatalogPath.value)}"
[model_providers.sub2api]
name = "OpenAI"
name = "Sub2API ${label}"
base_url = "${baseUrl}"
env_key = "SUB2API_API_KEY"
wire_api = "responses"
@@ -1207,7 +1255,11 @@ supports_websockets = false`
{
path: joinConfigPath(configDir, 'config.toml', isWindows),
content: configContent,
hint: t(`keys.useKeyModal.${platform}.codexConfigTomlHint`)
hint: t(
platform === 'deepseek' || platform === 'composite'
? `keys.useKeyModal.${platform}.codexConfigTomlHint`
: 'keys.useKeyModal.routedCodex.configTomlHint'
)
}
]
}
@@ -714,6 +714,45 @@ describe('UseKeyModal', () => {
)
})
it.each(['anthropic', 'gemini', 'antigravity', 'kimi', 'zhipu'] as const)(
'offers Codex catalog configuration for the %s routed group',
async (platform) => {
const wrapper = mount(UseKeyModal, {
props: {
show: true,
apiKey: `sk-${platform}-test`,
baseUrl: 'https://example.com/v1',
platform
},
global: {
stubs: {
BaseDialog: {
template: '<div><slot /><slot name="footer" /></div>'
},
Icon: {
template: '<span />'
}
}
}
})
const codexTab = wrapper.findAll('button').find((button) =>
button.text().includes('keys.useKeyModal.cliTabs.codexCli')
)
expect(codexTab).toBeDefined()
await codexTab!.trigger('click')
await nextTick()
expect(wrapper.find('[data-testid="codex-model-catalog"]').exists()).toBe(true)
const config = wrapper.findAll('pre code')
.map((code) => code.text())
.find((content) => content.includes('[model_providers.sub2api]'))
expect(config).toContain('model_catalog_json = "~/.codex/codex-models.json"')
expect(config).toContain('base_url = "https://example.com/v1"')
expect(config).toContain('wire_api = "responses"')
}
)
// Scenario: the platform-preferred model remains selected when the downloaded catalog contains it.
it('keeps the preferred Composite default when it exists in the catalog', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({