feat(codex): support remote model catalogs in client config

This commit is contained in:
lyy0709
2026-09-29 13:26:44 -07:00
parent a60a29549f
commit b5efbe3f4c
6 changed files with 176 additions and 43 deletions
+25 -4
View File
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
buildCodexModelCatalogUrl,
buildCodexModelsManifestUrl,
fetchCodexModelsManifest
} from '../codex'
@@ -11,10 +12,18 @@ describe('Codex models API', () => {
it('builds the authenticated Codex manifest endpoint from the public API base', () => {
expect(buildCodexModelsManifestUrl('https://example.com/api/v1/')).toBe(
'https://example.com/api/v1/models?client_version=0.147.0'
'https://example.com/api/v1/models?client_version=0.158.0'
)
})
it.each([
['https://example.com', 'https://example.com/v1/models'],
['https://example.com/api/v1/', 'https://example.com/api/v1/models'],
['', `${window.location.origin}/v1/models`]
])('builds a version-free provider catalog URL from %s', (baseUrl, expected) => {
expect(buildCodexModelCatalogUrl(baseUrl)).toBe(expected)
})
it('fetches a manifest with the current API key without adding it to the catalog', async () => {
const manifest = {
models: [
@@ -43,14 +52,14 @@ describe('Codex models API', () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => manifest
text: async () => JSON.stringify(manifest)
})
vi.stubGlobal('fetch', fetchMock)
const result = await fetchCodexModelsManifest('https://example.com/v1', 'sk-user-test')
expect(fetchMock).toHaveBeenCalledWith(
'https://example.com/v1/models?client_version=0.147.0',
'https://example.com/v1/models?client_version=0.158.0',
expect.objectContaining({
headers: {
Accept: 'application/json',
@@ -59,6 +68,7 @@ describe('Codex models API', () => {
})
)
expect(result.modelCount).toBe(2)
expect(result.responseBytes).toBe(new TextEncoder().encode(JSON.stringify(manifest)).byteLength)
expect(JSON.parse(result.content)).toEqual(manifest)
expect(result.content).toContain('"effort": "xhigh"')
expect(result.content).toContain('"input_modalities"')
@@ -70,10 +80,21 @@ describe('Codex models API', () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ object: 'list', data: [] })
text: async () => JSON.stringify({ object: 'list', data: [] })
}))
await expect(fetchCodexModelsManifest('https://example.com/v1', 'sk-user-test'))
.rejects.toThrow('valid manifest')
})
it('measures UTF-8 response bytes before formatting the downloadable catalog', async () => {
const text = '{"models":[{"slug":"test","description":"模型"}]}'
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, text: async () => text }))
const result = await fetchCodexModelsManifest('https://example.com', 'sk-test')
expect(result.responseBytes).toBe(new TextEncoder().encode(text).byteLength)
expect(result.responseBytes).toBeGreaterThan(text.length)
expect(result.content).toBe(JSON.stringify(JSON.parse(text), null, 2))
})
})
+11 -5
View File
@@ -1,9 +1,10 @@
export interface CodexModelsManifestResult {
content: string
modelCount: number
responseBytes: number
}
const DEFAULT_CODEX_CLIENT_VERSION = '0.147.0'
const DEFAULT_CODEX_CLIENT_VERSION = '0.158.0'
function normalizeCodexBaseUrl(baseUrl: string): string {
const fallback = typeof window !== 'undefined' ? window.location.origin : ''
@@ -16,9 +17,12 @@ export function buildCodexModelsManifestUrl(
baseUrl: string,
clientVersion = DEFAULT_CODEX_CLIENT_VERSION
): string {
const url = normalizeCodexBaseUrl(baseUrl)
const params = new URLSearchParams({ client_version: clientVersion })
return `${url}/models?${params.toString()}`
return `${buildCodexModelCatalogUrl(baseUrl)}?${params.toString()}`
}
export function buildCodexModelCatalogUrl(baseUrl: string): string {
return `${normalizeCodexBaseUrl(baseUrl)}/models`
}
function isCodexModelsManifest(value: unknown): value is { models: unknown[] } {
@@ -44,13 +48,15 @@ export async function fetchCodexModelsManifest(
throw new Error(`Codex models request failed with status ${response.status}`)
}
const payload: unknown = await response.json()
const text = await response.text()
const payload: unknown = JSON.parse(text)
if (!isCodexModelsManifest(payload)) {
throw new Error('Codex models response is not a valid manifest')
}
return {
content: JSON.stringify(payload, null, 2),
modelCount: payload.models.length
modelCount: payload.models.length,
responseBytes: new TextEncoder().encode(text).byteLength
}
}
+34 -14
View File
@@ -186,7 +186,19 @@
{{ t('keys.useKeyModal.codexModelCatalog.description') }}
</p>
<p class="mt-1 truncate font-mono text-xs text-gray-700 dark:text-gray-300">
{{ codexModelCatalogPath }}
{{ codexModelCatalogMode === 'remote' ? codexModelCatalogUrl : codexModelCatalogPath }}
</p>
<select
v-model="codexModelCatalogMode"
data-testid="codex-model-catalog-mode"
:aria-label="t('keys.useKeyModal.codexModelCatalog.mode')"
class="input mt-2 text-sm"
>
<option value="remote" :disabled="codexModelCatalogOversized">{{ t('keys.useKeyModal.codexModelCatalog.remote') }}</option>
<option value="file">{{ t('keys.useKeyModal.codexModelCatalog.local') }}</option>
</select>
<p v-if="codexModelCatalogOversized" class="mt-2 text-xs text-amber-700 dark:text-amber-300">
{{ t('keys.useKeyModal.codexModelCatalog.oversized') }}
</p>
</div>
<button
@@ -261,7 +273,7 @@ import { saveAs } from 'file-saver'
import BaseDialog from '@/components/common/BaseDialog.vue'
import Icon from '@/components/icons/Icon.vue'
import { useClipboard } from '@/composables/useClipboard'
import { fetchCodexModelsManifest } from '@/api/codex'
import { buildCodexModelCatalogUrl, fetchCodexModelsManifest } from '@/api/codex'
import type { GroupPlatform } from '@/types'
import {
findCodexCatalogModel,
@@ -311,13 +323,20 @@ type CodexModelManifestState = 'idle' | 'loading' | 'ready' | 'error'
const codexModelManifestState = ref<CodexModelManifestState>('idle')
const codexModelManifestContent = ref('')
const codexModelManifestModelCount = ref(0)
const codexModelCatalogMode = ref<'remote' | 'file'>('remote')
const codexModelManifestResponseBytes = ref(0)
const codexModelCatalogOversized = computed(() => codexModelManifestResponseBytes.value > 1024 * 1024)
const codexModelCatalogUrl = computed(() => buildCodexModelCatalogUrl(props.baseUrl))
const codexLocalCatalogToml = computed(() => codexModelCatalogMode.value === 'file'
? `model_catalog_json = "${CODEX_MODEL_CATALOG_CONFIG_PATH}"\n`
: '')
let codexModelManifestController: AbortController | null = null
let codexModelManifestRequestID = 0
const showCodexModelCatalog = computed(() =>
props.show &&
props.platform !== 'openai' &&
activeClientTab.value === 'codex'
(activeClientTab.value === 'codex' ||
(props.platform === 'openai' && activeClientTab.value === 'codex-ws'))
)
const codexModelCatalogPath = computed(() => {
@@ -632,6 +651,7 @@ function resetCodexModelManifest() {
codexModelManifestState.value = 'idle'
codexModelManifestContent.value = ''
codexModelManifestModelCount.value = 0
codexModelManifestResponseBytes.value = 0
}
async function loadCodexModelManifest() {
@@ -648,6 +668,8 @@ async function loadCodexModelManifest() {
if (requestID !== codexModelManifestRequestID) return
codexModelManifestContent.value = result.content
codexModelManifestModelCount.value = result.modelCount
codexModelManifestResponseBytes.value = result.responseBytes
if (codexModelCatalogOversized.value) codexModelCatalogMode.value = 'file'
codexModelManifestState.value = 'ready'
} catch (error) {
const errorName = error && typeof error === 'object' && 'name' in error
@@ -961,13 +983,13 @@ function generateOpenAIFiles(baseUrl: string, apiKey: string): FileConfig[] {
model = "${model}"
review_model = "${model}"
${reasoningEffortLine}disable_response_storage = true
network_access = "enabled"
${codexLocalCatalogToml.value}network_access = "enabled"
windows_wsl_setup_acknowledged = true
[model_providers.OpenAI]
name = "OpenAI"
base_url = "${baseUrl}"
wire_api = "responses"
${codexModelCatalogMode.value === 'remote' ? `model_catalog_url = "${escapeTomlBasicString(buildCodexModelCatalogUrl(baseUrl))}"\n` : ''}wire_api = "responses"
${generateCodexProviderAuthConfig(apiKey)}
[features]
@@ -1193,8 +1215,7 @@ function generateGrokCodexFiles(baseUrl: string, apiKey: string): FileConfig[] {
model_provider = "sub2api"
model = "${model}"
model_catalog_json = "${CODEX_MODEL_CATALOG_CONFIG_PATH}"
# Optional:
${codexLocalCatalogToml.value}# Optional:
# review_model = "${model}"
# model_reasoning_effort = "medium"
# model_context_window = 500000
@@ -1205,7 +1226,7 @@ model_catalog_json = "${CODEX_MODEL_CATALOG_CONFIG_PATH}"
[model_providers.sub2api]
name = "Sub2API Grok"
base_url = "${baseUrl}"
# Prefer env_key (variable NAME). Do not combine with experimental_bearer_token.
${codexModelCatalogMode.value === 'remote' ? `model_catalog_url = "${escapeTomlBasicString(buildCodexModelCatalogUrl(baseUrl))}"\n` : ''}# Prefer env_key (variable NAME). Do not combine with experimental_bearer_token.
env_key = "SUB2API_API_KEY"
# Fallback only if you cannot set env (discouraged — keeps secret on disk):
# experimental_bearer_token = "${apiKey}"
@@ -1274,12 +1295,11 @@ model_provider = "sub2api"
model = "${model}"
review_model = "${model}"
disable_response_storage = true
model_catalog_json = "${CODEX_MODEL_CATALOG_CONFIG_PATH}"
${codexLocalCatalogToml.value}
[model_providers.sub2api]
name = "Sub2API ${label}"
base_url = "${baseUrl}"
env_key = "SUB2API_API_KEY"
${codexModelCatalogMode.value === 'remote' ? `model_catalog_url = "${escapeTomlBasicString(buildCodexModelCatalogUrl(baseUrl))}"\n` : ''}env_key = "SUB2API_API_KEY"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = false`
@@ -1309,13 +1329,13 @@ function generateOpenAIWsFiles(baseUrl: string, apiKey: string): FileConfig[] {
model = "${model}"
review_model = "${model}"
${reasoningEffortLine}disable_response_storage = true
network_access = "enabled"
${codexLocalCatalogToml.value}network_access = "enabled"
windows_wsl_setup_acknowledged = true
[model_providers.OpenAI]
name = "OpenAI"
base_url = "${baseUrl}"
wire_api = "responses"
${codexModelCatalogMode.value === 'remote' ? `model_catalog_url = "${escapeTomlBasicString(buildCodexModelCatalogUrl(baseUrl))}"\n` : ''}wire_api = "responses"
supports_websockets = true
${generateCodexProviderAuthConfig(apiKey)}
@@ -493,6 +493,8 @@ describe('UseKeyModal', () => {
expect(configToml).toContain('requires_openai_auth = false')
expect(configToml).toContain('experimental_bearer_token = "sk-test"')
expect(configToml).toContain('http_headers = { "x-openai-actor-authorization" = "local-image-extension" }')
expect(configToml).toContain('model_catalog_url = "https://example.com/v1/models"')
expect(configToml).not.toContain('model_catalog_json')
expect(configToml).not.toContain('env_key')
expect(configToml).not.toContain('image_generation')
expect(codeBlocks).not.toContain('{\n "OPENAI_API_KEY": "sk-test"\n}')
@@ -592,6 +594,8 @@ describe('UseKeyModal', () => {
const codeBlocks = wrapper.findAll('pre code').map((code) => code.text())
const configToml = codeBlocks.find((content) => content.includes('supports_websockets = true'))
expect(configToml).toContain('model_catalog_url = "https://example.com/v1/models"')
expect(configToml).not.toContain('model_catalog_json')
expect(wrapper.get('[data-testid="codex-auth-mode-api-key"]').attributes('aria-checked')).toBe('true')
expect(configToml).toBeDefined()
@@ -795,8 +799,8 @@ describe('UseKeyModal', () => {
expect(fable.options.thinking).not.toHaveProperty('budgetTokens')
})
// Scenario: API Key users can fetch a routed group catalog and reference it from config.toml.
it('offers a downloadable Codex catalog for Composite API keys', async () => {
// Scenario: API Key users can load a routed catalog remotely or download it for older clients.
it('offers remote and downloadable Codex catalogs for Composite API keys', async () => {
const manifest = {
models: [
{
@@ -818,7 +822,7 @@ describe('UseKeyModal', () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => manifest
text: async () => JSON.stringify(manifest)
})
vi.stubGlobal('fetch', fetchMock)
@@ -851,14 +855,18 @@ describe('UseKeyModal', () => {
const unixConfig = wrapper.findAll('pre code')
.map((code) => code.text())
.find((content) => content.includes('[model_providers.sub2api]'))
expect(unixConfig).toContain('model_catalog_json = "~/.codex/codex-models.json"')
expect(unixConfig).toContain('[model_providers.sub2api]\nname = "Sub2API Composite"\nbase_url = "https://example.com/v1"\nmodel_catalog_url = "https://example.com/v1/models"')
expect(unixConfig).not.toContain('model_catalog_json')
expect(unixConfig).toContain('env_key = "SUB2API_API_KEY"')
expect(fetchMock).not.toHaveBeenCalled()
await wrapper.get('[data-testid="codex-model-catalog-mode"]').setValue('file')
await wrapper.get('[data-testid="codex-model-catalog-fetch"]').trigger('click')
await flushPromises()
expect(fetchMock).toHaveBeenCalledWith(
'https://example.com/v1/models?client_version=0.147.0',
'https://example.com/v1/models?client_version=0.158.0',
expect.objectContaining({
headers: expect.objectContaining({ Authorization: 'Bearer sk-composite-test' })
})
@@ -872,6 +880,8 @@ describe('UseKeyModal', () => {
expect(loadedUnixConfig).toContain('model = "claude-opus-4-8"')
expect(loadedUnixConfig).toContain('review_model = "claude-opus-4-8"')
expect(loadedUnixConfig).not.toContain('model = "gpt-5.5"')
expect(loadedUnixConfig).toContain('model_catalog_json = "~/.codex/codex-models.json"')
expect(loadedUnixConfig).not.toContain('model_catalog_url')
const downloadButton = wrapper.findAll('button').find((button) =>
button.text().includes('keys.useKeyModal.codexModelCatalog.download')
@@ -897,7 +907,7 @@ describe('UseKeyModal', () => {
.toContain('%userprofile%\\.codex\\codex-models.json')
})
it.each(['anthropic', 'gemini', 'antigravity', 'kimi', 'zhipu', 'minimax'] as const)(
it.each(['anthropic', 'gemini', 'antigravity', 'grok', 'kimi', 'zhipu', 'deepseek', 'minimax', 'opencode_go'] as const)(
'offers Codex catalog configuration for the %s routed group',
async (platform) => {
const wrapper = mount(UseKeyModal, {
@@ -930,7 +940,8 @@ describe('UseKeyModal', () => {
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('model_catalog_url = "https://example.com/v1/models"')
expect(config).not.toContain('model_catalog_json')
expect(config).toContain('base_url = "https://example.com/v1"')
expect(config).toContain('wire_api = "responses"')
}
@@ -941,7 +952,7 @@ describe('UseKeyModal', () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
text: async () => JSON.stringify({
models: [
{ slug: 'claude-opus-4-8' },
{ slug: 'gpt-5.5' }
@@ -983,7 +994,7 @@ describe('UseKeyModal', () => {
expect(config).toContain('review_model = "gpt-5.5"')
})
it('omits the Codex catalog for OpenAI in both transport modes and on both platforms', async () => {
it('offers remote and optional file catalogs for OpenAI in both transport modes and on both platforms', async () => {
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
const wrapper = mount(UseKeyModal, {
@@ -1013,10 +1024,77 @@ describe('UseKeyModal', () => {
.map((code) => code.text())
.find((content) => content.includes('model_provider = "OpenAI"'))
expect(configToml).toContain('model = "gpt-5.5"')
expect(configToml).toContain('[model_providers.OpenAI]\nname = "OpenAI"\nbase_url = "https://example.com/v1"\nmodel_catalog_url = "https://example.com/v1/models"')
expect(configToml).not.toContain('model_catalog_json')
expect(wrapper.find('[data-testid="codex-model-catalog"]').exists()).toBe(false)
expect(configToml).toContain('requires_openai_auth = true')
expect(configToml).not.toContain('client_version')
expect(wrapper.find('[data-testid="codex-model-catalog"]').exists()).toBe(true)
await wrapper.get('[data-testid="codex-model-catalog-mode"]').setValue('file')
const fileConfig = wrapper.findAll('pre code')
.map((code) => code.text())
.find((content) => content.includes('model_provider = "OpenAI"'))!
expect(fileConfig).toContain('model_catalog_json = "~/.codex/codex-models.json"')
expect(fileConfig).not.toContain('model_catalog_url')
expect(fileConfig.indexOf('model_catalog_json')).toBeLessThan(fileConfig.indexOf('[model_providers.OpenAI]'))
await wrapper.get('[data-testid="codex-model-catalog-mode"]').setValue('remote')
}
}
expect(fetchMock).not.toHaveBeenCalled()
})
it.each([1024 * 1024 - 1, 1024 * 1024, 1024 * 1024 + 1])(
'uses file mode only when the manifest exceeds the remote byte limit (%s bytes)',
async (responseBytes) => {
const manifest = { models: [{ slug: 'gpt-5.5', description: '' }] }
manifest.models[0]!.description = 'x'.repeat(responseBytes - JSON.stringify(manifest).length)
const responseText = JSON.stringify(manifest)
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
status: 200,
text: async () => responseText
}))
const wrapper = mount(UseKeyModal, {
props: {
show: true,
apiKey: 'sk-test',
baseUrl: 'https://example.com/v1',
platform: 'openai'
},
global: {
stubs: {
BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' },
Icon: { template: '<span />' }
}
}
})
await wrapper.get('[data-testid="codex-model-catalog-fetch"]').trigger('click')
await flushPromises()
const oversized = responseBytes > 1024 * 1024
const mode = wrapper.get('[data-testid="codex-model-catalog-mode"]')
expect((mode.element as HTMLSelectElement).value).toBe(oversized ? 'file' : 'remote')
expect((mode.get('option[value="remote"]').element as HTMLOptionElement).disabled).toBe(oversized)
const config = wrapper.findAll('pre code').map((code) => code.text())
.find((content) => content.includes('model_provider = "OpenAI"'))!
if (oversized) {
expect(config).toContain('model_catalog_json = "~/.codex/codex-models.json"')
expect(config).not.toContain('model_catalog_url')
expect(wrapper.text()).toContain('keys.useKeyModal.codexModelCatalog.oversized')
await wrapper.findAll('button').find((button) =>
button.text().includes('keys.useKeyModal.codexModelCatalog.download')
)!.trigger('click')
const downloaded = saveAsMock.mock.calls[0]?.[0] as Blob
expect(JSON.parse(await readBlobAsText(downloaded))).toEqual(manifest)
await wrapper.setProps({ apiKey: 'sk-new-group' })
expect((mode.get('option[value="remote"]').element as HTMLOptionElement).disabled).toBe(false)
expect(wrapper.text()).not.toContain('keys.useKeyModal.codexModelCatalog.oversized')
} else {
expect(config).toContain('model_catalog_url = "https://example.com/v1/models"')
expect(config).not.toContain('model_catalog_json')
}
}
)
})
+9 -5
View File
@@ -219,29 +219,33 @@ export default {
deepseek: {
description: 'Configure Claude Code, Codex, or OpenCode through the current DeepSeek group.',
codexDescription: 'Configure Codex with API key authentication through the current DeepSeek group.',
codexConfigTomlHint: 'Download the model catalog below, save both files under the Codex config directory, and restart Codex.',
codexConfigTomlHint: 'Save config.toml and restart Codex to load the remote catalog. In local file mode, also download the catalog to the configured path.',
codexNote: 'Export SUB2API_API_KEY before starting Codex. The downloaded catalog contains model metadata only, not your API key.',
},
minimax: {
description: 'Configure Claude Code, Codex, or OpenCode through the current MiniMax group.',
codexDescription: 'Configure Codex with API key authentication through the current MiniMax group.',
codexConfigTomlHint: 'Download the model catalog below, save both files under the Codex config directory, and restart Codex.',
codexConfigTomlHint: 'Save config.toml and restart Codex to load the remote catalog. In local file mode, also download the catalog to the configured path.',
codexNote: 'Export SUB2API_API_KEY before starting Codex. The downloaded catalog contains model metadata only, not your API key.',
},
composite: {
description: 'Configure supported clients through the current Composite routing group.',
codexDescription: 'Configure Codex with API key authentication and the complete model catalog for this Composite group.',
codexConfigTomlHint: 'Download the model catalog below, save both files under the Codex config directory, and restart Codex.',
codexConfigTomlHint: 'Save config.toml and restart Codex to load the remote catalog. In local file mode, also download the catalog to the configured path.',
codexNote: 'Export SUB2API_API_KEY before starting Codex. Model requests are routed by the selected catalog slug.',
},
routedCodex: {
description: 'Configure Codex with the complete model catalog for the current routed group.',
configTomlHint: 'Download the model catalog below, save both files under the Codex config directory, and restart Codex.',
configTomlHint: 'Save config.toml and restart Codex to load the remote catalog. In local file mode, also download the catalog to the configured path.',
note: 'Export SUB2API_API_KEY before starting Codex. The downloaded catalog contains model metadata only, not your API key.',
},
codexModelCatalog: {
mode: 'Catalog source',
remote: 'Remote catalog (Codex 0.156.0+)',
local: 'Local file (older clients)',
oversized: 'The complete catalog exceeds the 1 MiB remote limit. Local file mode is selected; download it to the configured path.',
title: 'Codex model catalog',
description: 'Fetch with this API key, then save the catalog at the path referenced by config.toml.',
description: 'Codex loads and refreshes the remote catalog using your configured authentication. For local file mode, fetch the catalog below and save it at the configured path.',
fetch: 'Fetch catalog',
retry: 'Retry',
download: 'Download catalog',
+9 -5
View File
@@ -223,29 +223,33 @@ export default {
deepseek: {
description: '通过当前 DeepSeek 分组配置 Claude Code、Codex 或 OpenCode。',
codexDescription: '使用 API Key 配置 Codex,并通过当前 DeepSeek 分组发送请求。',
codexConfigTomlHint: '下载下方模型目录,将两个文件保存到 Codex 配置目录后重启 Codex。',
codexConfigTomlHint: '保存 config.toml 后重启 Codex,客户端会加载远程目录。使用本地文件模式时,还需下载目录并保存到配置中的路径。',
codexNote: '启动 Codex 前先导出 SUB2API_API_KEY。下载的目录只包含模型元数据,不包含 API Key。'
},
minimax: {
description: '通过当前 MiniMax 分组配置 Claude Code、Codex 或 OpenCode。',
codexDescription: '使用 API Key 配置 Codex,并通过当前 MiniMax 分组发送请求。',
codexConfigTomlHint: '下载下方模型目录,将两个文件保存到 Codex 配置目录后重启 Codex。',
codexConfigTomlHint: '保存 config.toml 后重启 Codex,客户端会加载远程目录。使用本地文件模式时,还需下载目录并保存到配置中的路径。',
codexNote: '启动 Codex 前先导出 SUB2API_API_KEY。下载的目录只包含模型元数据,不包含 API Key。'
},
composite: {
description: '通过当前 Composite 路由分组配置受支持的客户端。',
codexDescription: '使用 API Key 和当前 Composite 分组的完整模型目录配置 Codex。',
codexConfigTomlHint: '下载下方模型目录,将两个文件保存到 Codex 配置目录后重启 Codex。',
codexConfigTomlHint: '保存 config.toml 后重启 Codex,客户端会加载远程目录。使用本地文件模式时,还需下载目录并保存到配置中的路径。',
codexNote: '启动 Codex 前先导出 SUB2API_API_KEY;分组会根据目录中选中的模型路由请求。'
},
routedCodex: {
description: '使用当前路由分组的完整模型目录配置 Codex。',
configTomlHint: '下载下方模型目录,将两个文件保存到 Codex 配置目录后重启 Codex。',
configTomlHint: '保存 config.toml 后重启 Codex,客户端会加载远程目录。使用本地文件模式时,还需下载目录并保存到配置中的路径。',
note: '启动 Codex 前先导出 SUB2API_API_KEY。下载的目录只包含模型元数据,不包含 API Key。'
},
codexModelCatalog: {
mode: '目录来源',
remote: '远程目录(Codex 0.156.0+)',
local: '本地文件(旧版客户端)',
oversized: '完整目录超过远程加载的 1 MiB 限制,已改为本地文件。请下载目录并保存到配置中的路径。',
title: 'Codex 模型目录',
description: '使用当前 API Key 获取目录,并保存到 config.toml 引用的路径。',
description: 'Codex 会使用配置中的认证信息加载并刷新远程目录。使用本地文件模式时,请在下方获取目录并保存到配置中的路径。',
fetch: '获取目录',
retry: '重试',
download: '下载目录',