diff --git a/frontend/src/api/__tests__/codex.spec.ts b/frontend/src/api/__tests__/codex.spec.ts index 57b19d7557..3264d1abb3 100644 --- a/frontend/src/api/__tests__/codex.spec.ts +++ b/frontend/src/api/__tests__/codex.spec.ts @@ -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)) + }) }) diff --git a/frontend/src/api/codex.ts b/frontend/src/api/codex.ts index 948fa1eee9..8e2553ac81 100644 --- a/frontend/src/api/codex.ts +++ b/frontend/src/api/codex.ts @@ -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 } } diff --git a/frontend/src/components/keys/UseKeyModal.vue b/frontend/src/components/keys/UseKeyModal.vue index 604af3048c..9abeef4640 100644 --- a/frontend/src/components/keys/UseKeyModal.vue +++ b/frontend/src/components/keys/UseKeyModal.vue @@ -186,7 +186,19 @@ {{ t('keys.useKeyModal.codexModelCatalog.description') }}

- {{ codexModelCatalogPath }} + {{ codexModelCatalogMode === 'remote' ? codexModelCatalogUrl : codexModelCatalogPath }} +

+ +

+ {{ t('keys.useKeyModal.codexModelCatalog.oversized') }}