mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
feat(codex): support remote model catalogs in client config
This commit is contained in:
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '下载目录',
|
||||
|
||||
Reference in New Issue
Block a user