diff --git a/.agents/skills/fetching-models-pricing/SKILL.md b/.agents/skills/fetching-models-pricing/SKILL.md index 5ebb1b0dee..22f45981e2 100644 --- a/.agents/skills/fetching-models-pricing/SKILL.md +++ b/.agents/skills/fetching-models-pricing/SKILL.md @@ -10,11 +10,11 @@ Maintain the per-token rate cards in: | Provider | Table | Live catalog | Preferred rate source | |---|---|---|---| | Copilot | `packages/provider-copilot/src/pricing.ts` | Copilot `/models` | model vendor's first-party API | -| Codex | `packages/provider-codex/src/pricing.ts` | authenticated `/codex/models` | OpenAI API pricing | +| ChatGPT | `packages/provider-chatgpt/src/pricing.ts` | authenticated `/codex/models` | OpenAI API pricing | | Claude Code | `packages/provider-claude-code/src/pricing.ts` | authenticated Anthropic `/v1/models` | Anthropic API pricing | | Ollama | `packages/provider-ollama/src/pricing.ts` | `/api/tags` + `/api/show` | Ollama official standard rates; vendor/host valuation for other models | -Copilot, Codex and Claude Code use API-equivalent valuation. Ollama uses its +Copilot, ChatGPT and Claude Code use API-equivalent valuation. Ollama uses its published standard token rates for covered models and retains vendor/host valuation for other models. These are request estimates; account consumption from `/api/usage` includes Ollama billing adjustments such as off-peak discounts. @@ -80,7 +80,7 @@ subset for a model. USD per million tokens. `tokenPricingEntry` and `tokenBasePricing` apply the existing `perMillionTokenRates` conversion, so their resulting `PriceVector` values are USD per base token. Do not divide manually or pass number literals. - Follow `packages/provider-codex/src/pricing.ts` for a complete production + Follow `packages/provider-chatgpt/src/pricing.ts` for a complete production example instead of copying a rate vector into this skill. `collectModelPricingIssues` in @@ -138,7 +138,7 @@ ineligible. normalizes nothing itself. - Claude Code resolves pricing from the dated raw upstream id before catalog aliases are merged into public ids. -- Codex and Ollama use the raw upstream slug directly. +- ChatGPT and Ollama use the raw upstream slug directly. ## Source Cautions diff --git a/.agents/skills/probing-prod-upstream/SKILL.md b/.agents/skills/probing-prod-upstream/SKILL.md index ffa4aa50de..e5eca1dac3 100644 --- a/.agents/skills/probing-prod-upstream/SKILL.md +++ b/.agents/skills/probing-prod-upstream/SKILL.md @@ -21,7 +21,7 @@ and non-production fixtures only. refresh token and leave production holding an invalid predecessor, interrupting production token renewal. This is a production continuity constraint. Do not extract refresh tokens, invoke refresh endpoints, call automatic-refresh helpers -(including `ensureCodexAccessToken` and `ensureClaudeCodeAccessToken`), or route +(including `ensureChatGPTAccessToken` and `ensureClaudeCodeAccessToken`), or route through a gateway operation that may trigger renewal. For providers with refresh-token renewal, use the existing access token. If it is missing, expired, or rejected, stop probing that account and report the blocker; let production @@ -110,7 +110,7 @@ Do not import a provider entrypoint that can renew tokens or persist state. - **Copilot:** follow [the Copilot access reference](references/copilot.md) for PAT exchange, token-advertised endpoints, and Copilot Chat headers. -- **Codex / Claude Code:** use the selected account's persisted +- **ChatGPT / Claude Code:** use the selected account's persisted `state_json.accounts[].accessToken.token` and its expiry. Read `fetch.ts` and the protocol transport for headers and endpoints; join the credential to the configured account identity. All refresh-token paths remain forbidden. diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index 8fe88fac46..bcaaa6b72d 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -114,7 +114,7 @@ jobs: - name: lint (gateway) targets: packages/gateway - name: lint (other packages) - targets: eslint.config.ts vitest.config.ts packages/agent-setup packages/http packages/interceptor packages/platform packages/protocols packages/provider packages/provider-azure packages/provider-claude-code packages/provider-codex packages/provider-copilot packages/provider-custom packages/provider-ollama packages/proxy packages/test-utils packages/translate scripts tools + targets: eslint.config.ts vitest.config.ts packages/agent-setup packages/http packages/interceptor packages/platform packages/protocols packages/provider packages/provider-azure packages/provider-chatgpt packages/provider-claude-code packages/provider-copilot packages/provider-custom packages/provider-ollama packages/proxy packages/test-utils packages/translate scripts tools name: ${{ matrix.name }} steps: - name: Checkout diff --git a/AGENTS.md b/AGENTS.md index bfc241dabb..de11e59a6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,8 +65,8 @@ | Package | `packages/protocols` | Defines protocol contracts. | | Package | `packages/provider` | Defines provider contracts. | | Package | `packages/provider-azure` | Integrates Azure OpenAI. | +| Package | `packages/provider-chatgpt` | Integrates ChatGPT subscriptions. | | Package | `packages/provider-claude-code` | Integrates Claude Code subscriptions. | -| Package | `packages/provider-codex` | Integrates OpenAI Codex subscriptions. | | Package | `packages/provider-copilot` | Integrates GitHub Copilot subscriptions. | | Package | `packages/provider-custom` | Integrates OpenAI-compatible providers. | | Package | `packages/provider-ollama` | Integrates Ollama. | diff --git a/apps/web/__tests__/browser-entrypoints_test.ts b/apps/web/__tests__/browser-entrypoints_test.ts index e83a1e0554..431abe918c 100644 --- a/apps/web/__tests__/browser-entrypoints_test.ts +++ b/apps/web/__tests__/browser-entrypoints_test.ts @@ -51,7 +51,7 @@ test('browser entrypoints exclude backend modules even without tree-shaking', as expect(modules.size).toBeGreaterThan(packages.length); expect(entryExports.get('gateway')).toEqual([]); expect([...modules].filter(id => - (/\/packages\/(gateway|platform|http|provider-(azure|codex|claude-code|copilot|ollama))\//.test(id) + (/\/packages\/(gateway|platform|http|provider-(azure|chatgpt|claude-code|copilot|ollama))\//.test(id) && !id.endsWith('/gateway/src/browser.ts')) || /\/apps\/platform-(node|cloudflare)\//.test(id))).toEqual([]); expect([...modules].filter(id => diff --git a/apps/web/__tests__/components/search/eligibility_test.ts b/apps/web/__tests__/components/search/eligibility_test.ts index b6cb0b8709..617c4ef11a 100644 --- a/apps/web/__tests__/components/search/eligibility_test.ts +++ b/apps/web/__tests__/components/search/eligibility_test.ts @@ -4,15 +4,15 @@ import type { ControlPlaneModel, UpstreamRecord } from '../../../src/api/types'; import { eligibleSearchUpstreams } from '../../../src/components/search/eligibility'; describe('OpenAI search passthrough eligibility', () => { - it('keeps only enabled Codex or Custom upstreams with chat models', () => { + it('keeps only enabled ChatGPT or Custom upstreams with chat models', () => { const upstreams = [ - { id: 'codex', kind: 'codex', enabled: true }, + { id: 'chatgpt', kind: 'chatgpt', enabled: true }, { id: 'custom', kind: 'custom', enabled: false }, { id: 'ollama', kind: 'ollama', enabled: true }, ] as UpstreamRecord[]; const models = [{ - id: 'gpt-5', kind: 'chat', upstreams: [{ id: 'codex', kind: 'codex', name: 'Codex', hue: 210 }], + id: 'gpt-5', kind: 'chat', upstreams: [{ id: 'chatgpt', kind: 'chatgpt', name: 'ChatGPT', hue: 210 }], }] as ControlPlaneModel[]; - expect(eligibleSearchUpstreams(upstreams, models).map(upstream => upstream.id)).toEqual(['codex']); + expect(eligibleSearchUpstreams(upstreams, models).map(upstream => upstream.id)).toEqual(['chatgpt']); }); }); diff --git a/apps/web/__tests__/components/upstream-editor/codex-import_test.tsx b/apps/web/__tests__/components/upstream-editor/chatgpt-import_test.tsx similarity index 96% rename from apps/web/__tests__/components/upstream-editor/codex-import_test.tsx rename to apps/web/__tests__/components/upstream-editor/chatgpt-import_test.tsx index 2341b4cba1..1fc0d246d3 100644 --- a/apps/web/__tests__/components/upstream-editor/codex-import_test.tsx +++ b/apps/web/__tests__/components/upstream-editor/chatgpt-import_test.tsx @@ -6,12 +6,12 @@ import { upstreamRecord } from '../../api/upstream-fixture'; import { renderInApp } from '../../render'; import { settle } from '../../settle'; -const PREVIEW_PATH = '/api/upstreams/codex/import/preview'; -const EXCHANGE_PATH = '/api/upstreams/codex/import/exchange'; +const PREVIEW_PATH = '/api/upstreams/chatgpt/import/preview'; +const EXCHANGE_PATH = '/api/upstreams/chatgpt/import/exchange'; -const record = upstreamRecord('up_codex', { - name: 'Codex', - kind: 'codex', +const record = upstreamRecord('up_chatgpt', { + name: 'ChatGPT', + kind: 'chatgpt', config: { accounts: [] }, state: { accounts: [] }, }); @@ -63,7 +63,7 @@ const openImport = async () => { await settle(); }; -describe('Codex credential import', () => { +describe('ChatGPT credential import', () => { it('offers all three sources and asks for no authorization URL until the OAuth tab is chosen', async () => { await openImport(); diff --git a/apps/web/__tests__/components/upstream-editor/codex-quota-expiry_test.tsx b/apps/web/__tests__/components/upstream-editor/chatgpt-quota-expiry_test.tsx similarity index 79% rename from apps/web/__tests__/components/upstream-editor/codex-quota-expiry_test.tsx rename to apps/web/__tests__/components/upstream-editor/chatgpt-quota-expiry_test.tsx index aec70f21bc..11b8caab63 100644 --- a/apps/web/__tests__/components/upstream-editor/codex-quota-expiry_test.tsx +++ b/apps/web/__tests__/components/upstream-editor/chatgpt-quota-expiry_test.tsx @@ -1,8 +1,8 @@ import { act, screen, within } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CodexAccountCard } from '../../../src/components/upstream-editor/codex-account-card'; -import type { CodexRecord } from '../../../src/components/upstreams/codex-account'; +import { ChatGPTAccountCard } from '../../../src/components/upstream-editor/chatgpt-account-card'; +import type { ChatGPTRecord } from '../../../src/components/upstreams/chatgpt-account'; import { UpstreamSignals } from '../../../src/components/upstreams/signals'; import { upstreamRecord } from '../../api/upstream-fixture'; import { stubLocalStorage } from '../../local-storage-stub'; @@ -15,10 +15,10 @@ const observed = '2026-07-28T11:00:00.000Z'; const primaryReset = '2026-07-28T13:00:00.000Z'; const secondaryReset = '2026-08-01T12:00:00.000Z'; const record = upstreamRecord('', { - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ email: 'fixture@example.com', chatgptAccountId: 'fixture', chatgptUserId: 'fixture', planType: 'plus' }] }, state: { accounts: [{ chatgptAccountId: 'fixture', state: 'active', state_updated_at: observed }] }, -}) as CodexRecord; +}) as ChatGPTRecord; // Control-plane projections retain observed usage and date only known exhausted windows. const cases = [ @@ -34,7 +34,7 @@ afterEach(() => vi.useRealTimers()); beforeEach(() => { primeDefaultSession(); }); -describe('Codex projected quota on list and card', () => { +describe('ChatGPT projected quota on list and card', () => { it.each(cases)('expires the known timer without new traffic: $name', async ({ primary, secondary, until, ...scenario }) => { vi.useFakeTimers(); vi.setSystemTime('2026-07-28T12:00:00.000Z'); @@ -49,8 +49,8 @@ describe('Codex projected quota on list and card', () => { secondary_reset_after_at: 'secondaryReset' in scenario ? scenario.secondaryReset : secondaryReset, ratelimited_until: until, }; - const projected = { ...record, codex_quota: { premium: quota } }; - const view = renderInApp(<>
); + const projected = { ...record, chatgpt_quota: { premium: quota } }; + const view = renderInApp(<>
); const list = within(screen.getByTestId('list')); const card = within(screen.getByTestId('card')); expect(list.getByText('Rate limited')).toBeTruthy(); @@ -72,10 +72,10 @@ describe('Codex projected quota on list and card', () => { expect(card.getByText('Heavy usage (100%)')).toBeTruthy(); expect(list.getAllByText('0%').length).toBeGreaterThan(0); expect(card.getAllByText('0% remaining').length).toBeGreaterThan(0); - expect(projected.codex_quota.premium).toEqual(quota); + expect(projected.chatgpt_quota.premium).toEqual(quota); - const fresh = { ...projected, codex_quota: { premium: { ...quota, primary_used_percent: 12, secondary_used_percent: 35, ratelimited_until: undefined } } }; - view.rerender(<>
); + const fresh = { ...projected, chatgpt_quota: { premium: { ...quota, primary_used_percent: 12, secondary_used_percent: 35, ratelimited_until: undefined } } }; + view.rerender(<>
); expect(card.getByText('Active')).toBeTruthy(); expect(list.getByText('88%')).toBeTruthy(); expect(card.getByText('88% remaining')).toBeTruthy(); @@ -83,8 +83,8 @@ describe('Codex projected quota on list and card', () => { }); it('keeps an exhausted quota without inventing a timed restriction when its reset is unknown', () => { - const projected = { ...record, codex_quota: { premium: { observed_at: observed, primary_used_percent: 100 } } }; - renderInApp(<>); + const projected = { ...record, chatgpt_quota: { premium: { observed_at: observed, primary_used_percent: 100 } } }; + renderInApp(<>); expect(screen.queryByText('Rate limited')).toBeNull(); expect(screen.queryByText(/^Rate-limited until/)).toBeNull(); expect(screen.getByText('Heavy usage (100%)')).toBeTruthy(); diff --git a/apps/web/__tests__/components/upstream-editor/codex-reset-cards_test.tsx b/apps/web/__tests__/components/upstream-editor/chatgpt-reset-cards_test.tsx similarity index 83% rename from apps/web/__tests__/components/upstream-editor/codex-reset-cards_test.tsx rename to apps/web/__tests__/components/upstream-editor/chatgpt-reset-cards_test.tsx index 8f402d0460..7636c52732 100644 --- a/apps/web/__tests__/components/upstream-editor/codex-reset-cards_test.tsx +++ b/apps/web/__tests__/components/upstream-editor/chatgpt-reset-cards_test.tsx @@ -1,9 +1,9 @@ import { fireEvent, screen, waitFor, within } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CodexAccountCard } from '../../../src/components/upstream-editor/codex-account-card'; -import { CodexResetCards } from '../../../src/components/upstream-editor/codex-reset-cards'; -import type { CodexRecord } from '../../../src/components/upstreams/codex-account'; +import { ChatGPTAccountCard } from '../../../src/components/upstream-editor/chatgpt-account-card'; +import { ChatGPTResetCards } from '../../../src/components/upstream-editor/chatgpt-reset-cards'; +import type { ChatGPTRecord } from '../../../src/components/upstreams/chatgpt-account'; import { upstreamRecord } from '../../api/upstream-fixture'; import { stubLocalStorage } from '../../local-storage-stub'; import { renderInApp } from '../../render'; @@ -21,11 +21,11 @@ const card = { description: 'Ready to redeem', }; -const record = upstreamRecord('up_codex', { - kind: 'codex', +const record = upstreamRecord('up_chatgpt', { + kind: 'chatgpt', config: { accounts: [{ email: 'alice@example.com', chatgptAccountId: 'acc_test', chatgptUserId: 'usr_test', planType: 'plus' }] }, state: { accounts: [{ chatgptAccountId: 'acc_test', state: 'active', state_updated_at: '2026-06-17T00:00:00Z' }] }, -}) as CodexRecord; +}) as ChatGPTRecord; let consumeBodies: Array<{ idempotency_key: string }>; let consumeAttempts: number; @@ -40,10 +40,10 @@ beforeEach(() => { vi.spyOn(globalThis.crypto, 'randomUUID').mockReturnValue('00000000-0000-4000-8000-000000000001'); vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const path = new URL(String(input), 'http://localhost').pathname; - if (path === '/api/upstreams/codex/reset-credits') { + if (path === '/api/upstreams/chatgpt/reset-credits') { return Response.json({ reset_credits: { available_count: 1, credits: [card] } }); } - if (path === '/api/upstreams/codex/reset-credits/consume') { + if (path === '/api/upstreams/chatgpt/reset-credits/consume') { consumeAttempts += 1; consumeBodies.push(JSON.parse(String(init?.body)) as { idempotency_key: string }); if (failFirstConsume && consumeAttempts === 1) return Response.json({ error: 'temporary failure' }, { status: 502 }); @@ -62,10 +62,10 @@ afterEach(() => { vi.unstubAllGlobals(); }); -describe('Codex reset cards', () => { +describe('ChatGPT reset cards', () => { it('loads, confirms, and reuses one redemption key when a retry succeeds', async () => { const onQuotaReset = vi.fn(); - renderInApp(); + renderInApp(); expect(await screen.findByText('Full reset')).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: 'Use' })); @@ -82,16 +82,16 @@ describe('Codex reset cards', () => { '00000000-0000-4000-8000-000000000001', '00000000-0000-4000-8000-000000000001', ]); - expect(await screen.findByText('The Codex rate-limit windows were reset.')).toBeTruthy(); + expect(await screen.findByText('The ChatGPT rate-limit windows were reset.')).toBeTruthy(); expect(screen.queryByText('Full reset')).toBeNull(); expect(screen.getByText(/The card was processed, but Floway could not refresh all details/)).toBeTruthy(); }); it('removes both the quota windows and account credit summary after a reset', async () => { failFirstConsume = false; - const accountRecord: CodexRecord = { + const accountRecord: ChatGPTRecord = { ...record, - codex_quota: { + chatgpt_quota: { codex: { observed_at: '2026-06-17T00:00:00Z', primary_used_percent: 100, @@ -100,7 +100,7 @@ describe('Codex reset cards', () => { }, }, }; - const view = renderInApp(); + const view = renderInApp(); expect(screen.getByText('credits: 1')).toBeTruthy(); fireEvent.click(await screen.findByRole('button', { name: 'Use' })); @@ -108,13 +108,13 @@ describe('Codex reset cards', () => { fireEvent.click(await within(dialog).findByRole('button', { name: 'Use reset card' })); await waitFor(() => expect(screen.queryByText('credits: 1')).toBeNull()); - expect(screen.getByText('No quota snapshots yet - Codex calls populate them.')).toBeTruthy(); + expect(screen.getByText('No quota snapshots yet - ChatGPT calls populate them.')).toBeTruthy(); - view.rerender(); + view.rerender(); expect(screen.queryByText('credits: 1')).toBeNull(); - view.rerender(); @@ -125,7 +125,7 @@ describe('Codex reset cards', () => { vi.mocked(crypto.randomUUID) .mockReturnValueOnce('00000000-0000-4000-8000-000000000001') .mockReturnValueOnce('00000000-0000-4000-8000-000000000002'); - renderInApp(); + renderInApp(); fireEvent.click(await screen.findByRole('button', { name: 'Use' })); let dialog = await screen.findByRole('dialog'); fireEvent.click(await within(dialog).findByRole('button', { name: 'Use reset card' })); @@ -143,7 +143,7 @@ describe('Codex reset cards', () => { }); it('does not redeem when confirmation is cancelled', async () => { - renderInApp(); + renderInApp(); fireEvent.click(await screen.findByRole('button', { name: 'Use' })); fireEvent.click(await within(await screen.findByRole('dialog')).findByRole('button', { name: 'Cancel' })); await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()); @@ -152,7 +152,7 @@ describe('Codex reset cards', () => { it('shows a load failure without pretending there are no cards or echoing upstream input', async () => { vi.mocked(fetch).mockResolvedValueOnce(Response.json({ error: 'sensitive stored input' }, { status: 502 })); - renderInApp(); + renderInApp(); expect(await screen.findByText('Could not load reset cards. Try refreshing again.')).toBeTruthy(); expect(screen.queryByText(/No reset cards are available/)).toBeNull(); expect(screen.queryByText('sensitive stored input')).toBeNull(); @@ -170,7 +170,7 @@ describe('Codex reset cards', () => { ], }, })); - renderInApp(); + renderInApp(); expect(await screen.findByText('Expired')).toBeTruthy(); expect(screen.getByText('future_status')).toBeTruthy(); expect(screen.queryByRole('button', { name: 'Use' })).toBeNull(); @@ -180,7 +180,7 @@ describe('Codex reset cards', () => { }); it('does not load reset cards for an unsaved account', () => { - renderInApp(); + renderInApp(); expect(screen.queryByText('Reset cards')).toBeNull(); expect(fetch).not.toHaveBeenCalled(); }); @@ -189,7 +189,7 @@ describe('Codex reset cards', () => { vi.mocked(fetch).mockResolvedValueOnce(Response.json({ reset_credits: { available_count: 0, credits: [{ ...card, status: 'redeemed' }] }, })); - renderInApp(); + renderInApp(); const redeemed = await screen.findByRole('button', { name: 'Redeemed' }); expect(redeemed.hasAttribute('disabled')).toBe(true); fireEvent.click(redeemed); diff --git a/apps/web/__tests__/components/upstream-editor/page-credential-submit_test.tsx b/apps/web/__tests__/components/upstream-editor/page-credential-submit_test.tsx index e8386c7ba9..7ef116790a 100644 --- a/apps/web/__tests__/components/upstream-editor/page-credential-submit_test.tsx +++ b/apps/web/__tests__/components/upstream-editor/page-credential-submit_test.tsx @@ -48,7 +48,7 @@ vi.mock('../../../src/components/upstream-editor/config-sidebar', () => ({ vi.mock('../../../src/components/upstream-editor/workspace', () => ({ UpstreamWorkspace: () => null })); -const blueprint = upstreamRecord('', { kind: 'codex', config: { accounts: [] }, state: { accounts: [] } }); +const blueprint = upstreamRecord('', { kind: 'chatgpt', config: { accounts: [] }, state: { accounts: [] } }); const renderPage = (record: UpstreamRecord, mode: 'create' | 'edit') => { const router = createMemoryRouter([ @@ -65,17 +65,17 @@ const renderPage = (record: UpstreamRecord, mode: 'create' | 'edit') => { beforeEach(() => { vi.clearAllMocks(); - apiMocks.create.mockResolvedValue({ data: { ...blueprint, id: 'up_codex', ...imported }, error: null }); + apiMocks.create.mockResolvedValue({ data: { ...blueprint, id: 'up_chatgpt', ...imported }, error: null }); }); -test('creating a Codex upstream saves the imported credential', async () => { +test('creating a ChatGPT upstream saves the imported credential', async () => { renderPage(blueprint, 'create'); fireEvent.click(screen.getByRole('button', { name: 'Import credential' })); fireEvent.click(screen.getByRole('button', { name: i18n.t('dashboard.upstreamEditor.actions.save') })); await waitFor(() => expect(apiMocks.create).toHaveBeenCalledWith({ json: expect.objectContaining({ - kind: 'codex', + kind: 'chatgpt', config: imported.config, state: imported.state, }), @@ -83,8 +83,8 @@ test('creating a Codex upstream saves the imported credential', async () => { expect(screen.queryByText(i18n.t('dashboard.upstreamEditor.fetchDirty.unsavedCredential'))).toBeNull(); }); -test('editing a saved Codex upstream still rejects unsaved credential changes', async () => { - renderPage({ ...blueprint, id: 'up_codex' }, 'edit'); +test('editing a saved ChatGPT upstream still rejects unsaved credential changes', async () => { + renderPage({ ...blueprint, id: 'up_chatgpt' }, 'edit'); fireEvent.click(screen.getByRole('button', { name: 'Edit credential draft' })); fireEvent.click(screen.getByRole('button', { name: i18n.t('dashboard.upstreamEditor.actions.save') })); diff --git a/apps/web/__tests__/components/upstream-metrics/chart_test.tsx b/apps/web/__tests__/components/upstream-metrics/chart_test.tsx index 1ad8403937..efdac5b119 100644 --- a/apps/web/__tests__/components/upstream-metrics/chart_test.tsx +++ b/apps/web/__tests__/components/upstream-metrics/chart_test.tsx @@ -7,7 +7,7 @@ import { UpstreamMetricsChartSection } from '../../../src/components/upstream-me import { buildUpstreamMetricsChart, type UpstreamMetricsChart } from '../../../src/components/upstream-metrics/plot'; import { renderInApp } from '../../render'; import { upstreamMetricAxisPriority, type UpstreamMetricSample } from '@floway-dev/provider/browser'; -import { resolveUpstreamMetricDisplays } from '@floway-dev/provider-codex/browser'; +import { resolveUpstreamMetricDisplays } from '@floway-dev/provider-chatgpt/browser'; afterEach(() => vi.restoreAllMocks()); beforeEach(() => vi.spyOn(HTMLSpanElement.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 40, 12))); @@ -273,7 +273,7 @@ it('renders provider-converted observations consistently in curves, axes, and to { name: 'credits.balance', labels: {}, timestamp: 60_000, value: 200 }, ]; const series = resolveUpstreamMetricDisplays(source, { quotaDisplay: 'remaining' }); - const converted = buildUpstreamMetricsChart(series, { id: 'seat', kind: 'codex', name: 'Seat', hue: 210 }); + const converted = buildUpstreamMetricsChart(series, { id: 'seat', kind: 'chatgpt', name: 'Seat', hue: 210 }); const view = renderInApp(); const application = await screen.findByRole('application', { name: 'Seat' }); await waitFor(() => expect(view.container.querySelector('.recharts-area-curve')).not.toBeNull()); diff --git a/apps/web/__tests__/components/upstream-metrics/display-name_test.ts b/apps/web/__tests__/components/upstream-metrics/display-name_test.ts index 8e2a8ee723..e334f64161 100644 --- a/apps/web/__tests__/components/upstream-metrics/display-name_test.ts +++ b/apps/web/__tests__/components/upstream-metrics/display-name_test.ts @@ -29,7 +29,7 @@ it('renders arbitrary label values independently of JavaScript object prototypes }); it('selects each provider decoder from the current upstream', () => { - expect(displayOf('codex', { name: 'credits.balance', labels: {} }).unit).toBe('credits'); + expect(displayOf('chatgpt', { name: 'credits.balance', labels: {} }).unit).toBe('credits'); expect(displayOf('copilot', { name: 'quota.remaining', labels: { quota: 'premium_interactions' } }).unit).toBe('usd'); }); @@ -63,10 +63,10 @@ it('names dynamic quota windows by direction', async () => { await setLanguage(language); const translate = i18n.getFixedT(language) as unknown as TFunction; for (const [seconds, remaining, usage, namedRemaining, namedUsage] of cases) { - expect(displayOf('codex', { name: 'quota.window.used_percent', labels: { limit: 'codex', window_seconds: seconds } }, 'remaining', translate).name).toBe(remaining); - expect(displayOf('codex', { name: 'quota.window.used_percent', labels: { limit: 'codex', window_seconds: seconds } }, 'used', translate).name).toBe(usage); - expect(displayOf('codex', { name: 'quota.window.used_percent', labels: { limit: 'review', window_seconds: seconds } }, 'remaining', translate).name).toBe(namedRemaining); - expect(displayOf('codex', { name: 'quota.window.used_percent', labels: { limit: 'review', window_seconds: seconds } }, 'used', translate).name).toBe(namedUsage); + expect(displayOf('chatgpt', { name: 'quota.window.used_percent', labels: { limit: 'codex', window_seconds: seconds } }, 'remaining', translate).name).toBe(remaining); + expect(displayOf('chatgpt', { name: 'quota.window.used_percent', labels: { limit: 'codex', window_seconds: seconds } }, 'used', translate).name).toBe(usage); + expect(displayOf('chatgpt', { name: 'quota.window.used_percent', labels: { limit: 'review', window_seconds: seconds } }, 'remaining', translate).name).toBe(namedRemaining); + expect(displayOf('chatgpt', { name: 'quota.window.used_percent', labels: { limit: 'review', window_seconds: seconds } }, 'used', translate).name).toBe(namedUsage); } } }); @@ -77,7 +77,7 @@ it('renders equal fixed and encoded windows with consistent labels', async () => for (const quotaDisplay of ['remaining', 'used'] as const) { for (const [quota, seconds] of [['five_hour', 18_000], ['seven_day', 604_800]] as const) { const fixed = displayOf('claude-code', { name: 'quota.utilization', labels: { quota } }, quotaDisplay); - const encoded = displayOf('codex', { name: 'quota.window.used_percent', labels: { limit: 'codex', window_seconds: seconds } }, quotaDisplay); + const encoded = displayOf('chatgpt', { name: 'quota.window.used_percent', labels: { limit: 'codex', window_seconds: seconds } }, quotaDisplay); expect(fixed.name).toBe(encoded.name); } } diff --git a/apps/web/__tests__/components/upstreams/codex-account_test.ts b/apps/web/__tests__/components/upstreams/chatgpt-account_test.ts similarity index 74% rename from apps/web/__tests__/components/upstreams/codex-account_test.ts rename to apps/web/__tests__/components/upstreams/chatgpt-account_test.ts index 3a8d44a116..08f1689d3e 100644 --- a/apps/web/__tests__/components/upstreams/codex-account_test.ts +++ b/apps/web/__tests__/components/upstreams/chatgpt-account_test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest'; -import type { CodexQuotaSnapshotMap, CodexRateLimitResetCredit, UpstreamRecord } from '../../../src/api/types'; -import { accountStatus, codexRenewable, codexResetCreditIsUsable, findCredential, latestCredits, quotaEntries } from '../../../src/components/upstreams/codex-account'; +import type { ChatGPTQuotaSnapshotMap, ChatGPTRateLimitResetCredit, UpstreamRecord } from '../../../src/api/types'; +import { accountStatus, chatgptRenewable, chatgptResetCreditIsUsable, findCredential, latestCredits, quotaEntries } from '../../../src/components/upstreams/chatgpt-account'; -type CodexRecord = Extract; +type ChatGPTRecord = Extract; const ACCOUNT_ID = 'acct_0123456789abcdef'; const NOW = Date.parse('2026-07-28T12:00:00.000Z'); @@ -12,15 +12,15 @@ const FUTURE = '2026-07-28T13:00:00.000Z'; const activeCredential = { chatgptAccountId: ACCOUNT_ID, state: 'active' as const, state_updated_at: PAST }; const activeLookup = { kind: 'present' as const, credential: activeCredential }; -const record = (state: CodexRecord['state'], codexQuota?: CodexQuotaSnapshotMap): CodexRecord => ({ +const record = (state: ChatGPTRecord['state'], chatgptQuota?: ChatGPTQuotaSnapshotMap): ChatGPTRecord => ({ id: 'up_1', - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ email: 'a@example.com', chatgptAccountId: ACCOUNT_ID, chatgptUserId: 'u1', planType: 'plus' }] }, state, - codex_quota: codexQuota, -} as unknown as CodexRecord); + chatgpt_quota: chatgptQuota, +} as unknown as ChatGPTRecord); -describe('codex credential lookup', () => { +describe('ChatGPT credential lookup', () => { it('matches the configured account rather than taking the first stored one', () => { const state = { accounts: [ @@ -40,7 +40,7 @@ describe('codex credential lookup', () => { }); }); -describe('codex quota entries', () => { +describe('ChatGPT quota entries', () => { it('reads both windows and orders limits by name', () => { const entries = quotaEntries({ weekly: { observed_at: PAST, active_limit: 'Weekly', primary_used_percent: 20, primary_window_minutes: 10_080 }, @@ -66,7 +66,7 @@ describe('codex quota entries', () => { }); }); -describe('codex credits', () => { +describe('ChatGPT credits', () => { it('takes the freshest observation that carries a balance', () => { const credits = latestCredits({ stale: { observed_at: '2026-07-01T00:00:00.000Z', credits_balance: 10 }, @@ -82,22 +82,22 @@ describe('codex credits', () => { }); }); -describe('codex reset cards', () => { - const credit = (overrides: Partial = {}): CodexRateLimitResetCredit => ({ +describe('ChatGPT reset cards', () => { + const credit = (overrides: Partial = {}): ChatGPTRateLimitResetCredit => ({ id: 'credit-1', reset_type: 'codex_rate_limits', status: 'available', granted_at: PAST, expires_at: FUTURE, title: null, description: null, ...overrides, }); - it('only enables known available, unexpired Codex reset cards', () => { - expect(codexResetCreditIsUsable(credit(), NOW)).toBe(true); - expect(codexResetCreditIsUsable(credit({ status: 'redeemed' }), NOW)).toBe(false); - expect(codexResetCreditIsUsable(credit({ reset_type: 'future_reset' }), NOW)).toBe(false); - expect(codexResetCreditIsUsable(credit({ expires_at: PAST }), NOW)).toBe(false); - expect(codexResetCreditIsUsable(credit({ expires_at: null }), NOW)).toBe(true); + it('only enables known available, unexpired ChatGPT reset cards', () => { + expect(chatgptResetCreditIsUsable(credit(), NOW)).toBe(true); + expect(chatgptResetCreditIsUsable(credit({ status: 'redeemed' }), NOW)).toBe(false); + expect(chatgptResetCreditIsUsable(credit({ reset_type: 'future_reset' }), NOW)).toBe(false); + expect(chatgptResetCreditIsUsable(credit({ expires_at: PAST }), NOW)).toBe(false); + expect(chatgptResetCreditIsUsable(credit({ expires_at: null }), NOW)).toBe(true); }); }); -describe('codex account status', () => { +describe('ChatGPT account status', () => { it('reports the furthest live rate limit', () => { const entries = quotaEntries({ daily: { observed_at: PAST, ratelimited_until: FUTURE }, @@ -127,39 +127,39 @@ describe('codex account status', () => { }); }); -describe('codex renewability', () => { +describe('ChatGPT renewability', () => { it('reads the redacted flag the list hands back', () => { - expect(codexRenewable({ ...activeCredential, refresh_token_set: true })).toBe(true); - expect(codexRenewable({ ...activeCredential, refresh_token_set: false })).toBe(false); + expect(chatgptRenewable({ ...activeCredential, refresh_token_set: true })).toBe(true); + expect(chatgptRenewable({ ...activeCredential, refresh_token_set: false })).toBe(false); }); it('reads the raw token an import merges into the draft', () => { - expect(codexRenewable({ ...activeCredential, refresh_token: 'rt' })).toBe(true); - expect(codexRenewable({ ...activeCredential, refresh_token: null })).toBe(false); - expect(codexRenewable(activeCredential)).toBe(false); + expect(chatgptRenewable({ ...activeCredential, refresh_token: 'rt' })).toBe(true); + expect(chatgptRenewable({ ...activeCredential, refresh_token: null })).toBe(false); + expect(chatgptRenewable(activeCredential)).toBe(false); }); }); -describe('codex credential lookup without an account id', () => { +describe('ChatGPT credential lookup without an account id', () => { it('joins config and state on a shared null', () => { const nullIdRecord = { id: 'up_1', - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ email: null, chatgptAccountId: null, chatgptUserId: null, planType: null }] }, state: { accounts: [{ chatgptAccountId: null, state: 'active', state_updated_at: PAST, refresh_token_set: false }] }, - } as unknown as CodexRecord; + } as unknown as ChatGPTRecord; const lookup = findCredential(nullIdRecord); expect(lookup.kind).toBe('present'); - if (lookup.kind === 'present') expect(codexRenewable(lookup.credential)).toBe(false); + if (lookup.kind === 'present') expect(chatgptRenewable(lookup.credential)).toBe(false); }); it('still reports a mismatch when state names an account the config does not', () => { const mismatched = { id: 'up_1', - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ email: null, chatgptAccountId: null, chatgptUserId: null, planType: null }] }, state: { accounts: [{ chatgptAccountId: ACCOUNT_ID, state: 'active', state_updated_at: PAST }] }, - } as unknown as CodexRecord; + } as unknown as ChatGPTRecord; expect(findCredential(mismatched)).toEqual({ kind: 'account-id-mismatch', expectedAccountId: null }); }); }); diff --git a/apps/web/__tests__/components/upstreams/signals_test.ts b/apps/web/__tests__/components/upstreams/signals_test.ts index 2d5eaa8dba..9a73b676c9 100644 --- a/apps/web/__tests__/components/upstreams/signals_test.ts +++ b/apps/web/__tests__/components/upstreams/signals_test.ts @@ -70,10 +70,10 @@ describe('upstream readout by provider', () => { it('stacks what a reading reports on their own lines', () => { const detail = readoutOf({ - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType: 'plus' }] }, state: { accounts: [] }, - codex_quota: { plus: { observed_at: OBSERVED, primary_used_percent: 25, primary_window_minutes: 300 } }, + chatgpt_quota: { plus: { observed_at: OBSERVED, primary_used_percent: 25, primary_window_minutes: 300 } }, }).signals[0]?.detail; // A window with no reset still states the reading and when it was taken, and the // two are lines rather than one run. @@ -103,12 +103,12 @@ describe('upstream readout by provider', () => { // limit rather than inferring it from a percentage that can read low while it // holds, and states how long the wait still has to run rather than when it // ends. Hours are the largest unit it uses. - it('states a Codex rate limit last, as the time it still has to run', () => { + it('states a ChatGPT rate limit last, as the time it still has to run', () => { const rowFor = (until: string) => rowOf({ - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType: 'pro' }] }, state: { accounts: [] }, - codex_quota: { + chatgpt_quota: { premium: { observed_at: OBSERVED, primary_used_percent: 12, primary_window_minutes: 300, ratelimited_until: until }, }, }); @@ -120,12 +120,12 @@ describe('upstream readout by provider', () => { expect(rowFor('2026-07-28T12:00:01.000Z')).toBe('ChatGPT Pro | 88% 5h | Rate limited 1m'); }); - it('lets an elapsed Codex rate limit go rather than holding it on the row', () => { + it('lets an elapsed ChatGPT rate limit go rather than holding it on the row', () => { expect(rowOf({ - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType: 'pro' }] }, state: { accounts: [] }, - codex_quota: { premium: { observed_at: OBSERVED, primary_used_percent: 12, primary_window_minutes: 300, ratelimited_until: '2020-01-01T00:00:00.000Z' } }, + chatgpt_quota: { premium: { observed_at: OBSERVED, primary_used_percent: 12, primary_window_minutes: 300, ratelimited_until: '2020-01-01T00:00:00.000Z' } }, })).toBe('ChatGPT Pro | 88% 5h'); }); @@ -159,12 +159,12 @@ describe('upstream readout by provider', () => { expect(readoutOf({ kind: 'copilot', state: null })).toEqual({ plan: 'Copilot', signals: [] }); }); - it('names a Codex window by the length its header states, under the ChatGPT plan', () => { + it('names a ChatGPT window by the length its header states, under the ChatGPT plan', () => { const record = { - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType: 'pro' }] }, state: { accounts: [] }, - codex_quota: { + chatgpt_quota: { pro: { observed_at: OBSERVED, primary_used_percent: 25, primary_window_minutes: 300, @@ -177,28 +177,28 @@ describe('upstream readout by provider', () => { it('forwards a ChatGPT plan this dashboard has not seen', () => { expect(readoutOf({ - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType: 'ultra' }] }, state: { accounts: [] }, }).plan).toBe('ChatGPT ultra'); }); - it('falls back to a Codex window position when no length came with it', () => { + it('falls back to a ChatGPT window position when no length came with it', () => { const record = { - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType: 'plus' }] }, state: { accounts: [] }, - codex_quota: { plus: { observed_at: OBSERVED, primary_used_percent: 25 } }, + chatgpt_quota: { plus: { observed_at: OBSERVED, primary_used_percent: 25 } }, }; expect(rowOf(record)).toBe('ChatGPT Plus | 75% Primary'); }); - it('states the Codex limit observed last rather than every key the map holds', () => { + it('states the ChatGPT limit observed last rather than every key the map holds', () => { const record = { - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType: 'plus' }] }, state: { accounts: [] }, - codex_quota: { + chatgpt_quota: { stale: { observed_at: '2026-07-20T00:00:00.000Z', primary_used_percent: 90, primary_window_minutes: 300 }, current: { observed_at: OBSERVED, primary_used_percent: 12, primary_window_minutes: 300 }, }, @@ -206,12 +206,12 @@ describe('upstream readout by provider', () => { expect(rowOf(record)).toBe('ChatGPT Plus | 88% 5h'); }); - it('carries the Codex credit balance beside the windows', () => { + it('carries the ChatGPT credit balance beside the windows', () => { const withCredits = (credits: Record) => readoutOf({ - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType: 'plus' }] }, state: { accounts: [] }, - codex_quota: { plus: { observed_at: OBSERVED, ...credits } }, + chatgpt_quota: { plus: { observed_at: OBSERVED, ...credits } }, }).signals.map(signal => signal.value); expect(withCredits({ credits_has_credits: true, credits_balance: 42 })).toEqual(['42 credits']); @@ -270,7 +270,7 @@ describe('upstream readout by provider', () => { // groups `business` with its enterprise plans. it('names a ChatGPT plan as Codex itself displays it', () => { const planFor = (planType: string) => readoutOf({ - kind: 'codex', + kind: 'chatgpt', config: { accounts: [{ chatgptAccountId: 'acct_1', planType }] }, state: { accounts: [] }, }).plan; diff --git a/apps/web/package.json b/apps/web/package.json index 44a945ed7d..d4a9ddef0d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,7 +14,7 @@ "@floway-dev/protocols": "workspace:*", "@floway-dev/provider": "workspace:*", "@floway-dev/provider-claude-code": "workspace:*", - "@floway-dev/provider-codex": "workspace:*", + "@floway-dev/provider-chatgpt": "workspace:*", "@floway-dev/provider-copilot": "workspace:*", "@floway-dev/provider-custom": "workspace:*", "@floway-dev/provider-ollama": "workspace:*", diff --git a/apps/web/src/api/types.ts b/apps/web/src/api/types.ts index d59e0f310d..d07f5c0677 100644 --- a/apps/web/src/api/types.ts +++ b/apps/web/src/api/types.ts @@ -7,11 +7,11 @@ export type { ClaudeCodeAccountCredentialSummary, ClaudeCodeQuotaSnapshotData, ClaudeCodeQuotaWindow, - CodexAccountCredentialState, - CodexQuotaSnapshot, - CodexQuotaSnapshotMap, - CodexRateLimitResetCredit, - CodexRateLimitResetCredits, + ChatGPTAccountCredentialState, + ChatGPTQuotaSnapshot, + ChatGPTQuotaSnapshotMap, + ChatGPTRateLimitResetCredit, + ChatGPTRateLimitResetCredits, ProviderModelsFailureResponse, UpstreamRecord, } from '@floway-dev/gateway/browser'; diff --git a/apps/web/src/components/search/eligibility.ts b/apps/web/src/components/search/eligibility.ts index 6b36d8ac3c..29399df058 100644 --- a/apps/web/src/components/search/eligibility.ts +++ b/apps/web/src/components/search/eligibility.ts @@ -7,5 +7,5 @@ export const servesChatFor = (model: ControlPlaneModel, upstreamId: string) => export const eligibleSearchUpstreams = (upstreams: readonly UpstreamRecord[], models: readonly ControlPlaneModel[]) => upstreams.filter(upstream => upstream.enabled - && (upstream.kind === 'codex' || upstream.kind === 'custom') + && (upstream.kind === 'chatgpt' || upstream.kind === 'custom') && models.some(model => servesChatFor(model, upstream.id))); diff --git a/apps/web/src/components/upstream-editor/codex-account-card.tsx b/apps/web/src/components/upstream-editor/chatgpt-account-card.tsx similarity index 66% rename from apps/web/src/components/upstream-editor/codex-account-card.tsx rename to apps/web/src/components/upstream-editor/chatgpt-account-card.tsx index cee345940a..6e219514f8 100644 --- a/apps/web/src/components/upstream-editor/codex-account-card.tsx +++ b/apps/web/src/components/upstream-editor/chatgpt-account-card.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { CodexResetCards } from './codex-reset-cards'; +import { ChatGPTResetCards } from './chatgpt-reset-cards'; import { fluentComponents } from '../../fluent'; import { useTranslation } from '../../i18n/translation'; import { dateTime } from '../../lib/format-time'; @@ -10,14 +10,14 @@ import { useUserPreferences } from '../../lib/use-preferences'; import { StatusBadge } from '../ui/status-badge'; import { TruncationTooltip } from '../ui/truncation-tooltip'; import { shortAccountId } from '../upstreams/account-id'; -import { accountStatus, type CodexRecord, codexRenewable, findCredential, latestCredits, planLabel, quotaEntries } from '../upstreams/codex-account'; +import { accountStatus, type ChatGPTRecord, chatgptRenewable, findCredential, latestCredits, planLabel, quotaEntries } from '../upstreams/chatgpt-account'; import { ProviderIcon } from '../upstreams/provider-badge'; import { QuotaProgressRow } from '../upstreams/quota-progress-row'; import { WALL_CLOCK_REFRESH_MS } from '../upstreams/subscription-quota'; const { Badge, Text } = fluentComponents; -export function CodexAccountCard({ readOnly = false, record }: { readOnly?: boolean; record: CodexRecord }) { +export function ChatGPTAccountCard({ readOnly = false, record }: { readOnly?: boolean; record: ChatGPTRecord }) { const { t } = useTranslation(); const preferences = useUserPreferences(); const now = useNow(WALL_CLOCK_REFRESH_MS); @@ -27,44 +27,44 @@ export function CodexAccountCard({ readOnly = false, record }: { readOnly?: bool const credential = lookup.kind === 'present' ? lookup.credential : null; const [reset, setReset] = useState<{ accountId: string | null; at: number } | null>(null); const visibleQuota = reset?.accountId === account.chatgptAccountId - ? Object.fromEntries(Object.entries(record.codex_quota ?? {}).filter(([, snapshot]) => Date.parse(snapshot.observed_at) > reset.at)) - : record.codex_quota; + ? Object.fromEntries(Object.entries(record.chatgpt_quota ?? {}).filter(([, snapshot]) => Date.parse(snapshot.observed_at) > reset.at)) + : record.chatgpt_quota; const entries = quotaEntries(visibleQuota, now); const credits = latestCredits(visibleQuota); const status = accountStatus(lookup, entries); const statusLabel = status.reason === 'heavy' - ? t('dashboard.upstreamEditor.codex.status.heavy', { percent: status.percent }) + ? t('dashboard.upstreamEditor.chatgpt.status.heavy', { percent: status.percent }) : status.reason === 'rate-limited' - ? t('dashboard.upstreamEditor.codex.status.rateLimited', { time: dateTime(status.until, locale) }) - : t(`dashboard.upstreamEditor.codex.status.${status.reason}`); + ? t('dashboard.upstreamEditor.chatgpt.status.rateLimited', { time: dateTime(status.until, locale) }) + : t(`dashboard.upstreamEditor.chatgpt.status.${status.reason}`); - const renewable = credential === null ? null : codexRenewable(credential); + const renewable = credential === null ? null : chatgptRenewable(credential); const accountId = account.chatgptAccountId; const expiresAt = credential?.accessToken?.expiresAt ?? null; const bearerLabel = expiresAt !== null - ? t('dashboard.upstreamEditor.codex.expires', { time: dateTime(new Date(expiresAt).toISOString(), locale) }) + ? t('dashboard.upstreamEditor.chatgpt.expires', { time: dateTime(new Date(expiresAt).toISOString(), locale) }) : renewable === false - ? t('dashboard.upstreamEditor.codex.expiryUnknownAccessOnly') - : t('dashboard.upstreamEditor.codex.expiryUnknown'); + ? t('dashboard.upstreamEditor.chatgpt.expiryUnknownAccessOnly') + : t('dashboard.upstreamEditor.chatgpt.expiryUnknown'); return
- +
- {account.email ?? t('dashboard.upstreamEditor.codex.unknownEmail')} + {account.email ?? t('dashboard.upstreamEditor.chatgpt.unknownEmail')}
- {planLabel(account) ?? t('dashboard.upstreamEditor.codex.unknownPlan')} + {planLabel(account) ?? t('dashboard.upstreamEditor.chatgpt.unknownPlan')} {renewable !== null && - {t(renewable ? 'dashboard.upstreamEditor.codex.renewable' : 'dashboard.upstreamEditor.codex.accessOnly')} + {t(renewable ? 'dashboard.upstreamEditor.chatgpt.renewable' : 'dashboard.upstreamEditor.chatgpt.accessOnly')} } {credits?.credits_has_credits === false - ? {t('dashboard.upstreamEditor.codex.noCredits')} + ? {t('dashboard.upstreamEditor.chatgpt.noCredits')} : credits?.credits_balance !== undefined && - {t('dashboard.upstreamEditor.codex.credits', { balance: credits.credits_balance })} + {t('dashboard.upstreamEditor.chatgpt.credits', { balance: credits.credits_balance })} } {accountId === null - ? {t('dashboard.upstreamEditor.codex.unknownAccountId')} + ? {t('dashboard.upstreamEditor.chatgpt.unknownAccountId')} : {measureRef => {shortAccountId(accountId)}} } @@ -78,41 +78,41 @@ export function CodexAccountCard({ readOnly = false, record }: { readOnly?: bool {credential && {bearerLabel}} {entries.length === 0 - ? {t('dashboard.upstreamEditor.codex.noSnapshot')} + ? {t('dashboard.upstreamEditor.chatgpt.noSnapshot')} : entries.map(entry =>
{measureRef => {entry.label}} - {t('dashboard.upstreamEditor.codex.activeLimit')} + {t('dashboard.upstreamEditor.chatgpt.activeLimit')}
{entry.windows.map(item => { return - {t('dashboard.upstreamEditor.codex.windowMinutes', { minutes: item.windowMinutes })} + {t('dashboard.upstreamEditor.chatgpt.windowMinutes', { minutes: item.windowMinutes })} } footer={item.resetAt && - {t('dashboard.upstreamEditor.codex.resetsAt', { time: dateTime(item.resetAt, locale) })} + {t('dashboard.upstreamEditor.chatgpt.resetsAt', { time: dateTime(item.resetAt, locale) })} } />; })}
{entry.rateLimitedUntil && - {t('dashboard.upstreamEditor.codex.rateLimitedUntil', { time: dateTime(entry.rateLimitedUntil, locale) })} + {t('dashboard.upstreamEditor.chatgpt.rateLimitedUntil', { time: dateTime(entry.rateLimitedUntil, locale) })} } - {t('dashboard.upstreamEditor.codex.observed', { time: dateTime(entry.observedAt, locale) })} + {t('dashboard.upstreamEditor.chatgpt.observed', { time: dateTime(entry.observedAt, locale) })}
)} {credential?.state_updated_at && - {t('dashboard.upstreamEditor.codex.stateUpdated', { time: dateTime(credential.state_updated_at, locale) })} + {t('dashboard.upstreamEditor.chatgpt.stateUpdated', { time: dateTime(credential.state_updated_at, locale) })} } - {!readOnly && record.id !== '' && setReset({ accountId: account.chatgptAccountId, at: Date.now() })} diff --git a/apps/web/src/components/upstream-editor/codex-import.tsx b/apps/web/src/components/upstream-editor/chatgpt-import.tsx similarity index 79% rename from apps/web/src/components/upstream-editor/codex-import.tsx rename to apps/web/src/components/upstream-editor/chatgpt-import.tsx index 9ac735f250..7b605f5937 100644 --- a/apps/web/src/components/upstream-editor/codex-import.tsx +++ b/apps/web/src/components/upstream-editor/chatgpt-import.tsx @@ -19,11 +19,11 @@ import { StatusBadge } from '../ui/status-badge'; const { Button, Field, Radio, RadioGroup, Spinner, Tab, TabList, Text } = fluentComponents; -type CodexRecord = Extract; +type ChatGPTRecord = Extract; type PreviewCandidate = Awaited>['candidates'][number]; const previewJson = async (rawJson: string) => { - const result = await callApi(() => api.api.upstreams.codex.import.preview.$post({ json: { raw_json: rawJson } })); + const result = await callApi(() => api.api.upstreams.chatgpt.import.preview.$post({ json: { raw_json: rawJson } })); if (result.error) throw new Error(result.error.message); return result.data; }; @@ -47,10 +47,10 @@ const trimmed = (value: string): string | undefined => { return next.length > 0 ? next : undefined; }; -export function CodexImportForm({ hasAccount, onImported, record }: { +export function ChatGPTImportForm({ hasAccount, onImported, record }: { hasAccount: boolean; onImported: (patch: { config?: unknown; state?: unknown }) => void; - record: CodexRecord; + record: ChatGPTRecord; }) { const { t } = useTranslation(); const locale = useLocale(); @@ -80,7 +80,7 @@ export function CodexImportForm({ hasAccount, onImported, record }: { const runPreview = async () => { const text = json.trim(); setError(null); - if (text.length === 0) { setError(t('dashboard.upstreamEditor.codex.import.pasteFirst')); return; } + if (text.length === 0) { setError(t('dashboard.upstreamEditor.chatgpt.import.pasteFirst')); return; } setPreviewing(true); let data; try { @@ -96,20 +96,20 @@ export function CodexImportForm({ hasAccount, onImported, record }: { setCandidates(data.candidates); const usable = data.candidates.filter(candidate => candidate.issues.length === 0); setSelected(usable.length === 1 ? usable[0].sourceIndex : null); - if (usable.length === 0) setError(t('dashboard.upstreamEditor.codex.import.noValidAccounts')); + if (usable.length === 0) setError(t('dashboard.upstreamEditor.chatgpt.import.noValidAccounts')); }; const buildSource = (): Record => { if (tab === 'json') { const text = json.trim(); - if (text.length === 0) throw new Error(t('dashboard.upstreamEditor.codex.import.pasteFirst')); - if (previewedJson !== text) throw new Error(t('dashboard.upstreamEditor.codex.import.previewFirst')); + if (text.length === 0) throw new Error(t('dashboard.upstreamEditor.chatgpt.import.pasteFirst')); + if (previewedJson !== text) throw new Error(t('dashboard.upstreamEditor.chatgpt.import.previewFirst')); const candidate = candidates.find(entry => entry.sourceIndex === selected); - if (!candidate || candidate.issues.length > 0) throw new Error(t('dashboard.upstreamEditor.codex.import.selectAccount')); + if (!candidate || candidate.issues.length > 0) throw new Error(t('dashboard.upstreamEditor.chatgpt.import.selectAccount')); return { json: { raw_json: text, source_index: candidate.sourceIndex } }; } const accessToken = manual.accessToken.trim(); - if (accessToken.length === 0) throw new Error(t('dashboard.upstreamEditor.codex.import.accessTokenRequired')); + if (accessToken.length === 0) throw new Error(t('dashboard.upstreamEditor.chatgpt.import.accessTokenRequired')); return { manual: { access_token: accessToken, @@ -131,7 +131,7 @@ export function CodexImportForm({ hasAccount, onImported, record }: { } catch (err) { setBusy(false); setError(errorMessage(err)); return; } - const result = await callApi(() => api.api.upstreams.codex.import.exchange.$post({ + const result = await callApi(() => api.api.upstreams.chatgpt.import.exchange.$post({ json: { record: previewRecord(record, values), ...source } as never, })); setBusy(false); @@ -142,19 +142,19 @@ export function CodexImportForm({ hasAccount, onImported, record }: { }; const candidateExpiry = (candidate: PreviewCandidate): string => candidate.expiresAt === null - ? t('dashboard.upstreamEditor.codex.expiryUnknown') - : t('dashboard.upstreamEditor.codex.expires', { time: dateTime(new Date(candidate.expiresAt).toISOString(), locale) }); + ? t('dashboard.upstreamEditor.chatgpt.expiryUnknown') + : t('dashboard.upstreamEditor.chatgpt.expires', { time: dateTime(new Date(candidate.expiresAt).toISOString(), locale) }); return <> { setTab(String(data.value)); setError(null); }}> - {t('dashboard.upstreamEditor.codex.import.tabJson')} - {t('dashboard.upstreamEditor.codex.import.tabOAuth')} - {t('dashboard.upstreamEditor.codex.import.tabManual')} + {t('dashboard.upstreamEditor.chatgpt.import.tabJson')} + {t('dashboard.upstreamEditor.chatgpt.import.tabOAuth')} + {t('dashboard.upstreamEditor.chatgpt.import.tabManual')} {tab === 'json' &&
- {t('dashboard.upstreamEditor.codex.import.jsonHint')} - {t('dashboard.upstreamEditor.codex.import.jsonWarning')} + {t('dashboard.upstreamEditor.chatgpt.import.jsonHint')} + {t('dashboard.upstreamEditor.chatgpt.import.jsonWarning')}