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')}
@@ -164,10 +164,10 @@ export function CodexImportForm({ hasAccount, onImported, record }: {
icon={previewing ? : }
onClick={() => void runPreview()}
>
- {t('dashboard.upstreamEditor.codex.import.preview')}
+ {t('dashboard.upstreamEditor.chatgpt.import.preview')}
- {candidates.length > 0 &&
+ {candidates.length > 0 &&
setSelected(Number(data.value))}>
{candidates.map(candidate => 0}
@@ -176,17 +176,17 @@ export function CodexImportForm({ hasAccount, onImported, record }: {
label={
- {candidate.name ?? candidate.email ?? t('dashboard.upstreamEditor.codex.import.accountFallback', { index: candidate.sourceIndex + 1 })}
+ {candidate.name ?? candidate.email ?? t('dashboard.upstreamEditor.chatgpt.import.accountFallback', { index: candidate.sourceIndex + 1 })}
- {t(candidate.renewable ? 'dashboard.upstreamEditor.codex.renewable' : 'dashboard.upstreamEditor.codex.accessOnly')}
+ {t(candidate.renewable ? 'dashboard.upstreamEditor.chatgpt.renewable' : 'dashboard.upstreamEditor.chatgpt.accessOnly')}
- {candidate.chatgptAccountId ?? t('dashboard.upstreamEditor.codex.unknownAccountId')}
+ {candidate.chatgptAccountId ?? t('dashboard.upstreamEditor.chatgpt.unknownAccountId')}
- {`${candidate.planType ?? t('dashboard.upstreamEditor.codex.unknownPlan')} · ${candidateExpiry(candidate)}`}
+ {`${candidate.planType ?? t('dashboard.upstreamEditor.chatgpt.unknownPlan')} · ${candidateExpiry(candidate)}`}
{candidate.issues.map(issue =>
{issue})}
}
@@ -196,7 +196,7 @@ export function CodexImportForm({ hasAccount, onImported, record }: {
}
{tab === 'oauth' &&
}
{tab === 'manual' &&
- {t('dashboard.upstreamEditor.codex.import.manualHint')}
-
+ {t('dashboard.upstreamEditor.chatgpt.import.manualHint')}
+
setManual(draft => ({ ...draft, accessToken: data.value }))} />
-
+
setManual(draft => ({ ...draft, refreshToken: data.value }))} />
-
+
setManual(draft => ({ ...draft, idToken: data.value }))} />
-
+
setManual(draft => ({ ...draft, accountId: data.value }))} />
-
+
setManual(draft => ({ ...draft, email: data.value }))} />
-
+
setManual(draft => ({ ...draft, planType: data.value }))} />
-
+
setManual(draft => ({ ...draft, expiresAt: data.value }))} />
}
diff --git a/apps/web/src/components/upstream-editor/codex-reset-cards.tsx b/apps/web/src/components/upstream-editor/chatgpt-reset-cards.tsx
similarity index 75%
rename from apps/web/src/components/upstream-editor/codex-reset-cards.tsx
rename to apps/web/src/components/upstream-editor/chatgpt-reset-cards.tsx
index 2801f4b845..e7d0e3f39f 100644
--- a/apps/web/src/components/upstream-editor/codex-reset-cards.tsx
+++ b/apps/web/src/components/upstream-editor/chatgpt-reset-cards.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { api, callApi } from '../../api/client';
-import type { CodexRateLimitResetCredit, CodexRateLimitResetCredits } from '../../api/types';
+import type { ChatGPTRateLimitResetCredit, ChatGPTRateLimitResetCredits } from '../../api/types';
import { fluentComponents } from '../../fluent';
import { useTranslation } from '../../i18n/translation';
import { dateTime } from '../../lib/format-time';
@@ -14,13 +14,13 @@ import { ResourceListActions } from '../ui/resource-list';
import { SectionHeader } from '../ui/section-header';
import { useDialogInvocation } from '../ui/use-dialog-invocation';
import { useRefresh } from '../ui/use-refresh';
-import { codexResetCreditIsUsable, type CodexRecord } from '../upstreams/codex-account';
+import { chatgptResetCreditIsUsable, type ChatGPTRecord } from '../upstreams/chatgpt-account';
import { WALL_CLOCK_REFRESH_MS } from '../upstreams/subscription-quota';
const { Button, Text } = fluentComponents;
interface RedeemInvocation {
- credit: CodexRateLimitResetCredit;
+ credit: ChatGPTRateLimitResetCredit;
idempotencyKey: string;
}
@@ -37,14 +37,14 @@ const statusKey = (status: string): 'available' | 'expired' | 'redeemed' | 'rede
return null;
};
-export function CodexResetCards({ onQuotaReset, record }: {
+export function ChatGPTResetCards({ onQuotaReset, record }: {
onQuotaReset: () => void;
- record: CodexRecord;
+ record: ChatGPTRecord;
}) {
const { t } = useTranslation();
const locale = useLocale();
const now = useNow(WALL_CLOCK_REFRESH_MS);
- const [cards, setCards] = useState
(null);
+ const [cards, setCards] = useState(null);
const [loadError, setLoadError] = useState(null);
const [outcome, setOutcome] = useState<{ code: string; warning: string | null } | null>(null);
const [redeemError, setRedeemError] = useState(null);
@@ -54,7 +54,7 @@ export function CodexResetCards({ onQuotaReset, record }: {
const dialog = useDialogInvocation();
const envelope = useMemo(() => ({
id: record.id,
- kind: 'codex' as const,
+ kind: 'chatgpt' as const,
config: record.config,
state: record.state,
proxy_fallback_list: record.proxy_fallback_list,
@@ -62,13 +62,13 @@ export function CodexResetCards({ onQuotaReset, record }: {
const { cancel: cancelLoad, refresh: load, refreshing: loading } = useRefresh(useCallback(async (signal: AbortSignal) => {
setLoadError(null);
- const { data, error } = await callApi(() => api.api.upstreams.codex['reset-credits'].$post(
+ const { data, error } = await callApi(() => api.api.upstreams.chatgpt['reset-credits'].$post(
{ json: { record: envelope } },
{ init: { signal } },
));
if (signal.aborted) return;
if (error) {
- setLoadError(t('dashboard.upstreamEditor.codex.resetCards.loadError'));
+ setLoadError(t('dashboard.upstreamEditor.chatgpt.resetCards.loadError'));
return;
}
setCards(data.reset_credits);
@@ -86,7 +86,7 @@ export function CodexResetCards({ onQuotaReset, record }: {
setRedeeming(true);
setRedeemError(null);
try {
- const { data, error } = await callApi(() => api.api.upstreams.codex['reset-credits'].consume.$post({
+ const { data, error } = await callApi(() => api.api.upstreams.chatgpt['reset-credits'].consume.$post({
json: {
record: envelope,
credit_id: invocation.credit.id,
@@ -94,7 +94,7 @@ export function CodexResetCards({ onQuotaReset, record }: {
},
}));
if (error) {
- setRedeemError(t('dashboard.upstreamEditor.codex.resetCards.redeemError'));
+ setRedeemError(t('dashboard.upstreamEditor.chatgpt.resetCards.redeemError'));
return;
}
setCards(data.reset_credits);
@@ -114,26 +114,26 @@ export function CodexResetCards({ onQuotaReset, record }: {
return
void load()}
- refreshLabel={t('dashboard.upstreamEditor.codex.resetCards.refresh')}
+ refreshLabel={t('dashboard.upstreamEditor.chatgpt.resetCards.refresh')}
refreshing={loading}
/>}
/>
{cards?.credits.map(credit => {
- const usable = codexResetCreditIsUsable(credit, now);
+ const usable = chatgptResetCreditIsUsable(credit, now);
const knownStatus = credit.status === 'available' && credit.expires_at !== null && Date.parse(credit.expires_at) <= now
? 'expired'
: statusKey(credit.status);
return
- {credit.title ?? t('dashboard.upstreamEditor.codex.resetCards.defaultTitle')}
+ {credit.title ?? t('dashboard.upstreamEditor.chatgpt.resetCards.defaultTitle')}
{credit.description && {credit.description}}
- {t('dashboard.upstreamEditor.codex.resetCards.granted', { time: dateTime(credit.granted_at, locale) })}
+ {t('dashboard.upstreamEditor.chatgpt.resetCards.granted', { time: dateTime(credit.granted_at, locale) })}
{credit.expires_at === null
- ? t('dashboard.upstreamEditor.codex.resetCards.noExpiry')
- : t('dashboard.upstreamEditor.codex.resetCards.expires', { time: dateTime(credit.expires_at, locale) })}
+ ? t('dashboard.upstreamEditor.chatgpt.resetCards.noExpiry')
+ : t('dashboard.upstreamEditor.chatgpt.resetCards.expires', { time: dateTime(credit.expires_at, locale) })}
;
})}
{cards !== null && cards.credits.length === 0 && !loading && !loadError &&
- {t('dashboard.upstreamEditor.codex.resetCards.empty')}
+ {t('dashboard.upstreamEditor.chatgpt.resetCards.empty')}
}
{loadError && {loadError}}
{outcome && resultKey && setOutcome(null)}
>
{resultKey === 'unknown'
- ? t('dashboard.upstreamEditor.codex.resetCards.result.unknown', { code: outcome.code })
- : t(`dashboard.upstreamEditor.codex.resetCards.result.${resultKey}`)}
- {outcome.warning && {t('dashboard.upstreamEditor.codex.resetCards.refreshWarning')}}
+ ? t('dashboard.upstreamEditor.chatgpt.resetCards.result.unknown', { code: outcome.code })
+ : t(`dashboard.upstreamEditor.chatgpt.resetCards.result.${resultKey}`)}
+ {outcome.warning && {t('dashboard.upstreamEditor.chatgpt.resetCards.refreshWarning')}}
}
{dialog.invocation && selected && void redeem()}
onDismissError={() => setRedeemError(null)}
onExited={() => setRedeemError(null)}
onOpenChange={open => { if (!open) dialog.close(); }}
open={dialog.isOpen}
- title={t('dashboard.upstreamEditor.codex.resetCards.confirmTitle')}
+ title={t('dashboard.upstreamEditor.chatgpt.resetCards.confirmTitle')}
/>}
;
}
diff --git a/apps/web/src/components/upstream-editor/config-sidebar.tsx b/apps/web/src/components/upstream-editor/config-sidebar.tsx
index 3b953ed332..a23b60dc99 100644
--- a/apps/web/src/components/upstream-editor/config-sidebar.tsx
+++ b/apps/web/src/components/upstream-editor/config-sidebar.tsx
@@ -115,7 +115,7 @@ export function UpstreamConfigSidebar({
onRefreshModels={onRefreshModels}
/>
- {(record.kind === 'copilot' || record.kind === 'codex' || record.kind === 'claude-code' || record.kind === 'ollama') &&
(
diff --git a/apps/web/src/components/upstream-editor/data.ts b/apps/web/src/components/upstream-editor/data.ts
index d6d28d8c46..ddc3fa6171 100644
--- a/apps/web/src/components/upstream-editor/data.ts
+++ b/apps/web/src/components/upstream-editor/data.ts
@@ -80,7 +80,7 @@ export const providerDefaultName: Record = {
custom: 'Custom upstream',
azure: 'Azure AI',
copilot: 'GitHub Copilot',
- codex: 'ChatGPT Codex',
+ chatgpt: 'ChatGPT',
'claude-code': 'Claude Code',
ollama: 'Ollama',
};
@@ -303,7 +303,7 @@ export const createBody = (record: UpstreamRecord, values: UpstreamEditorValues)
proxy_fallback_list: values.proxyFallbackList,
model_prefix: values.modelPrefix,
config: configFromValues(record, values),
- ...((record.kind === 'copilot' || record.kind === 'codex' || record.kind === 'claude-code')
+ ...((record.kind === 'copilot' || record.kind === 'chatgpt' || record.kind === 'claude-code')
? { state: values.state }
: {}),
} as CreateUpstreamBody;
diff --git a/apps/web/src/components/upstream-editor/oauth-callback-import.tsx b/apps/web/src/components/upstream-editor/oauth-callback-import.tsx
index 3e4b52bd17..2b20986dbf 100644
--- a/apps/web/src/components/upstream-editor/oauth-callback-import.tsx
+++ b/apps/web/src/components/upstream-editor/oauth-callback-import.tsx
@@ -20,11 +20,11 @@ const { Button, Field, Link, Spinner, Text } = fluentComponents;
type OAuthFlow = 'oauth' | 'setup-token';
-type OAuthImportRecord = Extract;
+type OAuthImportRecord = Extract;
type OAuthCallbackImportProps =
| {
- kind: 'codex';
+ kind: 'chatgpt';
hasAccount: boolean;
record: OAuthImportRecord;
getValues: () => UpstreamEditorValues;
@@ -39,12 +39,12 @@ type OAuthCallbackImportProps =
onImported: (patch: { config?: unknown; state?: unknown }) => void;
};
-// The codex OAuth tab opens with a hint the claude-code OAuth and Setup-Token
+// The ChatGPT OAuth tab opens with a hint the claude-code OAuth and Setup-Token
// tabs never carried; the kind prop decides which copy this shared surface
// shows.
export function OAuthCallbackImport(props: OAuthCallbackImportProps) {
const { kind, hasAccount, record, getValues, onImported } = props;
- const flowKind: OAuthFlow = kind === 'codex' ? 'oauth' : props.flowKind;
+ const flowKind: OAuthFlow = kind === 'chatgpt' ? 'oauth' : props.flowKind;
const { t } = useTranslation();
const values = useWatch() as UpstreamEditorValues;
const { copy, outcomeFor } = useCopyToClipboard();
@@ -67,8 +67,8 @@ export function OAuthCallbackImport(props: OAuthCallbackImportProps) {
if (generation.current !== mine) return;
stashPkce(kind, flowKind, { verifier: pkce.verifier, state: pkce.state });
const body = { record: previewRecord(record, getValues()), challenge: pkce.challenge, state: pkce.state };
- const result = kind === 'codex'
- ? await callApi(() => api.api.upstreams.codex.oauth['authorize-url'].$post({ json: body }))
+ const result = kind === 'chatgpt'
+ ? await callApi(() => api.api.upstreams.chatgpt.oauth['authorize-url'].$post({ json: body }))
: flowKind === 'setup-token'
? await callApi(() => api.api.upstreams['claude-code']['setup-token']['authorize-url'].$post({ json: body }))
: await callApi(() => api.api.upstreams['claude-code'].oauth['authorize-url'].$post({ json: body }));
@@ -98,8 +98,8 @@ export function OAuthCallbackImport(props: OAuthCallbackImportProps) {
setBusy(false); setError(errorMessage(err)); return;
}
const editorRecord = previewRecord(record, values);
- const result = kind === 'codex'
- ? await callApi(() => api.api.upstreams.codex.import.exchange.$post({
+ const result = kind === 'chatgpt'
+ ? await callApi(() => api.api.upstreams.chatgpt.import.exchange.$post({
json: { record: editorRecord, callback: { code: parsed.code, verifier: recalled.verifier } },
}))
: flowKind === 'setup-token'
@@ -117,7 +117,7 @@ export function OAuthCallbackImport(props: OAuthCallbackImportProps) {
};
return
- {kind === 'codex' &&
{t('dashboard.upstreamEditor.codex.import.oauthHint')}}
+ {kind === 'chatgpt' &&
{t('dashboard.upstreamEditor.chatgpt.import.oauthHint')}}
{busy && !authorizeUrl
?
: authorizeUrl &&
diff --git a/apps/web/src/components/upstream-editor/page.tsx b/apps/web/src/components/upstream-editor/page.tsx
index b0f7b75cf2..b936d6f193 100644
--- a/apps/web/src/components/upstream-editor/page.tsx
+++ b/apps/web/src/components/upstream-editor/page.tsx
@@ -100,7 +100,7 @@ export function UpstreamEditorPage({ data }: { data: UpstreamEditorLoaderData })
// with, and the editor never sends it back.
if (data.mode !== 'create') return;
if (record.kind === 'copilot' && !values.config.githubToken) ctx.addIssue({ code: 'custom', message: 'dashboard.upstreamEditor.validation.copilot', path: ['config'] });
- if ((record.kind === 'codex' || record.kind === 'claude-code') && values.config.accounts.length === 0) ctx.addIssue({ code: 'custom', message: 'dashboard.upstreamEditor.validation.credential', path: ['config'] });
+ if ((record.kind === 'chatgpt' || record.kind === 'claude-code') && values.config.accounts.length === 0) ctx.addIssue({ code: 'custom', message: 'dashboard.upstreamEditor.validation.credential', path: ['config'] });
}), [data.mode, record.kind]);
const form = useForm({
defaultValues: initialValues,
@@ -153,7 +153,7 @@ export function UpstreamEditorPage({ data }: { data: UpstreamEditorLoaderData })
const manualModelsDirty = Boolean(formState.dirtyFields.manualModels);
const discoveryInputsDirty = hasUnsavedDiscoveryInputs(formState.dirtyFields)
|| (record.kind === 'ollama' && manualModelsDirty);
- const oauth = record.kind === 'copilot' || record.kind === 'codex' || record.kind === 'claude-code';
+ const oauth = record.kind === 'copilot' || record.kind === 'chatgpt' || record.kind === 'claude-code';
const fetchDialog = useDialogInvocation();
// Save resets the form and record in one commit. The immediately following
// Fetch must use that saved record even before React commits the new render.
diff --git a/apps/web/src/components/upstream-editor/provider-config.tsx b/apps/web/src/components/upstream-editor/provider-config.tsx
index e8d3742d39..5c196c8adb 100644
--- a/apps/web/src/components/upstream-editor/provider-config.tsx
+++ b/apps/web/src/components/upstream-editor/provider-config.tsx
@@ -8,9 +8,9 @@ import {
import { useEffect, useRef, useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
+import { ChatGPTAccountCard } from './chatgpt-account-card';
+import { ChatGPTImportForm } from './chatgpt-import';
import { ClaudeCodeAccountCard } from './claude-code-account-card';
-import { CodexAccountCard } from './codex-account-card';
-import { CodexImportForm } from './codex-import';
import { CopilotQuotaCard } from './copilot-quota-card';
import { CustomIngressHeaderRules } from './custom-ingress-header-rules';
import type { UpstreamEditorValues } from './data';
@@ -421,7 +421,7 @@ function CopilotConfig({ onPatch, readOnly, record }: {
;
}
-type OAuthKind = 'codex' | 'claude-code';
+type OAuthKind = 'chatgpt' | 'claude-code';
function OAuthConfig({ onPatch, readOnly, record }: {
readOnly: boolean;
record: Extract
;
@@ -438,8 +438,8 @@ function OAuthConfig({ onPatch, readOnly, record }: {
setRefreshing(true);
setError(null);
const body = { record: previewRecord(record, values) };
- const result = record.kind === 'codex'
- ? await callApi(() => api.api.upstreams.codex.oauth.refresh.$post({ json: body }))
+ const result = record.kind === 'chatgpt'
+ ? await callApi(() => api.api.upstreams.chatgpt.oauth.refresh.$post({ json: body }))
: await callApi(() => api.api.upstreams['claude-code'].oauth.refresh.$post({ json: body }));
setRefreshing(false);
if (result.error) { setError(result.error.message); return; }
@@ -487,8 +487,8 @@ function OAuthConfig({ onPatch, readOnly, record }: {
};
return
- {hasAccount && (record.kind === 'codex'
- ? ['config'], state: values.state as Extract['state'] }} />
+ {hasAccount && (record.kind === 'chatgpt'
+ ? ['config'], state: values.state as Extract['state'] }} />
: void probeQuota()}
probing={probing}
@@ -503,11 +503,11 @@ function OAuthConfig({ onPatch, readOnly, record }: {
}
{error && setError(null)}>{error}}
- {open && !readOnly && (record.kind === 'codex'
- ? { onPatch(patch, isPersisted(record)); setOpen(false); }}
- record={{ ...record, kind: 'codex', config: config as Extract['config'], state: values.state as Extract['state'] }}
+ record={{ ...record, kind: 'chatgpt', config: config as Extract['config'], state: values.state as Extract['state'] }}
/>
: <>
setTab(String(data.value))}>
diff --git a/apps/web/src/components/upstream-metrics/display-name.ts b/apps/web/src/components/upstream-metrics/display-name.ts
index 991423f8f3..bfa8623331 100644
--- a/apps/web/src/components/upstream-metrics/display-name.ts
+++ b/apps/web/src/components/upstream-metrics/display-name.ts
@@ -1,8 +1,8 @@
import type { UpstreamMetricsMetadata } from './data';
import type { TFunction } from '../../i18n/translation';
import { resolveUpstreamMetricDisplaySeries, type UpstreamMetricDisplay, type UpstreamMetricDisplayOptions, type UpstreamMetricDisplaySeries, type UpstreamMetricLabelKey, type UpstreamMetricSample } from '@floway-dev/provider/browser';
+import { resolveUpstreamMetricDisplays as chatgptDisplays } from '@floway-dev/provider-chatgpt/browser';
import { resolveUpstreamMetricDisplays as claudeCodeDisplays } from '@floway-dev/provider-claude-code/browser';
-import { resolveUpstreamMetricDisplays as codexDisplays } from '@floway-dev/provider-codex/browser';
import { resolveUpstreamMetricDisplays as copilotDisplays } from '@floway-dev/provider-copilot/browser';
import { resolveUpstreamMetricDisplays as ollamaDisplays } from '@floway-dev/provider-ollama/browser';
@@ -58,7 +58,7 @@ const decodeProviderMetrics = (
options: UpstreamMetricDisplayOptions,
): UpstreamMetricDisplaySeries[] => {
switch (kind) {
- case 'codex': return codexDisplays(samples, options);
+ case 'chatgpt': return chatgptDisplays(samples, options);
case 'copilot': return copilotDisplays(samples, options);
case 'claude-code': return claudeCodeDisplays(samples, options);
case 'ollama': return ollamaDisplays(samples, options);
diff --git a/apps/web/src/components/upstreams/codex-account.ts b/apps/web/src/components/upstreams/chatgpt-account.ts
similarity index 85%
rename from apps/web/src/components/upstreams/codex-account.ts
rename to apps/web/src/components/upstreams/chatgpt-account.ts
index dd19c8f06d..61577aee78 100644
--- a/apps/web/src/components/upstreams/codex-account.ts
+++ b/apps/web/src/components/upstreams/chatgpt-account.ts
@@ -5,16 +5,16 @@
import { heaviestPercent, type UsageHeavyOrActive, usageStatusFromHeaviest } from './subscription-quota';
import type {
- CodexAccountCredentialState,
- CodexQuotaSnapshot,
- CodexQuotaSnapshotMap,
- CodexRateLimitResetCredit,
+ ChatGPTAccountCredentialState,
+ ChatGPTQuotaSnapshot,
+ ChatGPTQuotaSnapshotMap,
+ ChatGPTRateLimitResetCredit,
UpstreamRecord,
} from '../../api/types';
-export type CodexRecord = Extract;
+export type ChatGPTRecord = Extract;
-export const codexResetCreditIsUsable = (credit: CodexRateLimitResetCredit, now: number): boolean =>
+export const chatgptResetCreditIsUsable = (credit: ChatGPTRateLimitResetCredit, now: number): boolean =>
credit.reset_type === 'codex_rate_limits'
&& credit.status === 'available'
&& (credit.expires_at === null || new Date(credit.expires_at).getTime() > now);
@@ -49,7 +49,7 @@ const PLAN_NAMES: Record = {
education: 'Edu',
};
-export const planLabel = (account: CodexRecord['config']['accounts'][number]): string | null =>
+export const planLabel = (account: ChatGPTRecord['config']['accounts'][number]): string | null =>
account.planType ? `ChatGPT ${PLAN_NAMES[account.planType] ?? account.planType}` : null;
export interface QuotaWindow {
@@ -68,10 +68,10 @@ export interface QuotaEntry {
}
export type CredentialLookup =
- | { kind: 'present'; credential: CodexAccountCredentialState }
+ | { kind: 'present'; credential: ChatGPTAccountCredentialState }
| { kind: 'account-id-mismatch'; expectedAccountId: string | null };
-export const findCredential = (record: CodexRecord): CredentialLookup => {
+export const findCredential = (record: ChatGPTRecord): CredentialLookup => {
const expectedAccountId = record.config.accounts[0].chatgptAccountId;
const credential = record.state.accounts.find(account => account.chatgptAccountId === expectedAccountId);
return credential ? { kind: 'present', credential } : { kind: 'account-id-mismatch', expectedAccountId };
@@ -80,7 +80,7 @@ export const findCredential = (record: CodexRecord): CredentialLookup => {
// The editor renders both the redacted row the list returns and the raw patch
// an import merges into the draft, and those two name the same fact
// differently.
-export const codexRenewable = (credential: CodexAccountCredentialState): boolean =>
+export const chatgptRenewable = (credential: ChatGPTAccountCredentialState): boolean =>
credential.refresh_token_set ?? (typeof credential.refresh_token === 'string' && credential.refresh_token.length > 0);
const window = (
@@ -95,7 +95,7 @@ const window = (
const stillRateLimited = (until: string | undefined, now: number): string | null =>
typeof until === 'string' && new Date(until).getTime() > now ? until : null;
-export const quotaEntries = (quota: CodexQuotaSnapshotMap | null | undefined, now: number): QuotaEntry[] =>
+export const quotaEntries = (quota: ChatGPTQuotaSnapshotMap | null | undefined, now: number): QuotaEntry[] =>
Object.entries(quota ?? {})
.toSorted(([left], [right]) => left.localeCompare(right))
.map(([key, snapshot]) => ({
@@ -114,8 +114,8 @@ export const quotaEntries = (quota: CodexQuotaSnapshotMap | null | undefined, no
export const latestQuotaEntry = (entries: QuotaEntry[]): QuotaEntry | null =>
entries.toSorted((left, right) => new Date(right.observedAt).getTime() - new Date(left.observedAt).getTime())[0] ?? null;
-export const latestCredits = (quota: CodexQuotaSnapshotMap | null | undefined): CodexQuotaSnapshot | null => {
- let newest: CodexQuotaSnapshot | null = null;
+export const latestCredits = (quota: ChatGPTQuotaSnapshotMap | null | undefined): ChatGPTQuotaSnapshot | null => {
+ let newest: ChatGPTQuotaSnapshot | null = null;
let newestObservedAt = Number.NEGATIVE_INFINITY;
for (const snapshot of Object.values(quota ?? {})) {
if (snapshot.credits_balance === undefined && snapshot.credits_has_credits === undefined) continue;
@@ -128,11 +128,11 @@ export const latestCredits = (quota: CodexQuotaSnapshotMap | null | undefined):
return newest;
};
-type CodexDangerStatus =
+type ChatGPTDangerStatus =
| { tone: 'danger'; reason: 'account-id-mismatch' | 'session-terminated' | 'refresh-failed'; detail?: string }
| { tone: 'danger'; reason: 'rate-limited'; until: string; detail?: string };
-export type AccountStatus = CodexDangerStatus | UsageHeavyOrActive;
+export type AccountStatus = ChatGPTDangerStatus | UsageHeavyOrActive;
export const accountStatus = (lookup: CredentialLookup, entries: QuotaEntry[]): AccountStatus => {
if (lookup.kind === 'account-id-mismatch') return { tone: 'danger', reason: 'account-id-mismatch' };
diff --git a/apps/web/src/components/upstreams/provider-badge.tsx b/apps/web/src/components/upstreams/provider-badge.tsx
index 2e9edcb146..6133fc5b58 100644
--- a/apps/web/src/components/upstreams/provider-badge.tsx
+++ b/apps/web/src/components/upstreams/provider-badge.tsx
@@ -23,7 +23,7 @@ const providerLabels: Record = {
custom: 'Custom',
azure: 'Azure',
copilot: 'Copilot',
- codex: 'Codex',
+ chatgpt: 'ChatGPT',
'claude-code': 'Claude Code',
ollama: 'Ollama',
};
@@ -112,8 +112,7 @@ export function ProviderBadge({ label, title, to, upstream }: {
const providerIconUrls: Record, string> = {
azure: azureIconUrl,
copilot: githubCopilotIconUrl,
- // Codex is the ChatGPT subscription, so it wears OpenAI's mark.
- codex: openaiIconUrl,
+ chatgpt: openaiIconUrl,
'claude-code': claudeIconUrl,
ollama: ollamaIconUrl,
};
@@ -123,7 +122,7 @@ const providerIconUrls: Record, string>
const providerIconMaskSizes: Record, string> = {
azure: '85% 85%',
copilot: '100% 100%',
- codex: '80% 80%',
+ chatgpt: '80% 80%',
'claude-code': '80% 80%',
ollama: '86% 86%',
};
diff --git a/apps/web/src/components/upstreams/signals.tsx b/apps/web/src/components/upstreams/signals.tsx
index a803f55741..e08c7696a1 100644
--- a/apps/web/src/components/upstreams/signals.tsx
+++ b/apps/web/src/components/upstreams/signals.tsx
@@ -4,8 +4,8 @@
// so an upstream with nothing to report contributes nothing and its row stays
// one line high.
+import { latestCredits, latestQuotaEntry, planLabel as chatgptPlanLabel, quotaEntries } from './chatgpt-account';
import { findCredential, planLabel as claudeCodePlanLabel, quotaWindows, rateLimitedUntil, WINDOW_MINUTES } from './claude-code-account';
-import { latestCredits, latestQuotaEntry, planLabel as codexPlanLabel, quotaEntries } from './codex-account';
import { copilotQuota, copilotQuotaName, readBuckets } from './copilot-quota';
import { planLabel as copilotPlanLabel } from './copilot-seat';
import { planLabel as ollamaPlanLabel } from './ollama-account';
@@ -137,9 +137,9 @@ const copilotSignals = (record: Extract, t:
}));
};
-const codexSignals = (record: Extract, t: TFunction, locale: string, now: number, quotaDisplay: QuotaDisplay): UpstreamSignal[] => {
- const entry = latestQuotaEntry(quotaEntries(record.codex_quota, now));
- const credits = latestCredits(record.codex_quota);
+const chatgptSignals = (record: Extract, t: TFunction, locale: string, now: number, quotaDisplay: QuotaDisplay): UpstreamSignal[] => {
+ const entry = latestQuotaEntry(quotaEntries(record.chatgpt_quota, now));
+ const credits = latestCredits(record.chatgpt_quota);
const signals: UpstreamSignal[] = entry === null ? [] : entry.windows.map(item => {
// Codex states each window's length in minutes and nothing else names it,
// so an unlabelled window falls back to the position it arrived in.
@@ -248,7 +248,7 @@ const upstreamSignals = (record: UpstreamRecord, t: TFunction, locale: string, n
case 'azure':
return [];
case 'copilot': return copilotSignals(record, t, locale, quotaDisplay);
- case 'codex': return codexSignals(record, t, locale, now, quotaDisplay);
+ case 'chatgpt': return chatgptSignals(record, t, locale, now, quotaDisplay);
case 'claude-code': return claudeCodeSignals(record, t, locale, now, quotaDisplay);
case 'ollama': return ollamaSignals(record, t, locale, quotaDisplay);
}
@@ -265,7 +265,7 @@ const upstreamPlan = (record: UpstreamRecord): string | null => {
return null;
case 'copilot': return copilotPlanLabel(record);
case 'ollama': return ollamaPlanLabel(record);
- case 'codex': return codexPlanLabel(record.config.accounts[0]);
+ case 'chatgpt': return chatgptPlanLabel(record.config.accounts[0]);
case 'claude-code': return claudeCodePlanLabel(record.config.accounts[0]);
}
};
diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts
index 7d0efabf68..be56ebb14c 100644
--- a/apps/web/src/i18n/locales/en.ts
+++ b/apps/web/src/i18n/locales/en.ts
@@ -65,7 +65,7 @@ const en = {
custom: 'Custom',
azure: 'Azure',
copilot: 'Copilot',
- codex: 'Codex',
+ chatgpt: 'ChatGPT',
'claude-code': 'Claude Code',
ollama: 'Ollama',
},
@@ -540,7 +540,7 @@ const en = {
custom: 'OpenAI- or Anthropic-compatible endpoint',
azure: 'Azure OpenAI or Foundry',
copilot: 'GitHub Copilot account',
- codex: 'ChatGPT Plus, Pro, or Team',
+ chatgpt: 'ChatGPT Plus, Pro, or Team',
'claude-code': 'Claude Pro, Max, or Team subscription',
ollama: 'ollama.com or self-hosted',
},
@@ -960,7 +960,7 @@ const en = {
deviceCode: 'Device code',
waiting: 'Waiting for authorization…',
},
- codex: {
+ chatgpt: {
accessOnly: 'Access only',
activeLimit: 'active limit',
credits: 'credits: {{balance, number}}',
@@ -968,7 +968,7 @@ const en = {
expiryUnknown: 'Expiry unknown',
expiryUnknownAccessOnly: 'Expiry unknown - usable until the upstream rejects it',
noCredits: 'no credits',
- noSnapshot: 'No quota snapshots yet - Codex calls populate them.',
+ noSnapshot: 'No quota snapshots yet - ChatGPT calls populate them.',
observed: 'Observed {{time}}',
rateLimitedUntil: 'Rate-limited until {{time}}',
renewable: 'Renewable',
@@ -983,7 +983,7 @@ const en = {
title: 'Reset cards',
available: '{{count, number}} available',
refresh: 'Refresh reset cards',
- defaultTitle: 'Codex rate-limit reset',
+ defaultTitle: 'ChatGPT rate-limit reset',
granted: 'Granted {{time}}',
expires: 'Expires {{time}}',
noExpiry: 'No expiry reported',
@@ -992,12 +992,12 @@ const en = {
empty: 'No reset cards are available for this ChatGPT subscription.',
status: { available: 'Available', expired: 'Expired', redeemed: 'Redeemed', redeeming: 'Redeeming' },
confirmTitle: 'Use this reset card?',
- confirmMessage: 'Use “{{title}}” now? This resets the eligible Codex rate-limit windows.',
+ confirmMessage: 'Use “{{title}}” now? This resets the eligible ChatGPT rate-limit windows.',
refreshWarning: 'The card was processed, but Floway could not refresh all details. Refresh reset cards to check their current state.',
loadError: 'Could not load reset cards. Try refreshing again.',
redeemError: 'Could not confirm the reset. Retry to check the same redemption safely.',
result: {
- reset: 'The Codex rate-limit windows were reset.',
+ reset: 'The ChatGPT rate-limit windows were reset.',
alreadyRedeemed: 'This request was already redeemed; no second card was consumed.',
nothingToReset: 'There are currently no eligible rate-limit windows to reset.',
noCredit: 'This reset card is no longer available.',
@@ -1453,10 +1453,10 @@ const en = {
},
passthrough: {
title: 'Passthrough OpenAI search',
- description: 'Route /alpha/search and OpenAI Responses hosted search through a selected Codex or OpenAI-compatible upstream',
+ description: 'Route /alpha/search and OpenAI Responses hosted search through a selected ChatGPT or OpenAI-compatible upstream',
upstream: 'Search upstream',
model: 'Search model',
- empty: 'Add an enabled Codex or Custom upstream with a chat model to use passthrough search.',
+ empty: 'Add an enabled ChatGPT or Custom upstream with a chat model to use passthrough search.',
},
unavailable: '{{id}} (unavailable)',
getKeyLink: 'Get API key',
diff --git a/apps/web/src/i18n/locales/zh-Hans.ts b/apps/web/src/i18n/locales/zh-Hans.ts
index 9d29228dd4..94b70fcae7 100644
--- a/apps/web/src/i18n/locales/zh-Hans.ts
+++ b/apps/web/src/i18n/locales/zh-Hans.ts
@@ -63,7 +63,7 @@ const zhHansCN = {
custom: '自定义',
azure: 'Azure',
copilot: 'Copilot',
- codex: 'Codex',
+ chatgpt: 'ChatGPT',
'claude-code': 'Claude Code',
ollama: 'Ollama',
},
@@ -515,7 +515,7 @@ const zhHansCN = {
custom: '兼容 OpenAI 或 Anthropic 的端点',
azure: 'Azure OpenAI 或 Foundry',
copilot: 'GitHub Copilot 账号',
- codex: 'ChatGPT Plus、Pro 或 Team',
+ chatgpt: 'ChatGPT Plus、Pro 或 Team',
'claude-code': 'Claude Pro、Max 或 Team 订阅',
ollama: 'ollama.com 或自托管服务',
},
@@ -917,7 +917,7 @@ const zhHansCN = {
deviceCode: '设备代码',
waiting: '正在等待授权…',
},
- codex: {
+ chatgpt: {
accessOnly: '仅访问令牌',
activeLimit: '当前限额',
credits: 'Credit 余额:{{balance, number}}',
@@ -925,7 +925,7 @@ const zhHansCN = {
expiryUnknown: '过期时间未知',
expiryUnknownAccessOnly: '过期时间未知,可用至上游拒绝为止',
noCredits: '无 Credit 余额',
- noSnapshot: '尚无余额快照,Codex 调用会写入。',
+ noSnapshot: '尚无余额快照,ChatGPT 调用会写入。',
observed: '观测于 {{time}}',
rateLimitedUntil: '限流至 {{time}}',
renewable: '可续期',
@@ -940,7 +940,7 @@ const zhHansCN = {
title: '重置卡',
available: '{{count, number}} 张可用',
refresh: '刷新重置卡',
- defaultTitle: 'Codex 限额重置',
+ defaultTitle: 'ChatGPT 限额重置',
granted: '发放于 {{time}}',
expires: '到期于 {{time}}',
noExpiry: '上游未提供到期时间',
@@ -949,12 +949,12 @@ const zhHansCN = {
empty: '此 ChatGPT 订阅当前没有可用的重置卡。',
status: { available: '可用', expired: '已过期', redeemed: '已兑换', redeeming: '兑换中' },
confirmTitle: '使用这张重置卡?',
- confirmMessage: '现在使用“{{title}}”?这会重置符合条件的 Codex 限额窗口。',
+ confirmMessage: '现在使用“{{title}}”?这会重置符合条件的 ChatGPT 限额窗口。',
refreshWarning: '重置卡已处理,但 Floway 未能刷新所有详情。请刷新重置卡以查看当前状态。',
loadError: '无法加载重置卡,请再次刷新。',
redeemError: '无法确认重置结果。重试将安全地检查同一次兑换。',
result: {
- reset: 'Codex 限额窗口已重置。',
+ reset: 'ChatGPT 限额窗口已重置。',
alreadyRedeemed: '此请求已兑换过,没有再次消耗重置卡。',
nothingToReset: '当前没有符合条件的限额窗口可供重置。',
noCredit: '这张重置卡已不可用。',
@@ -1389,7 +1389,7 @@ const zhHansCN = {
microsoftWebIq: 'Microsoft Web IQ',
jina: 'Jina',
},
- passthrough: { title: '透传 OpenAI 搜索', description: '将 /alpha/search 和 OpenAI Responses 托管搜索转发到指定的 Codex 或 OpenAI 兼容上游', upstream: '搜索上游', model: '搜索模型', empty: '请添加一个已启用且包含聊天模型的 Codex 或自定义上游。' },
+ passthrough: { title: '透传 OpenAI 搜索', description: '将 /alpha/search 和 OpenAI Responses 托管搜索转发到指定的 ChatGPT 或 OpenAI 兼容上游', upstream: '搜索上游', model: '搜索模型', empty: '请添加一个已启用且包含聊天模型的 ChatGPT 或自定义上游。' },
unavailable: '{{id}}(不可用)',
getKeyLink: '获取 API 密钥',
apiKeyLabel: 'API 密钥',
diff --git a/apps/web/src/routes/dashboard-providers-upstreams-copy.tsx b/apps/web/src/routes/dashboard-providers-upstreams-copy.tsx
index 6d8d70b203..db050dc47e 100644
--- a/apps/web/src/routes/dashboard-providers-upstreams-copy.tsx
+++ b/apps/web/src/routes/dashboard-providers-upstreams-copy.tsx
@@ -53,8 +53,8 @@ const copyableRecord = (source: UpstreamRecord, name: string, hue: number): Upst
},
state: null,
};
- case 'codex':
- return { ...base, kind: 'codex', config: { accounts: [] }, state: { accounts: [] } };
+ case 'chatgpt':
+ return { ...base, kind: 'chatgpt', config: { accounts: [] }, state: { accounts: [] } };
case 'claude-code':
return { ...base, kind: 'claude-code', config: { accounts: [] }, state: { accounts: [] } };
}
diff --git a/apps/web/src/routes/dashboard-providers-upstreams.tsx b/apps/web/src/routes/dashboard-providers-upstreams.tsx
index 4cfbc19436..c2e2b86f1c 100644
--- a/apps/web/src/routes/dashboard-providers-upstreams.tsx
+++ b/apps/web/src/routes/dashboard-providers-upstreams.tsx
@@ -35,7 +35,7 @@ import { useDialogInvocation } from '../components/ui/use-dialog-invocation';
import { usePollWhileVisible } from '../components/ui/use-poll-while-visible';
import { useRefresh } from '../components/ui/use-refresh';
import { shortAccountId } from '../components/upstreams/account-id';
-import { planLabel } from '../components/upstreams/codex-account';
+import { planLabel } from '../components/upstreams/chatgpt-account';
import { MODEL_ERROR_TOOLTIP_LENGTH, modelErrorExcerpt } from '../components/upstreams/model-error';
import { ProviderBadge, ProviderIcon } from '../components/upstreams/provider-badge';
import { UpstreamSignals } from '../components/upstreams/signals';
@@ -92,7 +92,7 @@ const PROVIDER_MENU_ORDER: readonly UpstreamProviderKind[] = [
'custom',
'azure',
'copilot',
- 'codex',
+ 'chatgpt',
'claude-code',
'ollama',
];
@@ -788,7 +788,7 @@ const buildModelCounts = (
};
// Who the upstream connects as. A subscription names an account; an endpoint
-// the operator configured names itself. A codex account appends its plan,
+// the operator configured names itself. A ChatGPT account appends its plan,
// through the same label the signals line uses, so the two lines agree.
const upstreamSummary = (record: ListedUpstreamRecord, t: TFunction): string => {
switch (record.kind) {
@@ -799,7 +799,7 @@ const upstreamSummary = (record: ListedUpstreamRecord, t: TFunction): string =>
// so its address is the whole identity.
case 'ollama': return record.state?.account?.email ?? (record.config.baseUrl || t('dashboard.upstreams.summary.ollama'));
case 'copilot': return record.config.user.login ? `${record.config.githubHost}/${record.config.user.login}` : t('dashboard.upstreams.summary.copilot');
- case 'codex': {
+ case 'chatgpt': {
const account = record.config.accounts[0];
if (!account) return t('dashboard.upstreams.summary.noAccount');
const identity = account.email
diff --git a/eslint.config.ts b/eslint.config.ts
index bc8b1ff363..e29cd45c99 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -74,8 +74,8 @@ const projectList = [
'./packages/protocols/tsconfig.json',
'./packages/provider/tsconfig.json',
'./packages/provider-azure/tsconfig.json',
+ './packages/provider-chatgpt/tsconfig.json',
'./packages/provider-claude-code/tsconfig.json',
- './packages/provider-codex/tsconfig.json',
'./packages/provider-copilot/tsconfig.json',
'./packages/provider-custom/tsconfig.json',
'./packages/provider-ollama/tsconfig.json',
diff --git a/packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts b/packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts
index 0857b0764a..2593d8e453 100644
--- a/packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts
+++ b/packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts
@@ -193,10 +193,10 @@ const OLLAMA_UPSTREAM: StoredUpstreamRecord = {
state: null,
};
-const CODEX_UPSTREAM: StoredUpstreamRecord = {
- id: 'up_codex_a',
- kind: 'codex',
- name: 'ChatGPT Codex (alice)',
+const CHATGPT_UPSTREAM: StoredUpstreamRecord = {
+ id: 'up_chatgpt_a',
+ kind: 'chatgpt',
+ name: 'ChatGPT (alice)',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 30,
@@ -862,21 +862,21 @@ test('ollama upstreams export and import round-trip', async () => {
assertEquals(await repo.upstreams.list(), [{ ...OLLAMA_UPSTREAM, sortOrder: 0 }]);
});
-test('codex upstreams export and import round-trip with state intact', async () => {
+test('ChatGPT upstreams export and import round-trip with state intact', async () => {
const { app, repo } = setup();
- await saveUpstreamForTest(repo.upstreams, CODEX_UPSTREAM);
+ await saveUpstreamForTest(repo.upstreams, CHATGPT_UPSTREAM);
await repo.webSearchConfig.save(DEFAULT_WEB_SEARCH_CONFIG);
const result = await doExport(app);
- const exportedCodex = result.data.upstreams.find((upstream: any) => upstream.id === 'up_codex_a');
- assertEquals(exportedCodex.config, CODEX_UPSTREAM.config);
- assertEquals(exportedCodex.state, CODEX_UPSTREAM.state);
+ const exportedChatGPT = result.data.upstreams.find((upstream: any) => upstream.id === 'up_chatgpt_a');
+ assertEquals(exportedChatGPT.config, CHATGPT_UPSTREAM.config);
+ assertEquals(exportedChatGPT.state, CHATGPT_UPSTREAM.state);
const replaceResult = await doImport(app, 'replace', {
...EMPTY_ACCESS_TRANSFER,
users: [SEED_ADMIN],
apiKeys: [],
- upstreams: [exportedCodex],
+ upstreams: [exportedChatGPT],
usage: [],
searchUsage: [],
upstreamMetrics: [],
@@ -885,12 +885,12 @@ test('codex upstreams export and import round-trip with state intact', async ()
siteSettings: { siteName: null },
});
assertEquals(replaceResult.status, 200);
- assertEquals(await repo.upstreams.list(), [{ ...CODEX_UPSTREAM, sortOrder: 0 }]);
+ assertEquals(await repo.upstreams.list(), [{ ...CHATGPT_UPSTREAM, sortOrder: 0 }]);
});
-test('codex import rejects when state is missing', async () => {
+test('ChatGPT import rejects when state is missing', async () => {
const { app } = setup();
- const { state: _dropped, ...stateless } = upstreamRecordToFullJson(CODEX_UPSTREAM);
+ const { state: _dropped, ...stateless } = upstreamRecordToFullJson(CHATGPT_UPSTREAM);
const result = await doImport(app, 'replace', {
...EMPTY_ACCESS_TRANSFER,
users: [SEED_ADMIN],
@@ -904,12 +904,12 @@ test('codex import rejects when state is missing', async () => {
siteSettings: { siteName: null },
});
assertEquals(result.status, 400);
- assertEquals(result.body.error.includes('codex upstream is missing state'), true);
+ assertEquals(result.body.error.includes('chatgpt upstream is missing state'), true);
});
-test('codex import rejects unknown keys in state', async () => {
+test('ChatGPT import rejects unknown keys in state', async () => {
const { app } = setup();
- const exported = upstreamRecordToFullJson(CODEX_UPSTREAM);
+ const exported = upstreamRecordToFullJson(CHATGPT_UPSTREAM);
const result = await doImport(app, 'replace', {
...EMPTY_ACCESS_TRANSFER,
users: [SEED_ADMIN],
@@ -1804,7 +1804,7 @@ test('a full v25 export re-imports verbatim — the export→import round trip i
await saveUpstreamForTest(repo.upstreams, COPILOT_UPSTREAM);
await saveUpstreamForTest(repo.upstreams, CUSTOM_UPSTREAM);
await saveUpstreamForTest(repo.upstreams, AZURE_UPSTREAM);
- await saveUpstreamForTest(repo.upstreams, CODEX_UPSTREAM);
+ await saveUpstreamForTest(repo.upstreams, CHATGPT_UPSTREAM);
await repo.usage.set(USAGE_1);
await repo.usage.set(USAGE_2);
await repo.webSearchUsage.set(WEB_SEARCH_USAGE_1);
@@ -1831,7 +1831,7 @@ test('a full v25 export re-imports verbatim — the export→import round trip i
assertEquals(result.body.imported, { users: 2, userPreferences: 0, groups: 0, accessRecords: 0, apiKeys: 2, upstreams: 4, proxies: 0, usage: 2, searchUsage: 2, upstreamMetrics: 0, performance: 2 });
// Spot-check fidelity across collection types (order-independent).
- assertEquals((await repo.upstreams.list()).find(u => u.id === 'up_codex_a')?.state, CODEX_UPSTREAM.state);
+ assertEquals((await repo.upstreams.list()).find(u => u.id === 'up_chatgpt_a')?.state, CHATGPT_UPSTREAM.state);
assertEquals((await repo.users.listIncludingDeleted()).find(u => u.id === USER_BOB.id), USER_BOB);
assertEquals((await repo.apiKeys.findByRawKey(KEY_B.key))?.userId, USER_BOB.id);
const restoredKeyA = await repo.apiKeys.findByRawKey(KEY_A.key);
diff --git a/packages/gateway/__tests__/control-plane/schemas_test.ts b/packages/gateway/__tests__/control-plane/schemas_test.ts
index d7a9dc9d71..2c95f48317 100644
--- a/packages/gateway/__tests__/control-plane/schemas_test.ts
+++ b/packages/gateway/__tests__/control-plane/schemas_test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest';
-import { codexImportExchangeBody, codexImportPreviewBody, createUpstreamBody } from '../../src/control-plane/schemas.ts';
+import { chatgptImportExchangeBody, chatgptImportPreviewBody, createUpstreamBody } from '../../src/control-plane/schemas.ts';
const baseAzure = {
kind: 'azure' as const,
@@ -17,20 +17,20 @@ const baseAzure = {
},
};
-describe('Codex import schemas', () => {
- const record = { id: '', kind: 'codex', config: { accounts: [] }, state: { accounts: [] } };
+describe('ChatGPT import schemas', () => {
+ const record = { id: '', kind: 'chatgpt', config: { accounts: [] }, state: { accounts: [] } };
test.each([
{ json: { raw_json: '{}', source_index: 0 } },
{ callback: { code: 'code', verifier: 'verifier' } },
{ manual: { access_token: 'opaque' } },
])('accepts exactly one import source', source => {
- expect(codexImportExchangeBody.safeParse({ record, ...source }).success).toBe(true);
+ expect(chatgptImportExchangeBody.safeParse({ record, ...source }).success).toBe(true);
});
test('rejects missing or multiple import sources', () => {
- expect(codexImportExchangeBody.safeParse({ record }).success).toBe(false);
- expect(codexImportExchangeBody.safeParse({
+ expect(chatgptImportExchangeBody.safeParse({ record }).success).toBe(false);
+ expect(chatgptImportExchangeBody.safeParse({
record,
json: { raw_json: '{}', source_index: 0 },
manual: { access_token: 'opaque' },
@@ -38,7 +38,7 @@ describe('Codex import schemas', () => {
});
test('requires only access_token for a manual import', () => {
- expect(codexImportExchangeBody.safeParse({
+ expect(chatgptImportExchangeBody.safeParse({
record,
manual: {
access_token: 'opaque',
@@ -48,12 +48,12 @@ describe('Codex import schemas', () => {
expires_at: null,
},
}).success).toBe(true);
- expect(codexImportExchangeBody.safeParse({ record, manual: {} }).success).toBe(false);
+ expect(chatgptImportExchangeBody.safeParse({ record, manual: {} }).success).toBe(false);
});
test('validates the JSON preview envelope', () => {
- expect(codexImportPreviewBody.safeParse({ raw_json: '{"accounts":[]}' }).success).toBe(true);
- expect(codexImportPreviewBody.safeParse({ raw_json: '' }).success).toBe(false);
+ expect(chatgptImportPreviewBody.safeParse({ raw_json: '{"accounts":[]}' }).success).toBe(true);
+ expect(chatgptImportPreviewBody.safeParse({ raw_json: '' }).success).toBe(false);
});
});
diff --git a/packages/gateway/__tests__/control-plane/upstreams/access-control_test.ts b/packages/gateway/__tests__/control-plane/upstreams/access-control_test.ts
index 3eb80fe6de..58c32464cd 100644
--- a/packages/gateway/__tests__/control-plane/upstreams/access-control_test.ts
+++ b/packages/gateway/__tests__/control-plane/upstreams/access-control_test.ts
@@ -344,9 +344,9 @@ test('Upstream.Edit.Full returns raw configuration, accepts full patches, and li
assertEquals(await proxies.json(), [{ id: 'proxy_a', name: 'Proxy A' }]);
});
-test('Codex JSON import preview is available to a signed-in non-admin', async () => {
+test('ChatGPT JSON import preview is available to a signed-in non-admin', async () => {
const { apiKey } = await setupAppTest();
- const response = await requestApp('/api/upstreams/codex/import/preview', {
+ const response = await requestApp('/api/upstreams/chatgpt/import/preview', {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-api-key': apiKey.key },
body: JSON.stringify({ raw_json: JSON.stringify({ accounts: [] }) }),
diff --git a/packages/gateway/__tests__/control-plane/upstreams/routes_test.ts b/packages/gateway/__tests__/control-plane/upstreams/routes_test.ts
index 5c3c8965d2..61005782d8 100644
--- a/packages/gateway/__tests__/control-plane/upstreams/routes_test.ts
+++ b/packages/gateway/__tests__/control-plane/upstreams/routes_test.ts
@@ -181,23 +181,23 @@ test('POST /api/upstreams creates Copilot upstream rows with redacted GitHub tok
// claude-code blueprints carry `{accounts: []}` and their asserters
// reject null on purpose.
-test('POST /api/upstreams rejects a codex create with null state', async () => {
+test('POST /api/upstreams rejects a ChatGPT create with null state', async () => {
const { adminSession } = await setupAppTest();
// Two-step setup: exchange returns a valid config+state patch, then we
// POST /api/upstreams with the config intact but a null state to prove
// the create-time state reader rejects it (before the state-hardening
// fix a client bypassing the exchange could persist this).
- const exchange = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
- record: blueprintEnvelope('codex'),
- json: codexJsonImport().json,
+ const exchange = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
+ record: blueprintEnvelope('chatgpt'),
+ json: chatgptJsonImport().json,
}));
assertEquals(exchange.status, 200);
const { patch } = (await exchange.json()) as { patch: { config: unknown; state: unknown } };
const resp = await requestApp('/api/upstreams', authed(adminSession, {
- kind: 'codex',
- name: 'Codex',
+ kind: 'chatgpt',
+ name: 'ChatGPT',
hue: 210,
config: patch.config,
state: null,
@@ -205,7 +205,7 @@ test('POST /api/upstreams rejects a codex create with null state', async () => {
}));
assertEquals(resp.status, 400);
const body = (await resp.json()) as { error: string };
- assertEquals(body.error.toLowerCase().includes('invalid state for codex'), true);
+ assertEquals(body.error.toLowerCase().includes('invalid state for chatgpt'), true);
});
test('POST /api/upstreams rejects a claude-code create with null state', async () => {
@@ -1031,10 +1031,10 @@ test('POST /api/upstreams/preview-models without an id still serves draft previe
);
});
-// --- Codex routes ---
+// --- ChatGPT routes ---
//
// The auth.json import path lets us drive the OAuth ingestion deterministically
-// without mocking the token-exchange roundtrip: parseCodexTokenClaims decodes
+// without mocking the token-exchange roundtrip: parseChatGPTTokenClaims decodes
// the id_token JWT directly. Build a fake JWT that carries the identity claims
// the production parser requires.
const encodeBase64Url = (input: string): string =>
@@ -1056,7 +1056,7 @@ const fakeIdToken = (claims: Record): string => {
// The `~/.codex/auth.json` envelope, which the import path detects by its
// `tokens` key and exposes as a single selectable source.
-const codexJsonImport = (overrides: Record = {}) => ({
+const chatgptJsonImport = (overrides: Record = {}) => ({
json: {
raw_json: JSON.stringify({
tokens: {
@@ -1070,30 +1070,30 @@ const codexJsonImport = (overrides: Record = {}) => ({
},
});
-// Two-step create flow: (1) exchange endpoint yields a codex config+state
+// Two-step create flow: (1) exchange endpoint yields a ChatGPT config+state
// patch from the fake auth.json blob, (2) POST /api/upstreams persists the
// merged draft. Mirrors the SPA editor's exchange-then-save flow so tests
-// touching subsequent codex actions see the same row a real user would
+// touching subsequent ChatGPT actions see the same row a real user would
// have.
-const createCodexUpstreamViaExchange = async (adminSession: string, overrides: Record = {}): Promise<{ id: string }> => {
- const exchange = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
- record: blueprintEnvelope('codex'),
- json: codexJsonImport(overrides).json,
+const createChatGPTUpstreamViaExchange = async (adminSession: string, overrides: Record = {}): Promise<{ id: string }> => {
+ const exchange = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
+ record: blueprintEnvelope('chatgpt'),
+ json: chatgptJsonImport(overrides).json,
}));
- if (exchange.status !== 200) throw new Error(`codex exchange failed: ${exchange.status} ${await exchange.text()}`);
+ if (exchange.status !== 200) throw new Error(`ChatGPT exchange failed: ${exchange.status} ${await exchange.text()}`);
const { patch } = (await exchange.json()) as { patch: { config: unknown; state: unknown } };
const create = await withMockedFetch(
() => jsonResponse({ error: 'forbidden' }, 403),
() => requestApp('/api/upstreams', authed(adminSession, {
- kind: 'codex',
- name: 'ChatGPT Codex',
+ kind: 'chatgpt',
+ name: 'ChatGPT',
hue: 210,
config: patch.config,
state: patch.state,
proxy_fallback_list: MOCKED_FETCH_EGRESS,
})),
);
- if (create.status !== 201) throw new Error(`codex create failed: ${create.status} ${await create.text()}`);
+ if (create.status !== 201) throw new Error(`ChatGPT create failed: ${create.status} ${await create.text()}`);
return (await create.json()) as { id: string };
};
@@ -1108,12 +1108,12 @@ const getRefreshIdentity = async (repo: { upstreams: { getById: (id: string) =>
return modelsRefreshIdentity(record);
};
-test('POST /api/upstreams/codex/oauth/authorize-url stamps SPA-provided challenge + state into the auth.openai.com URL', async () => {
+test('POST /api/upstreams/chatgpt/oauth/authorize-url stamps SPA-provided challenge + state into the auth.openai.com URL', async () => {
const { adminSession } = await setupAppTest();
const resp = await requestApp(
- '/api/upstreams/codex/oauth/authorize-url',
- authed(adminSession, { record: blueprintEnvelope('codex'), challenge: 'TEST_CHALLENGE', state: 'TEST_STATE' }),
+ '/api/upstreams/chatgpt/oauth/authorize-url',
+ authed(adminSession, { record: blueprintEnvelope('chatgpt'), challenge: 'TEST_CHALLENGE', state: 'TEST_STATE' }),
);
assertEquals(resp.status, 200);
const body = (await resp.json()) as { authorize_url: string };
@@ -1124,16 +1124,16 @@ test('POST /api/upstreams/codex/oauth/authorize-url stamps SPA-provided challeng
assertEquals(url.searchParams.get('state'), 'TEST_STATE');
});
-test('POST /api/upstreams/codex/import/exchange in create state (callback) returns a codex config+state patch from the SPA-supplied verifier', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange in create state (callback) returns a ChatGPT config+state patch from the SPA-supplied verifier', async () => {
const { adminSession } = await setupAppTest();
await withMockedFetch(
() => jsonResponse({ access_token: 'at_cb', refresh_token: 'rt_cb', id_token: fakeIdToken({}), expires_in: 600 }),
async () => {
const resp = await requestApp(
- '/api/upstreams/codex/import/exchange',
+ '/api/upstreams/chatgpt/import/exchange',
authed(adminSession, {
- record: blueprintEnvelope('codex'),
+ record: blueprintEnvelope('chatgpt'),
callback: { code: 'AUTH_CODE', verifier: 'TEST_VERIFIER' },
}),
);
@@ -1147,12 +1147,12 @@ test('POST /api/upstreams/codex/import/exchange in create state (callback) retur
);
});
-test('POST /api/upstreams/codex/import/exchange in create state (json) returns a codex config+state patch derived from the JWT', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange in create state (json) returns a ChatGPT config+state patch derived from the JWT', async () => {
const { adminSession } = await setupAppTest();
- const resp = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
- record: blueprintEnvelope('codex'),
- json: codexJsonImport().json,
+ const resp = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
+ record: blueprintEnvelope('chatgpt'),
+ json: chatgptJsonImport().json,
}));
assertEquals(resp.status, 200);
const body = (await resp.json()) as { patch: { config: JsonObject; state: JsonObject } };
@@ -1163,7 +1163,7 @@ test('POST /api/upstreams/codex/import/exchange in create state (json) returns a
assertEquals(body.patch.state.accounts[0].refresh_token, 'rt_test');
});
-test('POST /api/upstreams/codex/import/exchange imports a root account JSON object', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange imports a root account JSON object', async () => {
const { adminSession } = await setupAppTest();
const raw = JSON.stringify({
name: 'Primary',
@@ -1177,8 +1177,8 @@ test('POST /api/upstreams/codex/import/exchange imports a root account JSON obje
},
});
- const resp = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
- record: blueprintEnvelope('codex'),
+ const resp = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
+ record: blueprintEnvelope('chatgpt'),
json: { raw_json: raw, source_index: 0 },
}));
assertEquals(resp.status, 200);
@@ -1188,14 +1188,14 @@ test('POST /api/upstreams/codex/import/exchange imports a root account JSON obje
assertEquals(body.patch.state.accounts[0].refresh_token, 'refresh-root');
});
-test('POST /api/upstreams/codex/import/exchange imports a manual access-only credential without reaching the network', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange imports a manual access-only credential without reaching the network', async () => {
const { adminSession } = await setupAppTest();
await withMockedFetch(
() => { throw new Error('manual import must not fetch'); },
async () => {
- const resp = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
- record: blueprintEnvelope('codex'),
+ const resp = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
+ record: blueprintEnvelope('chatgpt'),
manual: { access_token: 'opaque' },
}));
assertEquals(resp.status, 200);
@@ -1208,14 +1208,14 @@ test('POST /api/upstreams/codex/import/exchange imports a manual access-only cre
);
});
-test('POST /api/upstreams/codex/import/exchange accepts optional manual email and plan for an opaque credential', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange accepts optional manual email and plan for an opaque credential', async () => {
const { adminSession } = await setupAppTest();
await withMockedFetch(
() => { throw new Error('manual import must not fetch'); },
async () => {
- const resp = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
- record: blueprintEnvelope('codex'),
+ const resp = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
+ record: blueprintEnvelope('chatgpt'),
manual: {
access_token: 'opaque',
account_id: 'acc_manual',
@@ -1232,7 +1232,7 @@ test('POST /api/upstreams/codex/import/exchange accepts optional manual email an
);
});
-test('POST /api/upstreams/codex/import/preview lists selectable candidates and no credential material', async () => {
+test('POST /api/upstreams/chatgpt/import/preview lists selectable candidates and no credential material', async () => {
const { adminSession } = await setupAppTest();
const raw = JSON.stringify({
accounts: [
@@ -1253,7 +1253,7 @@ test('POST /api/upstreams/codex/import/preview lists selectable candidates and n
await withMockedFetch(
() => { throw new Error('preview must not fetch'); },
async () => {
- const resp = await requestApp('/api/upstreams/codex/import/preview', authed(adminSession, { raw_json: raw }));
+ const resp = await requestApp('/api/upstreams/chatgpt/import/preview', authed(adminSession, { raw_json: raw }));
assertEquals(resp.status, 200);
const body = (await resp.json()) as { candidates: unknown };
assertEquals(body.candidates, [{
@@ -1274,7 +1274,7 @@ test('POST /api/upstreams/codex/import/preview lists selectable candidates and n
);
});
-test('POST /api/upstreams/codex/import/exchange re-parses and imports the selected JSON source index', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange re-parses and imports the selected JSON source index', async () => {
const { adminSession } = await setupAppTest();
const raw = JSON.stringify({
data: {
@@ -1284,8 +1284,8 @@ test('POST /api/upstreams/codex/import/exchange re-parses and imports the select
],
},
});
- const resp = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
- record: blueprintEnvelope('codex'),
+ const resp = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
+ record: blueprintEnvelope('chatgpt'),
json: { raw_json: raw, source_index: 1 },
}));
assertEquals(resp.status, 200);
@@ -1295,16 +1295,16 @@ test('POST /api/upstreams/codex/import/exchange re-parses and imports the select
assertEquals(body.patch.state.accounts[0].accessToken.token, 'second');
});
-test('POST /api/upstreams/codex/import/exchange in edit state persists the patch to the stored row', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange in edit state persists the patch to the stored row', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const initial = await createCodexUpstreamViaExchange(adminSession);
+ const initial = await createChatGPTUpstreamViaExchange(adminSession);
// Re-import with a rotated refresh_token to prove the exchange overwrites
// config + state on the existing row rather than appending an account.
- await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
+ await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, initial.id)),
- json: codexJsonImport({
+ json: chatgptJsonImport({
tokens: { access_token: 'at_v2', refresh_token: 'rt_v2', id_token: fakeIdToken({}) },
}).json,
}));
@@ -1314,11 +1314,11 @@ test('POST /api/upstreams/codex/import/exchange in edit state persists the patch
assertEquals(storedState.accounts[0].refresh_token, 'rt_v2');
});
-test('POST /api/upstreams/codex/import/exchange rejects when no import source is supplied', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange rejects when no import source is supplied', async () => {
const { adminSession } = await setupAppTest();
- const resp = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
- record: blueprintEnvelope('codex'),
+ const resp = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
+ record: blueprintEnvelope('chatgpt'),
}));
assertEquals(resp.status, 400);
const body = (await resp.json()) as { error: { issues?: Array<{ message: string }> } | string };
@@ -1326,22 +1326,22 @@ test('POST /api/upstreams/codex/import/exchange rejects when no import source is
assertEquals(JSON.stringify(body).includes('Provide exactly one of json, callback, or manual'), true);
});
-test('POST /api/upstreams/codex/oauth/refresh rejects a non-codex record with 400', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh rejects a non-ChatGPT record with 400', async () => {
const { adminSession } = await setupAppTest();
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, {
record: blueprintEnvelope('custom', { config: customConfig }),
}));
assertEquals(resp.status, 400);
const body = (await resp.json()) as { error: string };
- assertEquals(body.error.toLowerCase().includes('codex'), true);
+ assertEquals(body.error.toLowerCase().includes('chatgpt'), true);
});
-test('POST /api/upstreams/codex/oauth/refresh rejects a record in a terminal state with 400', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh rejects a record in a terminal state with 400', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const stored = await repo.upstreams.getById(created.id);
const storedState = stored!.state as { accounts: Array> };
await saveUpstreamForTest(repo.upstreams, {
@@ -1349,7 +1349,7 @@ test('POST /api/upstreams/codex/oauth/refresh rejects a record in a terminal sta
state: { accounts: storedState.accounts.map(a => ({ ...a, state: 'session_terminated' })) },
});
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
}));
assertEquals(resp.status, 400);
@@ -1357,10 +1357,10 @@ test('POST /api/upstreams/codex/oauth/refresh rejects a record in a terminal sta
assertEquals(body.error.includes('session_terminated'), true);
});
-test('POST /api/upstreams/codex/oauth/refresh rejects an access-only credential before touching proxy or OAuth', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh rejects an access-only credential before touching proxy or OAuth', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const stored = await getRecord(repo, created.id);
const state = stored.state as { accounts: Array> };
await saveUpstreamForTest(repo.upstreams, {
@@ -1371,7 +1371,7 @@ test('POST /api/upstreams/codex/oauth/refresh rejects an access-only credential
await withMockedFetch(
() => { throw new Error('access-only refresh must not fetch'); },
async () => {
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
}));
assertEquals(resp.status, 400);
@@ -1381,11 +1381,11 @@ test('POST /api/upstreams/codex/oauth/refresh rejects an access-only credential
);
});
-test('POST /api/upstreams/codex/oauth/refresh rotates the refresh token and persists to the row when the record has an id', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh rotates the refresh token and persists to the row when the record has an id', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
await withMockedFetch(
() => jsonResponse({
@@ -1395,7 +1395,7 @@ test('POST /api/upstreams/codex/oauth/refresh rotates the refresh token and pers
expires_in: 3600,
}),
async () => {
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
}));
assertEquals(resp.status, 200);
@@ -1409,11 +1409,11 @@ test('POST /api/upstreams/codex/oauth/refresh rotates the refresh token and pers
assertEquals(storedState.accounts[0].refresh_token, 'rt_rotated');
});
-test('POST /api/upstreams/codex/oauth/refresh flips the row to refresh_failed when OAuth rejects the refresh_token', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh flips the row to refresh_failed when OAuth rejects the refresh_token', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
await withMockedFetch(
() => new Response(
@@ -1421,7 +1421,7 @@ test('POST /api/upstreams/codex/oauth/refresh flips the row to refresh_failed wh
{ status: 400, headers: { 'content-type': 'application/json' } },
),
async () => {
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
}));
// 400, not 502: the upstream answered — it's the stored credential
@@ -1439,10 +1439,10 @@ test('POST /api/upstreams/codex/oauth/refresh flips the row to refresh_failed wh
assertEquals(typeof storedState.accounts[0].state_message, 'string');
});
-test('POST /api/upstreams/codex/reset-credits lists earned reset cards for the stored account', async () => {
+test('POST /api/upstreams/chatgpt/reset-credits lists earned reset cards for the stored account', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession, { tokens: { access_token: 'at_test', refresh_token: 'rt_test', id_token: fakeIdToken({}), expires_at: '2100-01-01T00:00:00Z' } });
+ const created = await createChatGPTUpstreamViaExchange(adminSession, { tokens: { access_token: 'at_test', refresh_token: 'rt_test', id_token: fakeIdToken({}), expires_at: '2100-01-01T00:00:00Z' } });
await withMockedFetch(
request => {
@@ -1459,7 +1459,7 @@ test('POST /api/upstreams/codex/reset-credits lists earned reset cards for the s
});
},
async () => {
- const resp = await requestApp('/api/upstreams/codex/reset-credits', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/reset-credits', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
}));
assertEquals(resp.status, 200);
@@ -1470,10 +1470,10 @@ test('POST /api/upstreams/codex/reset-credits lists earned reset cards for the s
);
});
-test('POST /api/upstreams/codex/reset-credits refreshes once after a definite upstream 401', async () => {
+test('POST /api/upstreams/chatgpt/reset-credits refreshes once after a definite upstream 401', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
let listAttempts = 0;
await withMockedFetch(
@@ -1491,10 +1491,10 @@ test('POST /api/upstreams/codex/reset-credits refreshes once after a definite up
id_token: fakeIdToken({}), expires_in: 3600,
});
}
- throw new Error(`unexpected Codex request to ${path}`);
+ throw new Error(`unexpected ChatGPT request to ${path}`);
},
async () => {
- const resp = await requestApp('/api/upstreams/codex/reset-credits', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/reset-credits', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
}));
assertEquals(resp.status, 200);
@@ -1506,10 +1506,10 @@ test('POST /api/upstreams/codex/reset-credits refreshes once after a definite up
assertEquals(updatedState.accounts[0].refresh_token, 'rt_rotated');
});
-test('POST /api/upstreams/codex/reset-credits/consume sends the stable key and invalidates stale quota', async () => {
+test('POST /api/upstreams/chatgpt/reset-credits/consume sends the stable key and invalidates stale quota', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession, { tokens: { access_token: 'at_test', refresh_token: 'rt_test', id_token: fakeIdToken({}), expires_at: '2100-01-01T00:00:00Z' } });
+ const created = await createChatGPTUpstreamViaExchange(adminSession, { tokens: { access_token: 'at_test', refresh_token: 'rt_test', id_token: fakeIdToken({}), expires_at: '2100-01-01T00:00:00Z' } });
const stored = await getRecord(repo, created.id);
const state = stored.state as { accounts: Array> };
await repo.upstreams.saveState(created.id, current => ({
@@ -1534,7 +1534,7 @@ test('POST /api/upstreams/codex/reset-credits/consume sends the stable key and i
return jsonResponse({ available_count: 0, credits: [] });
},
async () => {
- const resp = await requestApp('/api/upstreams/codex/reset-credits/consume', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/reset-credits/consume', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
credit_id: 'credit-1',
idempotency_key: 'redeem-stable',
@@ -1721,10 +1721,10 @@ test('PATCH /api/upstreams rejects config edits on a claude-code row', async ()
assertEquals(body.error.toLowerCase().includes('claude-code'), true);
});
-test('PATCH /api/upstreams accepts Codex display metadata edits', async () => {
+test('PATCH /api/upstreams accepts ChatGPT display metadata edits', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const patch = await requestApp(`/api/upstreams/${created.id}`, {
method: 'PATCH',
@@ -1743,10 +1743,10 @@ test('PATCH /api/upstreams accepts Codex display metadata edits', async () => {
});
});
-test('PATCH /api/upstreams rejects Codex account ID changes', async () => {
+test('PATCH /api/upstreams rejects ChatGPT account ID changes', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const patch = await requestApp(`/api/upstreams/${created.id}`, {
method: 'PATCH',
@@ -2032,15 +2032,15 @@ test('POST /api/upstreams/claude-code/oauth/refresh honors the record.proxy_fall
assertEquals(body.error.toLowerCase().includes('unknown proxy id'), true);
});
-test('POST /api/upstreams/codex/oauth/refresh honors the record.proxy_fallback_list over the persisted list', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh honors the record.proxy_fallback_list over the persisted list', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const envelope = envelopeFromRecord(await getRecord(repo, created.id));
envelope.proxy_fallback_list = [{ id: 'p_unknown' }];
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, { record: envelope }));
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, { record: envelope }));
assertEquals(resp.status, 400);
const body = (await resp.json()) as { error: string };
assertEquals(body.error.toLowerCase().includes('unknown proxy id'), true);
@@ -2118,13 +2118,13 @@ test('POST /api/upstreams/claude-code/oauth/exchange rejects a record.proxy_fall
// Only the callback source talks to auth.openai.com, so it is the only one
// whose egress chain has to resolve. A pasted document is parsed locally and
// must import even when the draft names a proxy that no longer exists.
-test('POST /api/upstreams/codex/import/exchange rejects a record.proxy_fallback_list referencing an unknown proxy id on the callback source', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange rejects a record.proxy_fallback_list referencing an unknown proxy id on the callback source', async () => {
const { adminSession } = await setupAppTest();
const resp = await requestApp(
- '/api/upstreams/codex/import/exchange',
+ '/api/upstreams/chatgpt/import/exchange',
authed(adminSession, {
- record: blueprintEnvelope('codex', { proxy_fallback_list: [{ id: 'p_unknown' }] }),
+ record: blueprintEnvelope('chatgpt', { proxy_fallback_list: [{ id: 'p_unknown' }] }),
callback: { code: 'AUTH_CODE', verifier: 'TEST_VERIFIER' },
}),
);
@@ -2133,14 +2133,14 @@ test('POST /api/upstreams/codex/import/exchange rejects a record.proxy_fallback_
assertEquals(body.error.toLowerCase().includes('unknown proxy id'), true);
});
-test('POST /api/upstreams/codex/import/exchange imports a pasted document without resolving egress', async () => {
+test('POST /api/upstreams/chatgpt/import/exchange imports a pasted document without resolving egress', async () => {
const { adminSession } = await setupAppTest();
const resp = await requestApp(
- '/api/upstreams/codex/import/exchange',
+ '/api/upstreams/chatgpt/import/exchange',
authed(adminSession, {
- record: blueprintEnvelope('codex', { proxy_fallback_list: [{ id: 'p_unknown' }] }),
- ...codexJsonImport(),
+ record: blueprintEnvelope('chatgpt', { proxy_fallback_list: [{ id: 'p_unknown' }] }),
+ ...chatgptJsonImport(),
}),
);
assertEquals(resp.status, 200);
@@ -2303,16 +2303,16 @@ test('POST /api/upstreams/claude-code/setup-token/exchange in edit state replace
assertEquals(storedState.accounts[0].accessToken?.token, 'st_v2');
});
-test('POST /api/upstreams/codex/oauth/refresh resolves an empty record.proxy_fallback_list to direct-connect egress', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh resolves an empty record.proxy_fallback_list to direct-connect egress', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
// Same boundary as the claude-code case: an empty list is accepted as
// "no policy" and resolves to direct-connect, rather than being rejected
// as an unknown proxy id.
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, {
record: { ...envelopeFromRecord(await getRecord(repo, created.id)), proxy_fallback_list: [] },
}));
assertEquals(resp.status, 502);
@@ -2529,19 +2529,19 @@ test('spec invariant (3): POST /api/upstreams/copilot/quota ignores record.flag_
assertEquals(stored?.flagOverrides, originalFlags);
});
-test('spec invariant (3): POST /api/upstreams/codex/import/exchange (edit state) ignores record.name mutation', async () => {
+test('spec invariant (3): POST /api/upstreams/chatgpt/import/exchange (edit state) ignores record.name mutation', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const record = await getRecord(repo, created.id);
const originalName = record.name;
const envelope = envelopeFromRecord(record);
envelope.name = 'Mutated';
- const resp = await requestApp('/api/upstreams/codex/import/exchange', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/import/exchange', authed(adminSession, {
record: envelope,
- json: codexJsonImport({
+ json: chatgptJsonImport({
tokens: { access_token: 'at_v2', refresh_token: 'rt_v2', id_token: fakeIdToken({}) },
}).json,
}));
@@ -2551,11 +2551,11 @@ test('spec invariant (3): POST /api/upstreams/codex/import/exchange (edit state)
assertEquals(stored?.name, originalName);
});
-test('spec invariant (3): POST /api/upstreams/codex/oauth/refresh ignores record.sort_order mutation', async () => {
+test('spec invariant (3): POST /api/upstreams/chatgpt/oauth/refresh ignores record.sort_order mutation', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const record = await getRecord(repo, created.id);
const originalSortOrder = record.sortOrder;
const envelope = envelopeFromRecord(record);
@@ -2564,7 +2564,7 @@ test('spec invariant (3): POST /api/upstreams/codex/oauth/refresh ignores record
await withMockedFetch(
() => jsonResponse({ access_token: 'at_rotated', refresh_token: 'rt_rotated', id_token: fakeIdToken({}), expires_in: 3600 }),
async () => {
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, { record: envelope }));
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, { record: envelope }));
assertEquals(resp.status, 200);
},
);
@@ -2727,7 +2727,7 @@ test('POST /api/upstreams/preview-models never writes the matching saved row', a
test('GET /api/upstreams/blueprint round-trips a shape-complete blank for every kind', async () => {
const { adminSession } = await setupAppTest();
- const kinds: UpstreamProviderKind[] = ['copilot', 'custom', 'azure', 'codex', 'claude-code', 'ollama'];
+ const kinds: UpstreamProviderKind[] = ['copilot', 'custom', 'azure', 'chatgpt', 'claude-code', 'ollama'];
for (const kind of kinds) {
const resp = await requestApp(`/api/upstreams/blueprint?kind=${kind}`, { headers: { 'x-floway-session': adminSession } });
assertEquals(resp.status, 200);
@@ -2767,7 +2767,7 @@ test('GET /api/upstreams/blueprint serves the record a new upstream starts as wi
// `flag_defaults` on the wire is the whole point of the blueprint;
// assert it lands on every kind so the dashboard's "Inherit → on/off"
// pill has data to render before Save.
- for (const kind of ['copilot', 'custom', 'azure', 'codex', 'claude-code', 'ollama']) {
+ for (const kind of ['copilot', 'custom', 'azure', 'chatgpt', 'claude-code', 'ollama']) {
const preview = (await (await requestApp(`/api/upstreams/blueprint?kind=${kind}`, { headers: { 'x-floway-session': adminSession } })).json()) as JsonObject;
assertEquals(typeof preview.flag_defaults['strip-billing-attribution'], 'boolean');
}
@@ -2786,11 +2786,11 @@ test('GET /api/upstreams/blueprint serves the record a new upstream starts as wi
assertEquals(ccPreview.flag_defaults['rewrite-disallowed-mid-conv-system-to-user'], true);
});
-test('GET /api/upstreams/:id returns the full record with fresh Codex quota for the edit page', async () => {
+test('GET /api/upstreams/:id returns the full record with fresh ChatGPT quota for the edit page', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const stored = await getRecord(repo, created.id);
const quota = {
observed_at: new Date().toISOString(),
@@ -2808,17 +2808,17 @@ test('GET /api/upstreams/:id returns the full record with fresh Codex quota for
assertEquals(resp.status, 200);
const body = (await resp.json()) as JsonObject;
assertEquals(body.id, created.id);
- assertEquals(body.codex_quota, { premium: quota });
+ assertEquals(body.chatgpt_quota, { premium: quota });
// Unredacted: the refresh_token secret is present verbatim, not projected
// as the list-view's `refresh_token_set` boolean.
assertEquals(body.state.accounts[0].refresh_token, 'rt_test');
assertEquals('refresh_token_set' in body.state.accounts[0], false);
});
-test('GET upstream list and detail repair legacy Codex expiry per family without rewriting state', async () => {
+test('GET upstream list and detail repair legacy ChatGPT expiry per family without rewriting state', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const stored = await getRecord(repo, created.id);
const primary = {
observed_at: '2026-06-05T00:00:00.000Z',
@@ -2843,7 +2843,7 @@ test('GET upstream list and detail repair legacy Codex expiry per family without
assertEquals(resp.status, 200);
const json = await resp.json() as JsonObject;
const body = path === '/api/upstreams' ? (json as JsonObject[]).find(item => item.id === created.id) : json;
- assertEquals(body?.codex_quota, {
+ assertEquals(body?.chatgpt_quota, {
primary: { ...primary, ratelimited_until: primary.primary_reset_after_at },
secondary,
successful,
@@ -2852,19 +2852,19 @@ test('GET upstream list and detail repair legacy Codex expiry per family without
assertEquals((await getRecord(repo, created.id)).state, state);
});
-test('GET /api/upstreams/:id returns null Codex quota when no fresh snapshot exists', async () => {
+test('GET /api/upstreams/:id returns null ChatGPT quota when no fresh snapshot exists', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const resp = await requestApp(`/api/upstreams/${created.id}`, { headers: { 'x-floway-session': adminSession } });
assertEquals(resp.status, 200);
const body = (await resp.json()) as JsonObject;
- assertEquals('codex_quota' in body, true);
- assertEquals(body.codex_quota, null);
+ assertEquals('chatgpt_quota' in body, true);
+ assertEquals(body.chatgpt_quota, null);
});
-test('GET /api/upstreams/:id omits Codex quota from non-Codex responses', async () => {
+test('GET /api/upstreams/:id omits ChatGPT quota from non-ChatGPT responses', async () => {
const { adminSession } = await setupAppTest();
const createdResp = await requestApp('/api/upstreams', authed(adminSession, createBody()));
assertEquals(createdResp.status, 201);
@@ -2873,7 +2873,7 @@ test('GET /api/upstreams/:id omits Codex quota from non-Codex responses', async
const resp = await requestApp(`/api/upstreams/${created.id}`, { headers: { 'x-floway-session': adminSession } });
assertEquals(resp.status, 200);
const body = (await resp.json()) as JsonObject;
- assertEquals('codex_quota' in body, false);
+ assertEquals('chatgpt_quota' in body, false);
});
test('GET /api/upstreams/:id returns 404 for an unknown id', async () => {
@@ -3051,11 +3051,11 @@ test('POST /api/upstreams/copilot/quota remaps upstream 401 to 502 and passes up
// terminal flip) surfaces the terminal error. These endpoint-level tests
// pin that inherited contract.
-test('POST /api/upstreams/codex/oauth/refresh recovers as success when a sibling rotated the refresh token mid-flight', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh recovers as success when a sibling rotated the refresh token mid-flight', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
const siblingExpiresAt = Date.now() + 3_600_000;
await withMockedFetch(
@@ -3088,7 +3088,7 @@ test('POST /api/upstreams/codex/oauth/refresh recovers as success when a sibling
throw new Error(`Unhandled fetch ${request.url}`);
},
async () => {
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
}));
assertEquals(resp.status, 200);
@@ -3102,11 +3102,11 @@ test('POST /api/upstreams/codex/oauth/refresh recovers as success when a sibling
assertEquals(storedState.accounts[0].accessToken?.token, 'at_sibling_rotated');
});
-test('POST /api/upstreams/codex/oauth/refresh surfaces terminal error when a sibling flipped the account to refresh_failed mid-flight', async () => {
+test('POST /api/upstreams/chatgpt/oauth/refresh surfaces terminal error when a sibling flipped the account to refresh_failed mid-flight', async () => {
const { repo, adminSession } = await setupAppTest();
await repo.upstreams.deleteAll();
- const created = await createCodexUpstreamViaExchange(adminSession);
+ const created = await createChatGPTUpstreamViaExchange(adminSession);
await withMockedFetch(
async (request: Request) => {
@@ -3136,7 +3136,7 @@ test('POST /api/upstreams/codex/oauth/refresh surfaces terminal error when a sib
throw new Error(`Unhandled fetch ${request.url}`);
},
async () => {
- const resp = await requestApp('/api/upstreams/codex/oauth/refresh', authed(adminSession, {
+ const resp = await requestApp('/api/upstreams/chatgpt/oauth/refresh', authed(adminSession, {
record: envelopeFromRecord(await getRecord(repo, created.id)),
}));
assertEquals(resp.status, 400);
diff --git a/packages/gateway/__tests__/control-plane/upstreams/serialize_test.ts b/packages/gateway/__tests__/control-plane/upstreams/serialize_test.ts
index b932b4a1c0..f60aedb43f 100644
--- a/packages/gateway/__tests__/control-plane/upstreams/serialize_test.ts
+++ b/packages/gateway/__tests__/control-plane/upstreams/serialize_test.ts
@@ -9,7 +9,7 @@ const timestamp = '2026-04-29T00:00:00.000Z';
test('serialized records exclude route-layer response projections and redacted access tokens', () => {
expectTypeOf<'modelsCache' extends keyof FullSerializedUpstreamRecord ? true : false>().toEqualTypeOf();
- expectTypeOf<'codex_quota' extends keyof FullSerializedUpstreamRecord ? true : false>().toEqualTypeOf();
+ expectTypeOf<'chatgpt_quota' extends keyof FullSerializedUpstreamRecord ? true : false>().toEqualTypeOf();
type ClaudeState = Extract['state'];
type AccessToken = ClaudeState['accounts'][number]['accessToken'];
expectTypeOf<'token' extends keyof NonNullable ? true : false>().toEqualTypeOf();
@@ -208,10 +208,10 @@ const claudeCodeBase = (overrides: { config?: unknown; state?: unknown }): Upstr
state: overrides.state ?? null,
} as unknown as UpstreamRecord);
-const codexBase = (overrides: { config?: unknown; state?: unknown }): UpstreamRecord => ({
+const chatgptBase = (overrides: { config?: unknown; state?: unknown }): UpstreamRecord => ({
id: 'up_cx_test',
- kind: 'codex',
- name: 'Codex',
+ kind: 'chatgpt',
+ name: 'ChatGPT',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 0,
@@ -254,8 +254,8 @@ test('upstreamRecordToJson throws when claude-code state.accounts is not an arra
expect(() => upstreamRecordToJson(record)).toThrow(/accounts must be an array/);
});
-test('upstreamRecordToJson exposes Codex capability and bearer timing without the bearer', () => {
- const result = upstreamRecordToJson(codexBase({
+test('upstreamRecordToJson exposes ChatGPT capability and bearer timing without the bearer', () => {
+ const result = upstreamRecordToJson(chatgptBase({
config: {
accounts: [{ email: null, chatgptAccountId: null, chatgptUserId: null, planType: null }],
},
@@ -280,26 +280,26 @@ test('upstreamRecordToJson exposes Codex capability and bearer timing without th
assertEquals(JSON.stringify(result).includes('bearer-secret'), false);
});
-test('upstreamRecordToJson throws when codex state.accessToken is a string', () => {
- const record = codexBase({
+test('upstreamRecordToJson throws when ChatGPT state.accessToken is a string', () => {
+ const record = chatgptBase({
state: { accounts: [{ chatgptAccountId: 'acc', refresh_token: null, state: 'active', state_updated_at: timestamp, openaiDeviceId: 'device', accessToken: 'not-an-object', quotaSnapshot: null }] },
});
expect(() => upstreamRecordToJson(record)).toThrow(/accessToken must be a plain object/);
});
-test('upstreamRecordToJson throws when codex config.accounts is not an array', () => {
- const record = codexBase({ config: { accounts: 'not-an-array' } });
+test('upstreamRecordToJson throws when ChatGPT config.accounts is not an array', () => {
+ const record = chatgptBase({ config: { accounts: 'not-an-array' } });
expect(() => upstreamRecordToJson(record)).toThrow(/accounts must be an array/);
});
-test('upstreamRecordToJson throws when codex state.accounts is not an array', () => {
- const record = codexBase({
+test('upstreamRecordToJson throws when ChatGPT state.accounts is not an array', () => {
+ const record = chatgptBase({
state: { accounts: 'not-an-array' },
});
expect(() => upstreamRecordToJson(record)).toThrow(/accounts must be an array/);
});
test('persisted subscription upstreams require credential state', () => {
- expect(() => upstreamRecordToJson(codexBase({}))).toThrow(/CodexUpstreamState must be a plain object/);
+ expect(() => upstreamRecordToJson(chatgptBase({}))).toThrow(/ChatGPTUpstreamState must be a plain object/);
expect(() => upstreamRecordToFullJson(claudeCodeBase({}))).toThrow(/ClaudeCodeUpstreamState must be a plain object/);
});
diff --git a/packages/gateway/__tests__/data-plane/chat/openai-responses/websocket_test.ts b/packages/gateway/__tests__/data-plane/chat/openai-responses/websocket_test.ts
index eef9808a16..ecb0bfdc4b 100644
--- a/packages/gateway/__tests__/data-plane/chat/openai-responses/websocket_test.ts
+++ b/packages/gateway/__tests__/data-plane/chat/openai-responses/websocket_test.ts
@@ -12,7 +12,7 @@ import { initBackgroundSchedulerResolver } from '../../../../src/runtime/backgro
import { installDumpStubs } from '../../../dump/test-fixtures.ts';
import { saveUpstreamForTest } from '../../../repo/upstreams.ts';
import { FakeTime } from '../../../test-time.ts';
-import { buildCodexUpstreamRecord, codexModels, copilotModels, flushAsyncWork, setupAppTest, sseResponse, sseOpenAIResponsesResponse, warmModelsForTest } from '../../../test-utils/app.ts';
+import { buildChatGPTUpstreamRecord, chatgptModels, copilotModels, flushAsyncWork, setupAppTest, sseResponse, sseOpenAIResponsesResponse, warmModelsForTest } from '../../../test-utils/app.ts';
import { trackBackground } from '../../../test-utils/background-tracker.ts';
import { installWorkerWebSocketRuntime, type TestWorkerWebSocket } from '../../../test-utils/worker-websocket.ts';
import { assert, assertEquals, assertExists, assertStringIncludes, jsonResponse, withMockedFetch } from '@floway-dev/test-utils';
@@ -1927,7 +1927,7 @@ test('OpenAI Responses WebSocket outer catch records a failed perf sample attrib
test('OpenAI Responses WebSocket dispatches each Codex turn with the metadata blob that turn carried', async () => {
const { apiKey, repo } = await setupAppTest();
- await saveUpstreamForTest(repo.upstreams, buildCodexUpstreamRecord({
+ await saveUpstreamForTest(repo.upstreams, buildChatGPTUpstreamRecord({
flagOverrides: { 'openai-responses-compact-decrypt': false },
}));
const upstreamBodies: Record[] = [];
@@ -1952,7 +1952,7 @@ test('OpenAI Responses WebSocket dispatches each Codex turn with the metadata bl
return jsonResponse({ token: 'copilot-access-token', expires_at: 4102444800, refresh_in: 3600, endpoints: { api: 'https://api.individual.githubcopilot.com' } });
}
if (url.pathname === '/models') return jsonResponse(copilotModels([]));
- if (url.pathname === '/backend-api/codex/models') return jsonResponse(codexModels([{ slug: 'gpt-5.4' }]));
+ if (url.pathname === '/backend-api/codex/models') return jsonResponse(chatgptModels([{ slug: 'gpt-5.4' }]));
if (url.pathname === '/backend-api/codex/responses') {
upstreamBodies.push(JSON.parse(await request.text()) as Record);
const turn = upstreamBodies.length;
@@ -2048,7 +2048,7 @@ test('OpenAI Responses WebSocket dispatches each Codex turn with the metadata bl
test('OpenAI Responses WebSocket replays Codex Lite input items without rebuilding their prefix', async () => {
const { apiKey, repo } = await setupAppTest();
- await saveUpstreamForTest(repo.upstreams, buildCodexUpstreamRecord());
+ await saveUpstreamForTest(repo.upstreams, buildChatGPTUpstreamRecord());
const upstreamBodies: Record[] = [];
const prefix = [
{ type: 'additional_tools', role: 'developer', id: 'at_ws_lite', tools: [{ type: 'namespace', name: 'functions', description: '', tools: [{ type: 'function', name: 'lookup', parameters: { type: 'object' } }] }] },
diff --git a/packages/gateway/__tests__/data-plane/codex/models_test.ts b/packages/gateway/__tests__/data-plane/codex/models_test.ts
index d278a30776..a1f9f8416d 100644
--- a/packages/gateway/__tests__/data-plane/codex/models_test.ts
+++ b/packages/gateway/__tests__/data-plane/codex/models_test.ts
@@ -38,8 +38,8 @@ const ultraCapabilities: CodexCatalogCapabilities = {
};
describe('assembleCodexCatalog', () => {
- test('uses the primary Codex provider default for a prefixed model without a matching client entry', () => {
- const provider = { ...stubModelCandidate().provider, kind: 'codex' as const };
+ test('uses the primary ChatGPT provider default for a prefixed model without a matching client entry', () => {
+ const provider = { ...stubModelCandidate().provider, kind: 'chatgpt' as const };
const model = {
...chat('codex/future-model', 'Future', 600000),
providerModels: {
@@ -53,9 +53,9 @@ describe('assembleCodexCatalog', () => {
expect(out.models[0]).toMatchObject({ slug: 'codex/future-model', context_window: 80000, max_context_window: 600000 });
});
- test('a secondary Codex provider cannot replace the primary provider budget', () => {
+ test('a secondary ChatGPT provider cannot replace the primary provider budget', () => {
const primary = stubModelCandidate().provider;
- const secondary = { ...primary, upstreamId: 'secondary', kind: 'codex' as const };
+ const secondary = { ...primary, upstreamId: 'secondary', kind: 'chatgpt' as const };
const model = {
...chat('gpt-5.5', 'Primary', 200000),
providerModels: {
diff --git a/packages/gateway/__tests__/data-plane/codex/routes_images_test.ts b/packages/gateway/__tests__/data-plane/codex/routes_images_test.ts
index ae32cffe5e..495b25432a 100644
--- a/packages/gateway/__tests__/data-plane/codex/routes_images_test.ts
+++ b/packages/gateway/__tests__/data-plane/codex/routes_images_test.ts
@@ -3,7 +3,7 @@ import { test } from 'vitest';
import type { InMemoryRepo } from '../../repo/memory.ts';
import { saveUpstreamForTest } from '../../repo/upstreams.ts';
import { copilotModels, MOCKED_FETCH_EGRESS, requestApp, requestAppWithWarmModels, setupAppTest } from '../../test-utils/app.ts';
-import { CODEX_USER_AGENT } from '@floway-dev/provider-codex';
+import { CHATGPT_USER_AGENT } from '@floway-dev/provider-chatgpt';
import { assertEquals, assertExists, jsonResponse, withMockedFetch } from '@floway-dev/test-utils';
const PNG_B64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/wEAAAAASUVORK5CYII=';
@@ -39,7 +39,7 @@ const saveAzureImages = async (repo: InMemoryRepo): Promise => {
const saveCodexImages = async (repo: InMemoryRepo): Promise => {
await saveUpstreamForTest(repo.upstreams, {
id: 'codex-image',
- kind: 'codex',
+ kind: 'chatgpt',
name: 'ChatGPT Team',
enabled: true,
usageRefreshIntervalMinutes: 0,
@@ -144,7 +144,7 @@ test('ChatGPT Codex accounts expose and serve the implicit gpt-image-2 model', a
assertEquals(publicCatalog.data.some(model => model.id === 'gpt-image-2'), true);
const codexModels = await requestApp('/azure-api.codex/models?client_version=0.147.0', {
- headers: { authorization: `Bearer ${apiKey.key}`, 'user-agent': CODEX_USER_AGENT },
+ headers: { authorization: `Bearer ${apiKey.key}`, 'user-agent': CHATGPT_USER_AGENT },
});
assertEquals(codexModels.status, 200);
const codexCatalog = await codexModels.json() as { models: { slug: string }[] };
diff --git a/packages/gateway/__tests__/data-plane/codex/routes_test.ts b/packages/gateway/__tests__/data-plane/codex/routes_test.ts
index 58a7bc40cf..ef14d5789b 100644
--- a/packages/gateway/__tests__/data-plane/codex/routes_test.ts
+++ b/packages/gateway/__tests__/data-plane/codex/routes_test.ts
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest';
import { mountCodexRoutes } from '../../../src/data-plane/codex/routes.ts';
import { type AuthVars, authMiddleware } from '../../../src/middleware/auth.ts';
import { saveUpstreamForTest } from '../../repo/upstreams.ts';
-import { buildCodexUpstreamRecord, copilotModels, requestApp, setupAppTest, warmModelsForTest } from '../../test-utils/app.ts';
+import { buildChatGPTUpstreamRecord, copilotModels, requestApp, setupAppTest, warmModelsForTest } from '../../test-utils/app.ts';
import { jsonResponse, withMockedFetch } from '@floway-dev/test-utils';
const buildCodexApp = () => {
@@ -52,7 +52,7 @@ describe('Codex model-provider routes', () => {
it('preserves backend defaults and override ceilings through catalog refresh and both model routes', async () => {
const { apiKey, repo } = await setupAppTest();
await repo.upstreams.deleteAll();
- await saveUpstreamForTest(repo.upstreams, buildCodexUpstreamRecord());
+ await saveUpstreamForTest(repo.upstreams, buildChatGPTUpstreamRecord());
const body = await withMockedFetch(
(request: Request): Response => {
const url = new URL(request.url);
@@ -87,7 +87,7 @@ describe('Codex model-provider routes', () => {
expect(bySlug['future-model']).toMatchObject({ context_window: 80000, max_context_window: 600000, auto_compact_token_limit: null });
expect(bySlug['uncapped-model']).toMatchObject({ context_window: 64000 });
expect(bySlug['uncapped-model']).not.toHaveProperty('max_context_window');
- const stored = await repo.upstreams.getById('up_codex');
+ const stored = await repo.upstreams.getById('up_chatgpt');
expect(stored?.modelsCache?.models.find(model => model.id === 'gpt-6.1-sol')?.limits.max_context_window_tokens).toBe(872000);
});
diff --git a/packages/gateway/__tests__/data-plane/providers/models-cache_test.ts b/packages/gateway/__tests__/data-plane/providers/models-cache_test.ts
index 28a1dfb5dd..f8383489e5 100644
--- a/packages/gateway/__tests__/data-plane/providers/models-cache_test.ts
+++ b/packages/gateway/__tests__/data-plane/providers/models-cache_test.ts
@@ -6,7 +6,7 @@ import { InvalidProxyConfigurationError } from '../../../src/dial/per-request.ts
import { createModelsRefreshScheduler, discoverDraftModels, modelsRefreshTarget, refreshModels, refreshModelsExplicit } from '../../../src/execution/models-refresh.ts';
import { modelsRefreshIdentity, seedModelsCache } from '../../repo/models-cache-fixture.ts';
import { saveUpstreamForTest } from '../../repo/upstreams.ts';
-import { buildCodexUpstreamRecord, buildCustomUpstreamRecord, setupAppTest } from '../../test-utils/app.ts';
+import { buildChatGPTUpstreamRecord, buildCustomUpstreamRecord, setupAppTest } from '../../test-utils/app.ts';
import { ProviderModelsUnavailableError } from '@floway-dev/provider';
import { jsonResponse, stubProviderModel, withMockedFetch } from '@floway-dev/test-utils';
@@ -387,17 +387,17 @@ test('model discovery redacts userinfo from a URL construction failure', async (
expect(message).not.toContain('user:secret');
});
-test('Codex OAuth failure cannot echo a refresh token into the model error', async () => {
+test('ChatGPT OAuth failure cannot echo a refresh token into the model error', async () => {
const { repo } = await setupAppTest();
await repo.upstreams.deleteAll();
- const codex = buildCodexUpstreamRecord();
- const state = codex.state as { accounts: Array> };
+ const chatgpt = buildChatGPTUpstreamRecord();
+ const state = chatgpt.state as { accounts: Array> };
await saveUpstreamForTest(repo.upstreams, {
- ...codex,
+ ...chatgpt,
state: { accounts: state.accounts.map(account => ({ ...account, accessToken: null })) },
});
- const record = await repo.upstreams.getById(codex.id);
- if (record === null) throw new Error('Codex upstream missing');
+ const record = await repo.upstreams.getById(chatgpt.id);
+ if (record === null) throw new Error('ChatGPT upstream missing');
await withMockedFetch(
async request => {
expect(new URL(request.url).pathname).toBe('/oauth/token');
@@ -414,17 +414,17 @@ test('Codex OAuth failure cannot echo a refresh token into the model error', asy
expect(message).not.toContain('rt_v1');
});
-test('Codex OAuth failure redacts a form-encoded refresh token echo', async () => {
+test('ChatGPT OAuth failure redacts a form-encoded refresh token echo', async () => {
const { repo } = await setupAppTest();
await repo.upstreams.deleteAll();
- const codex = buildCodexUpstreamRecord();
- const state = codex.state as { accounts: Array> };
+ const chatgpt = buildChatGPTUpstreamRecord();
+ const state = chatgpt.state as { accounts: Array> };
await saveUpstreamForTest(repo.upstreams, {
- ...codex,
+ ...chatgpt,
state: { accounts: state.accounts.map(account => ({ ...account, refresh_token: 'rt with spaces', accessToken: null })) },
});
- const record = await repo.upstreams.getById(codex.id);
- if (record === null) throw new Error('Codex upstream missing');
+ const record = await repo.upstreams.getById(chatgpt.id);
+ if (record === null) throw new Error('ChatGPT upstream missing');
await withMockedFetch(
async request => new Response(`denied ${await request.text()}`, { status: 503 }),
async () => {
@@ -468,21 +468,21 @@ test('Claude Code OAuth failure cannot echo a refresh token into the model error
expect(message).not.toContain('claude-refresh-token');
});
-test('a malformed Codex catalog cannot echo its bearer token in a parser error', async () => {
+test('a malformed ChatGPT catalog cannot echo its bearer token in a parser error', async () => {
const { repo } = await setupAppTest();
await repo.upstreams.deleteAll();
- await saveUpstreamForTest(repo.upstreams, buildCodexUpstreamRecord());
- const record = await repo.upstreams.getById('up_codex');
- if (record === null) throw new Error('Codex upstream missing');
+ await saveUpstreamForTest(repo.upstreams, buildChatGPTUpstreamRecord());
+ const record = await repo.upstreams.getById('up_chatgpt');
+ if (record === null) throw new Error('ChatGPT upstream missing');
await withMockedFetch(
- () => jsonResponse({ models: [{ slug: 'codex-access-token' }] }),
+ () => jsonResponse({ models: [{ slug: 'chatgpt-access-token' }] }),
async () => {
await expect(refreshModelsExplicit(modelsRefreshTarget(record), 'TEST')).rejects.toThrow('model entry [REDACTED] missing display_name');
},
);
const message = (await repo.upstreams.getById(record.id))?.modelsCache?.lastError?.message;
expect(message).toContain('[REDACTED]');
- expect(message).not.toContain('codex-access-token');
+ expect(message).not.toContain('chatgpt-access-token');
});
test('automatic proxy configuration failures are recorded and backed off', async () => {
diff --git a/packages/gateway/__tests__/data-plane/tools/web-search/alpha-search/upstream_test.ts b/packages/gateway/__tests__/data-plane/tools/web-search/alpha-search/upstream_test.ts
index b1ee2fd5cb..50b11385e4 100644
--- a/packages/gateway/__tests__/data-plane/tools/web-search/alpha-search/upstream_test.ts
+++ b/packages/gateway/__tests__/data-plane/tools/web-search/alpha-search/upstream_test.ts
@@ -27,7 +27,7 @@ const context: Parameters[0] = {
runtimeLocation: 'TEST',
};
-const dispatcherFor = async (kind: 'codex' | 'custom', inboundHeaderAllowlist: readonly InboundHeaderMatcher[] = []) => {
+const dispatcherFor = async (kind: 'chatgpt' | 'custom', inboundHeaderAllowlist: readonly InboundHeaderMatcher[] = []) => {
let observedHeaders: Headers | undefined;
const base = stubModelCandidate();
const provider = {
@@ -70,8 +70,8 @@ test('Alpha Search rejects unsupported providers as configuration failures', asy
await expect(resolveAlphaSearchDispatcher(context)).rejects.toThrow('Selected upstream does not support OpenAI search passthrough');
});
-test('Codex Alpha Search receives only its declared turn metadata', async () => {
- const { dispatcher, observedHeaders } = await dispatcherFor('codex', ['x-codex-turn-metadata']);
+test('ChatGPT Alpha Search receives only its declared turn metadata', async () => {
+ const { dispatcher, observedHeaders } = await dispatcherFor('chatgpt', ['x-codex-turn-metadata']);
await dispatcher({}, undefined, new Headers({
authorization: 'Bearer secret',
'x-codex-turn-metadata': '{"turn_id":"turn-1"}',
diff --git a/packages/gateway/__tests__/migrations/chatgpt-provider_test.ts b/packages/gateway/__tests__/migrations/chatgpt-provider_test.ts
new file mode 100644
index 0000000000..1d30ed1d47
--- /dev/null
+++ b/packages/gateway/__tests__/migrations/chatgpt-provider_test.ts
@@ -0,0 +1,30 @@
+import { DatabaseSync } from 'node:sqlite';
+
+import { expect, test } from 'vitest';
+
+import { migrationSqlByFilename } from '../repo/test-sqlite.ts';
+
+const migration = '0098_chatgpt_provider.sql';
+
+test('migrates ChatGPT upstreams without changing their state or other upstreams', () => {
+ const db = new DatabaseSync(':memory:');
+ for (const [filename, sql] of migrationSqlByFilename) {
+ if (filename === migration) {
+ db.exec(`
+ INSERT INTO upstreams (id, provider, name, created_at, updated_at, config_json, state_json, hue, config_version, usage_refresh_interval_minutes)
+ VALUES ('account', 'codex', 'Account', 'created', 'updated', '{"accounts":[{"chatgptAccountId":"account"}]}', '{"accounts":[{"refresh_token":"secret"}]}', 294, 7, 15),
+ ('other', 'custom', 'Other', 'created', 'updated', '{}', NULL, 42, 3, 5);
+ `);
+ }
+ db.exec(sql);
+ }
+ expect(db.prepare('SELECT id, provider, config_json, state_json, hue, config_version, usage_refresh_interval_minutes FROM upstreams ORDER BY id').all()).toEqual([
+ { id: 'account', provider: 'chatgpt', config_json: '{"accounts":[{"chatgptAccountId":"account"}]}', state_json: '{"accounts":[{"refresh_token":"secret"}]}', hue: 294, config_version: 7, usage_refresh_interval_minutes: 15 },
+ { id: 'other', provider: 'custom', config_json: '{}', state_json: null, hue: 42, config_version: 3, usage_refresh_interval_minutes: 5 },
+ ]);
+ expect(() => db.prepare("UPDATE upstreams SET provider = 'codex' WHERE id = 'account'").run()).toThrow();
+ expect(db.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('idx_upstreams_sort', 'idx_upstreams_provider_enabled_sort') ORDER BY name").all()).toEqual([
+ { name: 'idx_upstreams_provider_enabled_sort' }, { name: 'idx_upstreams_sort' },
+ ]);
+ db.close();
+});
diff --git a/packages/gateway/__tests__/repo/performance-overview-oracle_test.ts b/packages/gateway/__tests__/repo/performance-overview-oracle_test.ts
index 9363da7437..fa8860b3ec 100644
--- a/packages/gateway/__tests__/repo/performance-overview-oracle_test.ts
+++ b/packages/gateway/__tests__/repo/performance-overview-oracle_test.ts
@@ -78,7 +78,7 @@ test('Performance overview oracle counts zero-output-error rows as displayed req
[
record({
model: 'gpt-5.5-pro-2026-04-23',
- upstream: 'codex:1',
+ upstream: 'chatgpt:1',
requests: 3,
ttftSamplesOk: 0,
errorsWithOutput: 0,
@@ -131,14 +131,14 @@ test('Performance overview oracle splits rows by upstream when groupBy is upstre
const rows = aggregateSingle(
[
record({ upstream: 'copilot:1' }),
- record({ upstream: 'codex:2' }),
+ record({ upstream: 'chatgpt:2' }),
],
{ bucket: 'hour', groupBy: 'upstream', timezoneOffsetMinutes: 0 },
);
assertEquals(rows.length, 2);
const groups = rows.map(r => r.group).sort();
- assertEquals(groups, ['codex:2', 'copilot:1']);
+ assertEquals(groups, ['chatgpt:2', 'copilot:1']);
});
test('Performance overview oracle returns lower edge for overflow-bucket percentile', () => {
diff --git a/packages/gateway/__tests__/repo/sql_test.ts b/packages/gateway/__tests__/repo/sql_test.ts
index 377890248c..5abdb4367a 100644
--- a/packages/gateway/__tests__/repo/sql_test.ts
+++ b/packages/gateway/__tests__/repo/sql_test.ts
@@ -13,8 +13,8 @@ const goodAccount = { chatgptAccountId: 'aid', refresh_token: 'rt_v1', state: 'a
const GENERATION = '2026-06-05T00:00:00.000Z';
const baseRecord = (overrides: Partial = {}): StoredUpstreamRecord => ({
id: 'up_test',
- kind: 'codex',
- name: 'Codex Test',
+ kind: 'chatgpt',
+ name: 'ChatGPT Test',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 0,
diff --git a/packages/gateway/__tests__/repo/usage_test.ts b/packages/gateway/__tests__/repo/usage_test.ts
index 2409f34dd5..07ea701f43 100644
--- a/packages/gateway/__tests__/repo/usage_test.ts
+++ b/packages/gateway/__tests__/repo/usage_test.ts
@@ -23,7 +23,7 @@ const longPricing: PriceVector = { input_tokens: '0.00001', input_cache_read_tok
const record = (overrides: Partial): UsageRecord => ({
keyId: 'key-1',
model: 'gpt-5.6-sol',
- upstream: 'up_codex',
+ upstream: 'up_chatgpt',
modelKey: 'gpt-5.6-sol',
hour: '2026-07-12T00',
pricingSelector: {},
diff --git a/packages/gateway/__tests__/scheduled/upstreams_test.ts b/packages/gateway/__tests__/scheduled/upstreams_test.ts
index ef7f2dc39b..42ec54eaf9 100644
--- a/packages/gateway/__tests__/scheduled/upstreams_test.ts
+++ b/packages/gateway/__tests__/scheduled/upstreams_test.ts
@@ -22,7 +22,7 @@ const base = (id: string, kind: UpstreamRecord['kind'], config: unknown, state:
const copilot = (id = 'copilot') => base(id, 'copilot', {
githubHost: 'github.com', githubToken: 'github', user: { login: 'tester', avatar_url: '', name: null, id: 1 },
}, null);
-const codex = () => base('codex', 'codex', {
+const chatgpt = () => base('chatgpt', 'chatgpt', {
accounts: [{ email: 'test@example.com', chatgptAccountId: 'account', chatgptUserId: null, planType: 'plus' }],
}, { accounts: [{ chatgptAccountId: 'account', refresh_token: null, state: 'active', state_updated_at: new Date(NOW).toISOString(), openaiDeviceId: '11111111-2222-4333-8444-555555555555', accessToken: access, quotaSnapshot: null }] });
const claude = (setupToken = false) => base('claude', 'claude-code', {
@@ -41,7 +41,7 @@ beforeEach(async () => {
afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); });
test('Floway dispatches every provider and persists read-only subscription probes', async () => {
- for (const record of [copilot(), codex(), claude(), ollama(), base('custom', 'custom', { baseUrl: 'https://custom.example', authStyle: 'none', endpoints: {}, ingressHeadersRules: [], modelsFetch: { enabled: false }, models: [] }, null), base('azure', 'azure', { endpoint: 'https://test.openai.azure.com', apiKey: 'key', models: [{ upstreamModelId: 'model', endpoints: { openaiChatCompletions: {} } }] }, null)]) {
+ for (const record of [copilot(), chatgpt(), claude(), ollama(), base('custom', 'custom', { baseUrl: 'https://custom.example', authStyle: 'none', endpoints: {}, ingressHeadersRules: [], modelsFetch: { enabled: false }, models: [] }, null), base('azure', 'azure', { endpoint: 'https://test.openai.azure.com', apiKey: 'key', models: [{ upstreamModelId: 'model', endpoints: { openaiChatCompletions: {} } }] }, null)]) {
await repo.upstreams.insertForModels(record);
}
mocks.fetch.mockImplementation(async (url, init) => {
@@ -59,7 +59,7 @@ test('Floway dispatches every provider and persists read-only subscription probe
});
await runUpstreamScheduledTasks('TEST');
expect(mocks.fetch).toHaveBeenCalledTimes(5);
- expect((await repo.upstreams.getById('codex'))?.state).toMatchObject({ accounts: [{ quotaSnapshot: { codex: { data: { primary_used_percent: 25, primary_window_minutes: 300 } }, images: { data: { secondary_used_percent: 50, secondary_window_minutes: 10080 } } } }] });
+ expect((await repo.upstreams.getById('chatgpt'))?.state).toMatchObject({ accounts: [{ quotaSnapshot: { codex: { data: { primary_used_percent: 25, primary_window_minutes: 300 } }, images: { data: { secondary_used_percent: 50, secondary_window_minutes: 10080 } } } }] });
expect((await repo.upstreams.getById('copilot'))?.state).toMatchObject({ quotaSnapshot: { fetchedAt: NOW } });
expect((await repo.upstreams.getById('claude'))?.state).toMatchObject({ accounts: [{ usageProbeSnapshot: { fetchedAt: NOW, data: { five_hour: { utilization: 10 } } } }] });
expect((await repo.upstreams.getById('ollama'))?.state).toMatchObject({ usageProbe: { observation: { fetchedAt: NOW, data: { usage: usageBody(), balance: balanceBody() } } } });
@@ -88,13 +88,13 @@ test('a failed refresh keeps the previous usage observation', async () => {
});
test('a usage endpoint auth failure preserves inference credential health and other upstreams still run', async () => {
- await repo.upstreams.insertForModels(codex());
+ await repo.upstreams.insertForModels(chatgpt());
await repo.upstreams.insertForModels(ollama());
mocks.fetch.mockImplementation(async url => new URL(url).hostname === 'chatgpt.com'
? new Response('usage scope denied', { status: 403 })
: Response.json(new URL(url).pathname === '/api/usage' ? usageBody() : balanceBody()));
await expect(runUpstreamScheduledTasks('TEST')).rejects.toThrow();
- expect((await repo.upstreams.getById('codex'))?.state).toMatchObject({ accounts: [{ state: 'active' }] });
+ expect((await repo.upstreams.getById('chatgpt'))?.state).toMatchObject({ accounts: [{ state: 'active' }] });
expect((await repo.upstreams.getById('ollama'))?.state).toMatchObject({ usageProbe: { observation: { fetchedAt: NOW } } });
});
@@ -134,8 +134,8 @@ test('Ollama scheduled and request-time refreshes update the same usage and bala
expect(mocks.fetch).toHaveBeenCalledTimes(4);
});
-test('a fresh Codex bucket does not suppress a stale sibling bucket', async () => {
- const record = codex();
+test('a fresh ChatGPT bucket does not suppress a stale sibling bucket', async () => {
+ const record = chatgpt();
await repo.upstreams.insertForModels(record);
await repo.upstreams.saveState(record.id, current => {
const state = current as { accounts: Array> };
diff --git a/packages/gateway/__tests__/test-utils/app.ts b/packages/gateway/__tests__/test-utils/app.ts
index 59e2219357..ae00cba3ed 100644
--- a/packages/gateway/__tests__/test-utils/app.ts
+++ b/packages/gateway/__tests__/test-utils/app.ts
@@ -98,16 +98,16 @@ export const buildCopilotUpstreamRecord = (githubAccount: CopilotAccountFixture,
};
};
-export const buildCodexUpstreamRecord = (overrides: Partial = {}): UpstreamRecord => {
+export const buildChatGPTUpstreamRecord = (overrides: Partial = {}): UpstreamRecord => {
const config = {
- accounts: [{ email: 'codex@example.com', chatgptAccountId: 'acc', chatgptUserId: 'usr', planType: 'plus' }],
+ accounts: [{ email: 'chatgpt@example.com', chatgptAccountId: 'acc', chatgptUserId: 'usr', planType: 'plus' }],
};
const { config: overrideConfig, ...rest } = overrides;
return {
- id: 'up_codex',
- kind: 'codex',
- name: 'Codex',
+ id: 'up_chatgpt',
+ kind: 'chatgpt',
+ name: 'ChatGPT',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 200,
@@ -121,7 +121,7 @@ export const buildCodexUpstreamRecord = (overrides: Partial = {}
state: 'active',
state_updated_at: TEST_UPSTREAM_TIMESTAMP,
openaiDeviceId: '11111111-2222-4333-8444-555555555555',
- accessToken: { token: 'codex-access-token', expiresAt: Date.parse('2100-01-01T00:00:00.000Z'), refreshedAt: TEST_UPSTREAM_TIMESTAMP },
+ accessToken: { token: 'chatgpt-access-token', expiresAt: Date.parse('2100-01-01T00:00:00.000Z'), refreshedAt: TEST_UPSTREAM_TIMESTAMP },
quotaSnapshot: null,
}],
},
@@ -136,7 +136,7 @@ export const buildCodexUpstreamRecord = (overrides: Partial = {}
};
};
-export function codexModels(models: Array<{ slug: string; display_name?: string }>) {
+export function chatgptModels(models: Array<{ slug: string; display_name?: string }>) {
return {
models: models.map(model => ({
slug: model.slug,
diff --git a/packages/gateway/migrations/0098_chatgpt_provider.sql b/packages/gateway/migrations/0098_chatgpt_provider.sql
new file mode 100644
index 0000000000..fb8cd62cc0
--- /dev/null
+++ b/packages/gateway/migrations/0098_chatgpt_provider.sql
@@ -0,0 +1,30 @@
+CREATE TABLE upstreams_new (
+ id TEXT PRIMARY KEY,
+ provider TEXT NOT NULL CHECK (provider IN ('copilot', 'custom', 'azure', 'chatgpt', 'claude-code', 'ollama')),
+ name TEXT NOT NULL,
+ enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
+ sort_order INTEGER NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ config_json TEXT NOT NULL,
+ state_json TEXT NULL,
+ flag_overrides TEXT NOT NULL DEFAULT '[]',
+ disabled_public_model_ids TEXT NOT NULL DEFAULT '[]',
+ proxy_fallback_list_json TEXT NOT NULL DEFAULT '[]',
+ model_prefix_json TEXT NULL,
+ models_cache_json TEXT NULL,
+ hue INTEGER NOT NULL CHECK (hue >= 0 AND hue < 360),
+ config_version INTEGER NOT NULL DEFAULT 1 CHECK (typeof(config_version) = 'integer' AND config_version >= 1),
+ usage_refresh_interval_minutes INTEGER NOT NULL DEFAULT 0 CHECK (usage_refresh_interval_minutes >= 0)
+);
+
+INSERT INTO upstreams_new
+SELECT id, CASE provider WHEN 'codex' THEN 'chatgpt' ELSE provider END, name, enabled, sort_order,
+ created_at, updated_at, config_json, state_json, flag_overrides, disabled_public_model_ids,
+ proxy_fallback_list_json, model_prefix_json, models_cache_json, hue, config_version, usage_refresh_interval_minutes
+FROM upstreams;
+
+DROP TABLE upstreams;
+ALTER TABLE upstreams_new RENAME TO upstreams;
+CREATE INDEX idx_upstreams_sort ON upstreams (sort_order, created_at);
+CREATE INDEX idx_upstreams_provider_enabled_sort ON upstreams (provider, enabled, sort_order, created_at);
diff --git a/packages/gateway/package.json b/packages/gateway/package.json
index c91275a347..5c0eb5e03c 100644
--- a/packages/gateway/package.json
+++ b/packages/gateway/package.json
@@ -47,7 +47,7 @@
"@floway-dev/provider": "workspace:*",
"@floway-dev/provider-azure": "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/packages/gateway/src/control-plane/data-transfer/import-schema.ts b/packages/gateway/src/control-plane/data-transfer/import-schema.ts
index 8f6cccb9a4..ceabac84e7 100644
--- a/packages/gateway/src/control-plane/data-transfer/import-schema.ts
+++ b/packages/gateway/src/control-plane/data-transfer/import-schema.ts
@@ -19,8 +19,8 @@ import { parseUpstreamIdsValue } from '../shared/upstream-ids.ts';
import { BILLING_METRICS, canonicalizePricingSelector, type BillingMetric, parseNonNegativeDecimalString, parseUserPreferences, type PricingSelector, type UserPreferences } from '@floway-dev/protocols/common';
import { ALL_PROVIDER_KINDS, normalizeModelPrefix, normalizeUpstreamHue, normalizeUpstreamMetricIdentity, parseFlagOverridesWire, parsePerformanceOperation, type ProxyFallbackEntry, type UpstreamProviderKind, type UpstreamRecord } from '@floway-dev/provider';
import { assertAzureUpstreamRecord } from '@floway-dev/provider-azure';
+import { assertChatGPTUpstreamRecord, assertChatGPTUpstreamState } from '@floway-dev/provider-chatgpt';
import { assertClaudeCodeUpstreamRecord, assertClaudeCodeUpstreamState } from '@floway-dev/provider-claude-code';
-import { assertCodexUpstreamRecord, assertCodexUpstreamState } from '@floway-dev/provider-codex';
import { parseCopilotUpstreamConfig } from '@floway-dev/provider-copilot';
import { assertCustomUpstreamRecord } from '@floway-dev/provider-custom';
import { assertOllamaUpstreamRecord } from '@floway-dev/provider-ollama';
@@ -120,8 +120,8 @@ const normalizeUpstreamConfig = (record: UpstreamRecord): unknown => {
case 'custom': return assertCustomUpstreamRecord(record).config;
case 'azure': return assertAzureUpstreamRecord(record).config;
case 'ollama': return assertOllamaUpstreamRecord(record).config;
- case 'codex':
- assertCodexUpstreamRecord(record);
+ case 'chatgpt':
+ assertChatGPTUpstreamRecord(record);
return record.config;
case 'claude-code':
assertClaudeCodeUpstreamRecord(record);
@@ -130,13 +130,13 @@ const normalizeUpstreamConfig = (record: UpstreamRecord): unknown => {
}
};
-// Codex and Claude Code state contains refresh credentials and health that
+// ChatGPT and Claude Code state contains refresh credentials and health that
// cannot be re-derived, so it round-trips through their strict runtime
// assertions. Every other provider owns no durable state or can re-mint it.
const normalizeUpstreamState = (kind: UpstreamProviderKind, value: unknown): unknown => {
- if (kind !== 'codex' && kind !== 'claude-code') return null;
+ if (kind !== 'chatgpt' && kind !== 'claude-code') return null;
if (value === null || value === undefined) throw new Error(`${kind} upstream is missing state — re-export with current code`);
- if (kind === 'codex') assertCodexUpstreamState(value);
+ if (kind === 'chatgpt') assertChatGPTUpstreamState(value);
else assertClaudeCodeUpstreamState(value);
return value;
};
diff --git a/packages/gateway/src/control-plane/routes.ts b/packages/gateway/src/control-plane/routes.ts
index 4c0a276ba8..9f0fe6c71a 100644
--- a/packages/gateway/src/control-plane/routes.ts
+++ b/packages/gateway/src/control-plane/routes.ts
@@ -11,15 +11,15 @@ import { createAlias, deleteAlias, listAliases, updateAlias } from './model-alia
import { controlPlaneModels } from './models/routes.ts';
import { performanceOverview } from './performance/routes.ts';
import { createProxy, deleteProxy, listAllBackoffs, listProxies, listProxyBackoffs, listProxyOptions, resetProxyBackoffs, testProxy, updateProxy } from './proxies/routes.ts';
-import { accessRecordBody, upstreamMetricsQuery, upstreamOptionsQuery, authLoginBody, changeOwnPasswordBody, updateOwnPreferencesBody, claudeCodeOAuthAuthorizeUrlBody, claudeCodeOAuthExchangeBody, claudeCodeOAuthRefreshBody, claudeCodeProbeBody, claudeCodeSetupTokenAuthorizeUrlBody, claudeCodeSetupTokenExchangeBody, codexImportExchangeBody, codexImportPreviewBody, codexOAuthAuthorizeUrlBody, codexOAuthRefreshBody, codexRateLimitResetConsumeBody, codexRateLimitResetCreditsBody, copilotOAuthDeviceLoginPollBody, copilotOAuthDeviceLoginStartBody, copilotQuotaBody, createAliasBody, createGroupBody, createKeyBody, createProxyBody, createUpstreamBody, createUserBody, exportQuery, importBody, modelsQuery, ollamaUsageBody, performanceQuery, previewModelsBody, proxyOptionsQuery, resetBackoffBody, reorderAccessRecordsBody, reorderUpstreamsBody, resolveAccessBody, rotateKeyBody, siteSettingsBody, testProxyBody, tokenUsageOverviewQuery, tokenUsageQuery, updateAliasBody, updateGroupBody, updateKeyBody, updateProxyBody, updateUpstreamBody, updateUserBody, webSearchConfigSchema, webSearchUsageQuery } from './schemas.ts';
+import { accessRecordBody, upstreamMetricsQuery, upstreamOptionsQuery, authLoginBody, changeOwnPasswordBody, updateOwnPreferencesBody, claudeCodeOAuthAuthorizeUrlBody, claudeCodeOAuthExchangeBody, claudeCodeOAuthRefreshBody, claudeCodeProbeBody, claudeCodeSetupTokenAuthorizeUrlBody, claudeCodeSetupTokenExchangeBody, chatgptImportExchangeBody, chatgptImportPreviewBody, chatgptOAuthAuthorizeUrlBody, chatgptOAuthRefreshBody, chatgptRateLimitResetConsumeBody, chatgptRateLimitResetCreditsBody, copilotOAuthDeviceLoginPollBody, copilotOAuthDeviceLoginStartBody, copilotQuotaBody, createAliasBody, createGroupBody, createKeyBody, createProxyBody, createUpstreamBody, createUserBody, exportQuery, importBody, modelsQuery, ollamaUsageBody, performanceQuery, previewModelsBody, proxyOptionsQuery, resetBackoffBody, reorderAccessRecordsBody, reorderUpstreamsBody, resolveAccessBody, rotateKeyBody, siteSettingsBody, testProxyBody, tokenUsageOverviewQuery, tokenUsageQuery, updateAliasBody, updateGroupBody, updateKeyBody, updateProxyBody, updateUpstreamBody, updateUserBody, webSearchConfigSchema, webSearchUsageQuery } from './schemas.ts';
import { getWebSearchConfigRoute, putWebSearchConfigRoute, testWebSearchConfigRoute } from './search-config/routes.ts';
import { webSearchUsage } from './search-usage/routes.ts';
import { getSiteSettings, putSiteSettings } from './site-settings/routes.ts';
import { tokenUsageOverview } from './token-usage/overview.ts';
import { tokenUsage } from './token-usage/routes.ts';
import { upstreamMetrics } from './upstream-metrics/routes.ts';
+import { chatgptImportExchange, chatgptImportPreview, chatgptOAuthAuthorizeUrl, chatgptOAuthRefresh, chatgptRateLimitResetConsume, chatgptRateLimitResetCredits } from './upstreams/chatgpt.ts';
import { claudeCodeOAuthAuthorizeUrl, claudeCodeOAuthExchange, claudeCodeOAuthRefresh, claudeCodeProbe, claudeCodeSetupTokenAuthorizeUrl, claudeCodeSetupTokenExchange } from './upstreams/claude-code.ts';
-import { codexImportExchange, codexImportPreview, codexOAuthAuthorizeUrl, codexOAuthRefresh, codexRateLimitResetConsume, codexRateLimitResetCredits } from './upstreams/codex.ts';
import { copilotOAuthDeviceLoginPoll, copilotOAuthDeviceLoginStart, copilotQuota } from './upstreams/copilot.ts';
import { fetchSavedModels, previewModels } from './upstreams/models.ts';
import { ollamaUsage } from './upstreams/ollama.ts';
@@ -72,13 +72,13 @@ export const controlPlaneRoutes = new Hono<{ Variables: AuthVars }>()
.post('/api/upstreams/copilot/oauth/device-login/start', zValidator('json', copilotOAuthDeviceLoginStartBody), copilotOAuthDeviceLoginStart)
.post('/api/upstreams/copilot/oauth/device-login/poll', zValidator('json', copilotOAuthDeviceLoginPollBody), copilotOAuthDeviceLoginPoll)
.post('/api/upstreams/copilot/quota', zValidator('json', copilotQuotaBody), copilotQuota)
- .post('/api/upstreams/codex/import/preview', zValidator('json', codexImportPreviewBody), codexImportPreview)
+ .post('/api/upstreams/chatgpt/import/preview', zValidator('json', chatgptImportPreviewBody), chatgptImportPreview)
.post('/api/upstreams/preview-models', zValidator('json', previewModelsBody), previewModels)
- .post('/api/upstreams/codex/import/exchange', zValidator('json', codexImportExchangeBody), codexImportExchange)
- .post('/api/upstreams/codex/oauth/authorize-url', zValidator('json', codexOAuthAuthorizeUrlBody), codexOAuthAuthorizeUrl)
- .post('/api/upstreams/codex/oauth/refresh', zValidator('json', codexOAuthRefreshBody), codexOAuthRefresh)
- .post('/api/upstreams/codex/reset-credits', zValidator('json', codexRateLimitResetCreditsBody), codexRateLimitResetCredits)
- .post('/api/upstreams/codex/reset-credits/consume', zValidator('json', codexRateLimitResetConsumeBody), codexRateLimitResetConsume)
+ .post('/api/upstreams/chatgpt/import/exchange', zValidator('json', chatgptImportExchangeBody), chatgptImportExchange)
+ .post('/api/upstreams/chatgpt/oauth/authorize-url', zValidator('json', chatgptOAuthAuthorizeUrlBody), chatgptOAuthAuthorizeUrl)
+ .post('/api/upstreams/chatgpt/oauth/refresh', zValidator('json', chatgptOAuthRefreshBody), chatgptOAuthRefresh)
+ .post('/api/upstreams/chatgpt/reset-credits', zValidator('json', chatgptRateLimitResetCreditsBody), chatgptRateLimitResetCredits)
+ .post('/api/upstreams/chatgpt/reset-credits/consume', zValidator('json', chatgptRateLimitResetConsumeBody), chatgptRateLimitResetConsume)
.post('/api/upstreams/claude-code/oauth/authorize-url', zValidator('json', claudeCodeOAuthAuthorizeUrlBody), claudeCodeOAuthAuthorizeUrl)
.post('/api/upstreams/claude-code/oauth/exchange', zValidator('json', claudeCodeOAuthExchangeBody), claudeCodeOAuthExchange)
.post('/api/upstreams/claude-code/oauth/refresh', zValidator('json', claudeCodeOAuthRefreshBody), claudeCodeOAuthRefresh)
diff --git a/packages/gateway/src/control-plane/schemas.ts b/packages/gateway/src/control-plane/schemas.ts
index f21e84f031..56abea12de 100644
--- a/packages/gateway/src/control-plane/schemas.ts
+++ b/packages/gateway/src/control-plane/schemas.ts
@@ -402,13 +402,13 @@ const upstreamBaseFields = {
export const createUpstreamBody = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('custom'), ...upstreamBaseFields, config: customConfigSchema }),
z.object({ kind: z.literal('azure'), ...upstreamBaseFields, config: azureConfigSchema }),
- // Copilot / Codex / Claude Code carry OAuth-derived credentials that the
+ // Copilot / ChatGPT / Claude Code carry OAuth-derived credentials that the
// create page populates via the corresponding OAuth-exchange helper
// before Save. `state` is accepted here as an opaque payload; the
// per-kind assertXxxUpstreamRecord in the handler narrows it once the
// request lands.
z.object({ kind: z.literal('copilot'), ...upstreamBaseFields, config: z.unknown(), state: z.unknown().optional() }),
- z.object({ kind: z.literal('codex'), ...upstreamBaseFields, config: z.unknown(), state: z.unknown().optional() }),
+ z.object({ kind: z.literal('chatgpt'), ...upstreamBaseFields, config: z.unknown(), state: z.unknown().optional() }),
z.object({ kind: z.literal('claude-code'), ...upstreamBaseFields, config: z.unknown(), state: z.unknown().optional() }),
z.object({ kind: z.literal('ollama'), ...upstreamBaseFields, config: ollamaConfigSchema }),
]);
@@ -423,7 +423,7 @@ export const createUpstreamBody = z.discriminatedUnion('kind', [
// without this field the schema would silently strip it and the API would
// look like it had accepted the change.
export const updateUpstreamBody = z.object({
- kind: z.enum(['custom', 'azure', 'copilot', 'codex', 'claude-code', 'ollama']).optional(),
+ kind: z.enum(['custom', 'azure', 'copilot', 'chatgpt', 'claude-code', 'ollama']).optional(),
name: z.string().min(1).optional(),
enabled: z.boolean().optional(),
usage_refresh_interval_minutes: z.number().int().nonnegative().optional(),
@@ -435,7 +435,7 @@ export const updateUpstreamBody = z.object({
hue: upstreamHueSchema.optional(),
// Patches only carry field diffs, not per-kind shape validation — the
// handler dispatches on the existing row's kind and enforces the shape
- // there (Copilot/Codex/Claude Code reject a config patch outright, since
+ // there (Copilot/ChatGPT/Claude Code reject a config patch outright, since
// OAuth-managed slices belong to the action endpoints; the rest run
// through `assertXxxUpstreamRecord`). `z.record(z.unknown())` blocks
// primitives / arrays / null from reaching the handler as `config`.
@@ -467,7 +467,7 @@ export const upstreamRecordEnvelope = z.object({
const recordOnlyBody = z.object({ record: upstreamRecordEnvelope });
-// Shared authorize-url contract for the codex and claude-code authorize-url
+// Shared authorize-url contract for the ChatGPT and claude-code authorize-url
// endpoints: the draft record plus the SPA-held PKCE challenge/state pair.
const oauthAuthorizeUrlBody = z.object({ record: upstreamRecordEnvelope, challenge: z.string().min(1), state: z.string().min(1) });
@@ -480,7 +480,7 @@ export const copilotOAuthDeviceLoginPollBody = z.object({
export const copilotQuotaBody = recordOnlyBody;
-// --- codex OAuth (record-body contract) ---
+// --- ChatGPT OAuth (record-body contract) ---
//
// PKCE state is fully SPA-held: the dashboard mints `{verifier, challenge,
// state}` in the browser via Web Crypto, stores `{verifier, state}` in
@@ -488,15 +488,15 @@ export const copilotQuotaBody = recordOnlyBody;
// them into the upstream's authorize URL. The server never sees the
// verifier until the callback comes back as `{code, verifier}` on exchange.
-export const codexOAuthAuthorizeUrlBody = oauthAuthorizeUrlBody;
+export const chatgptOAuthAuthorizeUrlBody = oauthAuthorizeUrlBody;
// Preview takes no record: it reads a pasted document and reports what is in
// it, without touching any upstream.
-export const codexImportPreviewBody = z.object({
+export const chatgptImportPreviewBody = z.object({
raw_json: z.string().min(1),
});
-export const codexImportExchangeBody = z.object({
+export const chatgptImportExchangeBody = z.object({
record: upstreamRecordEnvelope,
json: z.object({
raw_json: z.string().min(1),
@@ -523,11 +523,11 @@ export const codexImportExchangeBody = z.object({
{ message: 'Provide exactly one of json, callback, or manual' },
);
-export const codexOAuthRefreshBody = recordOnlyBody;
+export const chatgptOAuthRefreshBody = recordOnlyBody;
-export const codexRateLimitResetCreditsBody = recordOnlyBody;
+export const chatgptRateLimitResetCreditsBody = recordOnlyBody;
-export const codexRateLimitResetConsumeBody = z.object({
+export const chatgptRateLimitResetConsumeBody = z.object({
record: upstreamRecordEnvelope,
credit_id: z.string().min(1),
idempotency_key: z.string().min(1),
diff --git a/packages/gateway/src/control-plane/upstreams/codex.ts b/packages/gateway/src/control-plane/upstreams/chatgpt.ts
similarity index 61%
rename from packages/gateway/src/control-plane/upstreams/codex.ts
rename to packages/gateway/src/control-plane/upstreams/chatgpt.ts
index ab4e632547..7866e6304f 100644
--- a/packages/gateway/src/control-plane/upstreams/codex.ts
+++ b/packages/gateway/src/control-plane/upstreams/chatgpt.ts
@@ -5,81 +5,81 @@ import { userFromContext } from '../../middleware/auth.ts';
import type { CtxWithJson } from '../../middleware/zod-validator.ts';
import { getRepo } from '../../repo/index.ts';
import { getRuntimeLocation } from '../../runtime/runtime-info.ts';
-import type { codexImportExchangeBody, codexImportPreviewBody, codexOAuthAuthorizeUrlBody, codexOAuthRefreshBody, codexRateLimitResetConsumeBody, codexRateLimitResetCreditsBody } from '../schemas.ts';
+import type { chatgptImportExchangeBody, chatgptImportPreviewBody, chatgptOAuthAuthorizeUrlBody, chatgptOAuthRefreshBody, chatgptRateLimitResetConsumeBody, chatgptRateLimitResetCreditsBody } from '../schemas.ts';
import { saveUpstream } from '../shared/save-upstreams.ts';
import type { Fetcher, UpstreamRecord } from '@floway-dev/provider';
import {
- buildCodexAuthorizeUrl,
- CodexRateLimitResetRequestError,
- assertCodexUpstreamRecord,
- clearCodexQuota,
- consumeCodexRateLimitResetCredit,
- fetchCodexRateLimitResetCredits,
- invalidateCodexAccessToken,
- type CodexUpstreamConfig,
- type CodexUpstreamState,
- CodexAccessOnlyCredentialError,
- CodexOAuthSessionTerminatedError,
- assertCodexUpstreamState,
- ensureCodexAccessToken,
- importCodexFromCallback,
- importCodexFromJson,
- importCodexFromManual,
- mintCodexAccessToken,
- persistCodexRefreshFailure,
- persistCodexRefreshTokenRotation,
- previewCodexJson,
-} from '@floway-dev/provider-codex';
+ buildChatGPTAuthorizeUrl,
+ ChatGPTRateLimitResetRequestError,
+ assertChatGPTUpstreamRecord,
+ clearChatGPTQuota,
+ consumeChatGPTRateLimitResetCredit,
+ fetchChatGPTRateLimitResetCredits,
+ invalidateChatGPTAccessToken,
+ type ChatGPTUpstreamConfig,
+ type ChatGPTUpstreamState,
+ ChatGPTAccessOnlyCredentialError,
+ ChatGPTOAuthSessionTerminatedError,
+ assertChatGPTUpstreamState,
+ ensureChatGPTAccessToken,
+ importChatGPTFromCallback,
+ importChatGPTFromJson,
+ importChatGPTFromManual,
+ mintChatGPTAccessToken,
+ persistChatGPTRefreshFailure,
+ persistChatGPTRefreshTokenRotation,
+ previewChatGPTJson,
+} from '@floway-dev/provider-chatgpt';
-class CodexActionError extends Error {
+class ChatGPTActionError extends Error {
constructor(readonly status: 400 | 404 | 502, message: string, options?: ErrorOptions) {
super(message, options);
- this.name = 'CodexActionError';
+ this.name = 'ChatGPTActionError';
}
}
-const ensureCodexControlPlaneAccessToken = async (opts: {
+const ensureChatGPTControlPlaneAccessToken = async (opts: {
accountId: string | null;
fetcher: Fetcher;
force?: boolean;
upstreamId: string;
}): Promise => {
const persistRotation = (newRefreshToken: string): Promise =>
- persistCodexRefreshTokenRotation(opts.upstreamId, opts.accountId, newRefreshToken);
+ persistChatGPTRefreshTokenRotation(opts.upstreamId, opts.accountId, newRefreshToken);
try {
- const access = await ensureCodexAccessToken(
+ const access = await ensureChatGPTAccessToken(
opts.upstreamId,
opts.accountId,
- refreshToken => mintCodexAccessToken(refreshToken, opts.fetcher, persistRotation),
+ refreshToken => mintChatGPTAccessToken(refreshToken, opts.fetcher, persistRotation),
opts.force,
);
return access.token;
} catch (cause) {
- if (cause instanceof CodexOAuthSessionTerminatedError) {
- await persistCodexRefreshFailure(opts.upstreamId, opts.accountId, cause.upstreamMessage);
- throw new CodexActionError(400, `Codex refresh failed: ${cause.upstreamMessage}. Re-run OAuth exchange to recover.`, { cause });
+ if (cause instanceof ChatGPTOAuthSessionTerminatedError) {
+ await persistChatGPTRefreshFailure(opts.upstreamId, opts.accountId, cause.upstreamMessage);
+ throw new ChatGPTActionError(400, `ChatGPT refresh failed: ${cause.upstreamMessage}. Re-run OAuth exchange to recover.`, { cause });
}
- throw new CodexActionError(502, errorMessage(cause), { cause });
+ throw new ChatGPTActionError(502, errorMessage(cause), { cause });
}
};
-const codexActionAccess = async (opts: {
+const chatgptActionAccess = async (opts: {
record: { id: string; kind: string; proxy_fallback_list?: UpstreamRecord['proxyFallbackList'] };
request: Request;
}): Promise<{ accessToken: string; accountId: string | null; fetcher: Fetcher }> => {
- if (opts.record.kind !== 'codex') throw new CodexActionError(400, 'Upstream is not a Codex upstream');
- if (opts.record.id === '') throw new CodexActionError(400, 'Codex reset credits require a persisted upstream');
+ if (opts.record.kind !== 'chatgpt') throw new ChatGPTActionError(400, 'Upstream is not a ChatGPT upstream');
+ if (opts.record.id === '') throw new ChatGPTActionError(400, 'ChatGPT reset credits require a persisted upstream');
const stored = await getRepo().upstreams.getById(opts.record.id);
- if (!stored) throw new CodexActionError(404, 'Upstream not found');
- if (stored.kind !== 'codex') throw new CodexActionError(400, 'Upstream is not a Codex upstream');
- assertCodexUpstreamRecord(stored);
- assertCodexUpstreamState(stored.state);
+ if (!stored) throw new ChatGPTActionError(404, 'Upstream not found');
+ if (stored.kind !== 'chatgpt') throw new ChatGPTActionError(400, 'Upstream is not a ChatGPT upstream');
+ assertChatGPTUpstreamRecord(stored);
+ assertChatGPTUpstreamState(stored.state);
const identity = stored.config.accounts[0];
const account = stored.state.accounts.find(entry => entry.chatgptAccountId === identity.chatgptAccountId);
- if (!account) throw new CodexActionError(400, 'Configured Codex account is missing from stored state');
+ if (!account) throw new ChatGPTActionError(400, 'Configured ChatGPT account is missing from stored state');
if (account.state !== 'active') {
- throw new CodexActionError(400, `Codex upstream is ${account.state}; re-run OAuth exchange to recover`);
+ throw new ChatGPTActionError(400, `ChatGPT upstream is ${account.state}; re-run OAuth exchange to recover`);
}
let fetcher: Fetcher;
@@ -90,10 +90,10 @@ const codexActionAccess = async (opts: {
runtimeLocation: getRuntimeLocation(opts.request),
});
} catch (cause) {
- throw new CodexActionError(400, errorMessage(cause), { cause });
+ throw new ChatGPTActionError(400, errorMessage(cause), { cause });
}
- const accessToken = await ensureCodexControlPlaneAccessToken({
+ const accessToken = await ensureChatGPTControlPlaneAccessToken({
upstreamId: opts.record.id,
accountId: identity.chatgptAccountId,
fetcher,
@@ -101,22 +101,22 @@ const codexActionAccess = async (opts: {
return { accessToken, accountId: identity.chatgptAccountId, fetcher };
};
-type CodexActionAccess = Awaited>;
+type ChatGPTActionAccess = Awaited>;
-const withCodexResetAccess = async (opts: {
- access: CodexActionAccess;
+const withChatGPTResetAccess = async (opts: {
+ access: ChatGPTActionAccess;
operation: (accessToken: string) => Promise;
upstreamId: string;
}): Promise => {
try {
return await opts.operation(opts.access.accessToken);
} catch (error) {
- if (!(error instanceof CodexRateLimitResetRequestError) || error.status !== 401) throw error;
+ if (!(error instanceof ChatGPTRateLimitResetRequestError) || error.status !== 401) throw error;
// A definite 401 means the consume did not run, so retrying it with the
// caller's same idempotency key is safe. Network failures remain ambiguous
// and deliberately bypass this branch.
- const retained = await invalidateCodexAccessToken(opts.upstreamId, opts.access.accountId, opts.access.accessToken);
- opts.access.accessToken = retained?.token ?? await ensureCodexControlPlaneAccessToken({
+ const retained = await invalidateChatGPTAccessToken(opts.upstreamId, opts.access.accountId, opts.access.accessToken);
+ opts.access.accessToken = retained?.token ?? await ensureChatGPTControlPlaneAccessToken({
upstreamId: opts.upstreamId,
accountId: opts.access.accountId,
fetcher: opts.access.fetcher,
@@ -126,47 +126,47 @@ const withCodexResetAccess = async (opts: {
};
const actionFailure = (error: unknown): { status: 400 | 404 | 502; message: string } =>
- error instanceof CodexActionError
+ error instanceof ChatGPTActionError
? { status: error.status, message: error.message }
: { status: 502, message: errorMessage(error) };
-// Codex credential import under the unified record-body contract. Create and
+// ChatGPT credential import under the unified record-body contract. Create and
// edit share one endpoint each: the caller posts the draft record; when
// `record.id !== ''` the produced patch is targeted-persisted, otherwise it is
// only returned for the front-end to merge into its draft.
-export const codexOAuthAuthorizeUrl = async (c: CtxWithJson) => {
+export const chatgptOAuthAuthorizeUrl = async (c: CtxWithJson) => {
const { record, challenge, state } = c.req.valid('json');
const accessError = await fullUpstreamActionError(userFromContext(c), record.id);
if (accessError) return c.json({ error: accessError.error }, accessError.status);
- return c.json({ authorize_url: buildCodexAuthorizeUrl({ state, codeChallenge: challenge }) });
+ return c.json({ authorize_url: buildChatGPTAuthorizeUrl({ state, codeChallenge: challenge }) });
};
// Reads a pasted document and reports the accounts in it so the operator can
// pick one. Takes no record and persists nothing — it is the step before any
// upstream is involved, which is also why its response carries identity and
// lifecycle only and never credential material.
-export const codexImportPreview = async (c: CtxWithJson) => {
+export const chatgptImportPreview = async (c: CtxWithJson) => {
const { raw_json: rawJson } = c.req.valid('json');
try {
- return c.json({ candidates: await previewCodexJson(rawJson) });
+ return c.json({ candidates: await previewChatGPTJson(rawJson) });
} catch (err) {
return c.json({ error: errorMessage(err) }, 400);
}
};
-export const codexImportExchange = async (c: CtxWithJson) => {
+export const chatgptImportExchange = async (c: CtxWithJson) => {
const body = c.req.valid('json');
const { record } = body;
const accessError = await fullUpstreamActionError(userFromContext(c), record.id);
if (accessError) return c.json({ error: accessError.error }, accessError.status);
- if (record.kind !== 'codex') return c.json({ error: 'Upstream is not a Codex upstream' }, 400);
+ if (record.kind !== 'chatgpt') return c.json({ error: 'Upstream is not a ChatGPT upstream' }, 400);
- let ingestion: { config: CodexUpstreamConfig; state: CodexUpstreamState };
+ let ingestion: { config: ChatGPTUpstreamConfig; state: ChatGPTUpstreamState };
try {
if (body.json !== undefined) {
- ingestion = await importCodexFromJson(body.json.raw_json, body.json.source_index);
+ ingestion = await importChatGPTFromJson(body.json.raw_json, body.json.source_index);
} else if (body.manual !== undefined) {
- ingestion = await importCodexFromManual(body.manual);
+ ingestion = await importChatGPTFromManual(body.manual);
} else {
// The callback is the only source that talks to auth.openai.com, so it
// is also the only one that needs the upstream's egress chain resolved.
@@ -176,7 +176,7 @@ export const codexImportExchange = async (c: CtxWithJson) => {
+export const chatgptOAuthRefresh = async (c: CtxWithJson) => {
const { record } = c.req.valid('json');
const accessError = await fullUpstreamActionError(userFromContext(c), record.id);
if (accessError) return c.json({ error: accessError.error }, accessError.status);
- if (record.kind !== 'codex') return c.json({ error: 'Upstream is not a Codex upstream' }, 400);
+ if (record.kind !== 'chatgpt') return c.json({ error: 'Upstream is not a ChatGPT upstream' }, 400);
// Refresh is a stateful action on a persisted row — it delegates to
- // `ensureCodexAccessToken` which reads state from DB, mints, and
+ // `ensureChatGPTAccessToken` which reads state from DB, mints, and
// CAS-writes back with sibling-rotation recovery. Create-state refresh
// has no target: the just-completed OAuth exchange handed the client a
// brand-new refresh_token that has no reason to rotate yet, and the
// front-end does not surface the button until Save lands the row.
if (record.id === '') return c.json({ error: 'refresh requires a persisted upstream' }, 400);
- assertCodexUpstreamState(record.state);
+ assertChatGPTUpstreamState(record.state);
const account = record.state.accounts[0];
if (account.state !== 'active') {
- return c.json({ error: `Codex upstream is ${account.state}; re-run OAuth exchange to recover` }, 400);
+ return c.json({ error: `ChatGPT upstream is ${account.state}; re-run OAuth exchange to recover` }, 400);
}
let fetcher: Fetcher;
@@ -240,22 +240,22 @@ export const codexOAuthRefresh = async (c: CtxWithJson => {
- await persistCodexRefreshTokenRotation(record.id, account.chatgptAccountId, newRefreshToken);
+ await persistChatGPTRefreshTokenRotation(record.id, account.chatgptAccountId, newRefreshToken);
};
try {
- await ensureCodexAccessToken(record.id, account.chatgptAccountId,
- refreshToken => mintCodexAccessToken(refreshToken, fetcher, persistRefreshTokenRotation),
+ await ensureChatGPTAccessToken(record.id, account.chatgptAccountId,
+ refreshToken => mintChatGPTAccessToken(refreshToken, fetcher, persistRefreshTokenRotation),
true);
} catch (err) {
- if (err instanceof CodexOAuthSessionTerminatedError) {
+ if (err instanceof ChatGPTOAuthSessionTerminatedError) {
// Terminal flip delegates to the provider-owned helper: clear the cached
// access token, mark the account refresh_failed so the dashboard renders
// the red badge and prompts a re-import.
- await persistCodexRefreshFailure(record.id, account.chatgptAccountId, err.upstreamMessage);
- return c.json({ error: `Codex refresh failed: ${err.upstreamMessage}. Re-run OAuth exchange to recover.` }, 400);
+ await persistChatGPTRefreshFailure(record.id, account.chatgptAccountId, err.upstreamMessage);
+ return c.json({ error: `ChatGPT refresh failed: ${err.upstreamMessage}. Re-run OAuth exchange to recover.` }, 400);
}
- if (err instanceof CodexAccessOnlyCredentialError) {
+ if (err instanceof ChatGPTAccessOnlyCredentialError) {
// An access-only credential has nothing to refresh from; surface the
// provider's re-import instruction verbatim.
return c.json({ error: err.message }, 400);
@@ -268,16 +268,16 @@ export const codexOAuthRefresh = async (c: CtxWithJson) => {
+export const chatgptRateLimitResetCredits = async (c: CtxWithJson) => {
const { record } = c.req.valid('json');
const accessError = await fullUpstreamActionError(userFromContext(c), record.id);
if (accessError) return c.json({ error: accessError.error }, accessError.status);
try {
- const access = await codexActionAccess({ record, request: c.req.raw });
- const resetCredits = await withCodexResetAccess({
+ const access = await chatgptActionAccess({ record, request: c.req.raw });
+ const resetCredits = await withChatGPTResetAccess({
upstreamId: record.id,
access,
- operation: accessToken => fetchCodexRateLimitResetCredits({
+ operation: accessToken => fetchChatGPTRateLimitResetCredits({
accessToken,
accountId: access.accountId,
fetcher: access.fetcher,
@@ -291,13 +291,13 @@ export const codexRateLimitResetCredits = async (c: CtxWithJson) => {
+export const chatgptRateLimitResetConsume = async (c: CtxWithJson) => {
const { record, credit_id: creditId, idempotency_key: idempotencyKey } = c.req.valid('json');
const accessError = await fullUpstreamActionError(userFromContext(c), record.id);
if (accessError) return c.json({ error: accessError.error }, accessError.status);
- let access: Awaited>;
+ let access: Awaited>;
try {
- access = await codexActionAccess({ record, request: c.req.raw });
+ access = await chatgptActionAccess({ record, request: c.req.raw });
} catch (error) {
const failure = actionFailure(error);
return c.json({ error: failure.message }, failure.status);
@@ -305,10 +305,10 @@ export const codexRateLimitResetConsume = async (c: CtxWithJson consumeCodexRateLimitResetCredit({
+ operation: accessToken => consumeChatGPTRateLimitResetCredit({
accessToken,
accountId: access.accountId,
creditId,
@@ -329,16 +329,16 @@ export const codexRateLimitResetConsume = async (c: CtxWithJson fetchCodexRateLimitResetCredits({
+ operation: accessToken => fetchChatGPTRateLimitResetCredits({
accessToken,
accountId: access.accountId,
fetcher: access.fetcher,
diff --git a/packages/gateway/src/control-plane/upstreams/routes.ts b/packages/gateway/src/control-plane/upstreams/routes.ts
index 4f2e85d910..7ecbf3c927 100644
--- a/packages/gateway/src/control-plane/upstreams/routes.ts
+++ b/packages/gateway/src/control-plane/upstreams/routes.ts
@@ -29,27 +29,27 @@ import {
type UpstreamRecord,
} from '@floway-dev/provider';
import { assertAzureUpstreamRecord } from '@floway-dev/provider-azure';
+import { type ChatGPTQuotaSnapshotMap, type ChatGPTUpstreamConfig, assertChatGPTUpstreamRecord, assertChatGPTUpstreamState, getChatGPTQuota, patchChatGPTIdentityMetadata } from '@floway-dev/provider-chatgpt';
import { assertClaudeCodeUpstreamRecord, readClaudeCodeUpstreamState } from '@floway-dev/provider-claude-code';
-import { type CodexQuotaSnapshotMap, type CodexUpstreamConfig, assertCodexUpstreamRecord, assertCodexUpstreamState, getCodexQuota, patchCodexIdentityMetadata } from '@floway-dev/provider-codex';
import { parseCopilotUpstreamConfig, readCopilotUpstreamState } from '@floway-dev/provider-copilot';
import { assertCustomUpstreamRecord } from '@floway-dev/provider-custom';
import { assertOllamaUpstreamRecord } from '@floway-dev/provider-ollama';
-type CodexQuotaProjection = { codex_quota?: CodexQuotaSnapshotMap | null };
+type ChatGPTQuotaProjection = { chatgpt_quota?: ChatGPTQuotaSnapshotMap | null };
type SerializedUpstreamRecord = FullSerializedUpstreamRecord | RedactedSerializedUpstreamRecord;
-type UpstreamResponse = SerializedUpstreamRecord & CodexQuotaProjection;
+type UpstreamResponse = SerializedUpstreamRecord & ChatGPTQuotaProjection;
type UpstreamWithCacheResponse = UpstreamResponse & {
modelsCache: ModelsCacheStatus;
};
-const codexQuotaForResponse = async (record: UpstreamRecord): Promise => {
- if (record.kind !== 'codex') return {};
- assertCodexUpstreamRecord(record);
+const chatgptQuotaForResponse = async (record: UpstreamRecord): Promise => {
+ if (record.kind !== 'chatgpt') return {};
+ assertChatGPTUpstreamRecord(record);
return {
- codex_quota: await getCodexQuota(record.id, record.config.accounts[0].chatgptAccountId),
+ chatgpt_quota: await getChatGPTQuota(record.id, record.config.accounts[0].chatgptAccountId),
};
};
@@ -67,7 +67,7 @@ const pruneDeletedProxyEntries = (
): ProxyFallbackEntry[] => entries.filter(entry => isDirectFallbackId(entry.id) || knownProxyIds.has(entry.id));
// Response-only projections, which serialize.ts excludes so it stays a pure
-// persisted-record transform: the Codex quota because it needs provider I/O,
+// persisted-record transform: the ChatGPT quota because it needs provider I/O,
// the models-cache freshness because a dump must not carry the catalog. The
// optional baseSerialize override lets callers swap in upstreamRecordToFullJson
// to round-trip unredacted secrets instead of the redacted default.
@@ -77,13 +77,13 @@ const serializeForResponse = async (
baseSerialize: (r: UpstreamRecord) => SerializedUpstreamRecord = upstreamRecordToJson,
includeQuota = true,
): Promise => {
- const codexQuota = includeQuota ? await codexQuotaForResponse(record) : {};
+ const chatgptQuota = includeQuota ? await chatgptQuotaForResponse(record) : {};
const serialized = baseSerialize(record);
return {
...serialized,
proxy_fallback_list: pruneDeletedProxyEntries(serialized.proxy_fallback_list, knownProxyIds),
modelsCache: modelsCacheStatus(record),
- ...codexQuota,
+ ...chatgptQuota,
};
};
@@ -133,8 +133,8 @@ const normalizeConfig = (record: UpstreamRecord): ValidationResult => {
if (record.kind === 'custom') return { ok: true, value: assertCustomUpstreamRecord(record).config };
if (record.kind === 'azure') return { ok: true, value: assertAzureUpstreamRecord(record).config };
if (record.kind === 'ollama') return { ok: true, value: assertOllamaUpstreamRecord(record).config };
- if (record.kind === 'codex') {
- assertCodexUpstreamRecord(record);
+ if (record.kind === 'chatgpt') {
+ assertChatGPTUpstreamRecord(record);
return { ok: true, value: record.config };
}
if (record.kind === 'claude-code') {
@@ -336,14 +336,14 @@ export const createUpstream = async (c: CtxWithJson)
const modelPrefix = modelPrefixResult.value;
const now = new Date().toISOString();
- // Copilot / Codex / Claude Code carry OAuth-derived server-owned fields
+ // Copilot / ChatGPT / Claude Code carry OAuth-derived server-owned fields
// (config.githubToken + config.user for Copilot; config.accounts +
// state.accounts for the multi-account providers) that the create page
// populated in draft via the corresponding OAuth-exchange helper before
// Save. The per-kind assertXxxUpstreamRecord below narrows those opaque
// payloads into their typed shape and rejects a POST that skipped the
// credential step.
- const stateFromBody = body.kind === 'copilot' || body.kind === 'codex' || body.kind === 'claude-code' ? body.state ?? null : null;
+ const stateFromBody = body.kind === 'copilot' || body.kind === 'chatgpt' || body.kind === 'claude-code' ? body.state ?? null : null;
const upstream: UpstreamRecord = {
id: shortId('up'),
kind: body.kind,
@@ -375,11 +375,11 @@ export const createUpstream = async (c: CtxWithJson)
// accepts state on create — a caller who bypasses the exchange helpers
// could otherwise persist garbage that only surfaces on the first
// data-plane call. Copilot's reader accepts null as the empty blueprint
- // shape; codex / claude-code assert against null too — their blueprints
+ // shape; chatgpt / claude-code assert against null too — their blueprints
// carry `{accounts: []}`, so an incoming null is a malformed request.
try {
if (upstream.kind === 'copilot') readCopilotUpstreamState(stateFromBody);
- else if (upstream.kind === 'codex') assertCodexUpstreamState(stateFromBody);
+ else if (upstream.kind === 'chatgpt') assertChatGPTUpstreamState(stateFromBody);
else if (upstream.kind === 'claude-code') readClaudeCodeUpstreamState(stateFromBody);
} catch (err) {
return c.json({ error: `Invalid state for ${upstream.kind}: ${errorMessage(err)}` }, 400);
@@ -432,14 +432,14 @@ export const updateUpstream = async (c: CtxWithJson ({
chatgptAccountId: account.chatgptAccountId,
@@ -112,7 +112,7 @@ export const upstreamRecordToJson = (upstream: UpstreamRecord): RedactedSerializ
: { expiresAt: account.accessToken.expiresAt, refreshedAt: account.accessToken.refreshedAt },
})),
};
- return { ...base, kind: 'codex', config: clone(upstream.config), state };
+ return { ...base, kind: 'chatgpt', config: clone(upstream.config), state };
}
case 'claude-code': {
assertClaudeCodeUpstreamRecord(upstream);
@@ -164,10 +164,10 @@ export const upstreamRecordToFullJson = (upstream: UpstreamRecord): FullSerializ
if (record.state !== null) assertCopilotUpstreamState(record.state);
return { ...base, kind: 'copilot', config: clone(record.config), state: clone(record.state) };
}
- case 'codex': {
- assertCodexUpstreamRecord(upstream);
- assertCodexUpstreamState(upstream.state);
- return { ...base, kind: 'codex', config: clone(upstream.config), state: clone(upstream.state) };
+ case 'chatgpt': {
+ assertChatGPTUpstreamRecord(upstream);
+ assertChatGPTUpstreamState(upstream.state);
+ return { ...base, kind: 'chatgpt', config: clone(upstream.config), state: clone(upstream.state) };
}
case 'claude-code': {
assertClaudeCodeUpstreamRecord(upstream);
@@ -209,7 +209,7 @@ export const blueprintUpstreamRecord = (kind: UpstreamProviderKind): BlueprintSe
return { ...base, kind, config: { baseUrl: '', authStyle: 'bearer', apiKey: '', endpoints: { openaiChatCompletions: {} }, ingressHeadersRules: [], modelsFetch: { enabled: true }, models: [] }, state: null };
case 'azure':
return { ...base, kind, config: { endpoint: '', apiKey: '', models: [] }, state: null };
- case 'codex':
+ case 'chatgpt':
return { ...base, kind, config: { accounts: [] }, state: { accounts: [] } };
case 'claude-code':
return { ...base, kind, config: { accounts: [] }, state: { accounts: [] } };
diff --git a/packages/gateway/src/control-plane/upstreams/types.ts b/packages/gateway/src/control-plane/upstreams/types.ts
index 706399f7e1..191dd5b9e6 100644
--- a/packages/gateway/src/control-plane/upstreams/types.ts
+++ b/packages/gateway/src/control-plane/upstreams/types.ts
@@ -7,6 +7,16 @@ import type {
UpstreamModelConfig,
} from '@floway-dev/provider';
import type { AzureUpstreamConfig as StoredAzureUpstreamConfig } from '@floway-dev/provider-azure';
+import type {
+ ChatGPTAccountCredential,
+ ChatGPTAccountIdentity,
+ ChatGPTQuotaSnapshot,
+ ChatGPTQuotaSnapshotMap,
+ ChatGPTRateLimitResetCredit,
+ ChatGPTRateLimitResetCredits,
+ ChatGPTUpstreamConfig as StoredChatGPTUpstreamConfig,
+ ChatGPTUpstreamState as StoredChatGPTUpstreamState,
+} from '@floway-dev/provider-chatgpt';
import type {
ClaudeCodeAccessTokenEntry,
ClaudeCodeAccountCredential,
@@ -18,16 +28,6 @@ import type {
ClaudeCodeUpstreamState as StoredClaudeCodeUpstreamState,
ClaudeCodeUsageProbeSnapshotEntry,
} from '@floway-dev/provider-claude-code';
-import type {
- CodexAccountCredential,
- CodexAccountIdentity,
- CodexQuotaSnapshot,
- CodexQuotaSnapshotMap,
- CodexRateLimitResetCredit,
- CodexRateLimitResetCredits,
- CodexUpstreamConfig as StoredCodexUpstreamConfig,
- CodexUpstreamState as StoredCodexUpstreamState,
-} from '@floway-dev/provider-codex';
import type {
CopilotQuotaSnapshotEntry,
CopilotSeatEntry as StoredCopilotSeatEntry,
@@ -43,7 +43,7 @@ import type {
OllamaUpstreamState as StoredOllamaUpstreamState,
} from '@floway-dev/provider-ollama';
-export type { ClaudeCodeQuotaWindow, CodexQuotaSnapshot, CodexQuotaSnapshotMap, CodexRateLimitResetCredit, CodexRateLimitResetCredits, CustomModelsFetch, ProxyFallbackEntry };
+export type { ClaudeCodeQuotaWindow, ChatGPTQuotaSnapshot, ChatGPTQuotaSnapshotMap, ChatGPTRateLimitResetCredit, ChatGPTRateLimitResetCredits, CustomModelsFetch, ProxyFallbackEntry };
export type { ProviderModelsFailureResponse } from '@floway-dev/provider';
type CustomConfigFields = Pick<
@@ -81,8 +81,8 @@ export interface CopilotUpstreamState {
quotaSnapshot: CopilotQuotaSnapshotEntry | null;
}
-export type CodexUpstreamConfig = Omit & {
- accounts: CodexAccountIdentity[];
+export type ChatGPTUpstreamConfig = Omit & {
+ accounts: ChatGPTAccountIdentity[];
};
export type OllamaUpstreamConfig = Omit & {
@@ -90,17 +90,17 @@ export type OllamaUpstreamConfig = Omit &
apiKeySet?: boolean;
};
-export type CodexAccountCredentialState = Pick<
- CodexAccountCredential,
+export type ChatGPTAccountCredentialState = Pick<
+ ChatGPTAccountCredential,
'chatgptAccountId' | 'state' | 'state_message' | 'state_updated_at'
> & {
- accessToken?: CodexAccountCredential['accessToken'];
+ accessToken?: ChatGPTAccountCredential['accessToken'];
refresh_token?: string | null;
refresh_token_set?: boolean;
};
-export interface CodexUpstreamState {
- accounts: CodexAccountCredentialState[];
+export interface ChatGPTUpstreamState {
+ accounts: ChatGPTAccountCredentialState[];
}
export type ClaudeCodeUpstreamConfig = Omit & {
@@ -150,8 +150,8 @@ type RedactedAzureConfig = Omit & { apiKeyS
type RedactedCopilotConfig = Omit & { githubTokenSet: boolean };
type RedactedOllamaConfig = Omit & { apiKeySet: boolean };
-type RedactedCodexCredential = Pick<
- CodexAccountCredential,
+type RedactedChatGPTCredential = Pick<
+ ChatGPTAccountCredential,
'chatgptAccountId' | 'state' | 'state_message' | 'state_updated_at'
> & { refresh_token_set: boolean };
@@ -169,7 +169,7 @@ export type RedactedSerializedUpstreamRecord =
| (SerializedUpstreamRecordBase & { kind: 'custom'; config: RedactedCustomConfig; state: null })
| (SerializedUpstreamRecordBase & { kind: 'azure'; config: RedactedAzureConfig; state: null })
| (SerializedUpstreamRecordBase & { kind: 'copilot'; config: RedactedCopilotConfig; state: CopilotUpstreamState | null })
- | (SerializedUpstreamRecordBase & { kind: 'codex'; config: StoredCodexUpstreamConfig; state: { accounts: RedactedCodexCredential[] } })
+ | (SerializedUpstreamRecordBase & { kind: 'chatgpt'; config: StoredChatGPTUpstreamConfig; state: { accounts: RedactedChatGPTCredential[] } })
| (SerializedUpstreamRecordBase & { kind: 'claude-code'; config: StoredClaudeCodeUpstreamConfig; state: { accounts: RedactedClaudeCodeCredential[] } })
| (SerializedUpstreamRecordBase & { kind: 'ollama'; config: RedactedOllamaConfig; state: StoredOllamaUpstreamState | null });
@@ -177,7 +177,7 @@ export type FullSerializedUpstreamRecord =
| (SerializedUpstreamRecordBase & { kind: 'custom'; config: StoredCustomUpstreamConfig; state: null })
| (SerializedUpstreamRecordBase & { kind: 'azure'; config: StoredAzureUpstreamConfig; state: null })
| (SerializedUpstreamRecordBase & { kind: 'copilot'; config: StoredCopilotUpstreamConfig; state: StoredCopilotUpstreamState | null })
- | (SerializedUpstreamRecordBase & { kind: 'codex'; config: StoredCodexUpstreamConfig; state: StoredCodexUpstreamState })
+ | (SerializedUpstreamRecordBase & { kind: 'chatgpt'; config: StoredChatGPTUpstreamConfig; state: StoredChatGPTUpstreamState })
| (SerializedUpstreamRecordBase & { kind: 'claude-code'; config: StoredClaudeCodeUpstreamConfig; state: StoredClaudeCodeUpstreamState })
| (SerializedUpstreamRecordBase & { kind: 'ollama'; config: StoredOllamaUpstreamConfig; state: StoredOllamaUpstreamState | null });
@@ -189,7 +189,7 @@ export type BlueprintSerializedUpstreamRecord =
| (BlueprintUpstreamRecordBase & { kind: 'custom'; config: StoredCustomUpstreamConfig; state: null })
| (BlueprintUpstreamRecordBase & { kind: 'azure'; config: StoredAzureUpstreamConfig; state: null })
| (BlueprintUpstreamRecordBase & { kind: 'copilot'; config: StoredCopilotUpstreamConfig; state: null })
- | (BlueprintUpstreamRecordBase & { kind: 'codex'; config: { accounts: CodexAccountIdentity[] }; state: { accounts: CodexAccountCredential[] } })
+ | (BlueprintUpstreamRecordBase & { kind: 'chatgpt'; config: { accounts: ChatGPTAccountIdentity[] }; state: { accounts: ChatGPTAccountCredential[] } })
| (BlueprintUpstreamRecordBase & { kind: 'claude-code'; config: { accounts: ClaudeCodeAccountIdentity[] }; state: { accounts: ClaudeCodeAccountCredential[] } })
| (BlueprintUpstreamRecordBase & { kind: 'ollama'; config: StoredOllamaUpstreamConfig; state: null });
@@ -201,8 +201,8 @@ export interface ModelsCacheStatus {
modelCount: number | null;
}
-type WithResponseProjections = T extends { kind: 'codex' }
- ? T & { modelsCache: ModelsCacheStatus; codex_quota: CodexQuotaSnapshotMap | null }
+type WithResponseProjections = T extends { kind: 'chatgpt' }
+ ? T & { modelsCache: ModelsCacheStatus; chatgpt_quota: ChatGPTQuotaSnapshotMap | null }
: T & { modelsCache: ModelsCacheStatus };
export type RedactedUpstreamResponse = WithResponseProjections;
@@ -217,7 +217,7 @@ export type UpstreamRecord =
| (DashboardUpstreamRecordBase & { kind: 'custom'; config: CustomUpstreamConfig; state: null })
| (DashboardUpstreamRecordBase & { kind: 'azure'; config: AzureUpstreamConfig; state: null })
| (DashboardUpstreamRecordBase & { kind: 'copilot'; config: CopilotUpstreamConfig; state: CopilotUpstreamState | StoredCopilotUpstreamState | null })
- | (DashboardUpstreamRecordBase & { kind: 'codex'; config: CodexUpstreamConfig; state: CodexUpstreamState; codex_quota?: CodexQuotaSnapshotMap | null })
+ | (DashboardUpstreamRecordBase & { kind: 'chatgpt'; config: ChatGPTUpstreamConfig; state: ChatGPTUpstreamState; chatgpt_quota?: ChatGPTQuotaSnapshotMap | null })
| (DashboardUpstreamRecordBase & { kind: 'claude-code'; config: ClaudeCodeUpstreamConfig; state: ClaudeCodeUpstreamState })
| (DashboardUpstreamRecordBase & { kind: 'ollama'; config: OllamaUpstreamConfig; state: StoredOllamaUpstreamState | null });
diff --git a/packages/gateway/src/data-plane/codex/models.ts b/packages/gateway/src/data-plane/codex/models.ts
index 9002ece028..bc89d44dfa 100644
--- a/packages/gateway/src/data-plane/codex/models.ts
+++ b/packages/gateway/src/data-plane/codex/models.ts
@@ -19,7 +19,7 @@ import { synthesizeCatalogEntry } from './synthesize.ts';
import type { ModelsRefreshScheduler } from '../../execution/models-refresh.ts';
import type { User } from '../../repo/types.ts';
import { enumerateAddressableModelIds, type AddressableIdEntry } from '../shared/listing/addressable.ts';
-import { codexModelContextWindow } from '@floway-dev/provider-codex';
+import { chatgptModelContextWindow } from '@floway-dev/provider-chatgpt';
// Pure transformation: client catalog + addressable entries →
// codex-shaped catalog (drops unlisted alternates and non-chat kinds).
@@ -61,10 +61,10 @@ export const assembleCodexCatalog = (
// different provider must use its own advertised input budget.
const primaryUpstream = entry.upstreams[0];
let codexContextWindow;
- if (primaryUpstream?.kind === 'codex') {
+ if (primaryUpstream?.kind === 'chatgpt') {
const providerModel = entry.model.providerModels?.[primaryUpstream.upstreamId];
if (providerModel === undefined) throw new Error(`Codex catalog model ${entry.id} has no primary provider model`);
- codexContextWindow = codexModelContextWindow(providerModel);
+ codexContextWindow = chatgptModelContextWindow(providerModel);
}
models.push(synthesizeCatalogEntry(entry.model, matchCatalog(entry.model.id), capabilities, catalogServiceTiers, codexContextWindow));
}
diff --git a/packages/gateway/src/data-plane/codex/synthesize.ts b/packages/gateway/src/data-plane/codex/synthesize.ts
index 55bf33cdcd..0e2222ea5c 100644
--- a/packages/gateway/src/data-plane/codex/synthesize.ts
+++ b/packages/gateway/src/data-plane/codex/synthesize.ts
@@ -1,13 +1,13 @@
// Matched client-catalog entries retain their model-specific instructions and
// opaque fields. Registry metadata controls the public identity, priced tiers,
-// modalities, reasoning and limits. Codex providers additionally supply their
+// modalities, reasoning and limits. ChatGPT providers additionally supply their
// private default window; other providers expose a single input budget.
import type { CatalogModel, CodexCatalogCapabilities, CodexReasoningLevel, CodexServiceTier } from './catalog.ts';
import { synthesizedBaseInstructions } from './synthesized-base-instructions.ts';
import type { Modality } from '@floway-dev/protocols/common';
import type { InternalModel } from '@floway-dev/provider';
-import type { CodexContextWindow } from '@floway-dev/provider-codex';
+import type { ChatGPTContextWindow } from '@floway-dev/provider-chatgpt';
// Keep the established 128K compaction policy for synthesized models whose
// provider has no published limit. Operators can replace it with a model limit.
@@ -79,7 +79,7 @@ export const synthesizeCatalogEntry = (
base?: CatalogModel,
capabilities: CodexCatalogCapabilities = {},
catalogServiceTiers: readonly CodexServiceTier[] = [],
- codexContextWindow?: CodexContextWindow,
+ codexContextWindow?: ChatGPTContextWindow,
): CatalogModel => {
const source: CatalogModel = base ?? BASELINE;
diff --git a/packages/gateway/src/data-plane/providers/registry.ts b/packages/gateway/src/data-plane/providers/registry.ts
index 1ff66a5f9e..5ef44a17e1 100644
--- a/packages/gateway/src/data-plane/providers/registry.ts
+++ b/packages/gateway/src/data-plane/providers/registry.ts
@@ -3,8 +3,8 @@ import { modelsRefreshInputHash } from '../../repo/models-refresh-inputs.ts';
import type { StoredUpstreamRecord } from '../../repo/types.ts';
import type { FlagDefaults, Provider, ProviderModule, UpstreamProviderKind, UpstreamRecord } from '@floway-dev/provider';
import { azureProviderModule } from '@floway-dev/provider-azure';
+import { chatgptProviderModule } from '@floway-dev/provider-chatgpt';
import { claudeCodeProviderModule } from '@floway-dev/provider-claude-code';
-import { codexProviderModule } from '@floway-dev/provider-codex';
import { copilotProviderModule } from '@floway-dev/provider-copilot';
import { customProviderModule } from '@floway-dev/provider-custom';
import { ollamaProviderModule } from '@floway-dev/provider-ollama';
@@ -13,7 +13,7 @@ const providersByKind: Record = {
copilot: copilotProviderModule,
custom: customProviderModule,
azure: azureProviderModule,
- codex: codexProviderModule,
+ chatgpt: chatgptProviderModule,
'claude-code': claudeCodeProviderModule,
ollama: ollamaProviderModule,
};
diff --git a/packages/gateway/src/data-plane/tools/web-search/alpha-search/upstream.ts b/packages/gateway/src/data-plane/tools/web-search/alpha-search/upstream.ts
index 17f9052031..92c080b4f9 100644
--- a/packages/gateway/src/data-plane/tools/web-search/alpha-search/upstream.ts
+++ b/packages/gateway/src/data-plane/tools/web-search/alpha-search/upstream.ts
@@ -43,14 +43,14 @@ export const resolveAlphaSearchDispatcher = async ({
if (candidate === undefined) {
throw new AlphaSearchModelNotFoundError();
}
- if (candidate.provider.kind !== 'codex' && candidate.provider.kind !== 'custom') {
+ if (candidate.provider.kind !== 'chatgpt' && candidate.provider.kind !== 'custom') {
throw new Error('Selected upstream does not support OpenAI search passthrough');
}
return async (body, signal, headers) => {
const { model: _callerModel, ...request } = body;
- // TODO: pin SearchRequest.id to one provider account when Codex upstreams
- // support account pools. The current Codex provider has one active account.
+ // TODO: pin SearchRequest.id to one provider account when ChatGPT upstreams
+ // support account pools. The current ChatGPT provider has one active account.
const result = await candidate.provider.instance.callAlphaSearch(
providerModelOf(candidate),
request,
diff --git a/packages/provider-codex/__tests__/access-token_test.ts b/packages/provider-chatgpt/__tests__/access-token_test.ts
similarity index 68%
rename from packages/provider-codex/__tests__/access-token_test.ts
rename to packages/provider-chatgpt/__tests__/access-token_test.ts
index 7e98fcfc06..a52702c91c 100644
--- a/packages/provider-codex/__tests__/access-token_test.ts
+++ b/packages/provider-chatgpt/__tests__/access-token_test.ts
@@ -2,23 +2,23 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { createUpstreamStateRepoStub, type UpstreamStateRepoStub } from './upstream-state-repo.ts';
import {
- ensureCodexAccessToken,
- invalidateCodexAccessToken,
- mintCodexAccessToken,
- putCodexAccessToken,
- type CodexAccessTokenEntry,
+ ensureChatGPTAccessToken,
+ invalidateChatGPTAccessToken,
+ mintChatGPTAccessToken,
+ putChatGPTAccessToken,
+ type ChatGPTAccessTokenEntry,
} from '../src/access-token.ts';
-import { CodexOAuthSessionTerminatedError } from '../src/auth/oauth.ts';
-import type { CodexUpstreamState } from '../src/state.ts';
+import { ChatGPTOAuthSessionTerminatedError } from '../src/auth/oauth.ts';
+import type { ChatGPTUpstreamState } from '../src/state.ts';
import { directFetcher, initProviderRepo, type UpstreamRecord } from '@floway-dev/provider';
const accountId = 'acc_1';
const upstreamId = 'up_a';
-const makeRecord = (state: CodexUpstreamState): UpstreamRecord => ({
+const makeRecord = (state: ChatGPTUpstreamState): UpstreamRecord => ({
id: upstreamId,
- kind: 'codex',
- name: 'Codex',
+ kind: 'chatgpt',
+ name: 'ChatGPT',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 0,
@@ -40,7 +40,7 @@ const baseAccount = {
state: 'active' as const,
state_updated_at: '2026-06-01T00:00:00.000Z',
openaiDeviceId: '11111111-2222-4333-8444-555555555555',
- accessToken: null as CodexAccessTokenEntry | null,
+ accessToken: null as ChatGPTAccessTokenEntry | null,
quotaSnapshot: null,
};
@@ -53,19 +53,19 @@ beforeEach(() => {
current = makeRecord({ accounts: [{ ...baseAccount }] });
// Write-through, so a subsequent read observes what the last write landed.
repo = createUpstreamStateRepoStub(() => current, state => {
- current = { ...current!, state: state as CodexUpstreamState };
+ current = { ...current!, state: state as ChatGPTUpstreamState };
});
initProviderRepo(() => ({ recordUpstreamMetric: async () => {}, upstreams: repo }));
});
afterEach(() => vi.restoreAllMocks());
-const storedState = (): CodexUpstreamState => current!.state as CodexUpstreamState;
+const storedState = (): ChatGPTUpstreamState => current!.state as ChatGPTUpstreamState;
-describe('putCodexAccessToken', () => {
+describe('putChatGPTAccessToken', () => {
test('persists the entry into the account slot, leaving the rest of the credential alone', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: '2026-06-01T00:00:00.000Z' };
- await putCodexAccessToken(upstreamId, accountId, entry);
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: '2026-06-01T00:00:00.000Z' };
+ await putChatGPTAccessToken(upstreamId, accountId, entry);
expect(repo.saveState).toHaveBeenCalledTimes(1);
expect(repo.saveState.mock.calls[0][0]).toBe(upstreamId);
expect(storedState()).toEqual({ accounts: [{ ...baseAccount, accessToken: entry }] });
@@ -78,8 +78,8 @@ describe('putCodexAccessToken', () => {
accessToken: { token: 'at_current', expiresAt: farFutureMs, refreshedAt: 'current', planType: 'free' },
}],
});
- const entry: CodexAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: 'new' };
- const effective = await putCodexAccessToken(upstreamId, accountId, entry, { planType: 'team' });
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: 'new' };
+ const effective = await putChatGPTAccessToken(upstreamId, accountId, entry, { planType: 'team' });
expect(effective.planType).toBe('free');
expect(storedState().accounts[0].accessToken?.planType).toBe('free');
});
@@ -88,59 +88,59 @@ describe('putCodexAccessToken', () => {
['team', 'free'],
['free', 'team'],
])('an older explicit %s observation cannot replace newer %s', async (olderPlan, newerPlan) => {
- const newer: CodexAccessTokenEntry = {
+ const newer: ChatGPTAccessTokenEntry = {
token: 'at_newer',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:02.000Z',
planType: newerPlan,
};
current = makeRecord({ accounts: [{ ...baseAccount, accessToken: newer }] });
- const older: CodexAccessTokenEntry = {
+ const older: ChatGPTAccessTokenEntry = {
token: 'at_older',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:01.000Z',
planType: olderPlan,
};
- const effective = await putCodexAccessToken(upstreamId, accountId, older);
+ const effective = await putChatGPTAccessToken(upstreamId, accountId, older);
const expected = { ...newer, planObservedAt: newer.refreshedAt };
expect(effective).toEqual(expected);
expect(storedState().accounts[0].accessToken).toEqual(expected);
});
test.each(['free', 'team'])('keeps the newer token while merging an older explicit %s plan', async olderPlan => {
- const newer: CodexAccessTokenEntry = {
+ const newer: ChatGPTAccessTokenEntry = {
token: 'at_newer',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:02.000Z',
};
current = makeRecord({ accounts: [{ ...baseAccount, accessToken: newer }] });
- const older: CodexAccessTokenEntry = {
+ const older: ChatGPTAccessTokenEntry = {
token: 'at_older',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:01.000Z',
planType: olderPlan,
};
- const effective = await putCodexAccessToken(upstreamId, accountId, older);
+ const effective = await putChatGPTAccessToken(upstreamId, accountId, older);
const expected = { ...newer, planType: olderPlan, planObservedAt: older.refreshedAt };
expect(effective).toEqual(expected);
expect(storedState().accounts[0].accessToken).toEqual(expected);
});
test('orders token and explicit plan observations independently', async () => {
- const tokenOnly: CodexAccessTokenEntry = {
+ const tokenOnly: ChatGPTAccessTokenEntry = {
token: 'at_t3',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:03.000Z',
};
- await putCodexAccessToken(upstreamId, accountId, tokenOnly);
- await putCodexAccessToken(upstreamId, accountId, {
+ await putChatGPTAccessToken(upstreamId, accountId, tokenOnly);
+ await putChatGPTAccessToken(upstreamId, accountId, {
token: 'at_t1',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:01.000Z',
planType: 'free',
planObservedAt: '2026-08-10T00:00:01.000Z',
});
- const effective = await putCodexAccessToken(upstreamId, accountId, {
+ const effective = await putChatGPTAccessToken(upstreamId, accountId, {
token: 'at_t2',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:02.000Z',
@@ -168,12 +168,12 @@ describe('putCodexAccessToken', () => {
},
}],
});
- const incoming: CodexAccessTokenEntry = {
+ const incoming: ChatGPTAccessTokenEntry = {
token: 'at_new',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:03.000Z',
};
- const effective = await putCodexAccessToken(upstreamId, accountId, incoming, {
+ const effective = await putChatGPTAccessToken(upstreamId, accountId, incoming, {
planType: 'plus',
observedAt: '2026-08-10T00:00:02.000Z',
});
@@ -186,8 +186,8 @@ describe('putCodexAccessToken', () => {
test('propagates storage failures so the request path surfaces them', async () => {
repo.saveState.mockRejectedValueOnce(new Error('D1 boom'));
- const entry: CodexAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: 'now' };
- await expect(putCodexAccessToken(upstreamId, accountId, entry)).rejects.toThrow('D1 boom');
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: 'now' };
+ await expect(putChatGPTAccessToken(upstreamId, accountId, entry)).rejects.toThrow('D1 boom');
});
// A minted access token is bookkeeping the next request re-derives, so an
@@ -196,33 +196,33 @@ describe('putCodexAccessToken', () => {
// error.
test('tolerates an upstream that disappeared mid-flight', async () => {
current = null;
- const entry: CodexAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: 'now' };
- await putCodexAccessToken(upstreamId, accountId, entry);
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: 'now' };
+ await putChatGPTAccessToken(upstreamId, accountId, entry);
expect(repo.writes).toEqual([]);
});
test('warns and writes nothing when the requested account is not in the pool', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: 'now' };
- await putCodexAccessToken(upstreamId, 'acc_other', entry);
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_new', expiresAt: farFutureMs, refreshedAt: 'now' };
+ await putChatGPTAccessToken(upstreamId, 'acc_other', entry);
expect(repo.writes).toEqual([]);
});
});
-describe('invalidateCodexAccessToken', () => {
+describe('invalidateChatGPTAccessToken', () => {
test('clears a populated access-token slot', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_x', expiresAt: farFutureMs, refreshedAt: 'now' };
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_x', expiresAt: farFutureMs, refreshedAt: 'now' };
current = makeRecord({ accounts: [{ ...baseAccount, accessToken: entry }] });
- await invalidateCodexAccessToken(upstreamId, accountId);
+ await invalidateChatGPTAccessToken(upstreamId, accountId);
expect(storedState().accounts[0].accessToken).toBeNull();
});
test('writes nothing when the slot is already null', async () => {
- await invalidateCodexAccessToken(upstreamId, accountId);
+ await invalidateChatGPTAccessToken(upstreamId, accountId);
expect(repo.writes).toEqual([]);
});
test('retains a sibling token that replaced the failed token', async () => {
- const winner: CodexAccessTokenEntry = {
+ const winner: ChatGPTAccessTokenEntry = {
token: 'at_winner',
expiresAt: farFutureMs,
refreshedAt: '2026-08-10T00:00:02.000Z',
@@ -230,76 +230,76 @@ describe('invalidateCodexAccessToken', () => {
planObservedAt: '2026-08-10T00:00:02.000Z',
};
current = makeRecord({ accounts: [{ ...baseAccount, accessToken: winner }] });
- const retained = await invalidateCodexAccessToken(upstreamId, accountId, 'at_failed');
+ const retained = await invalidateChatGPTAccessToken(upstreamId, accountId, 'at_failed');
expect(retained).toEqual(winner);
expect(storedState().accounts[0].accessToken).toEqual(winner);
expect(repo.writes).toEqual([]);
});
});
-describe('ensureCodexAccessToken', () => {
+describe('ensureChatGPTAccessToken', () => {
test('returns the cached token when still fresh and skips mint', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_x', expiresAt: farFutureMs, refreshedAt: 'now' };
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_x', expiresAt: farFutureMs, refreshedAt: 'now' };
current = makeRecord({ accounts: [{ ...baseAccount, accessToken: entry }] });
const mint = vi.fn();
- const out = await ensureCodexAccessToken(upstreamId, accountId, mint);
+ const out = await ensureChatGPTAccessToken(upstreamId, accountId, mint);
expect(out).toEqual(entry);
expect(mint).not.toHaveBeenCalled();
});
test('uses an access-only token with unknown expiry without minting', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_only', expiresAt: null, refreshedAt: 'now' };
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_only', expiresAt: null, refreshedAt: 'now' };
current = makeRecord({ accounts: [{ ...baseAccount, refresh_token: null, accessToken: entry }] });
const mint = vi.fn();
- expect(await ensureCodexAccessToken(upstreamId, accountId, mint)).toEqual(entry);
+ expect(await ensureChatGPTAccessToken(upstreamId, accountId, mint)).toEqual(entry);
expect(mint).not.toHaveBeenCalled();
});
test('spends an access-only token to its actual expiry, ignoring the renewable skew', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_only', expiresAt: Date.now() + 60_000, refreshedAt: 'now' };
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_only', expiresAt: Date.now() + 60_000, refreshedAt: 'now' };
current = makeRecord({ accounts: [{ ...baseAccount, refresh_token: null, accessToken: entry }] });
const mint = vi.fn();
- expect(await ensureCodexAccessToken(upstreamId, accountId, mint)).toEqual(entry);
+ expect(await ensureChatGPTAccessToken(upstreamId, accountId, mint)).toEqual(entry);
expect(mint).not.toHaveBeenCalled();
});
test('treats an unknown expiry on a renewable credential as unusable', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_unknown', expiresAt: null, refreshedAt: 'now' };
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_unknown', expiresAt: null, refreshedAt: 'now' };
current = makeRecord({ accounts: [{ ...baseAccount, accessToken: entry }] });
- const minted: CodexAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
+ const minted: ChatGPTAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
const mint = vi.fn().mockResolvedValue(minted);
- expect(await ensureCodexAccessToken(upstreamId, accountId, mint)).toEqual(minted);
+ expect(await ensureChatGPTAccessToken(upstreamId, accountId, mint)).toEqual(minted);
expect(mint).toHaveBeenCalledWith('rt_v1');
});
test('reports an expired access-only token as needing a re-import, without minting', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_only', expiresAt: Date.now() - 1, refreshedAt: 'now' };
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_only', expiresAt: Date.now() - 1, refreshedAt: 'now' };
current = makeRecord({ accounts: [{ ...baseAccount, refresh_token: null, accessToken: entry }] });
const mint = vi.fn();
- await expect(ensureCodexAccessToken(upstreamId, accountId, mint)).rejects.toThrow(/expired.*re-import/);
+ await expect(ensureChatGPTAccessToken(upstreamId, accountId, mint)).rejects.toThrow(/expired.*re-import/);
expect(mint).not.toHaveBeenCalled();
});
test('refuses a forced refresh of an access-only credential', async () => {
- const entry: CodexAccessTokenEntry = { token: 'at_only', expiresAt: null, refreshedAt: 'now' };
+ const entry: ChatGPTAccessTokenEntry = { token: 'at_only', expiresAt: null, refreshedAt: 'now' };
current = makeRecord({ accounts: [{ ...baseAccount, refresh_token: null, accessToken: entry }] });
const mint = vi.fn();
- await expect(ensureCodexAccessToken(upstreamId, accountId, mint, true)).rejects.toThrow(/cannot be refreshed/);
+ await expect(ensureChatGPTAccessToken(upstreamId, accountId, mint, true)).rejects.toThrow(/cannot be refreshed/);
expect(mint).not.toHaveBeenCalled();
});
test('keeps a null account id in a different in-flight slot from the string "null"', async () => {
current = makeRecord({ accounts: [{ ...baseAccount, chatgptAccountId: null }] });
- const minted: CodexAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
- const nullAccountEnsure = ensureCodexAccessToken(upstreamId, null, vi.fn().mockResolvedValue(minted));
- await expect(ensureCodexAccessToken(upstreamId, 'null', vi.fn())).rejects.toThrow(/not found/);
+ const minted: ChatGPTAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
+ const nullAccountEnsure = ensureChatGPTAccessToken(upstreamId, null, vi.fn().mockResolvedValue(minted));
+ await expect(ensureChatGPTAccessToken(upstreamId, 'null', vi.fn())).rejects.toThrow(/not found/);
await expect(nullAccountEnsure).resolves.toEqual(minted);
});
test('mints when nothing is cached, then persists', async () => {
- const minted: CodexAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
+ const minted: ChatGPTAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
const mint = vi.fn().mockResolvedValue(minted);
- const out = await ensureCodexAccessToken(upstreamId, accountId, mint);
+ const out = await ensureChatGPTAccessToken(upstreamId, accountId, mint);
expect(out).toEqual(minted);
expect(mint).toHaveBeenCalledWith('rt_v1');
expect(storedState().accounts[0].accessToken).toEqual(minted);
@@ -308,9 +308,9 @@ describe('ensureCodexAccessToken', () => {
test('mints when the cached token is within the refresh skew window', async () => {
const expiresSoon = Date.now() + 60 * 1000;
current = makeRecord({ accounts: [{ ...baseAccount, accessToken: { token: 'at_old', expiresAt: expiresSoon, refreshedAt: 'old' } }] });
- const minted: CodexAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
+ const minted: ChatGPTAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
const mint = vi.fn().mockResolvedValue(minted);
- const out = await ensureCodexAccessToken(upstreamId, accountId, mint);
+ const out = await ensureChatGPTAccessToken(upstreamId, accountId, mint);
expect(out).toEqual(minted);
expect(mint).toHaveBeenCalledWith('rt_v1');
});
@@ -323,8 +323,8 @@ describe('ensureCodexAccessToken', () => {
accessToken: { token: 'at_old', expiresAt: expiresSoon, refreshedAt: 'old', planType: 'team' },
}],
});
- const minted: CodexAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
- const out = await ensureCodexAccessToken(upstreamId, accountId, vi.fn().mockResolvedValue(minted));
+ const minted: ChatGPTAccessTokenEntry = { token: 'at_minted', expiresAt: farFutureMs, refreshedAt: 'now' };
+ const out = await ensureChatGPTAccessToken(upstreamId, accountId, vi.fn().mockResolvedValue(minted));
expect(out.planType).toBe('team');
expect(storedState().accounts[0].accessToken?.planType).toBe('team');
});
@@ -332,19 +332,19 @@ describe('ensureCodexAccessToken', () => {
test('throws when the upstream row is missing', async () => {
current = null;
const mint = vi.fn();
- await expect(ensureCodexAccessToken(upstreamId, accountId, mint)).rejects.toThrow(/not found/);
+ await expect(ensureChatGPTAccessToken(upstreamId, accountId, mint)).rejects.toThrow(/not found/);
expect(mint).not.toHaveBeenCalled();
});
test('throws when the requested account is not in the pool', async () => {
const mint = vi.fn();
- await expect(ensureCodexAccessToken(upstreamId, 'acc_other', mint)).rejects.toThrow(/acc_other/);
+ await expect(ensureChatGPTAccessToken(upstreamId, 'acc_other', mint)).rejects.toThrow(/acc_other/);
expect(mint).not.toHaveBeenCalled();
});
test('propagates mint errors without persisting', async () => {
const mint = vi.fn().mockRejectedValue(new Error('oauth boom'));
- await expect(ensureCodexAccessToken(upstreamId, accountId, mint)).rejects.toThrow(/oauth boom/);
+ await expect(ensureChatGPTAccessToken(upstreamId, accountId, mint)).rejects.toThrow(/oauth boom/);
expect(repo.writes).toEqual([]);
});
@@ -354,14 +354,14 @@ describe('ensureCodexAccessToken', () => {
// at_sibling. Re-read on recovery observes the new pair scoped to the same
// accountId; we should return it instead of destroying a working
// credential.
- const siblingEntry: CodexAccessTokenEntry = { token: 'at_sibling', expiresAt: farFutureMs, refreshedAt: 'sibling' };
+ const siblingEntry: ChatGPTAccessTokenEntry = { token: 'at_sibling', expiresAt: farFutureMs, refreshedAt: 'sibling' };
repo.getById.mockImplementationOnce(async () => current).mockImplementationOnce(async () => {
current = makeRecord({ accounts: [{ ...baseAccount, refresh_token: 'rt_v2', accessToken: siblingEntry }] });
return current;
});
- const mint = vi.fn().mockRejectedValue(new CodexOAuthSessionTerminatedError({ code: 'invalid_grant', message: 'replayed' }));
+ const mint = vi.fn().mockRejectedValue(new ChatGPTOAuthSessionTerminatedError({ code: 'invalid_grant', message: 'replayed' }));
- const out = await ensureCodexAccessToken(upstreamId, accountId, mint);
+ const out = await ensureChatGPTAccessToken(upstreamId, accountId, mint);
expect(out).toEqual(siblingEntry);
expect(mint).toHaveBeenCalledTimes(1);
// Recovery returns the sibling's cached token; no fresh persist from us.
@@ -372,8 +372,8 @@ describe('ensureCodexAccessToken', () => {
// Same RT on re-read means no sibling rotated; the refresh_token really
// is dead. The cache surfaces the original error; the data-plane / control-
// plane caller is responsible for the terminal-state flip.
- const mint = vi.fn().mockRejectedValue(new CodexOAuthSessionTerminatedError({ code: 'invalid_grant', message: 'revoked' }));
- await expect(ensureCodexAccessToken(upstreamId, accountId, mint)).rejects.toBeInstanceOf(CodexOAuthSessionTerminatedError);
+ const mint = vi.fn().mockRejectedValue(new ChatGPTOAuthSessionTerminatedError({ code: 'invalid_grant', message: 'revoked' }));
+ await expect(ensureChatGPTAccessToken(upstreamId, accountId, mint)).rejects.toBeInstanceOf(ChatGPTOAuthSessionTerminatedError);
expect(mint).toHaveBeenCalledTimes(1);
expect(repo.writes).toEqual([]);
});
@@ -382,14 +382,14 @@ describe('ensureCodexAccessToken', () => {
// Terminal codes other than invalid_grant signal credential death under
// any race scenario; the cache must not re-read state to second-guess
// them. Assert via the absence of a second getById call.
- const mint = vi.fn().mockRejectedValue(new CodexOAuthSessionTerminatedError({ code: 'app_session_terminated', message: 'gone' }));
- await expect(ensureCodexAccessToken(upstreamId, accountId, mint)).rejects.toBeInstanceOf(CodexOAuthSessionTerminatedError);
+ const mint = vi.fn().mockRejectedValue(new ChatGPTOAuthSessionTerminatedError({ code: 'app_session_terminated', message: 'gone' }));
+ await expect(ensureChatGPTAccessToken(upstreamId, accountId, mint)).rejects.toBeInstanceOf(ChatGPTOAuthSessionTerminatedError);
expect(repo.getById).toHaveBeenCalledTimes(1);
expect(repo.writes).toEqual([]);
});
});
-describe('mintCodexAccessToken', () => {
+describe('mintChatGPTAccessToken', () => {
test('stores the current plan from the refreshed id_token', async () => {
const idToken = [
Buffer.from('{}').toString('base64url'),
@@ -407,7 +407,7 @@ describe('mintCodexAccessToken', () => {
access_token: 'at', refresh_token: 'rt_v2', id_token: idToken, expires_in: 600,
}), { status: 200, headers: { 'content-type': 'application/json' } }));
const persistRotation = vi.fn(async () => {});
- const entry = await mintCodexAccessToken('rt_v1', directFetcher, persistRotation);
+ const entry = await mintChatGPTAccessToken('rt_v1', directFetcher, persistRotation);
expect(entry.planType).toBe('team');
expect(entry.planObservedAt).toBe(entry.refreshedAt);
expect(persistRotation).toHaveBeenCalledWith('rt_v2');
@@ -422,7 +422,7 @@ describe('mintCodexAccessToken', () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
access_token: 'at', refresh_token: 'rt_v2', id_token: idToken, expires_in: 600,
}), { status: 200, headers: { 'content-type': 'application/json' } }));
- const entry = await mintCodexAccessToken('rt_v1', directFetcher, async () => {});
+ const entry = await mintChatGPTAccessToken('rt_v1', directFetcher, async () => {});
expect(entry.planType).toBeUndefined();
});
@@ -436,7 +436,7 @@ describe('mintCodexAccessToken', () => {
access_token: 'at', refresh_token: 'rt_v2', id_token: idToken, expires_in: 600,
}), { status: 200, headers: { 'content-type': 'application/json' } }));
const persistRotation = vi.fn(async () => {});
- await expect(mintCodexAccessToken('rt_v1', directFetcher, persistRotation)).rejects.toThrow(/chatgpt_plan_type/);
+ await expect(mintChatGPTAccessToken('rt_v1', directFetcher, persistRotation)).rejects.toThrow(/chatgpt_plan_type/);
expect(persistRotation).toHaveBeenCalledWith('rt_v2');
});
});
diff --git a/packages/provider-codex/__tests__/auth/credential_test.ts b/packages/provider-chatgpt/__tests__/auth/credential_test.ts
similarity index 84%
rename from packages/provider-codex/__tests__/auth/credential_test.ts
rename to packages/provider-chatgpt/__tests__/auth/credential_test.ts
index 5efc609465..d9d6e0d4a5 100644
--- a/packages/provider-codex/__tests__/auth/credential_test.ts
+++ b/packages/provider-chatgpt/__tests__/auth/credential_test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest';
-import { normalizeCodexCredential } from '../../src/auth/credential.ts';
+import { normalizeChatGPTCredential } from '../../src/auth/credential.ts';
const encodeBase64Url = (text: string): string => btoa(text)
.replace(/\+/g, '-')
@@ -19,9 +19,9 @@ const claims = (accountId: string, extra: Record = {}): Record<
...extra,
});
-describe('normalizeCodexCredential', () => {
+describe('normalizeChatGPTCredential', () => {
test('accepts an opaque access token with an explicit account ID', () => {
- expect(normalizeCodexCredential({ accessToken: 'opaque', chatgptAccountId: 'account' })).toEqual({
+ expect(normalizeChatGPTCredential({ accessToken: 'opaque', chatgptAccountId: 'account' })).toEqual({
accessToken: 'opaque',
refreshToken: null,
expiresAt: null,
@@ -35,7 +35,7 @@ describe('normalizeCodexCredential', () => {
});
test('operator-typed identity wins over both id_token and access_token claims', () => {
- const result = normalizeCodexCredential({
+ const result = normalizeChatGPTCredential({
accessToken: makeJwt(claims('access', { exp: 2_000_000_000 })),
idToken: makeJwt(claims('id')),
chatgptAccountId: 'explicit',
@@ -57,7 +57,7 @@ describe('normalizeCodexCredential', () => {
// and access_token both carry email so we can confirm id wins the tie;
// only access_token carries plan and user id so we also confirm
// access_token still fills in what id_token leaves blank.
- const result = normalizeCodexCredential({
+ const result = normalizeChatGPTCredential({
accessToken: makeJwt({
'https://api.openai.com/auth': {
chatgpt_account_id: 'shared',
@@ -80,7 +80,7 @@ describe('normalizeCodexCredential', () => {
});
test('uses source expiry only when the access token has no exp claim', () => {
- const result = normalizeCodexCredential({
+ const result = normalizeChatGPTCredential({
accessToken: 'opaque',
chatgptAccountId: 'account',
expiresAt: 1_900_000_000_000,
@@ -89,14 +89,14 @@ describe('normalizeCodexCredential', () => {
});
test('rejects conflicting inferred account IDs', () => {
- expect(() => normalizeCodexCredential({
+ expect(() => normalizeChatGPTCredential({
accessToken: makeJwt(claims('access')),
idToken: makeJwt(claims('id')),
})).toThrow(/different ChatGPT accounts/);
});
test('lets an explicit account ID settle a conflict between the two tokens', () => {
- expect(normalizeCodexCredential({
+ expect(normalizeChatGPTCredential({
accessToken: makeJwt(claims('access')),
idToken: makeJwt(claims('id')),
chatgptAccountId: 'operator-choice',
@@ -104,7 +104,7 @@ describe('normalizeCodexCredential', () => {
});
test('keeps the account ID null when no token can provide one', () => {
- expect(normalizeCodexCredential({ accessToken: 'opaque' }).identity).toEqual({
+ expect(normalizeChatGPTCredential({ accessToken: 'opaque' }).identity).toEqual({
chatgptAccountId: null,
email: null,
chatgptUserId: null,
@@ -113,6 +113,6 @@ describe('normalizeCodexCredential', () => {
});
test('rejects an empty access token', () => {
- expect(() => normalizeCodexCredential({ accessToken: '' })).toThrow(/access token/);
+ expect(() => normalizeChatGPTCredential({ accessToken: '' })).toThrow(/access token/);
});
});
diff --git a/packages/provider-codex/__tests__/auth/import_test.ts b/packages/provider-chatgpt/__tests__/auth/import_test.ts
similarity index 79%
rename from packages/provider-codex/__tests__/auth/import_test.ts
rename to packages/provider-chatgpt/__tests__/auth/import_test.ts
index 1e4449dda9..b98f506df9 100644
--- a/packages/provider-codex/__tests__/auth/import_test.ts
+++ b/packages/provider-chatgpt/__tests__/auth/import_test.ts
@@ -1,11 +1,11 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import {
- importCodexFromCallback,
- importCodexFromJson,
- importCodexFromManual,
+ importChatGPTFromCallback,
+ importChatGPTFromJson,
+ importChatGPTFromManual,
parseSourceExpiry,
- previewCodexJson,
+ previewChatGPTJson,
} from '../../src/auth/import.ts';
import { directFetcher, type Fetcher } from '@floway-dev/provider';
@@ -34,15 +34,15 @@ const credential = (overrides: Record = {}): Record vi.restoreAllMocks());
-describe('Codex JSON import', () => {
+describe('ChatGPT JSON import', () => {
test.each([
- ['Codex auth JSON', (value: Record) => ({ tokens: value })],
+ ['ChatGPT auth JSON', (value: Record) => ({ tokens: value })],
['root account', (value: Record) => ({ name: 'Primary', platform: 'openai', type: 'oauth', credentials: value })],
['accounts envelope', (value: Record) => ({ accounts: [{ platform: 'openai', type: 'oauth', credentials: value }] })],
['data.accounts envelope', (value: Record) => ({ data: { accounts: [{ platform: 'openai', type: 'oauth', credentials: value }] } })],
])('previews and imports %s', async (_label, envelope) => {
const raw = JSON.stringify(envelope(credential()));
- const preview = await previewCodexJson(raw);
+ const preview = await previewChatGPTJson(raw);
expect(preview).toEqual([expect.objectContaining({
sourceIndex: 0,
chatgptAccountId: 'account',
@@ -50,7 +50,7 @@ describe('Codex JSON import', () => {
issues: [],
})]);
- const result = await importCodexFromJson(raw, 0);
+ const result = await importChatGPTFromJson(raw, 0);
expect(result.config.accounts).toEqual([{
email: 'person@example.test',
chatgptAccountId: 'account',
@@ -66,8 +66,8 @@ describe('Codex JSON import', () => {
expect(result.state.accounts[0].openaiDeviceId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
});
- test('accepts access-token-only Codex auth JSON', async () => {
- const result = await importCodexFromJson(JSON.stringify({
+ test('accepts access-token-only ChatGPT auth JSON', async () => {
+ const result = await importChatGPTFromJson(JSON.stringify({
tokens: { access_token: makeJwt({ ...identityPayload, exp: 2_000_000_000 }) },
}), 0);
expect(result.state.accounts[0].refresh_token).toBeNull();
@@ -75,7 +75,7 @@ describe('Codex JSON import', () => {
});
test('accepts an opaque auth token when account_id is explicit', async () => {
- const result = await importCodexFromJson(JSON.stringify({
+ const result = await importChatGPTFromJson(JSON.stringify({
tokens: { access_token: 'opaque', account_id: 'explicit-account' },
}), 0);
expect(result.config.accounts[0]).toEqual({
@@ -88,29 +88,29 @@ describe('Codex JSON import', () => {
});
test('rejects malformed, unsupported, or incomplete JSON', async () => {
- await expect(previewCodexJson('not json')).rejects.toThrow(/not valid JSON/);
- await expect(previewCodexJson('null')).rejects.toThrow();
- await expect(previewCodexJson('{}')).rejects.toThrow(/supported structure/);
- const preview = await previewCodexJson(JSON.stringify({ tokens: { account_id: 'account' } }));
+ await expect(previewChatGPTJson('not json')).rejects.toThrow(/not valid JSON/);
+ await expect(previewChatGPTJson('null')).rejects.toThrow();
+ await expect(previewChatGPTJson('{}')).rejects.toThrow(/supported structure/);
+ const preview = await previewChatGPTJson(JSON.stringify({ tokens: { account_id: 'account' } }));
expect(preview[0].issues).toEqual([expect.stringMatching(/access_token/)]);
});
});
-describe('manual Codex import', () => {
+describe('manual ChatGPT import', () => {
test('requires only access token when its JWT carries the account ID', async () => {
- const result = await importCodexFromManual({ access_token: makeJwt(identityPayload) });
+ const result = await importChatGPTFromManual({ access_token: makeJwt(identityPayload) });
expect(result.config.accounts[0].chatgptAccountId).toBe('account');
expect(result.state.accounts[0].refresh_token).toBeNull();
});
test('accepts an opaque access token without an account ID', async () => {
- const result = await importCodexFromManual({ access_token: 'opaque' });
+ const result = await importChatGPTFromManual({ access_token: 'opaque' });
expect(result.config.accounts[0].chatgptAccountId).toBeNull();
expect(result.state.accounts[0].chatgptAccountId).toBeNull();
});
test('accepts an opaque access token plus explicit account ID and ISO expiry', async () => {
- const result = await importCodexFromManual({
+ const result = await importChatGPTFromManual({
access_token: 'opaque',
account_id: 'explicit-account',
expires_at: '2030-01-01T00:00:00.000Z',
@@ -119,7 +119,7 @@ describe('manual Codex import', () => {
});
test('accepts optional email and plan for an opaque credential', async () => {
- const result = await importCodexFromManual({
+ const result = await importChatGPTFromManual({
access_token: 'opaque',
account_id: 'account',
email: 'operator@example.test',
@@ -133,7 +133,7 @@ describe('manual Codex import', () => {
});
test('operator-typed email and plan win over JWT identity claims', async () => {
- const result = await importCodexFromManual({
+ const result = await importChatGPTFromManual({
access_token: makeJwt(identityPayload),
email: 'typed@example.test',
plan_type: 'team',
@@ -145,7 +145,7 @@ describe('manual Codex import', () => {
});
});
-describe('Codex JSON account selection', () => {
+describe('ChatGPT JSON account selection', () => {
test('preserves source indexes while filtering non-OpenAI accounts', async () => {
const raw = JSON.stringify({
accounts: [
@@ -153,9 +153,9 @@ describe('Codex JSON account selection', () => {
{ platform: 'openai', type: 'oauth', credentials: credential({ chatgpt_account_id: 'selected' }) },
],
});
- const preview = await previewCodexJson(raw);
+ const preview = await previewChatGPTJson(raw);
expect(preview.map(candidate => candidate.sourceIndex)).toEqual([1]);
- const result = await importCodexFromJson(raw, 1);
+ const result = await importChatGPTFromJson(raw, 1);
expect(result.config.accounts[0].chatgptAccountId).toBe('selected');
});
@@ -165,10 +165,10 @@ describe('Codex JSON account selection', () => {
platform: 'openai', type: 'oauth', credentials: { access_token: 'private-value' },
}],
});
- const preview = await previewCodexJson(raw);
+ const preview = await previewChatGPTJson(raw);
expect(preview[0]).toMatchObject({ chatgptAccountId: null, issues: [] });
expect(JSON.stringify(preview)).not.toContain('private-value');
- const result = await importCodexFromJson(raw, 0);
+ const result = await importChatGPTFromJson(raw, 0);
expect(result.config.accounts[0].chatgptAccountId).toBeNull();
expect(result.state.accounts[0].chatgptAccountId).toBeNull();
});
@@ -179,15 +179,15 @@ describe('Codex JSON account selection', () => {
{ credentials: credential(), data: { accounts: [] } },
{ accounts: [], data: { accounts: [] } },
])('rejects ambiguous structures', async value => {
- await expect(previewCodexJson(JSON.stringify(value))).rejects.toThrow(/ambiguous/);
+ await expect(previewChatGPTJson(JSON.stringify(value))).rejects.toThrow(/ambiguous/);
});
test('rejects invalid indexes and unsupported selected accounts', async () => {
const single = JSON.stringify({ tokens: credential() });
- await expect(importCodexFromJson(single, -1)).rejects.toThrow(/non-negative integer/);
- await expect(importCodexFromJson(single, 1)).rejects.toThrow(/does not exist/);
- await expect(importCodexFromJson(JSON.stringify({ accounts: [] }), 0)).rejects.toThrow(/does not exist/);
- await expect(importCodexFromJson(JSON.stringify({
+ await expect(importChatGPTFromJson(single, -1)).rejects.toThrow(/non-negative integer/);
+ await expect(importChatGPTFromJson(single, 1)).rejects.toThrow(/does not exist/);
+ await expect(importChatGPTFromJson(JSON.stringify({ accounts: [] }), 0)).rejects.toThrow(/does not exist/);
+ await expect(importChatGPTFromJson(JSON.stringify({
accounts: [{
platform: 'anthropic', type: 'oauth', credentials: credential(),
}],
@@ -208,12 +208,12 @@ describe('parseSourceExpiry', () => {
});
});
-describe('importCodexFromCallback', () => {
+describe('importChatGPTFromCallback', () => {
test('exchanges code and returns config plus state', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
access_token: 'opaque-access', refresh_token: 'refresh-token', id_token: makeJwt(identityPayload), expires_in: 600,
}), { status: 200, headers: { 'content-type': 'application/json' } }));
- const result = await importCodexFromCallback({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher });
+ const result = await importChatGPTFromCallback({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher });
expect(result.config.accounts[0].email).toBe('person@example.test');
expect(result.state.accounts[0].refresh_token).toBe('refresh-token');
expect(result.state.accounts[0].accessToken?.token).toBe('opaque-access');
@@ -227,7 +227,7 @@ describe('importCodexFromCallback', () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
access_token: 'opaque-access', refresh_token: 'refresh-token', id_token: makeJwt({}), expires_in: 600,
}), { status: 200, headers: { 'content-type': 'application/json' } }));
- const result = await importCodexFromCallback({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher });
+ const result = await importChatGPTFromCallback({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher });
expect(result.config.accounts[0].chatgptAccountId).toBeNull();
expect(result.config.accounts[0].email).toBeNull();
});
@@ -236,7 +236,7 @@ describe('importCodexFromCallback', () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify({
access_token: 'opaque-access', refresh_token: 'refresh-token', id_token: makeJwt(identityPayload), expires_in: 600,
}), { status: 200, headers: { 'content-type': 'application/json' } }));
- await importCodexFromCallback({ code: 'CODE', codeVerifier: 'VER', fetcher });
+ await importChatGPTFromCallback({ code: 'CODE', codeVerifier: 'VER', fetcher });
expect(fetcher).toHaveBeenCalledTimes(1);
expect(fetcher.mock.calls[0][0]).toBe('https://auth.openai.com/oauth/token');
});
diff --git a/packages/provider-codex/__tests__/auth/jwt_test.ts b/packages/provider-chatgpt/__tests__/auth/jwt_test.ts
similarity index 66%
rename from packages/provider-codex/__tests__/auth/jwt_test.ts
rename to packages/provider-chatgpt/__tests__/auth/jwt_test.ts
index d4ecc85d59..f7abfb21af 100644
--- a/packages/provider-codex/__tests__/auth/jwt_test.ts
+++ b/packages/provider-chatgpt/__tests__/auth/jwt_test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest';
-import { parseCodexTokenClaims, tryParseCodexAccessTokenClaims } from '../../src/auth/jwt.ts';
+import { parseChatGPTTokenClaims, tryParseChatGPTAccessTokenClaims } from '../../src/auth/jwt.ts';
// Helper builds a minimal JWT with given payload. Signature segment is fake.
const encodeBase64Url = (text: string): string => {
@@ -16,7 +16,7 @@ const makeJwt = (payload: unknown): string => {
return `${header}.${body}.fake-signature`;
};
-describe('parseCodexTokenClaims', () => {
+describe('parseChatGPTTokenClaims', () => {
test('extracts every identity claim a full id_token carries', () => {
const token = makeJwt({
'https://api.openai.com/auth': {
@@ -26,7 +26,7 @@ describe('parseCodexTokenClaims', () => {
},
'https://api.openai.com/profile': { email: 'a@b.com' },
});
- expect(parseCodexTokenClaims(token, 'id_token')).toEqual({
+ expect(parseChatGPTTokenClaims(token, 'id_token')).toEqual({
email: 'a@b.com',
chatgptAccountId: 'acc_123',
chatgptUserId: 'user-abc',
@@ -36,7 +36,7 @@ describe('parseCodexTokenClaims', () => {
});
test('reports a claim the token does not carry as null', () => {
- expect(parseCodexTokenClaims(makeJwt({}), 'id_token')).toEqual({
+ expect(parseChatGPTTokenClaims(makeJwt({}), 'id_token')).toEqual({
email: null,
chatgptAccountId: null,
chatgptUserId: null,
@@ -46,16 +46,16 @@ describe('parseCodexTokenClaims', () => {
});
test('converts exp seconds to milliseconds', () => {
- expect(parseCodexTokenClaims(makeJwt({ exp: 2_000_000_000 })).expiresAt).toBe(2_000_000_000_000);
+ expect(parseChatGPTTokenClaims(makeJwt({ exp: 2_000_000_000 })).expiresAt).toBe(2_000_000_000_000);
});
test('rejects a token without 3 segments', () => {
- expect(() => parseCodexTokenClaims('not.a.jwt.really', 'id_token')).toThrow(/3 segments/);
- expect(() => parseCodexTokenClaims('one.two', 'id_token')).toThrow(/3 segments/);
+ expect(() => parseChatGPTTokenClaims('not.a.jwt.really', 'id_token')).toThrow(/3 segments/);
+ expect(() => parseChatGPTTokenClaims('one.two', 'id_token')).toThrow(/3 segments/);
});
test('rejects a token whose payload is not base64url-decodable JSON', () => {
- expect(() => parseCodexTokenClaims('aaa.!!!.bbb', 'id_token')).toThrow();
+ expect(() => parseChatGPTTokenClaims('aaa.!!!.bbb', 'id_token')).toThrow();
});
test('accepts top-level email when /profile is absent (observed real-world id_token shape)', () => {
@@ -63,24 +63,24 @@ describe('parseCodexTokenClaims', () => {
'https://api.openai.com/auth': { chatgpt_account_id: 'a' },
email: 'top-level@example.com',
});
- expect(parseCodexTokenClaims(token, 'id_token').email).toBe('top-level@example.com');
+ expect(parseChatGPTTokenClaims(token, 'id_token').email).toBe('top-level@example.com');
});
test('handles base64url padding-free encoding (real OpenAI tokens have no padding)', () => {
// encodeBase64Url already strips padding, matching real OpenAI tokens.
const token = makeJwt({ 'https://api.openai.com/auth': { chatgpt_account_id: 'a' } });
- expect(parseCodexTokenClaims(token, 'id_token').chatgptAccountId).toBe('a');
+ expect(parseChatGPTTokenClaims(token, 'id_token').chatgptAccountId).toBe('a');
});
});
-describe('tryParseCodexAccessTokenClaims', () => {
+describe('tryParseChatGPTAccessTokenClaims', () => {
test('reads an opaque access token as carrying no claims rather than failing', () => {
- expect(tryParseCodexAccessTokenClaims('opaque-access-token')).toBeNull();
+ expect(tryParseChatGPTAccessTokenClaims('opaque-access-token')).toBeNull();
});
test('reads claims off an access token that is a JWT', () => {
const token = makeJwt({ 'https://api.openai.com/auth': { chatgpt_account_id: 'acc_from_access' } });
- expect(tryParseCodexAccessTokenClaims(token)?.chatgptAccountId).toBe('acc_from_access');
+ expect(tryParseChatGPTAccessTokenClaims(token)?.chatgptAccountId).toBe('acc_from_access');
});
test('rejects noncanonical JWT payload encodings', () => {
@@ -91,8 +91,8 @@ describe('tryParseCodexAccessTokenClaims', () => {
});
const [header, payload, signature] = token.split('.');
expect(payload?.endsWith('Q')).toBe(true);
- expect(() => parseCodexTokenClaims(`${header}.${payload}=.${signature}`, 'access_token')).toThrow();
- expect(() => parseCodexTokenClaims(`${header}.${payload}\n.${signature}`, 'access_token')).toThrow();
- expect(() => parseCodexTokenClaims(`${header}.${payload!.slice(0, -1)}R.${signature}`, 'access_token')).toThrow();
+ expect(() => parseChatGPTTokenClaims(`${header}.${payload}=.${signature}`, 'access_token')).toThrow();
+ expect(() => parseChatGPTTokenClaims(`${header}.${payload}\n.${signature}`, 'access_token')).toThrow();
+ expect(() => parseChatGPTTokenClaims(`${header}.${payload!.slice(0, -1)}R.${signature}`, 'access_token')).toThrow();
});
});
diff --git a/packages/provider-codex/__tests__/auth/oauth_test.ts b/packages/provider-chatgpt/__tests__/auth/oauth_test.ts
similarity index 71%
rename from packages/provider-codex/__tests__/auth/oauth_test.ts
rename to packages/provider-chatgpt/__tests__/auth/oauth_test.ts
index e7c052cfe1..38b5e83485 100644
--- a/packages/provider-codex/__tests__/auth/oauth_test.ts
+++ b/packages/provider-chatgpt/__tests__/auth/oauth_test.ts
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
-import { buildCodexAuthorizeUrl, CodexOAuthSessionTerminatedError, exchangeCodexAuthorizationCode, refreshCodexAccessToken } from '../../src/auth/oauth.ts';
+import { buildChatGPTAuthorizeUrl, ChatGPTOAuthSessionTerminatedError, exchangeChatGPTAuthorizationCode, refreshChatGPTAccessToken } from '../../src/auth/oauth.ts';
import { directFetcher } from '@floway-dev/provider';
const okResponse = (body: unknown): Response => new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
@@ -8,18 +8,18 @@ const errorResponse = (status: number, body: unknown): Response => new Response(
afterEach(() => vi.restoreAllMocks());
-test('buildCodexAuthorizeUrl preserves the Codex CLI query surface and order', () => {
- expect(buildCodexAuthorizeUrl({ state: 'STATE', codeChallenge: 'CHALLENGE' })).toBe(
+test('buildChatGPTAuthorizeUrl preserves the Codex CLI query surface and order', () => {
+ expect(buildChatGPTAuthorizeUrl({ state: 'STATE', codeChallenge: 'CHALLENGE' })).toBe(
'https://auth.openai.com/oauth/authorize?response_type=code&client_id=app_EMoamEEZ73f0CkXaXp7hrann&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email+offline_access&state=STATE&code_challenge=CHALLENGE&code_challenge_method=S256&id_token_add_organizations=true&codex_cli_simplified_flow=true&originator=codex_cli_rs',
);
});
-describe('exchangeCodexAuthorizationCode', () => {
+describe('exchangeChatGPTAuthorizationCode', () => {
test('POSTs form data and returns parsed tokens', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(okResponse({
access_token: 'at', refresh_token: 'rt', id_token: 'it', expires_in: 600,
}));
- const result = await exchangeCodexAuthorizationCode({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher });
+ const result = await exchangeChatGPTAuthorizationCode({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher });
expect(result).toEqual({ access_token: 'at', refresh_token: 'rt', id_token: 'it', expires_in: 600 });
expect(fetchSpy).toHaveBeenCalledTimes(1);
const [url, init] = fetchSpy.mock.calls[0];
@@ -39,19 +39,19 @@ describe('exchangeCodexAuthorizationCode', () => {
test('throws session-terminated on app_session_terminated', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorResponse(400, { error: { code: 'app_session_terminated', message: 'Session ended' } }));
- await expect(exchangeCodexAuthorizationCode({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher })).rejects.toBeInstanceOf(CodexOAuthSessionTerminatedError);
+ await expect(exchangeChatGPTAuthorizationCode({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher })).rejects.toBeInstanceOf(ChatGPTOAuthSessionTerminatedError);
});
test('throws generic error on other 4xx, message includes status', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorResponse(400, { error: { code: 'invalid_grant', message: 'bad code' } }));
- await expect(exchangeCodexAuthorizationCode({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher })).rejects.toThrow(/400/);
+ await expect(exchangeChatGPTAuthorizationCode({ code: 'CODE', codeVerifier: 'VER', fetcher: directFetcher })).rejects.toThrow(/400/);
});
});
-describe('refreshCodexAccessToken', () => {
+describe('refreshChatGPTAccessToken', () => {
test('POSTs grant_type=refresh_token + scope', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(okResponse({ access_token: 'at2', refresh_token: 'rt2', id_token: 'it2', expires_in: 600 }));
- const result = await refreshCodexAccessToken('rt_old', directFetcher);
+ const result = await refreshChatGPTAccessToken('rt_old', directFetcher);
expect(result.access_token).toBe('at2');
expect(result.refresh_token).toBe('rt2');
const params = new URLSearchParams((fetchSpy.mock.calls[0][1] as RequestInit).body as string);
@@ -61,13 +61,13 @@ describe('refreshCodexAccessToken', () => {
expect(params.get('scope')).toBe('openid profile email offline_access');
});
- test('session-terminated → CodexOAuthSessionTerminatedError', async () => {
+ test('session-terminated → ChatGPTOAuthSessionTerminatedError', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorResponse(400, { error: { code: 'app_session_terminated', message: 'gone' } }));
- await expect(refreshCodexAccessToken('rt_dead', directFetcher)).rejects.toBeInstanceOf(CodexOAuthSessionTerminatedError);
+ await expect(refreshChatGPTAccessToken('rt_dead', directFetcher)).rejects.toBeInstanceOf(ChatGPTOAuthSessionTerminatedError);
});
- test('invalid_grant → CodexOAuthSessionTerminatedError (refresh-only)', async () => {
+ test('invalid_grant → ChatGPTOAuthSessionTerminatedError (refresh-only)', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorResponse(400, { error: { code: 'invalid_grant', message: 'Your refresh token has already been used to generate a new access token. Please try signing in again.' } }));
- await expect(refreshCodexAccessToken('rt_replayed', directFetcher)).rejects.toBeInstanceOf(CodexOAuthSessionTerminatedError);
+ await expect(refreshChatGPTAccessToken('rt_replayed', directFetcher)).rejects.toBeInstanceOf(ChatGPTOAuthSessionTerminatedError);
});
});
diff --git a/packages/provider-codex/__tests__/config_test.ts b/packages/provider-chatgpt/__tests__/config_test.ts
similarity index 69%
rename from packages/provider-codex/__tests__/config_test.ts
rename to packages/provider-chatgpt/__tests__/config_test.ts
index ea646d45d2..680b773915 100644
--- a/packages/provider-codex/__tests__/config_test.ts
+++ b/packages/provider-chatgpt/__tests__/config_test.ts
@@ -1,21 +1,21 @@
import { describe, expect, test } from 'vitest';
-import { assertCodexUpstreamRecord, patchCodexIdentityMetadata, type CodexUpstreamConfig } from '../src/config.ts';
+import { assertChatGPTUpstreamRecord, patchChatGPTIdentityMetadata, type ChatGPTUpstreamConfig } from '../src/config.ts';
import type { UpstreamRecord } from '@floway-dev/provider';
const goodAccount = { email: 'a@b.com', chatgptAccountId: 'a', chatgptUserId: 'u', planType: 'plus' };
-const good: CodexUpstreamConfig = { accounts: [goodAccount] };
+const good: ChatGPTUpstreamConfig = { accounts: [goodAccount] };
const wrap = (config: unknown): UpstreamRecord => ({
- id: 'up', kind: 'codex', name: 'n', enabled: true, usageRefreshIntervalMinutes: 0, sortOrder: 0,
+ id: 'up', kind: 'chatgpt', name: 'n', enabled: true, usageRefreshIntervalMinutes: 0, sortOrder: 0,
createdAt: '', updatedAt: '', config: config as UpstreamRecord['config'], state: null,
flagOverrides: {}, disabledPublicModelIds: [], proxyFallbackList: [], modelPrefix: null, modelsCache: null, hue: 210,
});
-describe('assertCodexUpstreamRecord (config validation)', () => {
+describe('assertChatGPTUpstreamRecord (config validation)', () => {
test('accepts complete identity or explicitly null identity fields', () => {
- expect(() => assertCodexUpstreamRecord(wrap(good))).not.toThrow();
- expect(() => assertCodexUpstreamRecord(wrap({
+ expect(() => assertChatGPTUpstreamRecord(wrap(good))).not.toThrow();
+ expect(() => assertChatGPTUpstreamRecord(wrap({
accounts: [{ email: null, chatgptAccountId: null, chatgptUserId: null, planType: null }],
}))).not.toThrow();
});
@@ -31,29 +31,29 @@ describe('assertCodexUpstreamRecord (config validation)', () => {
['empty accounts array', { accounts: [] }],
['multiple accounts (v1 invariant)', { accounts: [goodAccount, { ...goodAccount, chatgptAccountId: 'b' }] }],
])('rejects %s', (_label, value) => {
- expect(() => assertCodexUpstreamRecord(wrap(value))).toThrow();
+ expect(() => assertChatGPTUpstreamRecord(wrap(value))).toThrow();
});
test('rejects null / non-object configs', () => {
- expect(() => assertCodexUpstreamRecord(wrap(null))).toThrow();
- expect(() => assertCodexUpstreamRecord(wrap('a'))).toThrow();
- expect(() => assertCodexUpstreamRecord(wrap([]))).toThrow();
+ expect(() => assertChatGPTUpstreamRecord(wrap(null))).toThrow();
+ expect(() => assertChatGPTUpstreamRecord(wrap('a'))).toThrow();
+ expect(() => assertChatGPTUpstreamRecord(wrap([]))).toThrow();
});
});
-describe('assertCodexUpstreamRecord (record-level checks)', () => {
- test('rejects non-codex record', () => {
+describe('assertChatGPTUpstreamRecord (record-level checks)', () => {
+ test('rejects non-ChatGPT record', () => {
const record: UpstreamRecord = {
id: 'up', kind: 'copilot', name: 'n', enabled: true, usageRefreshIntervalMinutes: 0, sortOrder: 0,
createdAt: '', updatedAt: '', config: {}, state: null,
flagOverrides: {}, disabledPublicModelIds: [], proxyFallbackList: [], modelPrefix: null, modelsCache: null, hue: 210,
};
- expect(() => assertCodexUpstreamRecord(record)).toThrow();
+ expect(() => assertChatGPTUpstreamRecord(record)).toThrow();
});
});
-describe('patchCodexIdentityMetadata', () => {
+describe('patchChatGPTIdentityMetadata', () => {
test('updates nullable display metadata while retaining the account ID', () => {
- expect(patchCodexIdentityMetadata(good, {
+ expect(patchChatGPTIdentityMetadata(good, {
accounts: [{ email: null, chatgptAccountId: 'a', planType: 'pro' }],
})).toEqual({
accounts: [{ email: null, chatgptAccountId: 'a', chatgptUserId: 'u', planType: 'pro' }],
@@ -61,16 +61,16 @@ describe('patchCodexIdentityMetadata', () => {
});
test('preserves a null account ID and rejects account ID changes', () => {
- const withoutAccountId: CodexUpstreamConfig = {
+ const withoutAccountId: ChatGPTUpstreamConfig = {
accounts: [{ ...goodAccount, chatgptAccountId: null }],
};
- expect(patchCodexIdentityMetadata(withoutAccountId, {
+ expect(patchChatGPTIdentityMetadata(withoutAccountId, {
accounts: [{ chatgptAccountId: null, email: null }],
}).accounts[0].chatgptAccountId).toBeNull();
- expect(() => patchCodexIdentityMetadata(withoutAccountId, {
+ expect(() => patchChatGPTIdentityMetadata(withoutAccountId, {
accounts: [{ chatgptAccountId: 'other' }],
})).toThrow(/only be changed by re-importing/);
- expect(() => patchCodexIdentityMetadata(good, {
+ expect(() => patchChatGPTIdentityMetadata(good, {
accounts: [{ chatgptAccountId: null }],
})).toThrow(/only be changed by re-importing/);
});
@@ -81,6 +81,6 @@ describe('patchCodexIdentityMetadata', () => {
['multiple accounts', { accounts: [goodAccount, goodAccount] }],
['invalid metadata', { accounts: [{ email: '' }] }],
])('rejects %s', (_label, patch) => {
- expect(() => patchCodexIdentityMetadata(good, patch)).toThrow();
+ expect(() => patchChatGPTIdentityMetadata(good, patch)).toThrow();
});
});
diff --git a/packages/provider-codex/__tests__/fetch_test.ts b/packages/provider-chatgpt/__tests__/fetch_test.ts
similarity index 88%
rename from packages/provider-codex/__tests__/fetch_test.ts
rename to packages/provider-chatgpt/__tests__/fetch_test.ts
index a1ad9f4b5d..24ccb15ebf 100644
--- a/packages/provider-codex/__tests__/fetch_test.ts
+++ b/packages/provider-chatgpt/__tests__/fetch_test.ts
@@ -1,22 +1,22 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { createUpstreamStateRepoStub } from './upstream-state-repo.ts';
-import { CODEX_CLI_VERSION, CODEX_ORIGINATOR, CODEX_RESPONSES_LITE_CLIENT_METADATA_KEY, CODEX_RESPONSES_LITE_HEADER, CODEX_USER_AGENT } from '../src/constants.ts';
-import { callCodexAlphaSearch, callCodexOpenAIImagesGenerations, callCodexOpenAIResponses, callCodexOpenAIResponsesCompact, type CodexCallEffects } from '../src/fetch.ts';
+import { CHATGPT_CLI_VERSION, CHATGPT_ORIGINATOR, CHATGPT_RESPONSES_LITE_CLIENT_METADATA_KEY, CHATGPT_RESPONSES_LITE_HEADER, CHATGPT_USER_AGENT } from '../src/constants.ts';
+import { callChatGPTAlphaSearch, callChatGPTOpenAIImagesGenerations, callChatGPTOpenAIResponses, callChatGPTOpenAIResponsesCompact, type ChatGPTCallEffects } from '../src/fetch.ts';
import * as responsesLite from '../src/responses-lite.ts';
-import type { CodexAccessTokenEntry, CodexAccountCredential, CodexQuotaSnapshotEntryMap, CodexUpstreamState } from '../src/state.ts';
+import type { ChatGPTAccessTokenEntry, ChatGPTAccountCredential, ChatGPTQuotaSnapshotEntryMap, ChatGPTUpstreamState } from '../src/state.ts';
import type { ProtocolFrame } from '@floway-dev/protocols/common';
import { type OpenAIResponsesCompactionItemEx, collectOpenAIResponsesProtocolEventsToResult, type CanonicalOpenAIResponsesInputItem, type OpenAIResponsesOutputItemEx, type OpenAIResponsesResultEx, type OpenAIResponsesStreamEventEx } from '@floway-dev/protocols/openai-responses';
import { initProviderRepo, type UpstreamRecord } from '@floway-dev/provider';
import { noopUpstreamCallOptions, readJsonRequest, stubProviderModel } from '@floway-dev/test-utils';
-const makeEffects = (): CodexCallEffects => ({
+const makeEffects = (): ChatGPTCallEffects => ({
persistRefreshTokenRotation: vi.fn(async () => {}),
persistTerminalState: vi.fn(async () => {}),
});
-const activeAccount: CodexAccountCredential = { chatgptAccountId: 'acc', refresh_token: 'rt_v1', state: 'active', state_updated_at: '2026-01-01T00:00:00Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555', accessToken: null, quotaSnapshot: null };
-const accessOnlyAccount: CodexAccountCredential = { ...activeAccount, refresh_token: null };
+const activeAccount: ChatGPTAccountCredential = { chatgptAccountId: 'acc', refresh_token: 'rt_v1', state: 'active', state_updated_at: '2026-01-01T00:00:00Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555', accessToken: null, quotaSnapshot: null };
+const accessOnlyAccount: ChatGPTAccountCredential = { ...activeAccount, refresh_token: null };
const model = stubProviderModel({ id: 'gpt-5.4', display_name: 'gpt-5.4', endpoints: { openaiResponses: {} } });
const liteModel = stubProviderModel({ id: 'future-lite-model', endpoints: { openaiResponses: {} }, providerData: { useResponsesLite: true } });
const imageModel = stubProviderModel({ id: 'gpt-image-2', display_name: 'GPT-Image-2', kind: 'image', endpoints: { openaiImagesGenerations: {}, openaiImagesEdits: {} } });
@@ -24,16 +24,16 @@ const imageModel = stubProviderModel({ id: 'gpt-image-2', display_name: 'GPT-Ima
const upstreamId = 'up_a';
const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
-const farFutureAccessToken: CodexAccessTokenEntry = {
+const farFutureAccessToken: ChatGPTAccessTokenEntry = {
token: 'at_kv',
expiresAt: Date.now() + 24 * 60 * 60 * 1000,
refreshedAt: 'now',
};
-const makeRecord = (state: CodexUpstreamState): UpstreamRecord => ({
+const makeRecord = (state: ChatGPTUpstreamState): UpstreamRecord => ({
id: upstreamId,
- kind: 'codex',
- name: 'Codex',
+ kind: 'chatgpt',
+ name: 'ChatGPT',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 0,
@@ -52,18 +52,18 @@ const makeRecord = (state: CodexUpstreamState): UpstreamRecord => ({
let currentRecord: UpstreamRecord;
// Mirrors what the data-plane refresh hook persists when a fresh token arrives.
-const seedFreshAccessToken = (entry: CodexAccessTokenEntry = farFutureAccessToken): void => {
+const seedFreshAccessToken = (entry: ChatGPTAccessTokenEntry = farFutureAccessToken): void => {
currentRecord = makeRecord({ accounts: [{ ...activeAccount, accessToken: entry }] });
};
-const seedAccountState = (overrides: Partial): void => {
+const seedAccountState = (overrides: Partial): void => {
currentRecord = makeRecord({ accounts: [{ ...activeAccount, ...overrides }] });
};
-const readQuotaEntry = (): CodexQuotaSnapshotEntryMap | null =>
- (currentRecord.state as CodexUpstreamState).accounts[0].quotaSnapshot;
+const readQuotaEntry = (): ChatGPTQuotaSnapshotEntryMap | null =>
+ (currentRecord.state as ChatGPTUpstreamState).accounts[0].quotaSnapshot;
-// putCodexQuota fires-and-forgets via .catch(() => {}); yield to the task
+// putChatGPTQuota fires-and-forgets via .catch(() => {}); yield to the task
// queue so the saveState promise resolves before the caller asserts on state.
const flushMicrotasks = (): Promise => new Promise(resolve => setTimeout(resolve, 0));
@@ -73,7 +73,7 @@ beforeEach(() => {
initProviderRepo(() => ({
recordUpstreamMetric: async () => {},
upstreams: createUpstreamStateRepoStub(() => currentRecord, state => {
- currentRecord = { ...currentRecord, state: state as CodexUpstreamState };
+ currentRecord = { ...currentRecord, state: state as ChatGPTUpstreamState };
}),
}));
});
@@ -102,7 +102,7 @@ const sseResponse = (status = 200): Response => new Response(
const sseEventsResponse = (events: Record[]): Response => new Response(
`${events.map(event => `event: ${event.type as string}\ndata: ${JSON.stringify(event)}\n\n`).join('')}data: [DONE]\n\n`,
- { headers: { 'content-type': 'text/event-stream', [CODEX_RESPONSES_LITE_HEADER]: 'true', 'x-upstream-custom': 'retained' } },
+ { headers: { 'content-type': 'text/event-stream', [CHATGPT_RESPONSES_LITE_HEADER]: 'true', 'x-upstream-custom': 'retained' } },
);
const errorJson = (status: number, body: unknown, extraHeaders: Record = {}): Response =>
@@ -127,9 +127,9 @@ const idTokenWithoutPlan = (): string => [
Buffer.from('signature').toString('base64url'),
].join('.');
-describe('callCodexOpenAIResponses — gates', () => {
+describe('callChatGPTOpenAIResponses — gates', () => {
test('refuses non-active state with synthetic 503', async () => {
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: { ...activeAccount, state: 'session_terminated' },
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -153,7 +153,7 @@ describe('callCodexOpenAIResponses — gates', () => {
},
});
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -175,7 +175,7 @@ describe('callCodexOpenAIResponses — gates', () => {
const body = { error: { type: 'usage_limit_reached', message: 'controlled upstream rejection' } };
const upstream = errorJson(429, body, { 'retry-after': '60', 'x-upstream-request-id': 'fixture-429', 'x-codex-active-limit': 'premium', 'x-codex-primary-used-percent': '100', 'x-codex-primary-reset-after-seconds': '60' });
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(upstream);
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
expect(fetchSpy).toHaveBeenCalledTimes(1);
@@ -188,13 +188,13 @@ describe('callCodexOpenAIResponses — gates', () => {
});
});
-describe('callCodexOpenAIResponses — token freshness', () => {
+describe('callChatGPTOpenAIResponses — token freshness', () => {
test('refreshes before call when no cached access token', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(new Response(JSON.stringify({ access_token: 'at_new', refresh_token: 'rt_v2', id_token: idToken(), expires_in: 600 }), { status: 200 }))
.mockResolvedValueOnce(sseResponse());
const effects = makeEffects();
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects, call: noopUpstreamCallOptions(),
});
@@ -203,13 +203,13 @@ describe('callCodexOpenAIResponses — token freshness', () => {
const openaiResponsesInit = fetchSpy.mock.calls[1][1] as RequestInit;
expect(new Headers(openaiResponsesInit.headers).get('authorization')).toBe('Bearer at_new');
expect(effects.persistRefreshTokenRotation).toHaveBeenCalledWith('rt_v2');
- expect((currentRecord.state as CodexUpstreamState).accounts[0].accessToken?.token).toBe('at_new');
+ expect((currentRecord.state as ChatGPTUpstreamState).accounts[0].accessToken?.token).toBe('at_new');
});
test('reuses fresh state-cached access token without refreshing', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -223,7 +223,7 @@ describe('callCodexOpenAIResponses — token freshness', () => {
accessToken: { token: 'at_only', expiresAt: null, refreshedAt: 'now' },
});
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: accessOnlyAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -238,7 +238,7 @@ describe('callCodexOpenAIResponses — token freshness', () => {
accessToken: { token: 'at_only', expiresAt: Date.now() - 1, refreshedAt: 'now' },
});
const fetchSpy = vi.spyOn(globalThis, 'fetch');
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: accessOnlyAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -253,7 +253,7 @@ describe('callCodexOpenAIResponses — token freshness', () => {
test('persistTerminalState refresh_failed when /oauth/token returns app_session_terminated', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(errorJson(400, { error: { code: 'app_session_terminated', message: 'gone' } }));
const effects = makeEffects();
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects, call: noopUpstreamCallOptions(),
});
@@ -262,7 +262,7 @@ describe('callCodexOpenAIResponses — token freshness', () => {
});
});
-describe('Codex terminal output recovery', () => {
+describe('ChatGPT terminal output recovery', () => {
test.each([false, true])('restores native compaction before stream and result consumption with Responses Lite=%s', async useResponsesLite => {
const item: OpenAIResponsesCompactionItemEx = {
type: 'compaction', id: 'cmp_native', encrypted_content: 'OPAQUE_NATIVE_BLOB',
@@ -283,7 +283,7 @@ describe('Codex terminal output recovery', () => {
{ type: 'response.completed', response: terminal },
]));
const effects = makeEffects();
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model: { ...model, providerData: { useResponsesLite } },
body: { input: [{ type: 'compaction_trigger' } as unknown as CanonicalOpenAIResponsesInputItem], stream },
headers: new Headers(), effects, call: noopUpstreamCallOptions(),
@@ -327,7 +327,7 @@ describe('Codex terminal output recovery', () => {
{ type: 'response.output_item.done', output_index: 1, item: message },
{ type: 'response.completed', response: terminal },
]));
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model: { ...model, providerData: { useResponsesLite } },
body: { input: [], tools: [{ type: 'function', name: 'lookup', parameters: {} }], stream: false },
headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
@@ -338,7 +338,7 @@ describe('Codex terminal output recovery', () => {
});
});
-describe('Codex private Responses wire selection', () => {
+describe('Codex Responses wire selection', () => {
const tool = { type: 'function' as const, name: 'lookup', parameters: { type: 'object' } };
test.each([true, false, undefined])('catalog flag %s alone selects generate and compact wire formats', async useResponsesLite => {
@@ -358,12 +358,12 @@ describe('Codex private Responses wire selection', () => {
tool_choice: 'auto' as const,
text: { verbosity: 'high' as const },
service_tier: 'future_tier',
- client_metadata: { [CODEX_RESPONSES_LITE_CLIENT_METADATA_KEY]: marker, retained: { future: true } },
+ client_metadata: { [CHATGPT_RESPONSES_LITE_CLIENT_METADATA_KEY]: marker, retained: { future: true } },
temperature: 0.5,
};
const original = structuredClone(body);
- const headers = new Headers(marker === undefined ? {} : { [CODEX_RESPONSES_LITE_HEADER]: marker });
- const call = action === 'generate' ? callCodexOpenAIResponses : callCodexOpenAIResponsesCompact;
+ const headers = new Headers(marker === undefined ? {} : { [CHATGPT_RESPONSES_LITE_HEADER]: marker });
+ const call = action === 'generate' ? callChatGPTOpenAIResponses : callChatGPTOpenAIResponsesCompact;
const result = await call({
upstreamId, account: activeAccount,
model: { ...model, providerData: useResponsesLite === undefined ? undefined : { useResponsesLite } },
@@ -374,10 +374,10 @@ describe('Codex private Responses wire selection', () => {
expect(url).toBe(`https://chatgpt.com/backend-api/codex/responses${action === 'compact' ? '/compact' : ''}`);
const wire = await readJsonRequest(init as RequestInit) as Record;
const wireHeaders = new Headers(init?.headers);
- expect(wireHeaders.get(CODEX_RESPONSES_LITE_HEADER)).toBe(useResponsesLite ? 'true' : null);
+ expect(wireHeaders.get(CHATGPT_RESPONSES_LITE_HEADER)).toBe(useResponsesLite ? 'true' : null);
expect(wireHeaders.get('user-agent')).toBe('codex_cli_rs/0.159.3 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10');
expect(wireHeaders.get('version')).toBe('0.159.3');
- expect(headers.get(CODEX_RESPONSES_LITE_HEADER)).toBe(marker ?? null);
+ expect(headers.get(CHATGPT_RESPONSES_LITE_HEADER)).toBe(marker ?? null);
expect(body).toEqual(original);
expect(wire.text).toEqual(body.text);
expect(wire.service_tier).toBe('future_tier');
@@ -400,11 +400,11 @@ describe('Codex private Responses wire selection', () => {
}
if ('events' in result) {
expect(wire.client_metadata).toMatchObject({ retained: { future: true } });
- expect(wire.client_metadata).not.toHaveProperty(CODEX_RESPONSES_LITE_CLIENT_METADATA_KEY);
+ expect(wire.client_metadata).not.toHaveProperty(CHATGPT_RESPONSES_LITE_CLIENT_METADATA_KEY);
expect(wire.tool_choice).toBe('auto');
expect(wire.stream).toBe(true);
expect(wire.store).toBe(false);
- expect(result.headers?.get(CODEX_RESPONSES_LITE_HEADER)).toBe('true');
+ expect(result.headers?.get(CHATGPT_RESPONSES_LITE_HEADER)).toBe('true');
expect(result.headers?.get('x-upstream-custom')).toBe('retained');
for await (const _frame of result.events) { /* Drain the actual parser. */ }
} else {
@@ -417,7 +417,7 @@ describe('Codex private Responses wire selection', () => {
test.each(['generate', 'compact'] as const)('%s rejects malformed model data before any fetch', async action => {
const fetchSpy = vi.spyOn(globalThis, 'fetch');
- const call = action === 'generate' ? callCodexOpenAIResponses : callCodexOpenAIResponsesCompact;
+ const call = action === 'generate' ? callChatGPTOpenAIResponses : callChatGPTOpenAIResponsesCompact;
const opts = { upstreamId, account: activeAccount, body: { input: [] }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions() };
await expect(call({ ...opts, model: { ...model, providerData: { useResponsesLite: 'false' } } })).rejects.toThrow('useResponsesLite is not a boolean');
expect(fetchSpy).not.toHaveBeenCalled();
@@ -443,13 +443,13 @@ describe('Codex private Responses wire selection', () => {
upstream.headers.delete('content-type');
vi.spyOn(globalThis, 'fetch').mockResolvedValue(upstream);
const body = { input: [], instructions: 'Base', tools: [tool], parallel_tool_calls: true, reasoning: { effort: 'low', context: 'current_turn' }, stream };
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model: liteModel, body, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
if (!result.ok) throw new Error('expected successful Responses stream');
expect(result.headers?.get('content-type')).toBe('text/event-stream');
- expect(result.headers?.has(CODEX_RESPONSES_LITE_HEADER)).toBe(true);
- expect(upstream.headers.get(CODEX_RESPONSES_LITE_HEADER)).toBe('true');
+ expect(result.headers?.has(CHATGPT_RESPONSES_LITE_HEADER)).toBe(true);
+ expect(upstream.headers.get(CHATGPT_RESPONSES_LITE_HEADER)).toBe('true');
if (stream) {
const frames: ProtocolFrame[] = [];
for await (const frame of result.events) frames.push(frame);
@@ -479,7 +479,7 @@ describe('Codex private Responses wire selection', () => {
{ type: 'response.completed', response: resource },
];
vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseEventsResponse(events));
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model: liteModel,
body: {
input: [
@@ -510,7 +510,7 @@ describe('Codex private Responses wire selection', () => {
{ type: 'response.completed', response: resource },
]));
const requested = { effort: 'low', context: 'current_turn' };
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model: { ...model, providerData: { useResponsesLite } },
body: { input: [], ...(omitted ? {} : { reasoning: requested, parallel_tool_calls: true }) },
headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
@@ -531,7 +531,7 @@ describe('Codex private Responses wire selection', () => {
test.each(['top-level', 'input', 'mixed'] as const)('preserves the complete compact %s output window', async source => {
seedFreshAccessToken();
const opaque = { type: 'compaction', id: 'cmp_item', encrypted_content: 'opaque+encrypted==' };
- const body: responsesLite.CodexResponsesBody = {
+ const body: responsesLite.ChatGPTResponsesBody = {
instructions: 'Old instructions',
...(source === 'input' ? {} : { tools: [tool] }),
input: [
@@ -543,12 +543,12 @@ describe('Codex private Responses wire selection', () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
const wire = await readJsonRequest(init as RequestInit) as Record;
sentInput = wire.input as unknown[];
- expect(new Headers(init?.headers).get(CODEX_RESPONSES_LITE_HEADER)).toBe('true');
+ expect(new Headers(init?.headers).get(CHATGPT_RESPONSES_LITE_HEADER)).toBe('true');
expect(String(url)).toContain('/compact');
return Response.json({ id: 'cmp_resource', object: 'response.compaction', output: [...wire.input as unknown[], opaque] });
});
const opts = { upstreamId, account: activeAccount, model: liteModel, headers: new Headers({ 'thread-id': 'compact-replay-thread' }), effects: makeEffects(), call: noopUpstreamCallOptions() };
- const compact = await callCodexOpenAIResponsesCompact({ ...opts, body });
+ const compact = await callChatGPTOpenAIResponsesCompact({ ...opts, body });
if (!compact.ok) throw new Error('expected successful compact response');
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(compact.result.output).toEqual([...sentInput!, opaque]);
@@ -564,8 +564,8 @@ describe('Codex private Responses wire selection', () => {
],
future: { retained: true },
};
- vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify(upstream), { headers: { [CODEX_RESPONSES_LITE_HEADER]: 'true' } }));
- const result = await callCodexOpenAIResponsesCompact({
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify(upstream), { headers: { [CHATGPT_RESPONSES_LITE_HEADER]: 'true' } }));
+ const result = await callChatGPTOpenAIResponsesCompact({
upstreamId, account: activeAccount, model: liteModel,
body: { input: [], tools: [{ type: 'custom', name: 'patch' }] }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -576,7 +576,7 @@ describe('Codex private Responses wire selection', () => {
test.each(['generate', 'compact'] as const)('%s reuses prepared bytes and metadata choice across 401 retry', async action => {
seedFreshAccessToken();
const selectedModel = { ...liteModel, providerData: { useResponsesLite: true } };
- const encode = vi.spyOn(responsesLite, 'encodeCodexResponsesLiteRequest');
+ const encode = vi.spyOn(responsesLite, 'encodeChatGPTResponsesLiteRequest');
const bodies: string[] = [];
const headers: Headers[] = [];
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
@@ -589,7 +589,7 @@ describe('Codex private Responses wire selection', () => {
}
return action === 'generate' ? sseEventsResponse([]) : compactJsonResponse();
});
- const call = action === 'generate' ? callCodexOpenAIResponses : callCodexOpenAIResponsesCompact;
+ const call = action === 'generate' ? callChatGPTOpenAIResponses : callChatGPTOpenAIResponsesCompact;
const result = await call({
upstreamId, account: activeAccount, model: selectedModel,
body: { input: [], tools: [tool], instructions: 'Base' }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
@@ -601,8 +601,8 @@ describe('Codex private Responses wire selection', () => {
expect(bodies[1]).toBe(bodies[0]);
expect(JSON.parse(bodies[0]!).input[0].id).toMatch(/^at_/);
expect(headers[0]!.get('thread-id')).toMatch(UUID_V7_RE);
- expect(headers[0]!.get(CODEX_RESPONSES_LITE_HEADER)).toBe('true');
- expect(headers[1]!.get(CODEX_RESPONSES_LITE_HEADER)).toBe('true');
+ expect(headers[0]!.get(CHATGPT_RESPONSES_LITE_HEADER)).toBe('true');
+ expect(headers[1]!.get(CHATGPT_RESPONSES_LITE_HEADER)).toBe('true');
expect(headers[0]!.get('authorization')).toBe('Bearer at_kv');
expect(headers[1]!.get('authorization')).toBe('Bearer at_retry');
headers.forEach(header => header.delete('authorization'));
@@ -614,17 +614,17 @@ describe('Codex private Responses wire selection', () => {
for (const status of [400, 429, 503]) {
for (const marker of [undefined, 'true', 'false']) {
const upstream = new Response('upstream bytes\nnot necessarily JSON', {
- status, statusText: 'Upstream failure', headers: { 'content-type': 'application/problem+json', 'x-upstream-custom': 'retained', ...(marker === undefined ? {} : { [CODEX_RESPONSES_LITE_HEADER]: marker }) },
+ status, statusText: 'Upstream failure', headers: { 'content-type': 'application/problem+json', 'x-upstream-custom': 'retained', ...(marker === undefined ? {} : { [CHATGPT_RESPONSES_LITE_HEADER]: marker }) },
});
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(upstream);
- const call = action === 'generate' ? callCodexOpenAIResponses : callCodexOpenAIResponsesCompact;
+ const call = action === 'generate' ? callChatGPTOpenAIResponses : callChatGPTOpenAIResponsesCompact;
const result = await call({
upstreamId, account: activeAccount, model: liteModel, body: { input: [] },
- headers: new Headers({ [CODEX_RESPONSES_LITE_HEADER]: 'false' }), effects: makeEffects(), call: noopUpstreamCallOptions(),
+ headers: new Headers({ [CHATGPT_RESPONSES_LITE_HEADER]: 'false' }), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
if (result.ok) throw new Error('expected upstream error');
expect(result.response).toBe(upstream);
- expect(result.response.headers.get(CODEX_RESPONSES_LITE_HEADER)).toBe(marker ?? null);
+ expect(result.response.headers.get(CHATGPT_RESPONSES_LITE_HEADER)).toBe(marker ?? null);
expect(result.response.headers.get('content-type')).toBe('application/problem+json');
expect(result.response.statusText).toBe('Upstream failure');
expect(await result.response.text()).toBe('upstream bytes\nnot necessarily JSON');
@@ -633,11 +633,11 @@ describe('Codex private Responses wire selection', () => {
});
});
-describe('callCodexOpenAIResponses — upstream classification', () => {
+describe('callChatGPTOpenAIResponses — upstream classification', () => {
test('happy path: 200 → ok:true, quota persisted', async () => {
seedFreshAccessToken();
vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -651,9 +651,9 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('upstream body has store:false and stream:true forced even if caller passes otherwise', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
- model, body: { input: [], stream: false as unknown as true, store: true } as unknown as Parameters[0]['body'],
+ model, body: { input: [], stream: false as unknown as true, store: true } as unknown as Parameters[0]['body'],
headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
const body = await readJsonRequest(fetchSpy.mock.calls[0][1] as RequestInit) as Record;
@@ -665,14 +665,14 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('builds Codex responses headers and metadata from a clean set', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model,
body: {
input: [],
stream: true,
client_metadata: { 'x-codex-installation-id': 'downstream-installation' },
- } as unknown as Parameters[0]['body'],
+ } as unknown as Parameters[0]['body'],
headers: new Headers({
'cf-connecting-ip': '203.0.113.10',
forwarded: 'for=203.0.113.10',
@@ -694,9 +694,9 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
const headers = new Headers((fetchSpy.mock.calls[0][1] as RequestInit).headers);
expect(headers.get('authorization')).toBe('Bearer at_kv');
expect(headers.get('chatgpt-account-id')).toBe('acc');
- expect(headers.get('originator')).toBe(CODEX_ORIGINATOR);
- expect(headers.get('user-agent')).toBe(CODEX_USER_AGENT);
- expect(headers.get('version')).toBe(CODEX_CLI_VERSION);
+ expect(headers.get('originator')).toBe(CHATGPT_ORIGINATOR);
+ expect(headers.get('user-agent')).toBe(CHATGPT_USER_AGENT);
+ expect(headers.get('version')).toBe(CHATGPT_CLI_VERSION);
expect(headers.get('accept')).toBe('text/event-stream');
expect(headers.get('content-type')).toBe('application/json');
expect(headers.get('session-id')).toBe('downstream-session');
@@ -745,7 +745,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
accessToken: { token: 'at_only', expiresAt: null, refreshedAt: 'now' },
});
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId,
account,
model,
@@ -769,10 +769,10 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
headers: new Headers({ 'session-id': 'stable-session' }),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
- } satisfies Parameters[0];
+ } satisfies Parameters[0];
- await callCodexOpenAIResponses(request);
- await callCodexOpenAIResponses({ ...request, headers: new Headers({ 'session-id': 'stable-session' }) });
+ await callChatGPTOpenAIResponses(request);
+ await callChatGPTOpenAIResponses({ ...request, headers: new Headers({ 'session-id': 'stable-session' }) });
const firstHeaders = new Headers((fetchSpy.mock.calls[0][1] as RequestInit).headers);
const secondHeaders = new Headers((fetchSpy.mock.calls[1][1] as RequestInit).headers);
@@ -799,14 +799,14 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: { input: [], stream: true },
headers: new Headers({ 'session-id': 'session-a' }),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
});
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: { input: [], stream: true },
headers: new Headers({ 'session-id': 'session-b' }),
@@ -836,21 +836,21 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: { input: [], stream: true },
headers: new Headers({ 'session-id': 'cache-session' }),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
});
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: { input: [], stream: true, prompt_cache_key: 'caller-cache-key' },
headers: new Headers({ 'session-id': 'cache-session' }),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
});
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: { input: [], stream: true, prompt_cache_key: null },
headers: new Headers({ 'session-id': 'cache-session' }),
@@ -869,7 +869,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('preserves a hyphenated Codex session id for prompt cache', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model,
body: { input: [], stream: true },
@@ -886,7 +886,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('canonicalizes downstream session_id to the Codex session-id header', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model,
body: { input: [], stream: true },
@@ -903,7 +903,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('prefers downstream session-id over session_id when both are provided', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model,
body: { input: [], stream: true },
@@ -920,7 +920,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('generates a Codex session id when the downstream request has none', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -940,13 +940,13 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
instructions: 'You are helpful.',
input: [{ type: 'message', role: 'user', content: 'hello' }],
stream: true,
- } as unknown as Parameters[0]['body'],
+ } as unknown as Parameters[0]['body'],
headers: new Headers(),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
- } satisfies Parameters[0];
- await callCodexOpenAIResponses(turn);
- await callCodexOpenAIResponses(turn);
+ } satisfies Parameters[0];
+ await callChatGPTOpenAIResponses(turn);
+ await callChatGPTOpenAIResponses(turn);
const first = new Headers((fetchSpy.mock.calls[0][1] as RequestInit).headers).get('session-id');
const second = new Headers((fetchSpy.mock.calls[1][1] as RequestInit).headers).get('session-id');
@@ -958,13 +958,13 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('derives distinct session ids when only the instructions differ', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- const call = (instructions: string) => callCodexOpenAIResponses({
+ const call = (instructions: string) => callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: {
instructions,
input: [{ type: 'message', role: 'user', content: 'hello' }],
stream: true,
- } as unknown as Parameters[0]['body'],
+ } as unknown as Parameters[0]['body'],
headers: new Headers(),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
@@ -980,13 +980,13 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('derives distinct session ids when only the first user message differs', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- const call = (content: string) => callCodexOpenAIResponses({
+ const call = (content: string) => callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: {
instructions: 'System.',
input: [{ type: 'message', role: 'user', content }],
stream: true,
- } as unknown as Parameters[0]['body'],
+ } as unknown as Parameters[0]['body'],
headers: new Headers(),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
@@ -1003,7 +1003,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
const deviceId = '22222222-3333-4444-9555-666666666666';
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: { ...activeAccount, openaiDeviceId: deviceId },
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -1018,13 +1018,13 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('prefers a caller-supplied installation id from client_metadata over the account device id', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: { ...activeAccount, openaiDeviceId: 'account-device-id' },
model,
body: {
input: [], stream: true,
client_metadata: { 'x-codex-installation-id': 'caller-installation-id' },
- } as unknown as Parameters[0]['body'],
+ } as unknown as Parameters[0]['body'],
headers: new Headers(),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
@@ -1040,7 +1040,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('passes through caller thread-id and x-client-request-id when distinct from session-id', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: { input: [], stream: true },
headers: new Headers({
@@ -1064,7 +1064,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('merges caller-supplied x-codex-turn-metadata extras over the synthesized blob', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: { input: [], stream: true },
headers: new Headers({
@@ -1098,7 +1098,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('reads the turn-metadata blob from the body before the header', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: {
input: [], stream: true,
@@ -1112,7 +1112,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
turn_started_at_unix_ms: 1700000000002,
}),
},
- } as unknown as Parameters[0]['body'],
+ } as unknown as Parameters[0]['body'],
// A WebSocket upgrade's headers are frozen for the life of the socket,
// so they carry the connection's first turn, not this one.
headers: new Headers({
@@ -1144,14 +1144,14 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
const toolNamespacesInfo = { namespaces: [{ name: 'shell', tools: ['exec'] }] };
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: {
input: [], stream: true,
client_metadata: {
'x-codex-turn-metadata': JSON.stringify({ tool_namespaces_info: toolNamespacesInfo, thread_source: 'user' }),
},
- } as unknown as Parameters[0]['body'],
+ } as unknown as Parameters[0]['body'],
headers: new Headers({ 'session-id': 'sess' }),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
@@ -1173,12 +1173,12 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
test('preserves caller client_metadata extras while keeping identity-mirror keys gateway-owned', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model,
body: {
input: [], stream: true,
client_metadata: { 'x-extra-key': 'caller-supplied', session_id: ' ' },
- } as unknown as Parameters[0]['body'],
+ } as unknown as Parameters[0]['body'],
headers: new Headers({ 'session-id': 'header-session' }),
effects: makeEffects(),
call: noopUpstreamCallOptions(),
@@ -1201,7 +1201,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
seedFreshAccessToken();
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorJson(401, { error: { code: 'token_invalidated', message: 'session ended' } }));
const effects = makeEffects();
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects, call: noopUpstreamCallOptions(),
});
@@ -1218,7 +1218,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
const upstreamBody = { error: { code: 'token_invalidated', message: 're-import required' } };
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorJson(401, upstreamBody, { 'x-upstream-marker': 'kept' }));
const persistTerminalState = vi.fn(async () => { throw new Error('state write failed'); });
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: accessOnlyAccount,
model, body: { input: [], stream: true }, headers: new Headers(),
effects: { ...makeEffects(), persistTerminalState }, call: noopUpstreamCallOptions(),
@@ -1240,7 +1240,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
.mockResolvedValueOnce(new Response(JSON.stringify({ access_token: 'at2', refresh_token: 'rt_v2', id_token: idToken(), expires_in: 600 }), { status: 200 }))
.mockResolvedValueOnce(errorJson(401, { error: { code: 'expired_token', message: 'still expired' } }));
const effects = makeEffects();
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects, call: noopUpstreamCallOptions(),
});
@@ -1258,7 +1258,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
'x-codex-primary-reset-after-seconds': '3600',
'x-codex-secondary-reset-after-seconds': '7200',
}));
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -1273,7 +1273,7 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
seedFreshAccessToken();
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorJson(503, { error: 'unavailable' }));
const effects = makeEffects();
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects, call: noopUpstreamCallOptions(),
});
@@ -1290,15 +1290,15 @@ describe('callCodexOpenAIResponses — upstream classification', () => {
.mockResolvedValueOnce(sseResponse(401))
.mockResolvedValueOnce(errorJson(200, { access_token: 'at2', refresh_token: 'rt2', id_token: idTokenWithoutPlan(), expires_in: 600 }))
.mockResolvedValueOnce(sseResponse());
- const result = await callCodexOpenAIResponses({
+ const result = await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount, model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
expect(result.ok).toBe(true);
- expect((currentRecord.state as CodexUpstreamState).accounts[0].accessToken?.planType).toBe('free');
+ expect((currentRecord.state as ChatGPTUpstreamState).accounts[0].accessToken?.planType).toBe('free');
});
});
-describe('callCodexOpenAIResponses — background-write registration', () => {
+describe('callChatGPTOpenAIResponses — background-write registration', () => {
// Background state writes (quota snapshot on 2xx/429, access-token put on
// 401-retry) must reach the runtime's waitUntil slot so workerd does not
// cancel them the instant the streaming response returns to the client.
@@ -1308,7 +1308,7 @@ describe('callCodexOpenAIResponses — background-write registration', () => {
seedFreshAccessToken();
vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
const waitUntil = vi.fn<(promise: Promise) => void>();
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(),
call: { ...noopUpstreamCallOptions(), waitUntil },
@@ -1323,7 +1323,7 @@ describe('callCodexOpenAIResponses — background-write registration', () => {
.mockResolvedValueOnce(new Response(JSON.stringify({ access_token: 'at2', refresh_token: 'rt_v2', id_token: idToken(), expires_in: 600 }), { status: 200 }))
.mockResolvedValueOnce(sseResponse());
const waitUntil = vi.fn<(promise: Promise) => void>();
- await callCodexOpenAIResponses({
+ await callChatGPTOpenAIResponses({
upstreamId, account: activeAccount,
model, body: { input: [], stream: true }, headers: new Headers(), effects: makeEffects(),
call: { ...noopUpstreamCallOptions(), waitUntil },
@@ -1331,13 +1331,13 @@ describe('callCodexOpenAIResponses — background-write registration', () => {
// The access-token write is awaited because its CAS result carries the
// effective plan; only the successful retry's quota write is backgrounded.
expect(waitUntil).toHaveBeenCalledTimes(1);
- expect((currentRecord.state as CodexUpstreamState).accounts[0].accessToken?.token).toBe('at2');
+ expect((currentRecord.state as ChatGPTUpstreamState).accounts[0].accessToken?.token).toBe('at2');
});
});
-describe('callCodexOpenAIImagesGenerations', () => {
+describe('callChatGPTOpenAIImagesGenerations', () => {
test('does not replace a meaningful quota snapshot with a headerless image response', async () => {
- const quotaSnapshot: CodexQuotaSnapshotEntryMap = {
+ const quotaSnapshot: ChatGPTQuotaSnapshotEntryMap = {
premium: {
fetchedAt: 1,
data: { observed_at: '2026-01-01T00:00:00Z', active_limit: 'premium', primary_used_percent: 42 },
@@ -1346,9 +1346,9 @@ describe('callCodexOpenAIImagesGenerations', () => {
seedAccountState({ accessToken: { ...farFutureAccessToken, planType: 'plus' }, quotaSnapshot });
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorJson(200, { created: 1, data: [{ b64_json: 'aW1hZ2U=' }] }));
const waitUntil = vi.fn<(promise: Promise) => void>();
- const result = await callCodexOpenAIImagesGenerations({
+ const result = await callChatGPTOpenAIImagesGenerations({
upstreamId,
- account: (currentRecord.state as CodexUpstreamState).accounts[0],
+ account: (currentRecord.state as ChatGPTUpstreamState).accounts[0],
model: imageModel,
body: { prompt: 'an orange circle' },
fallbackPlanType: 'plus',
@@ -1367,9 +1367,9 @@ describe('callCodexOpenAIImagesGenerations', () => {
.mockResolvedValueOnce(errorJson(401, { error: { code: 'expired_token', message: 'expired' } }))
.mockResolvedValueOnce(errorJson(200, { access_token: 'at2', refresh_token: 'rt_v2', id_token: idToken('plus'), expires_in: 600 }))
.mockResolvedValueOnce(errorJson(200, { created: 1, data: [{ b64_json: 'aW1hZ2U=' }] }));
- const result = await callCodexOpenAIImagesGenerations({
+ const result = await callChatGPTOpenAIImagesGenerations({
upstreamId,
- account: (currentRecord.state as CodexUpstreamState).accounts[0],
+ account: (currentRecord.state as ChatGPTUpstreamState).accounts[0],
model: imageModel,
body: { prompt: 'an orange circle' },
fallbackPlanType: 'plus',
@@ -1385,8 +1385,8 @@ describe('callCodexOpenAIImagesGenerations', () => {
expect(firstHeaders.get('originator')).toBe('chatgpt_cca');
expect(secondHeaders.get('originator')).toBe('chatgpt_cca');
for (const headers of [firstHeaders, secondHeaders]) {
- expect(headers.get('version')).toBe(CODEX_CLI_VERSION);
- expect(headers.get('user-agent')).toBe(CODEX_USER_AGENT);
+ expect(headers.get('version')).toBe(CHATGPT_CLI_VERSION);
+ expect(headers.get('user-agent')).toBe(CHATGPT_USER_AGENT);
}
expect(firstHeaders.get('x-codex-image-turn-id')).toMatch(UUID_V7_RE);
expect(secondHeaders.get('x-codex-image-turn-id')).toBe(firstHeaders.get('x-codex-image-turn-id'));
@@ -1397,9 +1397,9 @@ describe('callCodexOpenAIImagesGenerations', () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(errorJson(200, {
access_token: 'at2', refresh_token: 'rt_v2', id_token: idToken('free'), expires_in: 600,
}));
- const result = await callCodexOpenAIImagesGenerations({
+ const result = await callChatGPTOpenAIImagesGenerations({
upstreamId,
- account: (currentRecord.state as CodexUpstreamState).accounts[0],
+ account: (currentRecord.state as ChatGPTUpstreamState).accounts[0],
model: imageModel,
body: { prompt: 'an orange circle' },
fallbackPlanType: 'plus',
@@ -1419,9 +1419,9 @@ describe('callCodexOpenAIImagesGenerations', () => {
.mockResolvedValueOnce(errorJson(200, {
access_token: 'at2', refresh_token: 'rt_v2', id_token: idToken('free'), expires_in: 600,
}));
- const result = await callCodexOpenAIImagesGenerations({
+ const result = await callChatGPTOpenAIImagesGenerations({
upstreamId,
- account: (currentRecord.state as CodexUpstreamState).accounts[0],
+ account: (currentRecord.state as ChatGPTUpstreamState).accounts[0],
model: imageModel,
body: { prompt: 'an orange circle' },
fallbackPlanType: 'plus',
@@ -1435,7 +1435,7 @@ describe('callCodexOpenAIImagesGenerations', () => {
test('does not invalidate a sibling token that won before the 401 was handled', async () => {
seedFreshAccessToken({ ...farFutureAccessToken, token: 'at_failed', planType: 'plus' });
- const winner: CodexAccessTokenEntry = {
+ const winner: ChatGPTAccessTokenEntry = {
token: 'at_winner',
expiresAt: farFutureAccessToken.expiresAt,
refreshedAt: '2026-08-10T00:00:02.000Z',
@@ -1446,7 +1446,7 @@ describe('callCodexOpenAIImagesGenerations', () => {
seedAccountState({ refresh_token: 'rt_winner', accessToken: winner });
return errorJson(401, { error: { code: 'expired_token', message: 'expired' } });
});
- const result = await callCodexOpenAIImagesGenerations({
+ const result = await callChatGPTOpenAIImagesGenerations({
upstreamId,
account: { ...activeAccount, accessToken: { ...farFutureAccessToken, token: 'at_failed', planType: 'plus' } },
model: imageModel,
@@ -1458,7 +1458,7 @@ describe('callCodexOpenAIImagesGenerations', () => {
});
expect(result.response.status).toBe(403);
expect(fetchSpy).toHaveBeenCalledTimes(1);
- expect((currentRecord.state as CodexUpstreamState).accounts[0].accessToken).toEqual(winner);
+ expect((currentRecord.state as ChatGPTUpstreamState).accounts[0].accessToken).toEqual(winner);
});
test('keeps the latest known plan when a retry refresh omits the plan claim', async () => {
@@ -1469,9 +1469,9 @@ describe('callCodexOpenAIImagesGenerations', () => {
access_token: 'at2', refresh_token: 'rt_v2', id_token: idTokenWithoutPlan(), expires_in: 600,
}))
.mockResolvedValueOnce(errorJson(200, { created: 1, data: [{ b64_json: 'aW1hZ2U=' }] }));
- const result = await callCodexOpenAIImagesGenerations({
+ const result = await callChatGPTOpenAIImagesGenerations({
upstreamId,
- account: (currentRecord.state as CodexUpstreamState).accounts[0],
+ account: (currentRecord.state as ChatGPTUpstreamState).accounts[0],
model: imageModel,
body: { prompt: 'an orange circle' },
fallbackPlanType: 'free',
@@ -1482,13 +1482,13 @@ describe('callCodexOpenAIImagesGenerations', () => {
expect(result.response.status).toBe(200);
expect(fetchSpy.mock.calls.filter(([url]) => String(url).includes('/images/generations'))).toHaveLength(2);
await flushMicrotasks();
- expect((currentRecord.state as CodexUpstreamState).accounts[0].accessToken?.planType).toBe('plus');
+ expect((currentRecord.state as ChatGPTUpstreamState).accounts[0].accessToken?.planType).toBe('plus');
});
});
-// `callCodexOpenAIResponsesCompact` shares OAuth + quota + 401-retry plumbing with
-// `callCodexOpenAIResponses` (both go through `prepareCodexCall` →
-// `dispatchCodexHttpCall` → `refreshAccessTokenForRetry`). The streaming
+// `callChatGPTOpenAIResponsesCompact` shares OAuth + quota + 401-retry plumbing with
+// `callChatGPTOpenAIResponses` (both go through `prepareChatGPTCall` →
+// `dispatchChatGPTHttpCall` → `refreshAccessTokenForRetry`). The streaming
// suite above pins those shared paths; this block exercises only the
// compact-specific wire contract — endpoint URL, `Accept: application/json`,
// body shape (no `stream`, no `store`), unary JSON decoding — plus the 401
@@ -1513,11 +1513,11 @@ const compactJsonResponse = (overrides?: Partial): Resp
}),
});
-describe('callCodexOpenAIResponsesCompact', () => {
+describe('callChatGPTOpenAIResponsesCompact', () => {
test('posts to /codex/responses/compact with application/json and no stream/store', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(compactJsonResponse());
- const result = await callCodexOpenAIResponsesCompact({
+ const result = await callChatGPTOpenAIResponsesCompact({
upstreamId, account: activeAccount, model,
body: { input: [{ type: 'message', role: 'user', content: 'hello' }] },
headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
@@ -1544,7 +1544,7 @@ describe('callCodexOpenAIResponsesCompact', () => {
seedFreshAccessToken();
vi.spyOn(globalThis, 'fetch').mockResolvedValue(compactJsonResponse());
const waitUntil = vi.fn<(promise: Promise) => void>();
- await callCodexOpenAIResponsesCompact({
+ await callChatGPTOpenAIResponsesCompact({
upstreamId, account: activeAccount, model,
body: { input: [] }, headers: new Headers(), effects: makeEffects(),
call: { ...noopUpstreamCallOptions(), waitUntil },
@@ -1559,7 +1559,7 @@ describe('callCodexOpenAIResponsesCompact', () => {
.mockResolvedValueOnce(new Response(JSON.stringify({ access_token: 'at2', refresh_token: 'rt_v2', id_token: idToken(), expires_in: 600 }), { status: 200 }))
.mockResolvedValueOnce(compactJsonResponse());
const effects = makeEffects();
- const result = await callCodexOpenAIResponsesCompact({
+ const result = await callChatGPTOpenAIResponsesCompact({
upstreamId, account: activeAccount, model,
body: { input: [] }, headers: new Headers(), effects, call: noopUpstreamCallOptions(),
});
@@ -1577,7 +1577,7 @@ describe('callCodexOpenAIResponsesCompact', () => {
.mockResolvedValueOnce(errorJson(401, { error: { code: 'expired_token', message: 'expired' } }))
.mockResolvedValueOnce(errorJson(200, { access_token: 'at2', refresh_token: 'rt2', id_token: idTokenWithoutPlan(), expires_in: 600 }))
.mockResolvedValueOnce(compactJsonResponse());
- const result = await callCodexOpenAIResponsesCompact({
+ const result = await callChatGPTOpenAIResponsesCompact({
upstreamId,
account: activeAccount,
model,
@@ -1587,14 +1587,14 @@ describe('callCodexOpenAIResponsesCompact', () => {
call: noopUpstreamCallOptions(),
});
expect(result.ok).toBe(true);
- expect((currentRecord.state as CodexUpstreamState).accounts[0].accessToken?.planType).toBe('free');
+ expect((currentRecord.state as ChatGPTUpstreamState).accounts[0].accessToken?.planType).toBe('free');
});
test('401 token_invalidated → persistTerminalState session_terminated, return synthetic 503', async () => {
seedFreshAccessToken();
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorJson(401, { error: { code: 'token_invalidated', message: 'session ended' } }));
const effects = makeEffects();
- const result = await callCodexOpenAIResponsesCompact({
+ const result = await callChatGPTOpenAIResponsesCompact({
upstreamId, account: activeAccount, model,
body: { input: [] }, headers: new Headers(), effects, call: noopUpstreamCallOptions(),
});
@@ -1612,7 +1612,7 @@ describe('callCodexOpenAIResponsesCompact', () => {
'x-codex-primary-reset-after-seconds': '3600',
'x-codex-secondary-reset-after-seconds': '7200',
}));
- const result = await callCodexOpenAIResponsesCompact({
+ const result = await callChatGPTOpenAIResponsesCompact({
upstreamId, account: activeAccount, model,
body: { input: [] }, headers: new Headers(), effects: makeEffects(), call: noopUpstreamCallOptions(),
});
@@ -1627,7 +1627,7 @@ describe('callCodexOpenAIResponsesCompact', () => {
seedFreshAccessToken();
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorJson(503, { error: 'unavailable' }));
const effects = makeEffects();
- const result = await callCodexOpenAIResponsesCompact({
+ const result = await callChatGPTOpenAIResponsesCompact({
upstreamId, account: activeAccount, model,
body: { input: [] }, headers: new Headers(), effects, call: noopUpstreamCallOptions(),
});
@@ -1639,7 +1639,7 @@ describe('callCodexOpenAIResponsesCompact', () => {
});
-describe('callCodexAlphaSearch', () => {
+describe('callChatGPTAlphaSearch', () => {
test('posts the search request to the ChatGPT Codex endpoint with selected model and account auth', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({
@@ -1648,7 +1648,7 @@ describe('callCodexAlphaSearch', () => {
results: [],
}), { status: 200, headers: { 'content-type': 'application/json' } }));
- const result = await callCodexAlphaSearch({
+ const result = await callChatGPTAlphaSearch({
upstreamId,
account: activeAccount,
model,
@@ -1680,7 +1680,7 @@ describe('callCodexAlphaSearch', () => {
.mockResolvedValueOnce(errorJson(401, { error: { code: 'expired_token', message: 'expired' } }))
.mockResolvedValueOnce(errorJson(200, { access_token: 'at2', refresh_token: 'rt2', id_token: idTokenWithoutPlan(), expires_in: 600 }))
.mockResolvedValueOnce(errorJson(200, { encrypted_output: null, output: 'Search result', results: [] }));
- const result = await callCodexAlphaSearch({
+ const result = await callChatGPTAlphaSearch({
upstreamId,
account: activeAccount,
model,
@@ -1690,14 +1690,14 @@ describe('callCodexAlphaSearch', () => {
call: noopUpstreamCallOptions(),
});
expect(result.response.status).toBe(200);
- expect((currentRecord.state as CodexUpstreamState).accounts[0].accessToken?.planType).toBe('free');
+ expect((currentRecord.state as ChatGPTUpstreamState).accounts[0].accessToken?.planType).toBe('free');
});
test('normalizes a missing request id and omits absent turn metadata', async () => {
seedFreshAccessToken();
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({ output: 'Search result' }), { status: 200 }));
- await callCodexAlphaSearch({
+ await callChatGPTAlphaSearch({
upstreamId,
account: activeAccount,
model,
diff --git a/packages/provider-codex/__tests__/ids_test.ts b/packages/provider-chatgpt/__tests__/ids_test.ts
similarity index 100%
rename from packages/provider-codex/__tests__/ids_test.ts
rename to packages/provider-chatgpt/__tests__/ids_test.ts
diff --git a/packages/provider-codex/__tests__/interceptors/openai-responses/action-pivot_test.ts b/packages/provider-chatgpt/__tests__/interceptors/openai-responses/action-pivot_test.ts
similarity index 92%
rename from packages/provider-codex/__tests__/interceptors/openai-responses/action-pivot_test.ts
rename to packages/provider-chatgpt/__tests__/interceptors/openai-responses/action-pivot_test.ts
index adc67636a9..7a3441ff84 100644
--- a/packages/provider-codex/__tests__/interceptors/openai-responses/action-pivot_test.ts
+++ b/packages/provider-chatgpt/__tests__/interceptors/openai-responses/action-pivot_test.ts
@@ -22,21 +22,21 @@ vi.mock('../../../src/interceptors/openai-responses/index.ts', async () => {
const original = await vi.importActual('../../../src/interceptors/openai-responses/index.ts');
return {
...original,
- CODEX_OPENAI_RESPONSES_BOUNDARY: [...original.CODEX_OPENAI_RESPONSES_BOUNDARY, pivotGenerateToCompact],
+ CHATGPT_OPENAI_RESPONSES_BOUNDARY: [...original.CHATGPT_OPENAI_RESPONSES_BOUNDARY, pivotGenerateToCompact],
};
});
// Imports below MUST follow the vi.mock so the provider module resolves
// against the mocked chain on first import.
-const { createCodexProvider } = await import('../../../src/provider.ts');
+const { createChatGPTProvider } = await import('../../../src/provider.ts');
const { noopUpstreamCallOptions, stubProviderModel } = await import('@floway-dev/test-utils');
const farFutureMs = Date.now() + 24 * 60 * 60 * 1000;
const baseRecord: UpstreamRecord = {
- id: 'up_codex_pivot',
- kind: 'codex',
- name: 'Codex (pivot tester)',
+ id: 'up_chatgpt_pivot',
+ kind: 'chatgpt',
+ name: 'ChatGPT (pivot tester)',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 0,
@@ -73,7 +73,7 @@ const compactJsonResponse = (): Response => new Response(
{ status: 200, headers: { 'content-type': 'application/json' } },
);
-test.each([true, false])('Codex projects a post-chain compact pivot after catalog-selected encoding (Lite=%s)', async useResponsesLite => {
+test.each([true, false])('ChatGPT projects a post-chain compact pivot after catalog-selected encoding (Lite=%s)', async useResponsesLite => {
let compactUrl: string | undefined;
let compactBody: Record | undefined;
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
@@ -87,7 +87,7 @@ test.each([true, false])('Codex projects a post-chain compact pivot after catalo
throw new Error(`unexpected fetch ${url}`);
});
- const instance = createCodexProvider(baseRecord);
+ const instance = createChatGPTProvider(baseRecord);
const result = await instance.instance.callOpenAIResponses(
stubProviderModel({ id: 'gpt-5.4', display_name: 'gpt-5.4', endpoints: { openaiResponses: {} }, providerData: { useResponsesLite } }),
{
diff --git a/packages/provider-codex/__tests__/interceptors/openai-responses/inject-default-instructions_test.ts b/packages/provider-chatgpt/__tests__/interceptors/openai-responses/inject-default-instructions_test.ts
similarity index 100%
rename from packages/provider-codex/__tests__/interceptors/openai-responses/inject-default-instructions_test.ts
rename to packages/provider-chatgpt/__tests__/interceptors/openai-responses/inject-default-instructions_test.ts
diff --git a/packages/provider-codex/__tests__/interceptors/openai-responses/strip-unsupported-fields_test.ts b/packages/provider-chatgpt/__tests__/interceptors/openai-responses/strip-unsupported-fields_test.ts
similarity index 100%
rename from packages/provider-codex/__tests__/interceptors/openai-responses/strip-unsupported-fields_test.ts
rename to packages/provider-chatgpt/__tests__/interceptors/openai-responses/strip-unsupported-fields_test.ts
diff --git a/packages/provider-codex/__tests__/models_test.ts b/packages/provider-chatgpt/__tests__/models_test.ts
similarity index 72%
rename from packages/provider-codex/__tests__/models_test.ts
rename to packages/provider-chatgpt/__tests__/models_test.ts
index 7137a49acc..ab3ac6e754 100644
--- a/packages/provider-codex/__tests__/models_test.ts
+++ b/packages/provider-chatgpt/__tests__/models_test.ts
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
-import { CODEX_CLI_VERSION, CODEX_ORIGINATOR, CODEX_USER_AGENT } from '../src/constants.ts';
-import { codexImageProviderModel, codexModelContextWindow, codexModelUsesResponsesLite, codexPlanSupportsImages, codexRawToProviderModel, fetchCodexCatalog, type CodexRawModel } from '../src/models.ts';
+import { CHATGPT_CLI_VERSION, CHATGPT_ORIGINATOR, CHATGPT_USER_AGENT } from '../src/constants.ts';
+import { chatgptImageProviderModel, chatgptModelContextWindow, chatgptModelUsesResponsesLite, chatgptPlanSupportsImages, chatgptRawToProviderModel, fetchChatGPTCatalog, type ChatGPTRawModel } from '../src/models.ts';
import { priceRequest } from '@floway-dev/protocols/common';
import { directFetcher, type FlagId } from '@floway-dev/provider';
@@ -9,14 +9,14 @@ const okJson = (body: unknown): Response => new Response(JSON.stringify(body), {
afterEach(() => vi.restoreAllMocks());
-test('Codex models share the upstream-bound OpenAI opaque blob scope', () => {
- const model = codexRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, new Set());
+test('ChatGPT models share the upstream-bound OpenAI opaque blob scope', () => {
+ const model = chatgptRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, new Set());
expect(model.upstreamModelId).toBe('gpt-5.4');
expect(model.opaqueBlobCompatibilityScope).toEqual({ bindToUpstream: true, key: 'openai' });
- expect(codexImageProviderModel(new Set()).opaqueBlobCompatibilityScope).toEqual({ bindToUpstream: true, key: 'openai' });
+ expect(chatgptImageProviderModel(new Set()).opaqueBlobCompatibilityScope).toEqual({ bindToUpstream: true, key: 'openai' });
});
-describe('fetchCodexCatalog', () => {
+describe('fetchChatGPTCatalog', () => {
test('calls /codex/models with auth + identity headers, returns parsed catalog from {models: [...]}', async () => {
const spy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [
@@ -25,19 +25,19 @@ describe('fetchCodexCatalog', () => {
{ slug: 'codex-auto-review', display_name: 'Codex Auto Review', visibility: 'hide', context_window: 272000, max_context_window: 1000000 },
],
}));
- const catalog = await fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
+ const catalog = await fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
expect(catalog).toHaveLength(3);
expect(catalog[0]).toEqual({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000, max_context_window: 1000000 });
expect(catalog[2]).toEqual({ id: 'codex-auto-review', display_name: 'Codex Auto Review', context_window: 272000, max_context_window: 1000000 });
expect(spy).toHaveBeenCalledTimes(1);
const [url, init] = spy.mock.calls[0];
- expect(url).toBe(`https://chatgpt.com/backend-api/codex/models?client_version=${CODEX_CLI_VERSION}`);
+ expect(url).toBe(`https://chatgpt.com/backend-api/codex/models?client_version=${CHATGPT_CLI_VERSION}`);
const headers = new Headers((init as RequestInit | undefined)?.headers);
expect(headers.get('authorization')).toBe('Bearer at');
expect(headers.get('chatgpt-account-id')).toBe('acc');
- expect(headers.get('originator')).toBe(CODEX_ORIGINATOR);
- expect(headers.get('user-agent')).toBe(CODEX_USER_AGENT);
- expect(headers.get('user-agent')).toBe(`codex_cli_rs/${CODEX_CLI_VERSION} (Mac OS 26.5.0; arm64) iTerm.app/3.6.10`);
+ expect(headers.get('originator')).toBe(CHATGPT_ORIGINATOR);
+ expect(headers.get('user-agent')).toBe(CHATGPT_USER_AGENT);
+ expect(headers.get('user-agent')).toBe(`codex_cli_rs/${CHATGPT_CLI_VERSION} (Mac OS 26.5.0; arm64) iTerm.app/3.6.10`);
expect(headers.get('openai-beta')).toBeNull();
});
@@ -51,16 +51,16 @@ describe('fetchCodexCatalog', () => {
supported_reasoning_levels: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'].map(effort => ({ effort })),
}],
}));
- const [raw] = await fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
- const model = codexRawToProviderModel(raw, new Set());
+ const [raw] = await fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
+ const model = chatgptRawToProviderModel(raw, new Set());
expect(spy.mock.calls[0][0]).toBe('https://chatgpt.com/backend-api/codex/models?client_version=0.159.3');
const headers = new Headers(spy.mock.calls[0][1]?.headers);
expect(headers.get('user-agent')).toBe('codex_cli_rs/0.159.3 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10');
expect(headers.get('version')).toBe('0.159.3');
expect(model.upstreamModelId).toBe('gpt-6.1-sol');
expect(model.limits.max_context_window_tokens).toBe(872000);
- expect(codexModelContextWindow(model)).toEqual({ context_window: 272000, max_context_window: 872000 });
- expect(codexModelUsesResponsesLite(model)).toBe(true);
+ expect(chatgptModelContextWindow(model)).toEqual({ context_window: 272000, max_context_window: 872000 });
+ expect(chatgptModelUsesResponsesLite(model)).toBe(true);
expect(model.chat).toEqual({
modalities: { input: ['text', 'image'], output: ['text'] },
reasoning: { effort: { supported: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], default: 'low' } },
@@ -70,7 +70,7 @@ describe('fetchCodexCatalog', () => {
test('omits the account header when the account ID is unknown', async () => {
const spy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({ models: [] }));
- await fetchCodexCatalog({ accessToken: 'at', accountId: null, fetcher: directFetcher });
+ await fetchChatGPTCatalog({ accessToken: 'at', accountId: null, fetcher: directFetcher });
const headers = new Headers((spy.mock.calls[0][1] as RequestInit).headers);
expect(headers.get('authorization')).toBe('Bearer at');
expect(headers.get('chatgpt-account-id')).toBeNull();
@@ -78,49 +78,49 @@ describe('fetchCodexCatalog', () => {
test('preserves the upstream response when catalog fetch fails', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('{"error":"unauthorized"}', { status: 401, headers: { 'retry-after': '30' } }));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toMatchObject({
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toMatchObject({
displayResponse: { status: 401, headers: [['content-type', 'text/plain;charset=UTF-8'], ['retry-after', '30']], body: '{\n "error": "unauthorized"\n}' },
});
});
test('throws on missing models key (forward-compatible shape guard)', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({ data: [] }));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/models array/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/models array/);
});
test('throws on entry missing slug', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({ models: [{ display_name: 'no slug here' }] }));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/slug/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/slug/);
});
test('throws on entry missing display_name', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({ models: [{ slug: 'gpt-x', context_window: 1 }] }));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/display_name/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/display_name/);
});
test('throws on entry missing context_window', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({ models: [{ slug: 'gpt-x', display_name: 'GPT-X' }] }));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/context_window/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/context_window/);
});
test.each([0, -1, 1.5, '872000', {}, Number.MAX_SAFE_INTEGER + 1])('rejects invalid maximum %j', async max_context_window => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [{ slug: 'future-model', display_name: 'Future', context_window: 272000, max_context_window }],
}));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: null, fetcher: directFetcher })).rejects.toThrow(/max_context_window/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: null, fetcher: directFetcher })).rejects.toThrow(/max_context_window/);
});
test.each([undefined, null])('preserves an unspecified maximum %j', async max_context_window => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [{ slug: 'future-model', display_name: 'Future', context_window: 272000, max_context_window }],
}));
- const [raw] = await fetchCodexCatalog({ accessToken: 'at', accountId: null, fetcher: directFetcher });
- const model = codexRawToProviderModel(raw, new Set());
+ const [raw] = await fetchChatGPTCatalog({ accessToken: 'at', accountId: null, fetcher: directFetcher });
+ const model = chatgptRawToProviderModel(raw, new Set());
expect(model.limits).toEqual({});
- expect(codexModelContextWindow(model)).toEqual({ context_window: 272000 });
+ expect(chatgptModelContextWindow(model)).toEqual({ context_window: 272000 });
});
- test('carries input_modalities, supported_reasoning_levels, default_reasoning_level through to CodexRawModel', async () => {
+ test('carries input_modalities, supported_reasoning_levels, default_reasoning_level through to ChatGPTRawModel', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [{
slug: 'gpt-5.5',
@@ -135,7 +135,7 @@ describe('fetchCodexCatalog', () => {
default_reasoning_level: 'medium',
}],
}));
- const catalog = await fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
+ const catalog = await fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
expect(catalog).toHaveLength(1);
expect(catalog[0]).toEqual({
id: 'gpt-5.5',
@@ -147,14 +147,14 @@ describe('fetchCodexCatalog', () => {
});
});
- test('carries supports_image_detail_original through to CodexRawModel', async () => {
+ test('carries supports_image_detail_original through to ChatGPTRawModel', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [
{ slug: 'gpt-img', display_name: 'GPT-Img', context_window: 1, supports_image_detail_original: true },
{ slug: 'gpt-noimg', display_name: 'GPT-NoImg', context_window: 1, supports_image_detail_original: false },
],
}));
- const catalog = await fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
+ const catalog = await fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
expect(catalog[0].image_detail_original).toBe(true);
expect(catalog[1].image_detail_original).toBe(false);
});
@@ -163,7 +163,7 @@ describe('fetchCodexCatalog', () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [{ slug: 'gpt-old', display_name: 'GPT-Old', context_window: 100000 }],
}));
- const catalog = await fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
+ const catalog = await fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
expect(catalog[0]).toEqual({ id: 'gpt-old', display_name: 'GPT-Old', context_window: 100000 });
expect(catalog[0].input_modalities).toBeUndefined();
expect(catalog[0].reasoning_efforts).toBeUndefined();
@@ -175,7 +175,7 @@ describe('fetchCodexCatalog', () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [{ slug: 'gpt-x', display_name: 'GPT-X', context_window: 1, supports_image_detail_original: 'yes' }],
}));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/supports_image_detail_original not a boolean/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/supports_image_detail_original not a boolean/);
});
test('parses the Responses Lite catalog flag without inferring from model names', async () => {
@@ -186,7 +186,7 @@ describe('fetchCodexCatalog', () => {
{ slug: 'legacy-model', display_name: 'Legacy', context_window: 3 },
],
}));
- const catalog = await fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
+ const catalog = await fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher });
expect(catalog.map(model => model.use_responses_lite)).toEqual([true, false, undefined]);
});
@@ -194,87 +194,87 @@ describe('fetchCodexCatalog', () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [{ slug: 'gpt-x', display_name: 'GPT-X', context_window: 1, use_responses_lite }],
}));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/use_responses_lite not a boolean/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/use_responses_lite not a boolean/);
});
test('throws on malformed input_modalities entry (unknown modality)', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [{ slug: 'gpt-x', display_name: 'GPT-X', context_window: 1, input_modalities: ['video'] }],
}));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/modality/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/modality/);
});
test('throws on malformed supported_reasoning_levels entry (missing effort)', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(okJson({
models: [{ slug: 'gpt-x', display_name: 'GPT-X', context_window: 1, supported_reasoning_levels: [{ description: 'no effort field' }] }],
}));
- await expect(fetchCodexCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/reasoning level entry malformed/);
+ await expect(fetchChatGPTCatalog({ accessToken: 'at', accountId: 'acc', fetcher: directFetcher })).rejects.toThrow(/reasoning level entry malformed/);
});
});
-describe('codexRawToProviderModel', () => {
+describe('chatgptRawToProviderModel', () => {
// The mapper just threads `enabledFlags` through onto the produced model;
// these unit tests exercise the rest of the shape with the empty set, and
// a dedicated test asserts the threading.
const noFlags: ReadonlySet = new Set();
test('exposes the maximum publicly and retains the default in private provider data', () => {
- const m = codexRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000, max_context_window: 872000 }, noFlags);
+ const m = chatgptRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000, max_context_window: 872000 }, noFlags);
expect(m.id).toBe('gpt-5.4');
expect(m.display_name).toBe('GPT-5.4');
expect(m.endpoints).toEqual({ openaiResponses: {} });
expect(m.kind).toBe('chat');
expect(m.limits.max_context_window_tokens).toBe(872000);
- expect(codexModelContextWindow(m)).toEqual({ context_window: 272000, max_context_window: 872000 });
+ expect(chatgptModelContextWindow(m)).toEqual({ context_window: 272000, max_context_window: 872000 });
expect(m.owned_by).toBe('openai');
});
test.each([true, false, undefined])('keeps catalog flag %s in opaque provider data only', use_responses_lite => {
- const model = codexRawToProviderModel({
+ const model = chatgptRawToProviderModel({
id: 'future-model', display_name: 'Future Model', context_window: 1, use_responses_lite,
}, noFlags);
expect(model.providerData).toEqual({ contextWindow: 1, useResponsesLite: use_responses_lite ?? false });
- expect(codexModelUsesResponsesLite(model)).toBe(use_responses_lite ?? false);
+ expect(chatgptModelUsesResponsesLite(model)).toBe(use_responses_lite ?? false);
expect(model.endpoints).toEqual({ openaiResponses: {} });
expect(model).not.toHaveProperty('useResponsesLite');
expect(model).not.toHaveProperty('use_responses_lite');
});
test('rejects a default exceeding the declared maximum', () => {
- expect(() => codexRawToProviderModel({ id: 'future-model', display_name: 'Future', context_window: 872000, max_context_window: 272000 }, noFlags))
+ expect(() => chatgptRawToProviderModel({ id: 'future-model', display_name: 'Future', context_window: 872000, max_context_window: 272000 }, noFlags))
.toThrow(/context_window exceeds max_context_window/);
});
test('recovers separate windows after provider metadata is serialized', () => {
- const model = codexRawToProviderModel({ id: 'future-model', display_name: 'Future', context_window: 80000, max_context_window: 600000 }, noFlags);
+ const model = chatgptRawToProviderModel({ id: 'future-model', display_name: 'Future', context_window: 80000, max_context_window: 600000 }, noFlags);
const restored = { ...model, providerData: JSON.parse(JSON.stringify(model.providerData)) as unknown };
- expect(codexModelContextWindow(restored)).toEqual({ context_window: 80000, max_context_window: 600000 });
+ expect(chatgptModelContextWindow(restored)).toEqual({ context_window: 80000, max_context_window: 600000 });
});
test.each([undefined, {}, { contextWindow: 0 }, { contextWindow: '272000' }])('rejects missing or invalid persisted default %j', providerData => {
- const model = codexRawToProviderModel({ id: 'future-model', display_name: 'Future', context_window: 272000 }, noFlags);
- expect(() => codexModelContextWindow({ ...model, providerData })).toThrow();
+ const model = chatgptRawToProviderModel({ id: 'future-model', display_name: 'Future', context_window: 272000 }, noFlags);
+ expect(() => chatgptModelContextWindow({ ...model, providerData })).toThrow();
});
test.each(['true', null, 1, {}, []])('rejects malformed raw and persisted flag %j', value => {
const raw = { id: 'gpt-x', display_name: 'GPT-X', context_window: 1 };
- expect(() => codexRawToProviderModel({ ...raw, use_responses_lite: value } as CodexRawModel, noFlags)).toThrow(/use_responses_lite not a boolean/);
- const model = codexRawToProviderModel(raw, noFlags);
- expect(() => codexModelUsesResponsesLite({ ...model, providerData: { useResponsesLite: value } })).toThrow(/useResponsesLite is not a boolean/);
+ expect(() => chatgptRawToProviderModel({ ...raw, use_responses_lite: value } as ChatGPTRawModel, noFlags)).toThrow(/use_responses_lite not a boolean/);
+ const model = chatgptRawToProviderModel(raw, noFlags);
+ expect(() => chatgptModelUsesResponsesLite({ ...model, providerData: { useResponsesLite: value } })).toThrow(/useResponsesLite is not a boolean/);
});
test.each([null, 'true', 1, []])('rejects malformed persisted providerData %j', providerData => {
- const model = codexRawToProviderModel({ id: 'gpt-x', display_name: 'GPT-X', context_window: 1 }, noFlags);
- expect(() => codexModelUsesResponsesLite({ ...model, providerData })).toThrow(/providerData is not an object/);
+ const model = chatgptRawToProviderModel({ id: 'gpt-x', display_name: 'GPT-X', context_window: 1 }, noFlags);
+ expect(() => chatgptModelUsesResponsesLite({ ...model, providerData })).toThrow(/providerData is not an object/);
});
test.each([undefined, {}, { unrelated: true }])('defaults missing persisted metadata to Standard: %j', providerData => {
- const model = codexRawToProviderModel({ id: 'gpt-6-astra', display_name: 'GPT-6 Astra', context_window: 1 }, noFlags);
- expect(codexModelUsesResponsesLite({ ...model, providerData })).toBe(false);
+ const model = chatgptRawToProviderModel({ id: 'gpt-6-astra', display_name: 'GPT-6 Astra', context_window: 1 }, noFlags);
+ expect(chatgptModelUsesResponsesLite({ ...model, providerData })).toBe(false);
});
test('attaches OpenAI-API-rate pricing for known slugs and treats codex-auto-review as gpt-5.4', () => {
- const flagship = codexRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, noFlags);
+ const flagship = chatgptRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, noFlags);
expect(flagship.pricing).toEqual({
entries: [
{ rates: { input_tokens: '0.0000025', input_cache_read_tokens: '0.00000025', output_tokens: '0.000015' } },
@@ -283,14 +283,14 @@ describe('codexRawToProviderModel', () => {
{ selector: { inputTokens: { operator: 'gt', value: 272000 } }, rates: { input_tokens: '0.000005', input_cache_read_tokens: '0.0000005', output_tokens: '0.0000225' } },
],
});
- const review = codexRawToProviderModel({ id: 'codex-auto-review', display_name: 'Codex Auto Review', context_window: 272000 }, noFlags);
+ const review = chatgptRawToProviderModel({ id: 'codex-auto-review', display_name: 'Codex Auto Review', context_window: 272000 }, noFlags);
expect(review.pricing).toEqual(flagship.pricing);
});
// End-to-end resolution check: serviceTier selectors must match the wire
// values billableServiceTier persists, not Codex's Rust enum names.
test('service-tier entries resolve through the wire-value strings', () => {
- const flagship = codexRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, noFlags);
+ const flagship = chatgptRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, noFlags);
if (!flagship.pricing) throw new Error('expected pricing to be defined');
expect(priceRequest(flagship.pricing, { serviceTier: 'priority', inputTokens: 0 }).rates).toEqual({
@@ -311,18 +311,18 @@ describe('codexRawToProviderModel', () => {
});
test('omits pricing for unknown slugs (forward-compat with new upstream models)', () => {
- const m = codexRawToProviderModel({ id: 'gpt-future-unreleased', display_name: 'X', context_window: 1 }, noFlags);
+ const m = chatgptRawToProviderModel({ id: 'gpt-future-unreleased', display_name: 'X', context_window: 1 }, noFlags);
expect(m.pricing).toBeUndefined();
});
test('threads the supplied enabledFlags onto the produced model', () => {
const flags: ReadonlySet = new Set(['openai-responses-web-search-shim']);
- const m = codexRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, flags);
+ const m = chatgptRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, flags);
expect(m.enabledFlags).toBe(flags);
});
test('populates chat when raw advertises both modalities and reasoning', () => {
- const m = codexRawToProviderModel({
+ const m = chatgptRawToProviderModel({
id: 'gpt-5.5',
display_name: 'GPT-5.5',
context_window: 272000,
@@ -337,8 +337,8 @@ describe('codexRawToProviderModel', () => {
});
});
- test('uses the Codex catalog capabilities and default for GPT-6.1 Sol', () => {
- const model = codexRawToProviderModel({
+ test('uses the ChatGPT catalog capabilities and default for GPT-6.1 Sol', () => {
+ const model = chatgptRawToProviderModel({
id: 'gpt-6.1-sol',
display_name: 'GPT-6.1-Sol',
context_window: 272000,
@@ -352,14 +352,14 @@ describe('codexRawToProviderModel', () => {
expect(model.limits.max_context_window_tokens).toBe(872000);
expect(model.endpoints).toEqual({ openaiResponses: {} });
expect(model.chat?.reasoning?.effort).toEqual({ supported: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], default: 'low' });
- expect(codexModelUsesResponsesLite(model)).toBe(true);
+ expect(chatgptModelUsesResponsesLite(model)).toBe(true);
expect(model.pricing).toBeDefined();
});
// Every codex catalog entry resolves a chat block: the mapper always states
// `image_detail_original`.
test('always states image_detail_original even when the raw entry is otherwise bare', () => {
- const m = codexRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, noFlags);
+ const m = chatgptRawToProviderModel({ id: 'gpt-5.4', display_name: 'GPT-5.4', context_window: 272000 }, noFlags);
expect(m.chat).toEqual({ image_detail_original: false });
});
@@ -367,7 +367,7 @@ describe('codexRawToProviderModel', () => {
// so a catalog predating the field carries none — and the mapper must resolve
// that unknown capability to false before the model reaches the synthesizer.
test('reports image_detail_original: false when the upstream entry omits the field', () => {
- const m = codexRawToProviderModel({
+ const m = chatgptRawToProviderModel({
id: 'gpt-5.4',
display_name: 'GPT-5.4',
context_window: 272000,
@@ -380,7 +380,7 @@ describe('codexRawToProviderModel', () => {
});
test('carries the upstream supports_image_detail_original through as chat.image_detail_original', () => {
- const m = codexRawToProviderModel({
+ const m = chatgptRawToProviderModel({
id: 'gpt-5.5',
display_name: 'GPT-5.5',
context_window: 272000,
@@ -398,7 +398,7 @@ describe('codexRawToProviderModel', () => {
// taking images while rejecting detail 'original', so the mapper must carry
// each fact on its own.
test('keeps image_detail_original independent of the modality list', () => {
- const m = codexRawToProviderModel({
+ const m = chatgptRawToProviderModel({
id: 'gpt-5.2',
display_name: 'GPT-5.2',
context_window: 272000,
@@ -410,7 +410,7 @@ describe('codexRawToProviderModel', () => {
});
test('sets chat.modalities but omits chat.reasoning when only modalities are present', () => {
- const m = codexRawToProviderModel({
+ const m = chatgptRawToProviderModel({
id: 'gpt-5.5',
display_name: 'GPT-5.5',
context_window: 272000,
@@ -420,7 +420,7 @@ describe('codexRawToProviderModel', () => {
});
test('derives default = medium when supported includes medium and default_reasoning_level absent', () => {
- const m = codexRawToProviderModel({
+ const m = chatgptRawToProviderModel({
id: 'gpt-5.5',
display_name: 'GPT-5.5',
context_window: 272000,
@@ -430,7 +430,7 @@ describe('codexRawToProviderModel', () => {
});
test('derives default = first when medium absent and default_reasoning_level absent', () => {
- const m = codexRawToProviderModel({
+ const m = chatgptRawToProviderModel({
id: 'gpt-5.5',
display_name: 'GPT-5.5',
context_window: 272000,
@@ -440,7 +440,7 @@ describe('codexRawToProviderModel', () => {
});
test('drops reasoning entirely when default_reasoning_level present but supported_reasoning_levels absent', () => {
- const m = codexRawToProviderModel({
+ const m = chatgptRawToProviderModel({
id: 'gpt-5.5',
display_name: 'GPT-5.5',
context_window: 272000,
@@ -450,7 +450,7 @@ describe('codexRawToProviderModel', () => {
});
test('throws when default_reasoning_effort is not in reasoning_efforts', () => {
- expect(() => codexRawToProviderModel({
+ expect(() => chatgptRawToProviderModel({
id: 'gpt-5.5',
display_name: 'GPT-5.5',
context_window: 272000,
@@ -460,7 +460,7 @@ describe('codexRawToProviderModel', () => {
});
});
-describe('Codex image capability', () => {
+describe('ChatGPT image capability', () => {
test.each([
['free', false],
[' FREE ', false],
@@ -469,12 +469,12 @@ describe('Codex image capability', () => {
['unknown', true],
[undefined, true],
])('plan %j image eligibility is %s', (planType, expected) => {
- expect(codexPlanSupportsImages(planType)).toBe(expected);
+ expect(chatgptPlanSupportsImages(planType)).toBe(expected);
});
test('projects gpt-image-2 as a separate image model', () => {
const flags: ReadonlySet = new Set();
- expect(codexImageProviderModel(flags)).toEqual({
+ expect(chatgptImageProviderModel(flags)).toEqual({
id: 'gpt-image-2',
upstreamModelId: 'gpt-image-2',
display_name: 'GPT-Image-2',
diff --git a/packages/provider-codex/__tests__/pricing_test.ts b/packages/provider-chatgpt/__tests__/pricing_test.ts
similarity index 86%
rename from packages/provider-codex/__tests__/pricing_test.ts
rename to packages/provider-chatgpt/__tests__/pricing_test.ts
index cf7edfa555..a63ddc72ac 100644
--- a/packages/provider-codex/__tests__/pricing_test.ts
+++ b/packages/provider-chatgpt/__tests__/pricing_test.ts
@@ -1,6 +1,6 @@
import { test } from 'vitest';
-import { pricingForCodexModelKey } from '../src/pricing.ts';
+import { pricingForChatGPTModelKey } from '../src/pricing.ts';
import { perMillionTokenRates, priceRequest, type PriceVector } from '@floway-dev/protocols/common';
import { assertEquals } from '@floway-dev/test-utils';
@@ -10,7 +10,7 @@ const published = (rates: PriceVector): PriceVector => perMillionTokenRates(rate
// standard/priority/flex × short/long. `priceRequest(pricing, tier,
// inputTokens threshold)` must return the explicit rates for each of the six
// entries; there is no silent composition of one axis onto the other.
-const CODEX_GPT_5_6_GRID = {
+const CHATGPT_GPT_5_6_GRID = {
'gpt-5.6-sol': {
standardShort: published({ input_tokens: '4', input_cache_read_tokens: '0.4', input_cache_write_tokens: '5', output_tokens: '20' }),
standardLong: published({ input_tokens: '8', input_cache_read_tokens: '0.8', input_cache_write_tokens: '10', output_tokens: '30' }),
@@ -37,9 +37,9 @@ const CODEX_GPT_5_6_GRID = {
},
} as const;
-for (const [modelKey, entries] of Object.entries(CODEX_GPT_5_6_GRID)) {
- test(`Codex ${modelKey} resolves every (service tier × input length) grid entry`, () => {
- const pricing = pricingForCodexModelKey(modelKey);
+for (const [modelKey, entries] of Object.entries(CHATGPT_GPT_5_6_GRID)) {
+ test(`ChatGPT ${modelKey} resolves every (service tier × input length) grid entry`, () => {
+ const pricing = pricingForChatGPTModelKey(modelKey);
assertEquals(priceRequest(pricing, { inputTokens: 0 }).rates, entries.standardShort);
assertEquals(priceRequest(pricing, { inputTokens: 272000 + 1 }).rates, entries.standardLong);
assertEquals(priceRequest(pricing, { serviceTier: 'priority', inputTokens: 0 }).rates, entries.priorityShort);
@@ -48,8 +48,8 @@ for (const [modelKey, entries] of Object.entries(CODEX_GPT_5_6_GRID)) {
assertEquals(priceRequest(pricing, { serviceTier: 'flex', inputTokens: 272000 + 1 }).rates, entries.flexLong);
});
- test(`Codex ${modelKey} declares one standard inputTokens >272000 entry`, () => {
- const pricing = pricingForCodexModelKey(modelKey);
+ test(`ChatGPT ${modelKey} declares one standard inputTokens >272000 entry`, () => {
+ const pricing = pricingForChatGPTModelKey(modelKey);
assertEquals(pricing?.entries.filter(entry => {
const coordinate = entry.selector?.inputTokens;
return entry.selector?.serviceTier === undefined && typeof coordinate === 'object' && coordinate.operator === 'gt' && coordinate.value === 272000;
@@ -57,8 +57,8 @@ for (const [modelKey, entries] of Object.entries(CODEX_GPT_5_6_GRID)) {
});
}
-test('Codex gpt-5.3-codex prices its Fast mode lane and declares no long-context band', () => {
- const pricing = pricingForCodexModelKey('gpt-5.3-codex');
+test('ChatGPT gpt-5.3-codex prices its Fast mode lane and declares no long-context band', () => {
+ const pricing = pricingForChatGPTModelKey('gpt-5.3-codex');
assertEquals(priceRequest(pricing, { inputTokens: 0 }).rates, published({ input_tokens: '1.75', input_cache_read_tokens: '0.175', output_tokens: '14' }));
assertEquals(priceRequest(pricing, { serviceTier: 'priority', inputTokens: 0 }).rates, published({ input_tokens: '3.5', input_cache_read_tokens: '0.35', output_tokens: '28' }));
// OpenAI publishes no long-context card for this model, so a long request
@@ -66,33 +66,33 @@ test('Codex gpt-5.3-codex prices its Fast mode lane and declares no long-context
assertEquals(priceRequest(pricing, { inputTokens: 272000 + 1 }).rates, published({ input_tokens: '1.75', input_cache_read_tokens: '0.175', output_tokens: '14' }));
});
-test('Codex gpt-5.5 keeps its explicit flex and priority entries', () => {
- const pricing = pricingForCodexModelKey('gpt-5.5');
+test('ChatGPT gpt-5.5 keeps its explicit flex and priority entries', () => {
+ const pricing = pricingForChatGPTModelKey('gpt-5.5');
assertEquals(priceRequest(pricing, { inputTokens: 0 }).rates, published({ input_tokens: '5', input_cache_read_tokens: '0.5', output_tokens: '30' }));
assertEquals(priceRequest(pricing, { serviceTier: 'flex', inputTokens: 0 }).rates, published({ input_tokens: '2.5', input_cache_read_tokens: '0.25', output_tokens: '15' }));
assertEquals(priceRequest(pricing, { serviceTier: 'priority', inputTokens: 0 }).rates, published({ input_tokens: '12.5', input_cache_read_tokens: '1.25', output_tokens: '75' }));
});
-test('Codex gpt-5.4 and gpt-5.4-mini keep their explicit flex and priority entries', () => {
- const gpt54 = pricingForCodexModelKey('gpt-5.4');
+test('ChatGPT gpt-5.4 and gpt-5.4-mini keep their explicit flex and priority entries', () => {
+ const gpt54 = pricingForChatGPTModelKey('gpt-5.4');
assertEquals(priceRequest(gpt54, { serviceTier: 'flex', inputTokens: 0 }).rates, published({ input_tokens: '1.25', input_cache_read_tokens: '0.13', output_tokens: '7.5' }));
assertEquals(priceRequest(gpt54, { serviceTier: 'priority', inputTokens: 0 }).rates, published({ input_tokens: '5', input_cache_read_tokens: '0.5', output_tokens: '30' }));
assertEquals(priceRequest(gpt54, { inputTokens: 272001 }).rates, published({ input_tokens: '5', input_cache_read_tokens: '0.5', output_tokens: '22.5' }));
assertEquals(priceRequest(gpt54, { serviceTier: 'priority', inputTokens: 272001 }).rates, published({ input_tokens: '2.5', input_cache_read_tokens: '0.25', output_tokens: '15' }));
assertEquals(priceRequest(gpt54, { serviceTier: 'flex', inputTokens: 272001 }).rates, published({ input_tokens: '2.5', input_cache_read_tokens: '0.25', output_tokens: '15' }));
- const mini = pricingForCodexModelKey('gpt-5.4-mini');
+ const mini = pricingForChatGPTModelKey('gpt-5.4-mini');
assertEquals(priceRequest(mini, { inputTokens: 0 }).rates, published({ input_tokens: '0.75', input_cache_read_tokens: '0.075', output_tokens: '4.5' }));
assertEquals(priceRequest(mini, { serviceTier: 'flex', inputTokens: 0 }).rates, published({ input_tokens: '0.375', input_cache_read_tokens: '0.0375', output_tokens: '2.25' }));
assertEquals(priceRequest(mini, { serviceTier: 'priority', inputTokens: 0 }).rates, published({ input_tokens: '1.5', input_cache_read_tokens: '0.15', output_tokens: '9' }));
});
-test('pricingForCodexModelKey returns null for an unknown slug', () => {
- assertEquals(pricingForCodexModelKey('totally-made-up-model'), null);
+test('pricingForChatGPTModelKey returns null for an unknown slug', () => {
+ assertEquals(pricingForChatGPTModelKey('totally-made-up-model'), null);
});
-test('Codex GPT-6 Astra resolves the announced standard, priority and flex rate grid', () => {
- const pricing = pricingForCodexModelKey('gpt-6-astra');
+test('ChatGPT GPT-6 Astra resolves the announced standard, priority and flex rate grid', () => {
+ const pricing = pricingForChatGPTModelKey('gpt-6-astra');
const cases = [
[{ inputTokens: 272000 }, { input_tokens: '10', input_cache_read_tokens: '1', input_cache_write_tokens: '12.5', output_tokens: '50' }],
[{ inputTokens: 272001 }, { input_tokens: '20', input_cache_read_tokens: '2', input_cache_write_tokens: '25', output_tokens: '75' }],
@@ -107,8 +107,8 @@ test('Codex GPT-6 Astra resolves the announced standard, priority and flex rate
}
});
-test('Codex GPT-6.1 Sol resolves its lower cache rate across every published tier and context band', () => {
- const pricing = pricingForCodexModelKey('gpt-6.1-sol');
+test('ChatGPT GPT-6.1 Sol resolves its lower cache rate across every published tier and context band', () => {
+ const pricing = pricingForChatGPTModelKey('gpt-6.1-sol');
const cases = [
[{ inputTokens: 272000 }, { input_tokens: '2', input_cache_read_tokens: '0.1', input_cache_write_tokens: '2.5', output_tokens: '10' }],
[{ inputTokens: 272001 }, { input_tokens: '4', input_cache_read_tokens: '0.2', input_cache_write_tokens: '5', output_tokens: '15' }],
@@ -121,10 +121,10 @@ test('Codex GPT-6.1 Sol resolves its lower cache rate across every published tie
for (const [facts, rates] of cases) {
assertEquals(priceRequest(pricing, facts).rates, published(rates));
}
- assertEquals(pricingForCodexModelKey('gpt-6.1-sol-fast'), null);
+ assertEquals(pricingForChatGPTModelKey('gpt-6.1-sol-fast'), null);
});
-test('Codex GPT-6 Sol and Luna resolve every published service tier and context band', () => {
+test('ChatGPT GPT-6 Sol and Luna resolve every published service tier and context band', () => {
const cases = {
'gpt-6-sol': [
[{ inputTokens: 272000 }, { input_tokens: '2', input_cache_read_tokens: '0.2', input_cache_write_tokens: '2.5', output_tokens: '10' }],
@@ -145,13 +145,13 @@ test('Codex GPT-6 Sol and Luna resolve every published service tier and context
} as const;
for (const [id, grid] of Object.entries(cases)) {
- const pricing = pricingForCodexModelKey(id);
+ const pricing = pricingForChatGPTModelKey(id);
for (const [facts, rates] of grid) assertEquals(priceRequest(pricing, facts).rates, published(rates));
}
});
-test('Codex GPT-6.1 Terra prices every published lane at half of GPT-6.1 Sol', () => {
- const pricing = pricingForCodexModelKey('gpt-6.1-terra');
+test('ChatGPT GPT-6.1 Terra prices every published lane at half of GPT-6.1 Sol', () => {
+ const pricing = pricingForChatGPTModelKey('gpt-6.1-terra');
const cases = [
[{ inputTokens: 272000 }, { input_tokens: '1', input_cache_read_tokens: '0.05', input_cache_write_tokens: '1.25', output_tokens: '5' }],
[{ inputTokens: 272001 }, { input_tokens: '2', input_cache_read_tokens: '0.1', input_cache_write_tokens: '2.5', output_tokens: '7.5' }],
@@ -164,20 +164,20 @@ test('Codex GPT-6.1 Terra prices every published lane at half of GPT-6.1 Sol', (
for (const [facts, rates] of cases) {
assertEquals(priceRequest(pricing, facts).rates, published(rates));
}
- assertEquals(pricingForCodexModelKey('gpt-6.1-terra-fast'), null);
+ assertEquals(pricingForChatGPTModelKey('gpt-6.1-terra-fast'), null);
});
-test('Codex Daybreak slugs price the models their access programs resolve to', () => {
+test('ChatGPT Daybreak slugs price the models their access programs resolve to', () => {
// Blue is the GPT-5.6 Sol program, Red the GPT-5.6 Cyber one. Neither slug
// advertises a service tier, so a priority selector stays on the base rates
// rather than inheriting the Fast lane its underlying model sells elsewhere.
- const blue = pricingForCodexModelKey('gpt-daybreak-blue-latest');
+ const blue = pricingForChatGPTModelKey('gpt-daybreak-blue-latest');
assertEquals(priceRequest(blue, { inputTokens: 272000 }).rates, published({ input_tokens: '4', input_cache_read_tokens: '0.4', input_cache_write_tokens: '5', output_tokens: '20' }));
assertEquals(priceRequest(blue, { inputTokens: 272001 }).rates, published({ input_tokens: '8', input_cache_read_tokens: '0.8', input_cache_write_tokens: '10', output_tokens: '30' }));
assertEquals(priceRequest(blue, { serviceTier: 'priority', inputTokens: 0 }).rates, published({ input_tokens: '4', input_cache_read_tokens: '0.4', input_cache_write_tokens: '5', output_tokens: '20' }));
const cyber = published({ input_tokens: '12.5', input_cache_read_tokens: '1.25', input_cache_write_tokens: '15.625', output_tokens: '75' });
- const red = pricingForCodexModelKey('gpt-daybreak-red-latest');
+ const red = pricingForChatGPTModelKey('gpt-daybreak-red-latest');
assertEquals(priceRequest(red, { inputTokens: 0 }).rates, cyber);
// OpenAI's cyber card publishes no long-context band.
assertEquals(priceRequest(red, { inputTokens: 372001 }).rates, cyber);
diff --git a/packages/provider-codex/__tests__/provider_test.ts b/packages/provider-chatgpt/__tests__/provider_test.ts
similarity index 87%
rename from packages/provider-codex/__tests__/provider_test.ts
rename to packages/provider-chatgpt/__tests__/provider_test.ts
index fe5c8b6eec..d5ac19b483 100644
--- a/packages/provider-codex/__tests__/provider_test.ts
+++ b/packages/provider-chatgpt/__tests__/provider_test.ts
@@ -1,20 +1,20 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { createUpstreamStateRepoStub, type UpstreamStateRepoStub } from './upstream-state-repo.ts';
-import { CODEX_RESPONSES_LITE_CLIENT_METADATA_KEY, CODEX_RESPONSES_LITE_HEADER } from '../src/constants.ts';
-import { createCodexProvider } from '../src/provider.ts';
-import type { CodexAccessTokenEntry, CodexUpstreamState } from '../src/state.ts';
+import { CHATGPT_RESPONSES_LITE_CLIENT_METADATA_KEY, CHATGPT_RESPONSES_LITE_HEADER } from '../src/constants.ts';
+import { createChatGPTProvider } from '../src/provider.ts';
+import type { ChatGPTAccessTokenEntry, ChatGPTUpstreamState } from '../src/state.ts';
import { directFetcher, initProviderRepo, type UpstreamRecord } from '@floway-dev/provider';
import { noopUpstreamCallOptions, readJsonRequest, stubProviderModel } from '@floway-dev/test-utils';
const farFutureMs = Date.now() + 24 * 60 * 60 * 1000;
-const freshAccessToken: CodexAccessTokenEntry = { token: 'at', expiresAt: farFutureMs, refreshedAt: 'now' };
+const freshAccessToken: ChatGPTAccessTokenEntry = { token: 'at', expiresAt: farFutureMs, refreshedAt: 'now' };
const baseRecord: UpstreamRecord = {
- id: 'up_codex',
- kind: 'codex',
- name: 'Codex Plus',
+ id: 'up_chatgpt',
+ kind: 'chatgpt',
+ name: 'ChatGPT Plus',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 0,
@@ -30,12 +30,12 @@ const baseRecord: UpstreamRecord = {
hue: 210,
};
-const recordWithAccessToken = (entry: CodexAccessTokenEntry = freshAccessToken): UpstreamRecord => ({
+const recordWithAccessToken = (entry: ChatGPTAccessTokenEntry = freshAccessToken): UpstreamRecord => ({
...baseRecord,
state: { accounts: [{ chatgptAccountId: 'acc', refresh_token: 'rt_v1', state: 'active', state_updated_at: '2026-01-01T00:00:00Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555', accessToken: entry, quotaSnapshot: null }] },
});
-const accessOnlyRecord = (entry: CodexAccessTokenEntry): UpstreamRecord => ({
+const accessOnlyRecord = (entry: ChatGPTAccessTokenEntry): UpstreamRecord => ({
...baseRecord,
state: { accounts: [{ chatgptAccountId: 'acc', refresh_token: null, state: 'active', state_updated_at: '2026-01-01T00:00:00Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555', accessToken: entry, quotaSnapshot: null }] },
});
@@ -46,7 +46,7 @@ let repo: UpstreamStateRepoStub;
beforeEach(() => {
current = recordWithAccessToken();
repo = createUpstreamStateRepoStub(() => current, state => {
- current = { ...current!, state: state as CodexUpstreamState };
+ current = { ...current!, state: state as ChatGPTUpstreamState };
});
initProviderRepo(() => ({ recordUpstreamMetric: async () => {}, upstreams: repo }));
});
@@ -91,9 +91,9 @@ const oauthTokenResponse = (overrides: Partial<{ access_token: string; refresh_t
expires_in: overrides.expires_in ?? 3600,
}), { status: 200, headers: new Headers({ 'content-type': 'application/json' }) });
-describe('createCodexProvider', () => {
+describe('createChatGPTProvider', () => {
test('owns request identity and turn metadata headers on the instance', () => {
- const provider = createCodexProvider(baseRecord);
+ const provider = createChatGPTProvider(baseRecord);
expect(provider.inboundHeaderAllowlist).toEqual([
'originator',
@@ -108,15 +108,15 @@ describe('createCodexProvider', () => {
});
test('returns an instance carrying provider kind and identity', async () => {
- const instance = createCodexProvider(baseRecord);
- expect(instance.kind).toBe('codex');
- expect(instance.upstreamId).toBe('up_codex');
- expect(instance.name).toBe('Codex Plus');
+ const instance = createChatGPTProvider(baseRecord);
+ expect(instance.kind).toBe('chatgpt');
+ expect(instance.upstreamId).toBe('up_chatgpt');
+ expect(instance.name).toBe('ChatGPT Plus');
});
test('getProvidedModels uses the cached access token when fresh and surfaces every catalog entry', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(modelsResponse());
- const instance = createCodexProvider(baseRecord);
+ const instance = createChatGPTProvider(baseRecord);
const models = await instance.instance.getProvidedModels(directFetcher);
// Provider surfaces both visible and hidden upstream models — operators
// can dispatch to `codex-auto-review` even though ChatGPT's UI hides it.
@@ -133,7 +133,7 @@ describe('createCodexProvider', () => {
const record = accessOnlyRecord({ token: 'at_only', expiresAt: null, refreshedAt: 'now' });
current = record;
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(modelsResponse());
- const models = await createCodexProvider(record).instance.getProvidedModels(directFetcher);
+ const models = await createChatGPTProvider(record).instance.getProvidedModels(directFetcher);
// Unknown plan fails open, so the provider-owned image model is surfaced too.
expect(models.map(m => m.id)).toEqual(['gpt-5.4', 'codex-auto-review', 'gpt-image-2']);
expect(fetchSpy).toHaveBeenCalledTimes(1);
@@ -144,7 +144,7 @@ describe('createCodexProvider', () => {
const record = accessOnlyRecord({ token: 'at_only', expiresAt: Date.now() - 1, refreshedAt: 'now' });
current = record;
const fetchSpy = vi.spyOn(globalThis, 'fetch');
- await expect(createCodexProvider(record).instance.getProvidedModels(directFetcher)).rejects.toThrow(/expired.*re-import/);
+ await expect(createChatGPTProvider(record).instance.getProvidedModels(directFetcher)).rejects.toThrow(/expired.*re-import/);
expect(fetchSpy).not.toHaveBeenCalled();
});
@@ -156,7 +156,7 @@ describe('createCodexProvider', () => {
if (url.includes('/codex/models')) return modelsResponse();
throw new Error(`unexpected fetch ${url}`);
});
- const instance = createCodexProvider(baseRecord);
+ const instance = createChatGPTProvider(baseRecord);
const models = await instance.instance.getProvidedModels(directFetcher);
expect(models.map(m => m.id)).toEqual(['gpt-5.4', 'codex-auto-review', 'gpt-image-2']);
const urls = fetchSpy.mock.calls.map(c => typeof c[0] === 'string' ? c[0] : (c[0] as URL | Request).toString());
@@ -167,14 +167,14 @@ describe('createCodexProvider', () => {
// token. Both changes must survive for the next caller to see a usable
// credential pair, which is what the second write applying on top of the
// first proves.
- const account = (current!.state as CodexUpstreamState).accounts[0];
+ const account = (current!.state as ChatGPTUpstreamState).accounts[0];
expect(account.refresh_token).toBe('rt_v2');
expect(account.accessToken?.token).toBe('at_minted');
});
test('getProvidedModels propagates catalog fetch failures', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('upstream down', { status: 502 }));
- const instance = createCodexProvider(baseRecord);
+ const instance = createChatGPTProvider(baseRecord);
await expect(instance.instance.getProvidedModels(directFetcher)).rejects.toMatchObject({
displayResponse: { status: 502, body: 'upstream down' },
});
@@ -186,7 +186,7 @@ describe('createCodexProvider', () => {
...baseRecord,
config: { accounts: [{ email: 'a@b.com', chatgptAccountId: 'acc', chatgptUserId: 'usr', planType: 'free' }] },
};
- const models = await createCodexProvider(freeRecord).instance.getProvidedModels(directFetcher);
+ const models = await createChatGPTProvider(freeRecord).instance.getProvidedModels(directFetcher);
expect(models.map(model => model.id)).toEqual(['gpt-5.4', 'codex-auto-review']);
});
@@ -196,14 +196,14 @@ describe('createCodexProvider', () => {
...baseRecord,
config: { accounts: [{ email: 'a@b.com', chatgptAccountId: 'acc', chatgptUserId: 'usr', planType: 'future-plan' }] },
};
- const models = await createCodexProvider(futureRecord).instance.getProvidedModels(directFetcher);
+ const models = await createChatGPTProvider(futureRecord).instance.getProvidedModels(directFetcher);
expect(models.map(model => model.id)).toContain('gpt-image-2');
});
test('getProvidedModels uses the refreshed access-token plan over import-time config', async () => {
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => modelsResponse());
current = recordWithAccessToken({ ...freshAccessToken, planType: 'free' });
- const downgraded = await createCodexProvider(baseRecord).instance.getProvidedModels(directFetcher);
+ const downgraded = await createChatGPTProvider(baseRecord).instance.getProvidedModels(directFetcher);
expect(downgraded.map(model => model.id)).not.toContain('gpt-image-2');
const importedFree: UpstreamRecord = {
@@ -211,7 +211,7 @@ describe('createCodexProvider', () => {
config: { accounts: [{ email: 'a@b.com', chatgptAccountId: 'acc', chatgptUserId: 'usr', planType: 'free' }] },
};
current = recordWithAccessToken({ ...freshAccessToken, planType: 'plus' });
- const upgraded = await createCodexProvider(importedFree).instance.getProvidedModels(directFetcher);
+ const upgraded = await createChatGPTProvider(importedFree).instance.getProvidedModels(directFetcher);
expect(upgraded.map(model => model.id)).toContain('gpt-image-2');
});
@@ -222,8 +222,8 @@ describe('createCodexProvider', () => {
if (url.includes('/oauth/token')) return new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400, headers: new Headers({ 'content-type': 'application/json' }) });
throw new Error(`unexpected fetch ${url}`);
});
- const instance = createCodexProvider(baseRecord);
- await expect(instance.instance.getProvidedModels(directFetcher)).rejects.toThrow(/Codex OAuth session terminated/);
+ const instance = createChatGPTProvider(baseRecord);
+ await expect(instance.instance.getProvidedModels(directFetcher)).rejects.toThrow(/ChatGPT OAuth session terminated/);
});
test('getProvidedModels resolves operator flag overrides into every ProviderModel', async () => {
@@ -236,7 +236,7 @@ describe('createCodexProvider', () => {
...baseRecord,
flagOverrides: { 'openai-responses-web-search-shim': true },
};
- const instance = createCodexProvider(recordWithOverride);
+ const instance = createChatGPTProvider(recordWithOverride);
const models = await instance.instance.getProvidedModels(directFetcher);
for (const m of models) {
expect(m.enabledFlags.has('rewrite-system-to-developer')).toBe(true);
@@ -246,7 +246,7 @@ describe('createCodexProvider', () => {
test('callOpenAIResponses preserves developer messages on the Codex wire', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- const instance = createCodexProvider(baseRecord);
+ const instance = createChatGPTProvider(baseRecord);
const result = await instance.instance.callOpenAIResponses(
stubProviderModel({ id: 'gpt-5.4', display_name: 'gpt-5.4', endpoints: { openaiResponses: {} } }),
{
@@ -264,7 +264,7 @@ describe('createCodexProvider', () => {
expect(result.ok).toBe(true);
expect(result.action).toBe('generate');
const init = fetchSpy.mock.calls[0]?.[1] as RequestInit | undefined;
- if (init === undefined) throw new Error('expected a Codex upstream request');
+ if (init === undefined) throw new Error('expected a ChatGPT upstream request');
const body = await readJsonRequest(init) as Record;
expect(body.instructions).toBe("You're a helpful assistant.");
expect(body.input).toEqual([
@@ -278,7 +278,7 @@ describe('createCodexProvider', () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => action === 'generate'
? sseResponse()
: new Response(JSON.stringify({ id: 'cmp_1', object: 'response.compaction', output: [] })));
- const provider = createCodexProvider(baseRecord);
+ const provider = createChatGPTProvider(baseRecord);
const model = stubProviderModel({ id: 'future-lite', endpoints: { openaiResponses: {} }, providerData: { useResponsesLite: true } });
const tool = { type: 'function' as const, name: 'lookup', parameters: { type: 'object' } };
const input = [
@@ -287,12 +287,12 @@ describe('createCodexProvider', () => {
{ type: 'message' as const, role: 'user' as const, content: 'hello' },
];
const options = noopUpstreamCallOptions();
- options.headers.set(CODEX_RESPONSES_LITE_HEADER, 'true');
+ options.headers.set(CHATGPT_RESPONSES_LITE_HEADER, 'true');
const result = await provider.instance.callOpenAIResponses(model, {
input,
tools: [{ type: 'custom', name: 'patch' }],
text: { verbosity: 'low' },
- client_metadata: { [CODEX_RESPONSES_LITE_CLIENT_METADATA_KEY]: 'true' },
+ client_metadata: { [CHATGPT_RESPONSES_LITE_CLIENT_METADATA_KEY]: 'true' },
} as Parameters[1], action, undefined, options);
expect(result.ok).toBe(true);
expect(result.action).toBe(action);
@@ -308,12 +308,12 @@ describe('createCodexProvider', () => {
...input,
]);
expect(input).toHaveLength(3);
- expect(options.headers.get(CODEX_RESPONSES_LITE_HEADER)).toBe('true');
+ expect(options.headers.get(CHATGPT_RESPONSES_LITE_HEADER)).toBe('true');
});
test('forwards an already encoded Lite prefix without generating another one', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(sseResponse());
- const provider = createCodexProvider(baseRecord);
+ const provider = createChatGPTProvider(baseRecord);
const model = stubProviderModel({ id: 'future-lite', endpoints: { openaiResponses: {} }, providerData: { useResponsesLite: true } });
const input = [
{ type: 'additional_tools' as const, role: 'developer' as const, id: 'at_caller', tools: [{ type: 'namespace' as const, name: 'functions', description: '', tools: [{ type: 'function' as const, name: 'lookup', parameters: { type: 'object' } }] }] },
@@ -324,16 +324,16 @@ describe('createCodexProvider', () => {
expect(result.ok).toBe(true);
const init = fetchSpy.mock.calls[0]?.[1] as RequestInit | undefined;
- if (init === undefined) throw new Error('expected a Codex upstream request');
+ if (init === undefined) throw new Error('expected a ChatGPT upstream request');
const wire = await readJsonRequest(init) as Record;
expect(wire.input).toEqual(input);
expect(wire).not.toHaveProperty('instructions');
- expect(new Headers(init.headers).get(CODEX_RESPONSES_LITE_HEADER)).toBe('true');
+ expect(new Headers(init.headers).get(CHATGPT_RESPONSES_LITE_HEADER)).toBe('true');
});
test('callOpenAIResponses re-reads state per request (operator re-import takes effect)', async () => {
- repo.getById.mockResolvedValueOnce({ ...baseRecord, state: { accounts: [{ chatgptAccountId: 'acc', refresh_token: 'rt_v1', state: 'session_terminated', state_updated_at: '2026-01-02T00:00:00Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555', accessToken: null, quotaSnapshot: null }] } as CodexUpstreamState });
- const instance = createCodexProvider(baseRecord);
+ repo.getById.mockResolvedValueOnce({ ...baseRecord, state: { accounts: [{ chatgptAccountId: 'acc', refresh_token: 'rt_v1', state: 'session_terminated', state_updated_at: '2026-01-02T00:00:00Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555', accessToken: null, quotaSnapshot: null }] } as ChatGPTUpstreamState });
+ const instance = createChatGPTProvider(baseRecord);
const result = await instance.instance.callOpenAIResponses(
stubProviderModel({ id: 'gpt-5.4', display_name: 'gpt-5.4', endpoints: { openaiResponses: {} } }),
{ input: [], stream: true },
@@ -350,7 +350,7 @@ describe('createCodexProvider', () => {
created: 1,
data: [{ b64_json: 'aW1hZ2U=' }],
}), { status: 200, headers: { 'content-type': 'application/json' } }));
- const instance = createCodexProvider(baseRecord);
+ const instance = createChatGPTProvider(baseRecord);
const model = stubProviderModel({ id: 'gpt-image-2', display_name: 'GPT-Image-2', kind: 'image', endpoints: { openaiImagesGenerations: {}, openaiImagesEdits: {} } });
const options = noopUpstreamCallOptions();
options.headers.set('x-codex-image-turn-id', 'turn-image');
@@ -372,7 +372,7 @@ describe('createCodexProvider', () => {
...baseRecord,
config: { accounts: [{ email: 'a@b.com', chatgptAccountId: 'acc', chatgptUserId: 'usr', planType: 'free' }] },
};
- const instance = createCodexProvider(freeRecord);
+ const instance = createChatGPTProvider(freeRecord);
const model = stubProviderModel({ id: 'gpt-image-2', display_name: 'GPT-Image-2', kind: 'image', endpoints: { openaiImagesGenerations: {}, openaiImagesEdits: {} } });
const result = await instance.instance.callOpenAIImagesGenerations(model, { prompt: 'an orange circle' }, undefined, noopUpstreamCallOptions());
expect(result.response.status).toBe(403);
@@ -384,7 +384,7 @@ describe('createCodexProvider', () => {
...baseRecord,
config: { accounts: [{ email: 'a@b.com', chatgptAccountId: 'acc', chatgptUserId: 'usr', planType: 'free' }] },
};
- const instance = createCodexProvider(freeRecord);
+ const instance = createChatGPTProvider(freeRecord);
const model = stubProviderModel({ id: 'gpt-image-2', display_name: 'GPT-Image-2', kind: 'image', endpoints: { openaiImagesGenerations: {}, openaiImagesEdits: {} } });
const result = await instance.instance.callOpenAIImagesEdits(model, {
images: [{ type: 'reference', reference: { image_url: 'https://example.test/image.png' } }],
@@ -401,7 +401,7 @@ describe('createCodexProvider', () => {
created: 1,
data: [{ b64_json: 'ZWRpdA==' }],
}), { status: 200, headers: { 'content-type': 'application/json' } }));
- const instance = createCodexProvider(baseRecord);
+ const instance = createChatGPTProvider(baseRecord);
const model = stubProviderModel({ id: 'gpt-image-2', display_name: 'GPT-Image-2', kind: 'image', endpoints: { openaiImagesGenerations: {}, openaiImagesEdits: {} } });
const options = noopUpstreamCallOptions();
options.headers.set('originator', 'chatgpt_cca');
@@ -426,8 +426,8 @@ describe('createCodexProvider', () => {
'callOpenAIChatCompletions',
'callAnthropicMessagesCountTokens',
'callAnthropicMessages',
- ] as const)('%s returns a synthetic 405 (data plane never dispatches these to Codex)', async method => {
- const instance = createCodexProvider(baseRecord);
+ ] as const)('%s returns a synthetic 405 (data plane never dispatches these to ChatGPT)', async method => {
+ const instance = createChatGPTProvider(baseRecord);
const model = stubProviderModel({ id: 'gpt-5.4', display_name: 'gpt-5.4', endpoints: { openaiResponses: {} } });
// @ts-expect-error: each method has a different body type; we only assert
// the synthetic 405 envelope is what comes back.
@@ -435,6 +435,6 @@ describe('createCodexProvider', () => {
expect(result.response.status).toBe(405);
const body = await result.response.json() as { error: { type: string; message: string } };
expect(body.error.type).toBe('method_not_allowed');
- expect(body.error.message).toMatch(/codex/i);
+ expect(body.error.message).toMatch(/chatgpt/i);
});
});
diff --git a/packages/provider-codex/__tests__/quota_test.ts b/packages/provider-chatgpt/__tests__/quota_test.ts
similarity index 67%
rename from packages/provider-codex/__tests__/quota_test.ts
rename to packages/provider-chatgpt/__tests__/quota_test.ts
index d704ecbfd1..82d6e45bbe 100644
--- a/packages/provider-codex/__tests__/quota_test.ts
+++ b/packages/provider-chatgpt/__tests__/quota_test.ts
@@ -2,29 +2,29 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { createUpstreamStateRepoStub, type UpstreamStateRepoStub } from './upstream-state-repo.ts';
import {
- CODEX_QUOTA_UNKNOWN_ACTIVE_LIMIT,
- codexQuotaActiveLimitKey,
- clearCodexQuota,
- getCodexQuota,
- hasCodexQuotaReading,
- parseCodexQuotaHeaders,
- putCodexQuota,
+ CHATGPT_QUOTA_UNKNOWN_ACTIVE_LIMIT,
+ chatgptQuotaActiveLimitKey,
+ clearChatGPTQuota,
+ getChatGPTQuota,
+ hasChatGPTQuotaReading,
+ parseChatGPTQuotaHeaders,
+ putChatGPTQuota,
} from '../src/quota.ts';
-import type { CodexQuotaSnapshot, CodexQuotaSnapshotEntryMap, CodexUpstreamState } from '../src/state.ts';
+import type { ChatGPTQuotaSnapshot, ChatGPTQuotaSnapshotEntryMap, ChatGPTUpstreamState } from '../src/state.ts';
import { initProviderRepo, type UpstreamRecord } from '@floway-dev/provider';
const accountId = 'acc_1';
const upstreamId = 'up_a';
-test('hasCodexQuotaReading ignores an observation timestamp without quota data', () => {
- expect(hasCodexQuotaReading({ observed_at: '2026-01-01T00:00:00Z' })).toBe(false);
- expect(hasCodexQuotaReading({ observed_at: '2026-01-01T00:00:00Z', plan_type: 'plus' })).toBe(true);
+test('hasChatGPTQuotaReading ignores an observation timestamp without quota data', () => {
+ expect(hasChatGPTQuotaReading({ observed_at: '2026-01-01T00:00:00Z' })).toBe(false);
+ expect(hasChatGPTQuotaReading({ observed_at: '2026-01-01T00:00:00Z', plan_type: 'plus' })).toBe(true);
});
-const makeRecord = (state: CodexUpstreamState): UpstreamRecord => ({
+const makeRecord = (state: ChatGPTUpstreamState): UpstreamRecord => ({
id: upstreamId,
- kind: 'codex',
- name: 'Codex',
+ kind: 'chatgpt',
+ name: 'ChatGPT',
enabled: true,
usageRefreshIntervalMinutes: 0,
sortOrder: 0,
@@ -47,7 +47,7 @@ const baseAccount = {
state_updated_at: '2026-06-01T00:00:00.000Z',
openaiDeviceId: '11111111-2222-4333-8444-555555555555',
accessToken: null,
- quotaSnapshot: null as CodexQuotaSnapshotEntryMap | null,
+ quotaSnapshot: null as ChatGPTQuotaSnapshotEntryMap | null,
};
let current: UpstreamRecord | null;
@@ -56,14 +56,14 @@ let repo: UpstreamStateRepoStub;
beforeEach(() => {
current = makeRecord({ accounts: [{ ...baseAccount }] });
repo = createUpstreamStateRepoStub(() => current, state => {
- current = { ...current!, state: state as CodexUpstreamState };
+ current = { ...current!, state: state as ChatGPTUpstreamState };
});
initProviderRepo(() => ({ recordUpstreamMetric: async () => {}, upstreams: repo }));
});
afterEach(() => vi.restoreAllMocks());
-describe('parseCodexQuotaHeaders', () => {
+describe('parseChatGPTQuotaHeaders', () => {
test('parses a 200 snapshot (no ratelimited_until)', () => {
const headers = new Headers({
'x-codex-active-limit': 'premium',
@@ -78,7 +78,7 @@ describe('parseCodexQuotaHeaders', () => {
'x-codex-credits-balance': '0',
});
const observedAt = new Date('2026-06-05T00:00:00.000Z');
- const snapshot = parseCodexQuotaHeaders(headers, { now: observedAt, isRateLimited: false });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: observedAt, isRateLimited: false });
expect(snapshot).toMatchObject({
observed_at: '2026-06-05T00:00:00.000Z',
active_limit: 'premium',
@@ -102,7 +102,7 @@ describe('parseCodexQuotaHeaders', () => {
'x-codex-primary-reset-after-seconds': '18000',
'x-codex-secondary-reset-after-seconds': '7200',
});
- const snapshot = parseCodexQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: false });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: false });
expect(snapshot.primary_reset_after_at).toBe(new Date(1780272000 * 1000).toISOString());
// No absolute instant for the secondary, so the offset still dates it.
expect(snapshot.secondary_reset_after_at).toBe('2026-06-05T02:00:00.000Z');
@@ -112,7 +112,7 @@ describe('parseCodexQuotaHeaders', () => {
// epoch seconds, which is why every client that reads it accepts both.
test('reads an absolute reset instant sent as RFC 3339', () => {
const headers = new Headers({ 'x-codex-primary-reset-at': '2026-06-05T05:00:00.000Z' });
- const snapshot = parseCodexQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: false });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: false });
expect(snapshot.primary_reset_after_at).toBe('2026-06-05T05:00:00.000Z');
});
@@ -124,7 +124,7 @@ describe('parseCodexQuotaHeaders', () => {
'x-codex-secondary-reset-at': '',
'x-codex-secondary-reset-after-seconds': '0',
});
- const snapshot = parseCodexQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: false });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: false });
expect(snapshot.secondary_reset_after_at).toBeUndefined();
});
@@ -132,7 +132,7 @@ describe('parseCodexQuotaHeaders', () => {
// zero balance rather than as nothing observed.
test('reads a blank numeric header as absent rather than as zero', () => {
const headers = new Headers({ 'x-codex-credits-balance': '', 'x-codex-secondary-used-percent': ' ' });
- const snapshot = parseCodexQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: false });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: false });
expect(snapshot.credits_balance).toBeUndefined();
expect(snapshot.secondary_used_percent).toBeUndefined();
});
@@ -144,7 +144,7 @@ describe('parseCodexQuotaHeaders', () => {
'x-codex-primary-reset-at': '1780272000',
'x-codex-secondary-reset-at': '1780358400',
});
- const snapshot = parseCodexQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: true });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: true });
expect(snapshot.ratelimited_until).toBe(new Date(1780358400 * 1000).toISOString());
});
@@ -156,7 +156,7 @@ describe('parseCodexQuotaHeaders', () => {
'x-codex-secondary-reset-after-seconds': '7200',
});
const observedAt = new Date('2026-06-05T00:00:00.000Z');
- const snapshot = parseCodexQuotaHeaders(headers, { now: observedAt, isRateLimited: true });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: observedAt, isRateLimited: true });
expect(snapshot.ratelimited_until).toBe('2026-06-05T02:00:00.000Z');
});
@@ -178,13 +178,13 @@ describe('parseCodexQuotaHeaders', () => {
});
if (primary !== undefined) headers.set('x-codex-primary-used-percent', primary);
if (secondary !== undefined) headers.set('x-codex-secondary-used-percent', secondary);
- const snapshot = parseCodexQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: true });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: true });
expect(snapshot.ratelimited_until).toBe(expected);
});
test.each(['primary', 'secondary'] as const)('retains the known horizon when the other exhausted %s window has no reset', missing => {
const known = missing === 'primary' ? 'secondary' : 'primary';
- const snapshot = parseCodexQuotaHeaders(new Headers({
+ const snapshot = parseChatGPTQuotaHeaders(new Headers({
'x-codex-primary-used-percent': '100',
'x-codex-secondary-used-percent': '100',
[`x-codex-${known}-reset-after-seconds`]: '3600',
@@ -201,7 +201,7 @@ describe('parseCodexQuotaHeaders', () => {
headers.set('x-codex-primary-reset-at', reset);
headers.set('x-codex-primary-reset-after-seconds', reset);
}
- const snapshot = parseCodexQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: true });
+ const snapshot = parseChatGPTQuotaHeaders(headers, { now: new Date('2026-06-05T00:00:00.000Z'), isRateLimited: true });
expect(snapshot.ratelimited_until).toBeUndefined();
expect(snapshot.primary_used_percent).toBe(100);
});
@@ -211,7 +211,7 @@ describe('parseCodexQuotaHeaders', () => {
['RFC 3339', '2026-06-05T03:00:00+02:00', '9999'],
['offset', '', '3600'],
])('uses the supported %s exhausted reset format', (_name, at, after) => {
- const snapshot = parseCodexQuotaHeaders(new Headers({
+ const snapshot = parseChatGPTQuotaHeaders(new Headers({
'x-codex-primary-used-percent': '100',
'x-codex-primary-reset-at': at,
'x-codex-primary-reset-after-seconds': after,
@@ -221,7 +221,7 @@ describe('parseCodexQuotaHeaders', () => {
test('normalizes string headers at the provider boundary', () => {
const observedAt = new Date('2026-06-05T00:00:00.000Z');
- const snapshot = parseCodexQuotaHeaders(new Headers({
+ const snapshot = parseChatGPTQuotaHeaders(new Headers({
'x-codex-active-limit': ' premium ',
'x-codex-plan-type': ' ',
}), { now: observedAt, isRateLimited: false });
@@ -229,36 +229,36 @@ describe('parseCodexQuotaHeaders', () => {
});
});
-describe('codexQuotaActiveLimitKey', () => {
+describe('chatgptQuotaActiveLimitKey', () => {
test('uses the active_limit when present', () => {
- expect(codexQuotaActiveLimitKey({ observed_at: 'now', active_limit: 'codex_bengalfox' })).toBe('codex_bengalfox');
+ expect(chatgptQuotaActiveLimitKey({ observed_at: 'now', active_limit: 'codex_bengalfox' })).toBe('codex_bengalfox');
});
test('falls back to unknown when the active_limit is missing or blank', () => {
- expect(codexQuotaActiveLimitKey({ observed_at: 'now' })).toBe(CODEX_QUOTA_UNKNOWN_ACTIVE_LIMIT);
- expect(codexQuotaActiveLimitKey({ observed_at: 'now', active_limit: ' ' })).toBe(CODEX_QUOTA_UNKNOWN_ACTIVE_LIMIT);
- expect(codexQuotaActiveLimitKey({ observed_at: 'now', active_limit: 'constructor' })).toBe(CODEX_QUOTA_UNKNOWN_ACTIVE_LIMIT);
+ expect(chatgptQuotaActiveLimitKey({ observed_at: 'now' })).toBe(CHATGPT_QUOTA_UNKNOWN_ACTIVE_LIMIT);
+ expect(chatgptQuotaActiveLimitKey({ observed_at: 'now', active_limit: ' ' })).toBe(CHATGPT_QUOTA_UNKNOWN_ACTIVE_LIMIT);
+ expect(chatgptQuotaActiveLimitKey({ observed_at: 'now', active_limit: 'constructor' })).toBe(CHATGPT_QUOTA_UNKNOWN_ACTIVE_LIMIT);
});
});
-describe('getCodexQuota', () => {
+describe('getChatGPTQuota', () => {
test('returns null when the upstream row is missing', async () => {
current = null;
- expect(await getCodexQuota(upstreamId, accountId)).toBeNull();
+ expect(await getChatGPTQuota(upstreamId, accountId)).toBeNull();
});
test('returns null when the account has no snapshot', async () => {
- expect(await getCodexQuota(upstreamId, accountId)).toBeNull();
+ expect(await getChatGPTQuota(upstreamId, accountId)).toBeNull();
});
test('returns the quota map when buckets are fresh', async () => {
- const snap: CodexQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium', primary_used_percent: 10 };
+ const snap: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium', primary_used_percent: 10 };
current = makeRecord({ accounts: [{ ...baseAccount, quotaSnapshot: { premium: { fetchedAt: Date.now(), data: snap } } }] });
- expect(await getCodexQuota(upstreamId, accountId)).toEqual({ premium: snap });
+ expect(await getChatGPTQuota(upstreamId, accountId)).toEqual({ premium: snap });
});
test('projects legacy expiry per family without changing stored observations or successful snapshots', async () => {
- const primary: CodexQuotaSnapshot = {
+ const primary: ChatGPTQuotaSnapshot = {
observed_at: '2026-06-05T00:00:00.000Z',
primary_used_percent: 100,
primary_reset_after_at: '2026-06-05T01:00:00.000Z',
@@ -266,12 +266,12 @@ describe('getCodexQuota', () => {
secondary_reset_after_at: '2026-06-09T00:00:00.000Z',
ratelimited_until: '2026-06-09T00:00:00.000Z',
};
- const secondary: CodexQuotaSnapshot = { ...primary, primary_used_percent: 35, secondary_used_percent: 100 };
- const unknown: CodexQuotaSnapshot = { ...primary, primary_used_percent: undefined, secondary_used_percent: undefined };
- const partial: CodexQuotaSnapshot = { ...secondary, primary_used_percent: 100, secondary_reset_after_at: undefined };
- const neither: CodexQuotaSnapshot = { ...primary, primary_used_percent: 35 };
- const untimed: CodexQuotaSnapshot = { ...primary, primary_reset_after_at: undefined };
- const invalid: CodexQuotaSnapshot = { ...primary, primary_reset_after_at: 'invalid' };
+ const secondary: ChatGPTQuotaSnapshot = { ...primary, primary_used_percent: 35, secondary_used_percent: 100 };
+ const unknown: ChatGPTQuotaSnapshot = { ...primary, primary_used_percent: undefined, secondary_used_percent: undefined };
+ const partial: ChatGPTQuotaSnapshot = { ...secondary, primary_used_percent: 100, secondary_reset_after_at: undefined };
+ const neither: ChatGPTQuotaSnapshot = { ...primary, primary_used_percent: 35 };
+ const untimed: ChatGPTQuotaSnapshot = { ...primary, primary_reset_after_at: undefined };
+ const invalid: ChatGPTQuotaSnapshot = { ...primary, primary_reset_after_at: 'invalid' };
const { ratelimited_until: _until, ...successful } = primary;
current = makeRecord({
accounts: [{
@@ -287,7 +287,7 @@ describe('getCodexQuota', () => {
const { ratelimited_until: _neitherUntil, ...neitherReading } = neither;
const { ratelimited_until: _untimedUntil, ...untimedReading } = untimed;
const { ratelimited_until: _invalidUntil, ...invalidReading } = invalid;
- expect(await getCodexQuota(upstreamId, accountId)).toEqual({
+ expect(await getChatGPTQuota(upstreamId, accountId)).toEqual({
primary: { ...primary, ratelimited_until: primary.primary_reset_after_at },
secondary,
unknown: unknownReading,
@@ -302,7 +302,7 @@ describe('getCodexQuota', () => {
});
test('reads quota from the null account slot', async () => {
- const snap: CodexQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium' };
+ const snap: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium' };
current = makeRecord({
accounts: [{
...baseAccount,
@@ -310,15 +310,15 @@ describe('getCodexQuota', () => {
quotaSnapshot: { premium: { fetchedAt: Date.now(), data: snap } },
}],
});
- expect(await getCodexQuota(upstreamId, null)).toEqual({ premium: snap });
+ expect(await getChatGPTQuota(upstreamId, null)).toEqual({ premium: snap });
});
// An old reading rendered with the instant it was taken tells an operator
// more than an empty card, so age withholds nothing -- the dashboard is the
// only reader, and traffic on the upstream replaces what it shows.
test('returns every bucket however long ago it was observed', async () => {
- const recent: CodexQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'recent' };
- const ancient: CodexQuotaSnapshot = { observed_at: '2026-06-01T00:00:00.000Z', active_limit: 'ancient' };
+ const recent: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'recent' };
+ const ancient: ChatGPTQuotaSnapshot = { observed_at: '2026-06-01T00:00:00.000Z', active_limit: 'ancient' };
current = makeRecord({
accounts: [{
...baseAccount,
@@ -328,86 +328,86 @@ describe('getCodexQuota', () => {
},
}],
});
- expect(await getCodexQuota(upstreamId, accountId)).toEqual({ recent, ancient });
+ expect(await getChatGPTQuota(upstreamId, accountId)).toEqual({ recent, ancient });
});
test('returns null when the account has an empty snapshot map', async () => {
current = makeRecord({ accounts: [{ ...baseAccount, quotaSnapshot: {} }] });
- expect(await getCodexQuota(upstreamId, accountId)).toBeNull();
+ expect(await getChatGPTQuota(upstreamId, accountId)).toBeNull();
});
test('returns null when the requested account is not in the pool', async () => {
- expect(await getCodexQuota(upstreamId, 'acc_other')).toBeNull();
+ expect(await getChatGPTQuota(upstreamId, 'acc_other')).toBeNull();
});
});
-describe('putCodexQuota', () => {
+describe('putChatGPTQuota', () => {
test('persists the snapshot under its active limit, leaving the rest of the credential alone', async () => {
- const snap: CodexQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium', primary_used_percent: 42 };
- await putCodexQuota(upstreamId, accountId, snap);
+ const snap: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium', primary_used_percent: 42 };
+ await putChatGPTQuota(upstreamId, accountId, snap);
expect(repo.saveState).toHaveBeenCalledTimes(1);
expect(repo.saveState.mock.calls[0][0]).toBe(upstreamId);
- const written = (current!.state as CodexUpstreamState).accounts[0].quotaSnapshot;
+ const written = (current!.state as ChatGPTUpstreamState).accounts[0].quotaSnapshot;
expect(written?.premium?.data).toEqual(snap);
expect(typeof written?.premium?.fetchedAt).toBe('number');
- expect({ ...(current!.state as CodexUpstreamState).accounts[0], quotaSnapshot: null }).toEqual({ ...baseAccount });
+ expect({ ...(current!.state as ChatGPTUpstreamState).accounts[0], quotaSnapshot: null }).toEqual({ ...baseAccount });
});
test('persists quota into the null account slot', async () => {
current = makeRecord({ accounts: [{ ...baseAccount, chatgptAccountId: null }] });
- const snap: CodexQuotaSnapshot = { observed_at: 'now', active_limit: 'premium' };
- await putCodexQuota(upstreamId, null, snap);
- const written = (current!.state as CodexUpstreamState).accounts[0];
+ const snap: ChatGPTQuotaSnapshot = { observed_at: 'now', active_limit: 'premium' };
+ await putChatGPTQuota(upstreamId, null, snap);
+ const written = (current!.state as ChatGPTUpstreamState).accounts[0];
expect(written.chatgptAccountId).toBeNull();
expect(written.quotaSnapshot?.premium.data).toEqual(snap);
});
test('preserves other active-limit buckets and replaces the matching bucket', async () => {
- const premium: CodexQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium', primary_used_percent: 10 };
+ const premium: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium', primary_used_percent: 10 };
current = makeRecord({ accounts: [{ ...baseAccount, quotaSnapshot: { premium: { fetchedAt: 1, data: premium } } }] });
- const bengalfox: CodexQuotaSnapshot = { observed_at: '2026-06-05T01:00:00.000Z', active_limit: 'codex_bengalfox', primary_used_percent: 20 };
- await putCodexQuota(upstreamId, accountId, bengalfox);
- let written = (current!.state as CodexUpstreamState).accounts[0].quotaSnapshot;
+ const bengalfox: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T01:00:00.000Z', active_limit: 'codex_bengalfox', primary_used_percent: 20 };
+ await putChatGPTQuota(upstreamId, accountId, bengalfox);
+ let written = (current!.state as ChatGPTUpstreamState).accounts[0].quotaSnapshot;
expect(written?.premium.data).toEqual(premium);
expect(written?.codex_bengalfox.data).toEqual(bengalfox);
- const nextPremium: CodexQuotaSnapshot = { observed_at: '2026-06-05T02:00:00.000Z', active_limit: 'premium', primary_used_percent: 30 };
- await putCodexQuota(upstreamId, accountId, nextPremium);
- written = (current!.state as CodexUpstreamState).accounts[0].quotaSnapshot;
+ const nextPremium: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T02:00:00.000Z', active_limit: 'premium', primary_used_percent: 30 };
+ await putChatGPTQuota(upstreamId, accountId, nextPremium);
+ written = (current!.state as ChatGPTUpstreamState).accounts[0].quotaSnapshot;
expect(Object.keys(written ?? {}).sort()).toEqual(['codex_bengalfox', 'premium']);
expect(written?.premium.data).toEqual(nextPremium);
expect(written?.codex_bengalfox.data).toEqual(bengalfox);
});
test('uses the unknown key when active_limit is absent', async () => {
- const snap: CodexQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', primary_used_percent: 42 };
- await putCodexQuota(upstreamId, accountId, snap);
- const written = (current!.state as CodexUpstreamState).accounts[0].quotaSnapshot;
+ const snap: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', primary_used_percent: 42 };
+ await putChatGPTQuota(upstreamId, accountId, snap);
+ const written = (current!.state as ChatGPTUpstreamState).accounts[0].quotaSnapshot;
expect(written?.unknown.data).toEqual(snap);
});
test('throws when the upstream disappeared mid-flight', async () => {
current = null;
- await expect(putCodexQuota(upstreamId, accountId, { observed_at: 'now' })).rejects.toThrow(/disappeared/);
+ await expect(putChatGPTQuota(upstreamId, accountId, { observed_at: 'now' })).rejects.toThrow(/disappeared/);
expect(repo.writes).toEqual([]);
});
test('throws when the requested account is not in the pool', async () => {
- await expect(putCodexQuota(upstreamId, 'acc_other', { observed_at: 'now' })).rejects.toThrow(/not found in upstream/);
+ await expect(putChatGPTQuota(upstreamId, 'acc_other', { observed_at: 'now' })).rejects.toThrow(/not found in upstream/);
expect(repo.writes).toEqual([]);
});
});
-describe('clearCodexQuota', () => {
+describe('clearChatGPTQuota', () => {
test('invalidates every observed bucket without touching the credential', async () => {
- const snap: CodexQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium', primary_used_percent: 90 };
+ const snap: ChatGPTQuotaSnapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'premium', primary_used_percent: 90 };
current = makeRecord({ accounts: [{ ...baseAccount, quotaSnapshot: { premium: { fetchedAt: 1, data: snap } } }] });
- await clearCodexQuota(upstreamId, accountId);
- expect((current!.state as CodexUpstreamState).accounts[0]).toEqual({ ...baseAccount, quotaSnapshot: null });
+ await clearChatGPTQuota(upstreamId, accountId);
+ expect((current!.state as ChatGPTUpstreamState).accounts[0]).toEqual({ ...baseAccount, quotaSnapshot: null });
});
test('does not write when the quota slot is already empty', async () => {
- await clearCodexQuota(upstreamId, accountId);
+ await clearChatGPTQuota(upstreamId, accountId);
expect(repo.writes).toEqual([]);
});
});
@@ -417,8 +417,8 @@ test('publishes self-contained usage observations with capture timestamps and re
const recordUpstreamMetric = vi.fn().mockRejectedValueOnce(failure).mockResolvedValue(undefined);
initProviderRepo(() => ({ upstreams: repo, recordUpstreamMetric }));
const snapshot = { observed_at: '2026-06-05T00:00:00.000Z', active_limit: 'codex', primary_used_percent: 42, primary_window_minutes: 300 };
- await expect(putCodexQuota(upstreamId, accountId, snapshot)).rejects.toBe(failure);
- await putCodexQuota(upstreamId, accountId, snapshot);
+ await expect(putChatGPTQuota(upstreamId, accountId, snapshot)).rejects.toBe(failure);
+ await putChatGPTQuota(upstreamId, accountId, snapshot);
expect(recordUpstreamMetric).toHaveBeenCalledTimes(2);
expect(recordUpstreamMetric).toHaveBeenLastCalledWith(upstreamId, 'quota.window.used_percent', { limit: 'codex', window_seconds: 18_000 }, 42, Date.parse(snapshot.observed_at));
});
diff --git a/packages/provider-codex/__tests__/rate-limit-resets_test.ts b/packages/provider-chatgpt/__tests__/rate-limit-resets_test.ts
similarity index 81%
rename from packages/provider-codex/__tests__/rate-limit-resets_test.ts
rename to packages/provider-chatgpt/__tests__/rate-limit-resets_test.ts
index a69bbac49b..271f1853de 100644
--- a/packages/provider-codex/__tests__/rate-limit-resets_test.ts
+++ b/packages/provider-chatgpt/__tests__/rate-limit-resets_test.ts
@@ -1,9 +1,9 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import {
- consumeCodexRateLimitResetCredit,
- fetchCodexRateLimitResetCredits,
- parseCodexRateLimitResetCredits,
+ consumeChatGPTRateLimitResetCredit,
+ fetchChatGPTRateLimitResetCredits,
+ parseChatGPTRateLimitResetCredits,
} from '../src/rate-limit-resets.ts';
import { readJsonRequest, testFetcher } from '@floway-dev/test-utils';
@@ -19,7 +19,7 @@ const detail = {
afterEach(() => vi.restoreAllMocks());
-describe('Codex rate-limit reset cards', () => {
+describe('ChatGPT rate-limit reset cards', () => {
test('lists cards with the official ChatGPT account headers and preserves future open strings', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(Response.json({
credits: [{ ...detail, reset_type: 'future_reset_type', status: 'future_status', ignored: true }],
@@ -27,7 +27,7 @@ describe('Codex rate-limit reset cards', () => {
total_earned_count: 4,
}));
- const result = await fetchCodexRateLimitResetCredits({
+ const result = await fetchChatGPTRateLimitResetCredits({
accessToken: 'at_test', accountId: 'acc_test', fetcher: testFetcher,
});
@@ -47,7 +47,7 @@ describe('Codex rate-limit reset cards', () => {
code: 'already_redeemed', credit: { id: 'ignored' }, windows_reset: 2,
}));
- const result = await consumeCodexRateLimitResetCredit({
+ const result = await consumeChatGPTRateLimitResetCredit({
accessToken: 'at_test', accountId: 'acc_test', creditId: 'credit-1',
idempotencyKey: 'redeem-123', fetcher: testFetcher,
});
@@ -63,20 +63,20 @@ describe('Codex rate-limit reset cards', () => {
test('omits the account header for credentials without an account ID', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(Response.json({ available_count: 0, credits: [] }));
- await fetchCodexRateLimitResetCredits({ accessToken: 'at_test', accountId: null, fetcher: testFetcher });
+ await fetchChatGPTRateLimitResetCredits({ accessToken: 'at_test', accountId: null, fetcher: testFetcher });
expect(new Headers(fetchSpy.mock.calls[0][1]?.headers).has('chatgpt-account-id')).toBe(false);
});
test('fails loudly on malformed card details', () => {
- expect(() => parseCodexRateLimitResetCredits({ available_count: 1, credits: [{ ...detail, id: '' }] }))
+ expect(() => parseChatGPTRateLimitResetCredits({ available_count: 1, credits: [{ ...detail, id: '' }] }))
.toThrow(/credits\[0\]\.id/);
- expect(() => parseCodexRateLimitResetCredits({ available_count: 1, credits: [{ ...detail, expires_at: 'tomorrow-ish' }] }))
+ expect(() => parseChatGPTRateLimitResetCredits({ available_count: 1, credits: [{ ...detail, expires_at: 'tomorrow-ish' }] }))
.toThrow(/credits\[0\]\.expires_at/);
});
test('retains upstream status and body on a failed list', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('slow down', { status: 429 }));
- const promise = fetchCodexRateLimitResetCredits({
+ const promise = fetchChatGPTRateLimitResetCredits({
accessToken: 'at_test', accountId: 'acc_test', fetcher: testFetcher,
});
await expect(promise).rejects.toMatchObject({
diff --git a/packages/provider-codex/__tests__/responses-lite_test.ts b/packages/provider-chatgpt/__tests__/responses-lite_test.ts
similarity index 84%
rename from packages/provider-codex/__tests__/responses-lite_test.ts
rename to packages/provider-chatgpt/__tests__/responses-lite_test.ts
index c737b92980..72f6cf554f 100644
--- a/packages/provider-codex/__tests__/responses-lite_test.ts
+++ b/packages/provider-chatgpt/__tests__/responses-lite_test.ts
@@ -1,9 +1,9 @@
import { describe, expect, test } from 'vitest';
-import { encodeCodexResponsesLiteRequest, type CodexResponsesBody } from '../src/responses-lite.ts';
+import { encodeChatGPTResponsesLiteRequest, type ChatGPTResponsesBody } from '../src/responses-lite.ts';
import type { OpenAIResponsesInputAdditionalToolsItem, CanonicalOpenAIResponsesInputItem, OpenAIResponsesTool } from '@floway-dev/protocols/openai-responses';
-const requestBody = (overrides: Partial = {}): CodexResponsesBody => ({
+const requestBody = (overrides: Partial = {}): ChatGPTResponsesBody => ({
input: [{ type: 'message', role: 'user', content: 'hello' }],
...overrides,
});
@@ -32,7 +32,7 @@ describe('Codex Responses Lite request encoding', () => {
reasoning: { effort: 'high' },
});
const original = structuredClone(body);
- const encoded = encodeCodexResponsesLiteRequest(body, 'thread');
+ const encoded = encodeChatGPTResponsesLiteRequest(body, 'thread');
expect(encoded.body).not.toHaveProperty('tools');
expect(encoded.body).not.toHaveProperty('instructions');
@@ -68,7 +68,7 @@ describe('Codex Responses Lite request encoding', () => {
instructions: '',
input: [initial, base, { type: 'message', role: 'user', content: 'First turn' }, later, { type: 'message', role: 'user', content: 'Second turn' }],
});
- const encoded = encodeCodexResponsesLiteRequest(body, 'thread');
+ const encoded = encodeChatGPTResponsesLiteRequest(body, 'thread');
expect(encoded.body.input).toEqual(body.input);
expect(encoded.body.input[0]).toBe(initial);
@@ -79,21 +79,21 @@ describe('Codex Responses Lite request encoding', () => {
});
test('emits an empty tools carrier only when the request has no positional carrier', () => {
- const empty = encodeCodexResponsesLiteRequest(requestBody({ instructions: '' }), 'thread');
+ const empty = encodeChatGPTResponsesLiteRequest(requestBody({ instructions: '' }), 'thread');
expect(empty.body.input[0]).toMatchObject({ type: 'additional_tools', tools: [] });
expect(empty.body).not.toHaveProperty('instructions');
const carrier = additionalTools('at_existing', []);
- const existing = encodeCodexResponsesLiteRequest(requestBody({ input: [carrier] }), 'thread');
+ const existing = encodeChatGPTResponsesLiteRequest(requestBody({ input: [carrier] }), 'thread');
expect(existing.body.input).toEqual([carrier]);
});
test('keeps generated IDs stable for one thread and isolates changed instructions', () => {
const body = requestBody({ tools: [functionTool('read')], instructions: 'First rules' });
- const first = encodeCodexResponsesLiteRequest(body, 'thread-a');
- const retry = encodeCodexResponsesLiteRequest(body, 'thread-a');
- const changed = encodeCodexResponsesLiteRequest({ ...body, instructions: 'Changed rules' }, 'thread-a');
- const otherThread = encodeCodexResponsesLiteRequest(body, 'thread-b');
+ const first = encodeChatGPTResponsesLiteRequest(body, 'thread-a');
+ const retry = encodeChatGPTResponsesLiteRequest(body, 'thread-a');
+ const changed = encodeChatGPTResponsesLiteRequest({ ...body, instructions: 'Changed rules' }, 'thread-a');
+ const otherThread = encodeChatGPTResponsesLiteRequest(body, 'thread-b');
expect(retry.body.input.slice(0, 2)).toEqual(first.body.input.slice(0, 2));
expect(itemId(changed.body.input[0])).toBe(itemId(first.body.input[0]));
@@ -109,7 +109,7 @@ describe('Codex Responses Lite request encoding', () => {
type: 'message' as const, role: 'user' as const, content: [image],
internal_chat_message_metadata_passthrough: { image: metadataImage },
};
- const encoded = encodeCodexResponsesLiteRequest(requestBody({
+ const encoded = encodeChatGPTResponsesLiteRequest(requestBody({
input: [message, { type: 'function_call_output', call_id: 'c1', output: [image] }],
tools: [{ ...functionTool('inspect'), parameters: { examples: [schemaImage] } }],
}), 'thread');
@@ -121,9 +121,9 @@ describe('Codex Responses Lite request encoding', () => {
});
test('preserves tool_choice and future request fields', () => {
- const tool_choice: CodexResponsesBody['tool_choice'] = { type: 'allowed_tools', mode: 'auto', tools: [{ type: 'function', name: 'read' }] };
+ const tool_choice: ChatGPTResponsesBody['tool_choice'] = { type: 'allowed_tools', mode: 'auto', tools: [{ type: 'function', name: 'read' }] };
const body = { ...requestBody({ tools: [functionTool('read')], tool_choice }), future_field: { value: 1 } };
- const encoded = encodeCodexResponsesLiteRequest(body, 'thread');
+ const encoded = encodeChatGPTResponsesLiteRequest(body, 'thread');
expect(encoded.body.tool_choice).toBe(tool_choice);
expect((encoded.body as unknown as Record).future_field).toEqual({ value: 1 });
});
diff --git a/packages/provider-codex/__tests__/responses-output_test.ts b/packages/provider-chatgpt/__tests__/responses-output_test.ts
similarity index 97%
rename from packages/provider-codex/__tests__/responses-output_test.ts
rename to packages/provider-chatgpt/__tests__/responses-output_test.ts
index 6765710a7d..3d50d6a8f5 100644
--- a/packages/provider-codex/__tests__/responses-output_test.ts
+++ b/packages/provider-chatgpt/__tests__/responses-output_test.ts
@@ -1,6 +1,6 @@
import { expect, test } from 'vitest';
-import { restoreCodexResponsesOutput } from '../src/responses-output.ts';
+import { restoreChatGPTResponsesOutput } from '../src/responses-output.ts';
import { doneFrame, eventFrame, type ProtocolFrame } from '@floway-dev/protocols/common';
import type { OpenAIResponsesOutputItemEx, OpenAIResponsesResultEx, OpenAIResponsesStreamEventEx } from '@floway-dev/protocols/openai-responses';
@@ -22,7 +22,7 @@ const restore = async (events: OpenAIResponsesStreamEventEx[]): Promise[] = [];
- for await (const frame of restoreCodexResponsesOutput(source())) frames.push(frame);
+ for await (const frame of restoreChatGPTResponsesOutput(source())) frames.push(frame);
return frames;
};
diff --git a/packages/provider-codex/__tests__/saffron_test.ts b/packages/provider-chatgpt/__tests__/saffron_test.ts
similarity index 100%
rename from packages/provider-codex/__tests__/saffron_test.ts
rename to packages/provider-chatgpt/__tests__/saffron_test.ts
diff --git a/packages/provider-codex/__tests__/state_test.ts b/packages/provider-chatgpt/__tests__/state_test.ts
similarity index 63%
rename from packages/provider-codex/__tests__/state_test.ts
rename to packages/provider-chatgpt/__tests__/state_test.ts
index 7b97a96823..b1c9c3628a 100644
--- a/packages/provider-codex/__tests__/state_test.ts
+++ b/packages/provider-chatgpt/__tests__/state_test.ts
@@ -1,16 +1,16 @@
import { describe, expect, test } from 'vitest';
-import { assertCodexUpstreamState, readCodexUpstreamState, type CodexUpstreamState } from '../src/state.ts';
+import { assertChatGPTUpstreamState, readChatGPTUpstreamState, type ChatGPTUpstreamState } from '../src/state.ts';
const goodAccount = { chatgptAccountId: 'acc_x', refresh_token: 'rt_x', state: 'active' as const, state_updated_at: '2026-01-01T00:00:00Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555' };
-const good: CodexUpstreamState = { accounts: [{ ...goodAccount, accessToken: null, quotaSnapshot: null }] };
+const good: ChatGPTUpstreamState = { accounts: [{ ...goodAccount, accessToken: null, quotaSnapshot: null }] };
-describe('assertCodexUpstreamState', () => {
+describe('assertChatGPTUpstreamState', () => {
test('accepts active state', () => {
- expect(() => assertCodexUpstreamState(good)).not.toThrow();
+ expect(() => assertChatGPTUpstreamState(good)).not.toThrow();
});
test('accepts terminal states with state_message', () => {
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{
chatgptAccountId: 'acc_x',
refresh_token: 'rt_x',
@@ -20,42 +20,42 @@ describe('assertCodexUpstreamState', () => {
openaiDeviceId: '11111111-2222-4333-8444-555555555555',
}],
})).not.toThrow();
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ chatgptAccountId: 'acc_x', refresh_token: 'rt_x', state: 'refresh_failed', state_updated_at: '2026-06-05T00:00:00.000Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555' }],
})).not.toThrow();
});
test('rejects missing state_updated_at', () => {
const { state_updated_at: _drop, ...withoutTimestamp } = goodAccount;
- expect(() => assertCodexUpstreamState({ accounts: [withoutTimestamp] })).toThrow(/state_updated_at/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [withoutTimestamp] })).toThrow(/state_updated_at/);
});
test('accepts null refresh_token and rejects an empty string', () => {
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount, refresh_token: null }] })).not.toThrow();
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount, refresh_token: '' }] })).toThrow(/refresh_token/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount, refresh_token: null }] })).not.toThrow();
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount, refresh_token: '' }] })).toThrow(/refresh_token/);
});
test('accepts a null chatgptAccountId and rejects missing or empty values', () => {
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, chatgptAccountId: null }],
})).not.toThrow();
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, chatgptAccountId: undefined }],
})).toThrow(/chatgptAccountId/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, chatgptAccountId: '' }],
})).toThrow(/chatgptAccountId/);
});
test('rejects unknown state value', () => {
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount, state: 'broken' }] })).toThrow(/state/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount, state: 'broken' }] })).toThrow(/state/);
});
test('rejects null / undefined / non-objects', () => {
- expect(() => assertCodexUpstreamState(null)).toThrow();
- expect(() => assertCodexUpstreamState(undefined)).toThrow();
- expect(() => assertCodexUpstreamState('s')).toThrow();
+ expect(() => assertChatGPTUpstreamState(null)).toThrow();
+ expect(() => assertChatGPTUpstreamState(undefined)).toThrow();
+ expect(() => assertChatGPTUpstreamState('s')).toThrow();
});
test('rejects unexpected keys at the top level', () => {
- expect(() => assertCodexUpstreamState({ ...good, extra_field: 'x' })).toThrow(/extra_field/);
+ expect(() => assertChatGPTUpstreamState({ ...good, extra_field: 'x' })).toThrow(/extra_field/);
});
test('rejects unexpected keys inside an account', () => {
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount, smuggled: 'x' }] })).toThrow(/smuggled/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount, smuggled: 'x' }] })).toThrow(/smuggled/);
});
// A key that also names an Object.prototype member is still an unknown key.
// `__proto__` only survives as an own property through JSON.parse, so every
@@ -65,57 +65,57 @@ describe('assertCodexUpstreamState', () => {
JSON.parse(`${JSON.stringify(target).slice(0, -1)},"${key}":1}`);
for (const key of ['constructor', '__proto__', 'toString', 'hasOwnProperty']) {
const account = { ...goodAccount, accessToken: null, quotaSnapshot: null };
- expect(() => assertCodexUpstreamState(withKey({ accounts: [account] }, key))).toThrow(new RegExp(key));
- expect(() => assertCodexUpstreamState({ accounts: [withKey(account, key)] })).toThrow(new RegExp(key));
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState(withKey({ accounts: [account] }, key))).toThrow(new RegExp(key));
+ expect(() => assertChatGPTUpstreamState({ accounts: [withKey(account, key)] })).toThrow(new RegExp(key));
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: withKey({ token: 't', expiresAt: 1, refreshedAt: 1 }, key) }],
})).toThrow(new RegExp(key));
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, quotaSnapshot: { primary: withKey({ fetchedAt: 1, data: {} }, key) } }],
})).toThrow(new RegExp(key));
}
});
test('rejects an empty accounts array (v1 invariant: exactly one)', () => {
- expect(() => assertCodexUpstreamState({ accounts: [] })).toThrow(/exactly one/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [] })).toThrow(/exactly one/);
});
test('rejects multiple accounts (v1 invariant: exactly one)', () => {
- expect(() => assertCodexUpstreamState({ accounts: [goodAccount, { ...goodAccount, chatgptAccountId: 'acc_y' }] })).toThrow(/exactly one/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [goodAccount, { ...goodAccount, chatgptAccountId: 'acc_y' }] })).toThrow(/exactly one/);
});
test('accepts accessToken absent, null, unknown-expiry, populated, or plan-observed', () => {
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount }] })).not.toThrow();
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount, accessToken: null }] })).not.toThrow();
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount }] })).not.toThrow();
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount, accessToken: null }] })).not.toThrow();
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: { token: 'at', expiresAt: null, refreshedAt: '2026-06-05T00:00:00Z' } }],
})).not.toThrow();
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: { token: 'at', expiresAt: 1_700_000_000_000, refreshedAt: '2026-06-05T00:00:00Z' } }],
})).not.toThrow();
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: { token: 'at', expiresAt: 1_700_000_000_000, refreshedAt: '2026-06-05T00:00:00Z', planType: 'plus' } }],
})).not.toThrow();
});
test('rejects malformed accessToken', () => {
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: { token: '', expiresAt: 1, refreshedAt: 'x' } }],
})).toThrow(/token/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: { token: 'at', expiresAt: 'soon', refreshedAt: 'x' } }],
})).toThrow(/expiresAt/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: { token: 'at', expiresAt: 1, refreshedAt: 'x', extra: 1 } }],
})).toThrow(/extra/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: { token: 'at', expiresAt: 1, refreshedAt: 'x', planType: '' } }],
})).toThrow(/planType/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, accessToken: { token: 'at', expiresAt: 1, refreshedAt: 'x', planObservedAt: 'x' } }],
})).toThrow(/requires planType/);
});
test('accepts quotaSnapshot absent / null / populated', () => {
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount, quotaSnapshot: null }] })).not.toThrow();
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount, quotaSnapshot: null }] })).not.toThrow();
+ expect(() => assertChatGPTUpstreamState({
accounts: [{
...goodAccount,
quotaSnapshot: { premium: { fetchedAt: 1_700_000_000_000, data: { observed_at: '2026-06-05T00:00:00Z' } } },
@@ -123,38 +123,38 @@ describe('assertCodexUpstreamState', () => {
})).not.toThrow();
});
test('rejects malformed quotaSnapshot', () => {
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, quotaSnapshot: { premium: { fetchedAt: 'soon', data: {} } } }],
})).toThrow(/fetchedAt/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, quotaSnapshot: { premium: { fetchedAt: 1, data: 'oops' } } }],
})).toThrow(/data/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, quotaSnapshot: { premium: { fetchedAt: 1, data: {}, extra: 1 } } }],
})).toThrow(/extra/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, quotaSnapshot: { premium: 123 } }],
})).toThrow(/premium/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, quotaSnapshot: { '': { fetchedAt: 1, data: {} } } }],
})).toThrow(/invalid active limit key/);
- expect(() => assertCodexUpstreamState({
+ expect(() => assertChatGPTUpstreamState({
accounts: [{ ...goodAccount, quotaSnapshot: { constructor: { fetchedAt: 1, data: {} } } }],
})).toThrow(/invalid active limit key/);
});
test('rejects missing / empty openaiDeviceId', () => {
const { openaiDeviceId: _drop, ...withoutDeviceId } = goodAccount;
- expect(() => assertCodexUpstreamState({ accounts: [withoutDeviceId] })).toThrow(/openaiDeviceId/);
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount, openaiDeviceId: '' }] })).toThrow(/openaiDeviceId/);
- expect(() => assertCodexUpstreamState({ accounts: [{ ...goodAccount, openaiDeviceId: 42 }] })).toThrow(/openaiDeviceId/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [withoutDeviceId] })).toThrow(/openaiDeviceId/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount, openaiDeviceId: '' }] })).toThrow(/openaiDeviceId/);
+ expect(() => assertChatGPTUpstreamState({ accounts: [{ ...goodAccount, openaiDeviceId: 42 }] })).toThrow(/openaiDeviceId/);
});
});
-describe('readCodexUpstreamState', () => {
+describe('readChatGPTUpstreamState', () => {
test('normalizes absent accessToken / quotaSnapshot to null', () => {
const fresh = { chatgptAccountId: 'acc_x', refresh_token: 'rt_x', state: 'active' as const, state_updated_at: '2026-01-01T00:00:00Z', openaiDeviceId: '11111111-2222-4333-8444-555555555555' };
- const out = readCodexUpstreamState({ accounts: [fresh] });
+ const out = readChatGPTUpstreamState({ accounts: [fresh] });
expect(out.accounts[0].accessToken).toBeNull();
expect(out.accounts[0].quotaSnapshot).toBeNull();
});
@@ -166,7 +166,7 @@ describe('readCodexUpstreamState', () => {
quotaSnapshot: { premium: { fetchedAt: 1_700_000_000_000, data: { observed_at: '2026-06-05T00:00:00Z' } } },
}],
};
- const out = readCodexUpstreamState(populated);
+ const out = readChatGPTUpstreamState(populated);
expect(out.accounts[0].accessToken).toEqual(populated.accounts[0].accessToken);
expect(out.accounts[0].quotaSnapshot).toEqual(populated.accounts[0].quotaSnapshot);
});
diff --git a/packages/provider-codex/__tests__/upstream-metrics_test.ts b/packages/provider-chatgpt/__tests__/upstream-metrics_test.ts
similarity index 76%
rename from packages/provider-codex/__tests__/upstream-metrics_test.ts
rename to packages/provider-chatgpt/__tests__/upstream-metrics_test.ts
index b437dbef4c..a9544113bd 100644
--- a/packages/provider-codex/__tests__/upstream-metrics_test.ts
+++ b/packages/provider-chatgpt/__tests__/upstream-metrics_test.ts
@@ -1,17 +1,17 @@
import { expect, it } from 'vitest';
-import { codexMetrics, resolveUpstreamMetricDisplay } from '../src/upstream-metrics.ts';
+import { chatgptMetrics, resolveUpstreamMetricDisplay } from '../src/upstream-metrics.ts';
import { upstreamMetricAxisPriority } from '@floway-dev/provider/browser';
-it('uses actual durations and active limits to identify reordered Codex windows', () => {
+it('uses actual durations and active limits to identify reordered ChatGPT windows', () => {
const snapshot = { observed_at: '2026-10-01T00:00:00Z', active_limit: 'codex', primary_used_percent: 25, primary_window_minutes: 300, secondary_used_percent: 50, secondary_window_minutes: 10080, credits_balance: 0 };
const reordered = { ...snapshot, primary_used_percent: 50, primary_window_minutes: 10080, secondary_used_percent: 25, secondary_window_minutes: 300 };
- expect(codexMetrics(snapshot).toSorted((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))))
- .toEqual(codexMetrics(reordered).toSorted((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))));
+ expect(chatgptMetrics(snapshot).toSorted((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))))
+ .toEqual(chatgptMetrics(reordered).toSorted((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))));
const identity = { name: 'quota.window.used_percent', labels: { limit: 'codex', window_seconds: 18_000 } };
expect(resolveUpstreamMetricDisplay(identity, { quotaDisplay: 'remaining' })).toMatchObject({ name: '', unit: 'percent', axisPriority: upstreamMetricAxisPriority.subscriptionQuota, windowSeconds: 18_000, emptyValue: null, form: 'area' });
- expect(codexMetrics(snapshot).find(metric => metric.name === 'credits.balance')?.value).toBe(0);
- expect(codexMetrics({ observed_at: snapshot.observed_at, primary_used_percent: 50 })).toHaveLength(0);
+ expect(chatgptMetrics(snapshot).find(metric => metric.name === 'credits.balance')?.value).toBe(0);
+ expect(chatgptMetrics({ observed_at: snapshot.observed_at, primary_used_percent: 50 })).toHaveLength(0);
});
it('displays the direction the preference names while preserving credit balances', () => {
diff --git a/packages/provider-codex/__tests__/upstream-state-repo.ts b/packages/provider-chatgpt/__tests__/upstream-state-repo.ts
similarity index 98%
rename from packages/provider-codex/__tests__/upstream-state-repo.ts
rename to packages/provider-chatgpt/__tests__/upstream-state-repo.ts
index 30ba922170..8b582a83e8 100644
--- a/packages/provider-codex/__tests__/upstream-state-repo.ts
+++ b/packages/provider-chatgpt/__tests__/upstream-state-repo.ts
@@ -11,7 +11,7 @@ export interface UpstreamStateRepoStub {
writes: unknown[];
}
-// Stand-in for the SQL upstream repo, faithful to the parts the Codex state
+// Stand-in for the SQL upstream repo, faithful to the parts the ChatGPT state
// writers depend on: the mutator sees the stored document, a result that
// serializes identically to it is not written, and a missing row throws.
export const createUpstreamStateRepoStub = (
diff --git a/packages/provider-codex/package.json b/packages/provider-chatgpt/package.json
similarity index 93%
rename from packages/provider-codex/package.json
rename to packages/provider-chatgpt/package.json
index 4849bfb22f..3df262ca6e 100644
--- a/packages/provider-codex/package.json
+++ b/packages/provider-chatgpt/package.json
@@ -1,5 +1,5 @@
{
- "name": "@floway-dev/provider-codex",
+ "name": "@floway-dev/provider-chatgpt",
"private": true,
"version": "0.0.0",
"type": "module",
diff --git a/packages/provider-codex/src/access-token.ts b/packages/provider-chatgpt/src/access-token.ts
similarity index 68%
rename from packages/provider-codex/src/access-token.ts
rename to packages/provider-chatgpt/src/access-token.ts
index 00c55f2ba6..5fc3a1e7ca 100644
--- a/packages/provider-codex/src/access-token.ts
+++ b/packages/provider-chatgpt/src/access-token.ts
@@ -1,17 +1,17 @@
-import { parseCodexIdTokenPlanType } from './auth/jwt.ts';
-import { CodexOAuthSessionTerminatedError, codexTokenExpiresAt, refreshCodexAccessToken } from './auth/oauth.ts';
-import { findCodexAccountIndex, readCodexUpstreamState, replaceCodexAccount, type CodexAccessTokenEntry } from './state.ts';
+import { parseChatGPTIdTokenPlanType } from './auth/jwt.ts';
+import { ChatGPTOAuthSessionTerminatedError, chatgptTokenExpiresAt, refreshChatGPTAccessToken } from './auth/oauth.ts';
+import { findChatGPTAccountIndex, readChatGPTUpstreamState, replaceChatGPTAccount, type ChatGPTAccessTokenEntry } from './state.ts';
import { getProviderRepo, UpstreamGoneError, type Fetcher } from '@floway-dev/provider';
-export type { CodexAccessTokenEntry };
+export type { ChatGPTAccessTokenEntry };
// An access-only credential has no refresh token, so there is nothing to
// re-mint from. Distinct from a session termination: the credential was never
// renewable, and the only recovery is a re-import.
-export class CodexAccessOnlyCredentialError extends Error {
+export class ChatGPTAccessOnlyCredentialError extends Error {
constructor(message: string) {
super(message);
- this.name = 'CodexAccessOnlyCredentialError';
+ this.name = 'ChatGPTAccessOnlyCredentialError';
}
}
@@ -27,49 +27,49 @@ const REFRESH_SKEW_MS = 5 * 60 * 1000;
// expiry signal such a credential has. For a renewable credential the same
// unknown expiry reads as unusable instead: it can just mint a new one, and a
// token of unknown remaining life is not worth a request.
-const isAccessTokenUsable = (entry: CodexAccessTokenEntry, renewable: boolean): boolean => {
+const isAccessTokenUsable = (entry: ChatGPTAccessTokenEntry, renewable: boolean): boolean => {
if (entry.expiresAt === null) return !renewable;
return entry.expiresAt > Date.now() + (renewable ? REFRESH_SKEW_MS : 0);
};
-export interface CodexPlanObservation {
+export interface ChatGPTPlanObservation {
planType: string;
observedAt?: string;
}
-export const codexPlanObservation = (entry: CodexAccessTokenEntry | null | undefined): CodexPlanObservation | null =>
+export const chatgptPlanObservation = (entry: ChatGPTAccessTokenEntry | null | undefined): ChatGPTPlanObservation | null =>
entry?.planType === undefined
? null
: { planType: entry.planType, observedAt: entry.planObservedAt ?? entry.refreshedAt };
-const observationTime = (observation: CodexPlanObservation): number => {
+const observationTime = (observation: ChatGPTPlanObservation): number => {
const parsed = observation.observedAt === undefined ? Number.NaN : Date.parse(observation.observedAt);
return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
};
const latestPlanObservation = (
- first: CodexPlanObservation | null,
- second: CodexPlanObservation | null,
- fallback: CodexPlanObservation | undefined,
-): CodexPlanObservation | null => {
+ first: ChatGPTPlanObservation | null,
+ second: ChatGPTPlanObservation | null,
+ fallback: ChatGPTPlanObservation | undefined,
+): ChatGPTPlanObservation | null => {
const observations = [first, second, fallback ?? null]
- .filter((value): value is CodexPlanObservation => value !== null);
+ .filter((value): value is ChatGPTPlanObservation => value !== null);
if (observations.length === 0) return null;
return observations.reduce((latest, candidate) =>
observationTime(candidate) > observationTime(latest) ? candidate : latest);
};
-const mergeCodexAccessTokenEntry = (
- incoming: CodexAccessTokenEntry,
- current: CodexAccessTokenEntry | null | undefined,
- fallbackPlan: CodexPlanObservation | undefined,
-): CodexAccessTokenEntry => {
+const mergeChatGPTAccessTokenEntry = (
+ incoming: ChatGPTAccessTokenEntry,
+ current: ChatGPTAccessTokenEntry | null | undefined,
+ fallbackPlan: ChatGPTPlanObservation | undefined,
+): ChatGPTAccessTokenEntry => {
const incomingTime = Date.parse(incoming.refreshedAt);
const currentTime = current === null || current === undefined ? Number.NEGATIVE_INFINITY : Date.parse(current.refreshedAt);
const token = Number.isFinite(currentTime) && (!Number.isFinite(incomingTime) || currentTime >= incomingTime)
? current!
: incoming;
- const plan = latestPlanObservation(codexPlanObservation(incoming), codexPlanObservation(current), fallbackPlan);
+ const plan = latestPlanObservation(chatgptPlanObservation(incoming), chatgptPlanObservation(current), fallbackPlan);
const { planType: _planType, planObservedAt: _planObservedAt, ...tokenFields } = token;
return plan === null
? tokenFields
@@ -80,27 +80,27 @@ const mergeCodexAccessTokenEntry = (
};
};
-interface PersistCodexAccessTokenOptions {
+interface PersistChatGPTAccessTokenOptions {
upstreamId: string;
accountId: string | null;
- entry: CodexAccessTokenEntry | null;
+ entry: ChatGPTAccessTokenEntry | null;
where: string;
- fallbackPlan?: CodexPlanObservation;
+ fallbackPlan?: ChatGPTPlanObservation;
}
// The whole change is expressed against the state the repo hands us, so a
// write that loses its race is simply replayed against the winner's document
// and both changes survive. Storage failures propagate so the request path
// surfaces them rather than silently running on a stale cached token.
-const persistAccessToken = async (opts: PersistCodexAccessTokenOptions): Promise => {
+const persistAccessToken = async (opts: PersistChatGPTAccessTokenOptions): Promise => {
// The mutator is replayed on a lost race, so the diagnostic is recorded and
// emitted once afterwards rather than logged from inside it.
let accountMissing = false;
let effectiveEntry = opts.entry;
try {
await getProviderRepo().upstreams.saveState(opts.upstreamId, current => {
- const state = readCodexUpstreamState(current);
- const idx = findCodexAccountIndex(state, opts.accountId);
+ const state = readChatGPTUpstreamState(current);
+ const idx = findChatGPTAccountIndex(state, opts.accountId);
if (idx < 0) {
accountMissing = true;
return current;
@@ -111,59 +111,59 @@ const persistAccessToken = async (opts: PersistCodexAccessTokenOptions): Promise
if (opts.entry === null && state.accounts[idx].accessToken === null) return current;
effectiveEntry = opts.entry === null
? null
- : mergeCodexAccessTokenEntry(opts.entry, state.accounts[idx].accessToken, opts.fallbackPlan);
+ : mergeChatGPTAccessTokenEntry(opts.entry, state.accounts[idx].accessToken, opts.fallbackPlan);
if (JSON.stringify(effectiveEntry) === JSON.stringify(state.accounts[idx].accessToken)) return current;
- return replaceCodexAccount(state, idx, account => ({ ...account, accessToken: effectiveEntry }));
+ return replaceChatGPTAccount(state, idx, account => ({ ...account, accessToken: effectiveEntry }));
});
} catch (err) {
// A minted access token is bookkeeping the next request re-derives, so an
// operator deleting the upstream mid-request is not worth failing that
// request over. Every other storage failure still propagates.
if (!(err instanceof UpstreamGoneError)) throw err;
- console.warn(`${opts.where}: Codex upstream ${opts.upstreamId} disappeared mid-request`);
+ console.warn(`${opts.where}: ChatGPT upstream ${opts.upstreamId} disappeared mid-request`);
return effectiveEntry;
}
if (accountMissing) {
- console.warn(`${opts.where}: Codex account ${opts.accountId} not found in upstream ${opts.upstreamId}`);
+ console.warn(`${opts.where}: ChatGPT account ${opts.accountId} not found in upstream ${opts.upstreamId}`);
}
return effectiveEntry;
};
-export const putCodexAccessToken = async (
+export const putChatGPTAccessToken = async (
upstreamId: string,
accountId: string | null,
- entry: CodexAccessTokenEntry,
- fallbackPlan?: CodexPlanObservation,
-): Promise =>
- (await persistAccessToken({ upstreamId, accountId, entry, where: 'putCodexAccessToken', fallbackPlan })) ?? entry;
+ entry: ChatGPTAccessTokenEntry,
+ fallbackPlan?: ChatGPTPlanObservation,
+): Promise =>
+ (await persistAccessToken({ upstreamId, accountId, entry, where: 'putChatGPTAccessToken', fallbackPlan })) ?? entry;
-export const invalidateCodexAccessToken = async (
+export const invalidateChatGPTAccessToken = async (
upstreamId: string,
accountId: string | null,
expectedToken?: string,
-): Promise => {
+): Promise => {
if (expectedToken === undefined) {
- return await persistAccessToken({ upstreamId, accountId, entry: null, where: 'invalidateCodexAccessToken' });
+ return await persistAccessToken({ upstreamId, accountId, entry: null, where: 'invalidateChatGPTAccessToken' });
}
- let retained: CodexAccessTokenEntry | null = null;
+ let retained: ChatGPTAccessTokenEntry | null = null;
await getProviderRepo().upstreams.saveState(upstreamId, current => {
- const state = readCodexUpstreamState(current);
- const idx = findCodexAccountIndex(state, accountId);
- if (idx < 0) throw new Error(`invalidateCodexAccessToken: Codex account ${accountId} not found in upstream ${upstreamId}`);
+ const state = readChatGPTUpstreamState(current);
+ const idx = findChatGPTAccountIndex(state, accountId);
+ if (idx < 0) throw new Error(`invalidateChatGPTAccessToken: ChatGPT account ${accountId} not found in upstream ${upstreamId}`);
const entry = state.accounts[idx].accessToken;
if (entry !== null && entry.token !== expectedToken) {
retained = entry;
return current;
}
if (entry === null) return current;
- return replaceCodexAccount(state, idx, account => ({ ...account, accessToken: null }));
+ return replaceChatGPTAccount(state, idx, account => ({ ...account, accessToken: null }));
});
return retained;
};
// Reads, mints, and persists. The mint callback is responsible for routing
// the rotated refresh_token through the upstream's persistence hook;
-// `mintCodexAccessToken` below is the standard implementation.
+// `mintChatGPTAccessToken` below is the standard implementation.
//
// Refresh-race recovery: when the mint throws `invalid_grant`, it might mean
// either (a) the refresh_token is genuinely revoked, or (b) a sibling worker
@@ -189,24 +189,24 @@ export const invalidateCodexAccessToken = async (
//
// Scope: per-isolate only. Cross-isolate siblings still race and are
// caught by `recoverFromRefreshRace` — same trade-off as claude-code.
-const inFlightEnsures = new Map>();
+const inFlightEnsures = new Map>();
-export const ensureCodexAccessToken = async (
+export const ensureChatGPTAccessToken = async (
upstreamId: string,
accountId: string | null,
- mint: (refreshToken: string) => Promise,
+ mint: (refreshToken: string) => Promise,
// When true, skip the "cached access_token is still fresh" fast-path and
// always mint a fresh one. Dashboard's Refresh button sets this so the
// operator sees the row's tokens actually rotate; the data plane leaves
// it false so a live request served from cache stays cheap.
force = false,
-): Promise => {
+): Promise => {
// `null` is a legitimate account id, so the key has to keep it distinct from
// the string "null" a template would produce.
const key = JSON.stringify([upstreamId, accountId, force]);
const existing = inFlightEnsures.get(key);
if (existing) return await existing;
- const promise = ensureCodexAccessTokenInner(upstreamId, accountId, mint, true, force);
+ const promise = ensureChatGPTAccessTokenInner(upstreamId, accountId, mint, true, force);
inFlightEnsures.set(key, promise);
try {
return await promise;
@@ -215,18 +215,18 @@ export const ensureCodexAccessToken = async (
}
};
-const ensureCodexAccessTokenInner = async (
+const ensureChatGPTAccessTokenInner = async (
upstreamId: string,
accountId: string | null,
- mint: (refreshToken: string) => Promise,
+ mint: (refreshToken: string) => Promise,
recoveryAllowed: boolean,
force: boolean,
-): Promise => {
+): Promise => {
const fresh = await getProviderRepo().upstreams.getById(upstreamId);
- if (!fresh) throw new Error(`Codex upstream ${upstreamId} not found`);
- const state = readCodexUpstreamState(fresh.state);
+ if (!fresh) throw new Error(`ChatGPT upstream ${upstreamId} not found`);
+ const state = readChatGPTUpstreamState(fresh.state);
const account = state.accounts.find(a => a.chatgptAccountId === accountId);
- if (!account) throw new Error(`Codex account ${accountId} not found in upstream ${upstreamId}`);
+ if (!account) throw new Error(`ChatGPT account ${accountId} not found in upstream ${upstreamId}`);
const renewable = account.refresh_token !== null;
if (account.accessToken && isAccessTokenUsable(account.accessToken, renewable) && !force) {
return account.accessToken;
@@ -236,12 +236,12 @@ const ensureCodexAccessTokenInner = async (
// operator's fix is the same but the reason changes what they check.
if (account.refresh_token === null) {
if (force) {
- throw new CodexAccessOnlyCredentialError('Codex access-only credentials cannot be refreshed; re-import the credential');
+ throw new ChatGPTAccessOnlyCredentialError('ChatGPT access-only credentials cannot be refreshed; re-import the credential');
}
if (account.accessToken !== null && account.accessToken.expiresAt !== null && account.accessToken.expiresAt <= Date.now()) {
- throw new CodexAccessOnlyCredentialError('Codex access token has expired and cannot be refreshed; re-import the credential');
+ throw new ChatGPTAccessOnlyCredentialError('ChatGPT access token has expired and cannot be refreshed; re-import the credential');
}
- throw new CodexAccessOnlyCredentialError('Codex access-only credential has no usable access token; re-import the credential');
+ throw new ChatGPTAccessOnlyCredentialError('ChatGPT access-only credential has no usable access token; re-import the credential');
}
const refreshToken = account.refresh_token;
@@ -249,7 +249,7 @@ const ensureCodexAccessTokenInner = async (
try {
minted = await mint(refreshToken);
} catch (err) {
- if (err instanceof CodexOAuthSessionTerminatedError && err.code === 'invalid_grant' && recoveryAllowed) {
+ if (err instanceof ChatGPTOAuthSessionTerminatedError && err.code === 'invalid_grant' && recoveryAllowed) {
const recovered = await recoverFromRefreshRace(upstreamId, accountId, refreshToken, mint);
if (recovered) return recovered;
}
@@ -259,15 +259,15 @@ const ensureCodexAccessTokenInner = async (
upstreamId,
accountId,
entry: minted,
- where: 'ensureCodexAccessToken',
- fallbackPlan: codexPlanObservation(account.accessToken) ?? undefined,
+ where: 'ensureChatGPTAccessToken',
+ fallbackPlan: chatgptPlanObservation(account.accessToken) ?? undefined,
})) ?? minted;
};
// `invalid_grant` ambiguity: dead refresh token, or a sibling worker raced
// us and we hold the rotated-out copy. Re-read state for the same
// `accountId` slot and compare. The "sibling rotated but no cached access
-// token yet" subcase (e.g. a concurrent `invalidateCodexAccessToken`
+// token yet" subcase (e.g. a concurrent `invalidateChatGPTAccessToken`
// cleared it) re-enters the refresh flow once with the fresh RT in hand;
// the depth guard prevents runaway recursion if recovery itself observes a
// stale view. Returns `null` when the original error should be re-raised as
@@ -276,29 +276,29 @@ const recoverFromRefreshRace = async (
upstreamId: string,
accountId: string | null,
usedRefreshToken: string,
- mint: (refreshToken: string) => Promise,
-): Promise => {
+ mint: (refreshToken: string) => Promise,
+): Promise => {
const reread = await getProviderRepo().upstreams.getById(upstreamId);
if (!reread) return null;
- const rereadState = readCodexUpstreamState(reread.state);
+ const rereadState = readChatGPTUpstreamState(reread.state);
const rereadAccount = rereadState.accounts.find(a => a.chatgptAccountId === accountId);
if (!rereadAccount) return null;
if (rereadAccount.state !== 'active') return null;
if (rereadAccount.refresh_token === usedRefreshToken) return null;
console.info(
- `Codex refresh-race recovered for upstream ${upstreamId} account ${accountId}: sibling rotated, using their access token`,
+ `ChatGPT refresh-race recovered for upstream ${upstreamId} account ${accountId}: sibling rotated, using their access token`,
);
if (rereadAccount.accessToken && isAccessTokenUsable(rereadAccount.accessToken, rereadAccount.refresh_token !== null)) {
return rereadAccount.accessToken;
}
// Sibling rotated the refresh token but no usable access token sits in
- // state — most likely an `invalidateCodexAccessToken` ran between the
+ // state — most likely an `invalidateChatGPTAccessToken` ran between the
// sibling's rotation and our re-read. Re-enter the refresh flow once with
// the live RT; the re-entrant call sees the rotated row and goes straight
// through the standard mint path. The depth guard suppresses a second
// recovery attempt — if `invalid_grant` strikes again the refresh token
// really is dead and we want the terminal flip.
- return await ensureCodexAccessTokenInner(upstreamId, accountId, mint, false, false);
+ return await ensureChatGPTAccessTokenInner(upstreamId, accountId, mint, false, false);
};
// Mints a fresh access token via /oauth/token and routes the rotated
@@ -307,18 +307,18 @@ const recoverFromRefreshRace = async (
// rotations each call's new refresh_token must reach the hook before the
// next attempt reads state, otherwise an unhandled rejection can swallow the
// rotated token and the upstream eventually returns app_session_terminated.
-export const mintCodexAccessToken = async (
+export const mintChatGPTAccessToken = async (
refreshToken: string,
fetcher: Fetcher,
persistRefreshTokenRotation: (newRefreshToken: string) => Promise,
-): Promise => {
- const tokens = await refreshCodexAccessToken(refreshToken, fetcher);
+): Promise => {
+ const tokens = await refreshChatGPTAccessToken(refreshToken, fetcher);
await persistRefreshTokenRotation(tokens.refresh_token);
- const planType = tokens.id_token === undefined ? undefined : parseCodexIdTokenPlanType(tokens.id_token);
+ const planType = tokens.id_token === undefined ? undefined : parseChatGPTIdTokenPlanType(tokens.id_token);
const refreshedAt = new Date().toISOString();
return {
token: tokens.access_token,
- expiresAt: codexTokenExpiresAt(tokens.expires_in),
+ expiresAt: chatgptTokenExpiresAt(tokens.expires_in),
refreshedAt,
...(planType === undefined ? {} : { planType, planObservedAt: refreshedAt }),
};
diff --git a/packages/provider-codex/src/auth/credential.ts b/packages/provider-chatgpt/src/auth/credential.ts
similarity index 83%
rename from packages/provider-codex/src/auth/credential.ts
rename to packages/provider-chatgpt/src/auth/credential.ts
index ed745c9ac1..70221a356f 100644
--- a/packages/provider-codex/src/auth/credential.ts
+++ b/packages/provider-chatgpt/src/auth/credential.ts
@@ -1,11 +1,11 @@
-import type { CodexAccountIdentity } from '../config.ts';
+import type { ChatGPTAccountIdentity } from '../config.ts';
import { optionalString, requireString } from './guards.ts';
-import { parseCodexTokenClaims, tryParseCodexAccessTokenClaims, type CodexTokenClaims } from './jwt.ts';
+import { parseChatGPTTokenClaims, tryParseChatGPTAccessTokenClaims, type ChatGPTTokenClaims } from './jwt.ts';
// The one shape every import source funnels through. JSON exports, the OAuth
// callback, and hand-typed fields all differ in what they carry, so they state
// what they know here and let one place decide what the account's identity is.
-export interface CodexCredentialInput {
+export interface ChatGPTCredentialInput {
accessToken: string;
refreshToken?: string | null;
idToken?: string | null;
@@ -16,14 +16,14 @@ export interface CodexCredentialInput {
planType?: string | null;
}
-export interface NormalizedCodexCredential {
+export interface NormalizedChatGPTCredential {
accessToken: string;
refreshToken: string | null;
expiresAt: number | null;
- identity: CodexAccountIdentity;
+ identity: ChatGPTAccountIdentity;
}
-export const normalizeCodexCredential = (input: CodexCredentialInput): NormalizedCodexCredential => {
+export const normalizeChatGPTCredential = (input: ChatGPTCredentialInput): NormalizedChatGPTCredential => {
const accessToken = requireString(input.accessToken, 'access token');
const refreshToken = optionalString(input.refreshToken, 'refresh token');
const idToken = optionalString(input.idToken, 'id token');
@@ -33,8 +33,8 @@ export const normalizeCodexCredential = (input: CodexCredentialInput): Normalize
const explicitPlanType = optionalString(input.planType, 'plan type');
const sourceExpiresAt = normalizeExpiry(input.expiresAt);
- const idClaims = idToken === null ? EMPTY_CLAIMS : parseCodexTokenClaims(idToken, 'id_token');
- const accessClaims = tryParseCodexAccessTokenClaims(accessToken) ?? EMPTY_CLAIMS;
+ const idClaims = idToken === null ? EMPTY_CLAIMS : parseChatGPTTokenClaims(idToken, 'id_token');
+ const accessClaims = tryParseChatGPTAccessTokenClaims(accessToken) ?? EMPTY_CLAIMS;
// Two tokens naming different accounts means the bundle was assembled wrong,
// and guessing which half is right would silently attribute traffic and
@@ -69,7 +69,7 @@ export const normalizeCodexCredential = (input: CodexCredentialInput): Normalize
};
};
-const EMPTY_CLAIMS: CodexTokenClaims = {
+const EMPTY_CLAIMS: ChatGPTTokenClaims = {
email: null,
chatgptAccountId: null,
chatgptUserId: null,
diff --git a/packages/provider-codex/src/auth/guards.ts b/packages/provider-chatgpt/src/auth/guards.ts
similarity index 100%
rename from packages/provider-codex/src/auth/guards.ts
rename to packages/provider-chatgpt/src/auth/guards.ts
diff --git a/packages/provider-codex/src/auth/import.ts b/packages/provider-chatgpt/src/auth/import.ts
similarity index 75%
rename from packages/provider-codex/src/auth/import.ts
rename to packages/provider-chatgpt/src/auth/import.ts
index c04a1d8d18..4621b05db5 100644
--- a/packages/provider-codex/src/auth/import.ts
+++ b/packages/provider-chatgpt/src/auth/import.ts
@@ -1,16 +1,16 @@
-import type { CodexUpstreamConfig } from '../config.ts';
-import type { CodexUpstreamState } from '../state.ts';
-import { normalizeCodexCredential, type CodexCredentialInput, type NormalizedCodexCredential } from './credential.ts';
+import type { ChatGPTUpstreamConfig } from '../config.ts';
+import type { ChatGPTUpstreamState } from '../state.ts';
+import { normalizeChatGPTCredential, type ChatGPTCredentialInput, type NormalizedChatGPTCredential } from './credential.ts';
import { isObject, optionalString, requireObject, requireString } from './guards.ts';
-import { codexTokenExpiresAt, exchangeCodexAuthorizationCode } from './oauth.ts';
+import { chatgptTokenExpiresAt, exchangeChatGPTAuthorizationCode } from './oauth.ts';
import type { Fetcher } from '@floway-dev/provider';
-export interface CodexImportResult {
- config: CodexUpstreamConfig;
- state: CodexUpstreamState;
+export interface ChatGPTImportResult {
+ config: ChatGPTUpstreamConfig;
+ state: ChatGPTUpstreamState;
}
-export interface CodexManualImportInput {
+export interface ChatGPTManualImportInput {
access_token: string;
refresh_token?: string | null;
id_token?: string | null;
@@ -23,7 +23,7 @@ export interface CodexManualImportInput {
// What the dashboard shows before the operator commits to one account. It
// carries identity and lifecycle only — never credential material, because
// this crosses the wire to a browser purely so a human can pick a row.
-export interface CodexJsonPreviewCandidate {
+export interface ChatGPTJsonPreviewCandidate {
sourceIndex: number;
name: string | null;
email: string | null;
@@ -35,7 +35,7 @@ export interface CodexJsonPreviewCandidate {
issues: string[];
}
-const buildCodexImportResult = (credential: NormalizedCodexCredential, now: string): CodexImportResult => ({
+const buildChatGPTImportResult = (credential: NormalizedChatGPTCredential, now: string): ChatGPTImportResult => ({
config: { accounts: [credential.identity] },
state: {
accounts: [{
@@ -67,8 +67,8 @@ const buildCodexImportResult = (credential: NormalizedCodexCredential, now: stri
// Hand-typed fields. Only the bearer is required; everything else either fills
// a gap the tokens cannot answer or overrides a claim the operator knows to be
// stale.
-export const importCodexFromManual = async (input: CodexManualImportInput): Promise => {
- const credential = normalizeCodexCredential({
+export const importChatGPTFromManual = async (input: ChatGPTManualImportInput): Promise => {
+ const credential = normalizeChatGPTCredential({
accessToken: input.access_token,
refreshToken: input.refresh_token,
idToken: input.id_token,
@@ -77,7 +77,7 @@ export const importCodexFromManual = async (input: CodexManualImportInput): Prom
planType: input.plan_type,
expiresAt: parseSourceExpiry(input.expires_at, 'manual expires_at'),
});
- return buildCodexImportResult(credential, new Date().toISOString());
+ return buildChatGPTImportResult(credential, new Date().toISOString());
};
// Exchange the authorization code for tokens, then read identity off the
@@ -86,15 +86,15 @@ export const importCodexFromManual = async (input: CodexManualImportInput): Prom
// auth.openai.com (the endpoint rejects state with 400). The token exchange is
// the only network hop on this path, so `fetcher` is where the caller picks
// egress for the whole import.
-export const importCodexFromCallback = async (opts: { code: string; codeVerifier: string; fetcher: Fetcher }): Promise => {
- const tokens = await exchangeCodexAuthorizationCode({ code: opts.code, codeVerifier: opts.codeVerifier, fetcher: opts.fetcher });
- const credential = normalizeCodexCredential({
+export const importChatGPTFromCallback = async (opts: { code: string; codeVerifier: string; fetcher: Fetcher }): Promise => {
+ const tokens = await exchangeChatGPTAuthorizationCode({ code: opts.code, codeVerifier: opts.codeVerifier, fetcher: opts.fetcher });
+ const credential = normalizeChatGPTCredential({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
idToken: tokens.id_token,
- expiresAt: codexTokenExpiresAt(tokens.expires_in),
+ expiresAt: chatgptTokenExpiresAt(tokens.expires_in),
});
- return buildCodexImportResult(credential, new Date().toISOString());
+ return buildChatGPTImportResult(credential, new Date().toISOString());
};
// Lists what a pasted document holds without committing to any of it, so the
@@ -102,8 +102,8 @@ export const importCodexFromCallback = async (opts: { code: string; codeVerifier
// parse is reported with its reason rather than dropped, so a document that
// looks right but is not says why; a row for another provider is dropped,
// because it was never a candidate here.
-export const previewCodexJson = async (rawJson: string): Promise =>
- parseCodexJsonSources(rawJson).flatMap(source => {
+export const previewChatGPTJson = async (rawJson: string): Promise =>
+ parseChatGPTJsonSources(rawJson).flatMap(source => {
if (!source.supported) return [];
try {
const credential = source.normalize();
@@ -133,21 +133,21 @@ export const previewCodexJson = async (rawJson: string): Promise => {
- if (!Number.isInteger(sourceIndex) || sourceIndex < 0) throw new TypeError('Codex JSON source index must be a non-negative integer');
- const source = parseCodexJsonSources(rawJson).find(candidate => candidate.sourceIndex === sourceIndex);
- if (!source) throw new Error(`Codex JSON source index ${sourceIndex} does not exist`);
- if (!source.supported) throw new Error(`Codex JSON source index ${sourceIndex} is not an OpenAI OAuth account`);
- return buildCodexImportResult(source.normalize(), new Date().toISOString());
+export const importChatGPTFromJson = async (rawJson: string, sourceIndex: number): Promise => {
+ if (!Number.isInteger(sourceIndex) || sourceIndex < 0) throw new TypeError('ChatGPT JSON source index must be a non-negative integer');
+ const source = parseChatGPTJsonSources(rawJson).find(candidate => candidate.sourceIndex === sourceIndex);
+ if (!source) throw new Error(`ChatGPT JSON source index ${sourceIndex} does not exist`);
+ if (!source.supported) throw new Error(`ChatGPT JSON source index ${sourceIndex} is not an OpenAI OAuth account`);
+ return buildChatGPTImportResult(source.normalize(), new Date().toISOString());
};
// One account the pasted document describes. `normalize` stays lazy so the
// preview can report a parse failure per row instead of failing the document.
-interface CodexJsonSource {
+interface ChatGPTJsonSource {
sourceIndex: number;
name: string | null;
supported: boolean;
- normalize: () => NormalizedCodexCredential;
+ normalize: () => NormalizedChatGPTCredential;
}
// The envelopes accounts are exported in differ in nesting and in how they
@@ -160,10 +160,10 @@ interface CodexJsonSource {
// they hold. Two envelopes in one document is refused rather than ranked: the
// document was assembled by something we do not know, and importing the wrong
// half is worse than asking for a cleaner paste.
-const parseCodexJsonSources = (rawJson: string): CodexJsonSource[] => {
- const root = parseJsonObject(rawJson, 'Codex credential JSON');
+const parseChatGPTJsonSources = (rawJson: string): ChatGPTJsonSource[] => {
+ const root = parseJsonObject(rawJson, 'ChatGPT credential JSON');
const data = root.data;
- const matchers: Array<() => CodexJsonSource[]> = [];
+ const matchers: Array<() => ChatGPTJsonSource[]> = [];
if (Object.hasOwn(root, 'tokens')) {
matchers.push(() => [{
@@ -174,24 +174,24 @@ const parseCodexJsonSources = (rawJson: string): CodexJsonSource[] => {
}]);
}
if (Object.hasOwn(root, 'credentials')) {
- matchers.push(() => [makeAccountSource(root, 0, 'Codex JSON root')]);
+ matchers.push(() => [makeAccountSource(root, 0, 'ChatGPT JSON root')]);
}
if (Object.hasOwn(root, 'accounts')) {
- matchers.push(() => parseAccountSources(root.accounts, 'Codex JSON accounts'));
+ matchers.push(() => parseAccountSources(root.accounts, 'ChatGPT JSON accounts'));
}
if (isObject(data) && Object.hasOwn(data, 'accounts')) {
- matchers.push(() => parseAccountSources(data.accounts, 'Codex JSON data.accounts'));
+ matchers.push(() => parseAccountSources(data.accounts, 'ChatGPT JSON data.accounts'));
}
- if (matchers.length === 0) throw new TypeError('Codex credential JSON does not match a supported structure');
- if (matchers.length > 1) throw new Error('Codex credential JSON is ambiguous: multiple supported structures are present');
+ if (matchers.length === 0) throw new TypeError('ChatGPT credential JSON does not match a supported structure');
+ if (matchers.length > 1) throw new Error('ChatGPT credential JSON is ambiguous: multiple supported structures are present');
return matchers[0]();
};
// `~/.codex/auth.json`: the CLI's own on-disk format, tokens under `.tokens`.
-const normalizeAuthJsonRoot = (root: Record): NormalizedCodexCredential => {
+const normalizeAuthJsonRoot = (root: Record): NormalizedChatGPTCredential => {
const tokens = requireObject(root.tokens, 'Codex auth JSON tokens');
- return normalizeCodexCredential({
+ return normalizeChatGPTCredential({
accessToken: requireString(tokens.access_token, 'Codex auth JSON tokens.access_token'),
refreshToken: optionalString(tokens.refresh_token, 'Codex auth JSON tokens.refresh_token'),
idToken: optionalString(tokens.id_token, 'Codex auth JSON tokens.id_token'),
@@ -200,7 +200,7 @@ const normalizeAuthJsonRoot = (root: Record): NormalizedCodexCr
});
};
-const parseAccountSources = (value: unknown, where: string): CodexJsonSource[] => {
+const parseAccountSources = (value: unknown, where: string): ChatGPTJsonSource[] => {
if (!Array.isArray(value)) throw new TypeError(`${where} must be an array`);
return value.map((entry, sourceIndex) => makeAccountSource(
requireObject(entry, `${where}[${sourceIndex}]`),
@@ -209,7 +209,7 @@ const parseAccountSources = (value: unknown, where: string): CodexJsonSource[] =
));
};
-const makeAccountSource = (account: Record, sourceIndex: number, where: string): CodexJsonSource => ({
+const makeAccountSource = (account: Record, sourceIndex: number, where: string): ChatGPTJsonSource => ({
sourceIndex,
// A label, not a credential field. A document that types it as something
// else still imports; the row just falls back to another label.
@@ -218,9 +218,9 @@ const makeAccountSource = (account: Record, sourceIndex: number
normalize: () => normalizeAccountRecord(account, where),
});
-const normalizeAccountRecord = (account: Record, where: string): NormalizedCodexCredential => {
+const normalizeAccountRecord = (account: Record, where: string): NormalizedChatGPTCredential => {
const credentials = requireObject(account.credentials, `${where}.credentials`);
- const input: CodexCredentialInput = {
+ const input: ChatGPTCredentialInput = {
accessToken: requireString(credentials.access_token, `${where}.credentials.access_token`),
refreshToken: optionalString(credentials.refresh_token, `${where}.credentials.refresh_token`),
idToken: optionalString(credentials.id_token, `${where}.credentials.id_token`),
@@ -230,7 +230,7 @@ const normalizeAccountRecord = (account: Record, where: string)
planType: optionalString(credentials.plan_type, `${where}.credentials.plan_type`),
expiresAt: parseSourceExpiry(credentials.expires_at, `${where}.credentials.expires_at`),
};
- return normalizeCodexCredential(input);
+ return normalizeChatGPTCredential(input);
};
// Multi-provider exports tag each account. An untagged account is taken as
diff --git a/packages/provider-codex/src/auth/jwt.ts b/packages/provider-chatgpt/src/auth/jwt.ts
similarity index 91%
rename from packages/provider-codex/src/auth/jwt.ts
rename to packages/provider-chatgpt/src/auth/jwt.ts
index db8282bbd3..cfd84d17bd 100644
--- a/packages/provider-codex/src/auth/jwt.ts
+++ b/packages/provider-chatgpt/src/auth/jwt.ts
@@ -8,7 +8,7 @@
import { isObject } from './guards.ts';
import { decodeCanonicalBase64url } from '@floway-dev/protocols/common';
-export interface CodexTokenClaims {
+export interface ChatGPTTokenClaims {
email: string | null;
chatgptAccountId: string | null;
chatgptUserId: string | null;
@@ -16,7 +16,7 @@ export interface CodexTokenClaims {
expiresAt: number | null;
}
-export const parseCodexTokenClaims = (token: string, label = 'token'): CodexTokenClaims => {
+export const parseChatGPTTokenClaims = (token: string, label = 'token'): ChatGPTTokenClaims => {
const payload = decodeJwtPayload(token, label);
const auth = payload['https://api.openai.com/auth'];
const profile = payload['https://api.openai.com/profile'];
@@ -39,9 +39,9 @@ export const parseCodexTokenClaims = (token: string, label = 'token'): CodexToke
// An access token may be opaque. A decode failure therefore means only that
// identity and expiry cannot be inferred from it; it says nothing about
// whether the bearer works.
-export const tryParseCodexAccessTokenClaims = (accessToken: string): CodexTokenClaims | null => {
+export const tryParseChatGPTAccessTokenClaims = (accessToken: string): ChatGPTTokenClaims | null => {
try {
- return parseCodexTokenClaims(accessToken, 'access_token');
+ return parseChatGPTTokenClaims(accessToken, 'access_token');
} catch {
return null;
}
@@ -52,7 +52,7 @@ export const tryParseCodexAccessTokenClaims = (accessToken: string): CodexTokenC
// profile claims from a later id_token. Missing plan returns `undefined` so
// callers can preserve the latest observation or use the import-time identity;
// malformed present claims still surface.
-export const parseCodexIdTokenPlanType = (idToken: string): string | undefined => {
+export const parseChatGPTIdTokenPlanType = (idToken: string): string | undefined => {
const payload = decodeJwtPayload(idToken, 'id_token');
const auth = payload['https://api.openai.com/auth'];
if (auth === undefined) return undefined;
diff --git a/packages/provider-codex/src/auth/oauth.ts b/packages/provider-chatgpt/src/auth/oauth.ts
similarity index 77%
rename from packages/provider-codex/src/auth/oauth.ts
rename to packages/provider-chatgpt/src/auth/oauth.ts
index 3791c644ee..9f82e3476b 100644
--- a/packages/provider-codex/src/auth/oauth.ts
+++ b/packages/provider-chatgpt/src/auth/oauth.ts
@@ -1,18 +1,18 @@
import {
- CODEX_AUTHORIZE_URL,
- CODEX_CLIENT_ID,
- CODEX_OAUTH_SCOPE,
- CODEX_OAUTH_TOKEN_URL,
- CODEX_OAUTH_USER_AGENT,
- CODEX_ORIGINATOR,
- CODEX_REDIRECT_URI,
+ CHATGPT_AUTHORIZE_URL,
+ CHATGPT_CLIENT_ID,
+ CHATGPT_OAUTH_SCOPE,
+ CHATGPT_OAUTH_TOKEN_URL,
+ CHATGPT_OAUTH_USER_AGENT,
+ CHATGPT_ORIGINATOR,
+ CHATGPT_REDIRECT_URI,
} from '../constants.ts';
import type { Fetcher } from '@floway-dev/provider';
// What /token actually guarantees. A refresh response has been observed to
// omit `id_token`, so the field is optional here and each caller states
// whether its own path needs one.
-export interface CodexOAuthRefreshTokens {
+export interface ChatGPTOAuthRefreshTokens {
access_token: string;
refresh_token: string;
// Lifetime in seconds, relative to the server's clock at issue time.
@@ -23,7 +23,7 @@ export interface CodexOAuthRefreshTokens {
id_token?: string;
}
-export interface CodexOAuthTokens extends CodexOAuthRefreshTokens {
+export interface ChatGPTOAuthTokens extends ChatGPTOAuthRefreshTokens {
id_token: string;
}
@@ -31,20 +31,20 @@ export interface CodexOAuthTokens extends CodexOAuthRefreshTokens {
// store an absolute unix-ms expiry, so convert it anchored at the current
// wall clock. The gap between that anchor and the server's issue time sits
// inside the refresh window the access-token module already tolerates.
-export const codexTokenExpiresAt = (expiresInSeconds: number): number => Date.now() + expiresInSeconds * 1000;
+export const chatgptTokenExpiresAt = (expiresInSeconds: number): number => Date.now() + expiresInSeconds * 1000;
-export const buildCodexAuthorizeUrl = (input: { state: string; codeChallenge: string }): string => {
- const url = new URL(CODEX_AUTHORIZE_URL);
+export const buildChatGPTAuthorizeUrl = (input: { state: string; codeChallenge: string }): string => {
+ const url = new URL(CHATGPT_AUTHORIZE_URL);
url.searchParams.set('response_type', 'code');
- url.searchParams.set('client_id', CODEX_CLIENT_ID);
- url.searchParams.set('redirect_uri', CODEX_REDIRECT_URI);
- url.searchParams.set('scope', CODEX_OAUTH_SCOPE);
+ url.searchParams.set('client_id', CHATGPT_CLIENT_ID);
+ url.searchParams.set('redirect_uri', CHATGPT_REDIRECT_URI);
+ url.searchParams.set('scope', CHATGPT_OAUTH_SCOPE);
url.searchParams.set('state', input.state);
url.searchParams.set('code_challenge', input.codeChallenge);
url.searchParams.set('code_challenge_method', 'S256');
url.searchParams.set('id_token_add_organizations', 'true');
url.searchParams.set('codex_cli_simplified_flow', 'true');
- url.searchParams.set('originator', CODEX_ORIGINATOR);
+ url.searchParams.set('originator', CHATGPT_ORIGINATOR);
return url.toString();
};
@@ -56,12 +56,12 @@ export const buildCodexAuthorizeUrl = (input: { state: string; codeChallenge: st
// `invalid_grant` — the only terminal code that might mean "a sibling
// worker just rotated the refresh token, and our copy is stale" — from
// codes that signal genuine credential death under any race scenario.
-export class CodexOAuthSessionTerminatedError extends Error {
+export class ChatGPTOAuthSessionTerminatedError extends Error {
readonly code: string;
readonly upstreamMessage: string;
constructor(args: { code: string; message: string }) {
- super(`Codex OAuth session terminated: ${args.message}`);
- this.name = 'CodexOAuthSessionTerminatedError';
+ super(`ChatGPT OAuth session terminated: ${args.message}`);
+ this.name = 'ChatGPTOAuthSessionTerminatedError';
this.code = args.code;
this.upstreamMessage = args.message;
}
@@ -97,16 +97,16 @@ const REFRESH_TERMINAL_OAUTH_CODES: ReadonlySet = new Set([
'access_denied',
]);
-const codexTokenRequest = async (
+const chatgptTokenRequest = async (
body: URLSearchParams,
terminalCodes: ReadonlySet,
fetcher: Fetcher,
-): Promise => {
- const response = await fetcher(CODEX_OAUTH_TOKEN_URL, {
+): Promise => {
+ const response = await fetcher(CHATGPT_OAUTH_TOKEN_URL, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
- 'user-agent': CODEX_OAUTH_USER_AGENT,
+ 'user-agent': CHATGPT_OAUTH_USER_AGENT,
accept: 'application/json',
},
body: body.toString(),
@@ -137,19 +137,19 @@ const codexTokenRequest = async (
if (message === null && typeof root?.detail === 'string') message = root.detail as string;
message ??= rawText.slice(0, 256);
if (code && terminalCodes.has(code)) {
- throw new CodexOAuthSessionTerminatedError({ code, message });
+ throw new ChatGPTOAuthSessionTerminatedError({ code, message });
}
- throw new Error(`Codex OAuth /token returned ${response.status}: ${message}`);
+ throw new Error(`ChatGPT OAuth /token returned ${response.status}: ${message}`);
}
- if (root === null) throw new Error('Codex OAuth /token response is not an object');
+ if (root === null) throw new Error('ChatGPT OAuth /token response is not an object');
for (const key of ['access_token', 'refresh_token'] as const) {
if (typeof root[key] !== 'string' || root[key] === '') {
- throw new Error(`Codex OAuth /token response missing ${key}`);
+ throw new Error(`ChatGPT OAuth /token response missing ${key}`);
}
}
if (typeof root.expires_in !== 'number' || !Number.isFinite(root.expires_in)) {
- throw new Error('Codex OAuth /token response missing expires_in');
+ throw new Error('ChatGPT OAuth /token response missing expires_in');
}
return {
access_token: root.access_token as string,
@@ -165,7 +165,7 @@ const codexTokenRequest = async (
// egress) keeps every call site honest: callers that want direct egress
// pass `directFetcher` themselves, and the import path can't accidentally
// bypass an operator-configured proxy.
-export const exchangeCodexAuthorizationCode = async (opts: { code: string; codeVerifier: string; fetcher: Fetcher }): Promise => {
+export const exchangeChatGPTAuthorizationCode = async (opts: { code: string; codeVerifier: string; fetcher: Fetcher }): Promise => {
// auth.openai.com rejects exchanges that include a `state` parameter with
// 400 unknown_parameter (live-probed). The upstream Codex CLI's
// `exchange_code_for_tokens` in codex-rs/login/src/server.rs deliberately
@@ -174,17 +174,17 @@ export const exchangeCodexAuthorizationCode = async (opts: { code: string; codeV
// provider-claude-code.)
const body = new URLSearchParams({
grant_type: 'authorization_code',
- client_id: CODEX_CLIENT_ID,
+ client_id: CHATGPT_CLIENT_ID,
code: opts.code,
- redirect_uri: CODEX_REDIRECT_URI,
+ redirect_uri: CHATGPT_REDIRECT_URI,
code_verifier: opts.codeVerifier,
});
- const tokens = await codexTokenRequest(body, EXCHANGE_TERMINAL_OAUTH_CODES, opts.fetcher);
+ const tokens = await chatgptTokenRequest(body, EXCHANGE_TERMINAL_OAUTH_CODES, opts.fetcher);
// The authorization-code grant is the one path that must return an
// id_token: it is a fresh consent, and the identity claims it carries are
// the only ones this flow ever gets to see.
if (tokens.id_token === undefined) {
- throw new Error('Codex OAuth /token response missing id_token');
+ throw new Error('ChatGPT OAuth /token response missing id_token');
}
return { ...tokens, id_token: tokens.id_token };
};
@@ -192,19 +192,19 @@ export const exchangeCodexAuthorizationCode = async (opts: { code: string; codeV
// `fetcher` is required because the refresh has an associated upstream
// and must flow through that upstream's proxy-aware fallback chain rather
// than direct egress.
-export const refreshCodexAccessToken = async (refreshToken: string, fetcher: Fetcher): Promise => {
+export const refreshChatGPTAccessToken = async (refreshToken: string, fetcher: Fetcher): Promise => {
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
- client_id: CODEX_CLIENT_ID,
- scope: CODEX_OAUTH_SCOPE,
+ client_id: CHATGPT_CLIENT_ID,
+ scope: CHATGPT_OAUTH_SCOPE,
});
// OAuth `invalid_grant` on the refresh path is ambiguous on its own — it
// can mean a genuinely revoked/expired refresh_token, *or* that a sibling
// worker raced us, won the rotation, and our copy is now stale. The
// access-token module's `recoverFromRefreshRace` distinguishes by re-reading
// upstream state; the other codes here always mean credential death.
- const tokens = await codexTokenRequest(body, REFRESH_TERMINAL_OAUTH_CODES, fetcher);
+ const tokens = await chatgptTokenRequest(body, REFRESH_TERMINAL_OAUTH_CODES, fetcher);
// A refresh only re-mints the bearer. Whatever identity the account has was
// settled at import, so the id_token here (when present) is used for the
// account's latest plan observation, never to re-litigate identity.
diff --git a/packages/provider-codex/src/browser.ts b/packages/provider-chatgpt/src/browser.ts
similarity index 100%
rename from packages/provider-codex/src/browser.ts
rename to packages/provider-chatgpt/src/browser.ts
diff --git a/packages/provider-codex/src/config.ts b/packages/provider-chatgpt/src/config.ts
similarity index 57%
rename from packages/provider-codex/src/config.ts
rename to packages/provider-chatgpt/src/config.ts
index 01b259fbf2..5a7f62d6b0 100644
--- a/packages/provider-codex/src/config.ts
+++ b/packages/provider-chatgpt/src/config.ts
@@ -1,37 +1,37 @@
import { assertAllowedObjectKeys, assertStringOrNull } from './auth/guards.ts';
import type { UpstreamRecord } from '@floway-dev/provider';
-// One Codex account's operator-managed identity, derived from explicit import
+// One ChatGPT account's operator-managed identity, derived from explicit import
// fields and whatever claims the supplied tokens happen to carry. Mutating
// credentials (refresh_token, access_token, credential health) live in
-// CodexUpstreamState instead.
+// ChatGPTUpstreamState instead.
//
// Every field is nullable because an import source may know none of them: an
// opaque access token carries no claims, and an operator holding only a bearer
// has nothing else to type in. `null` is the recorded absence — the field is
// always present in the persisted document, never omitted.
-export interface CodexAccountIdentity {
+export interface ChatGPTAccountIdentity {
email: string | null;
chatgptAccountId: string | null;
chatgptUserId: string | null;
planType: string | null;
}
-// Codex config is an account pool. v1 always carries exactly one entry —
+// ChatGPT config is an account pool. v1 always carries exactly one entry —
// typed as a 1-tuple so callers can index accounts[0] without a nullable
// cushion. The wire shape stays array-of-accounts so a future fan-out /
// round-robin pool feature can widen the tuple without a schema migration;
// ordering is operator-controlled and stable.
-export interface CodexUpstreamConfig {
- accounts: [CodexAccountIdentity];
+export interface ChatGPTUpstreamConfig {
+ accounts: [ChatGPTAccountIdentity];
}
-export type CodexUpstreamRecord = UpstreamRecord & {
- kind: 'codex';
- config: CodexUpstreamConfig;
+export type ChatGPTUpstreamRecord = UpstreamRecord & {
+ kind: 'chatgpt';
+ config: ChatGPTUpstreamConfig;
};
-const IDENTITY_KEYS: readonly (keyof CodexAccountIdentity)[] = ['email', 'chatgptAccountId', 'chatgptUserId', 'planType'];
+const IDENTITY_KEYS: readonly (keyof ChatGPTAccountIdentity)[] = ['email', 'chatgptAccountId', 'chatgptUserId', 'planType'];
const CONFIG_KEYS_SET: ReadonlySet = new Set(['accounts']);
const IDENTITY_KEYS_SET: ReadonlySet = new Set(IDENTITY_KEYS);
@@ -40,20 +40,20 @@ const IDENTITY_KEYS_SET: ReadonlySet = new Set(IDENTITY_KEYS);
// infer, but `chatgptAccountId` is the join key between config and state, so
// changing it would orphan the stored credential. It may only be restated as
// what it already is; moving to a different account means re-importing.
-export const patchCodexIdentityMetadata = (
- current: CodexUpstreamConfig,
+export const patchChatGPTIdentityMetadata = (
+ current: ChatGPTUpstreamConfig,
patch: Record,
-): CodexUpstreamConfig => {
- assertAllowedObjectKeys(patch, 'Codex config metadata patch', CONFIG_KEYS_SET);
+): ChatGPTUpstreamConfig => {
+ assertAllowedObjectKeys(patch, 'ChatGPT config metadata patch', CONFIG_KEYS_SET);
if (patch.accounts === undefined) return current;
if (!Array.isArray(patch.accounts) || patch.accounts.length !== 1) {
- throw new TypeError('Codex config metadata patch accounts must hold exactly one account');
+ throw new TypeError('ChatGPT config metadata patch accounts must hold exactly one account');
}
- const accountPatch = assertAllowedObjectKeys(patch.accounts[0], 'Codex config metadata patch account', IDENTITY_KEYS_SET);
+ const accountPatch = assertAllowedObjectKeys(patch.accounts[0], 'ChatGPT config metadata patch account', IDENTITY_KEYS_SET);
if (accountPatch.chatgptAccountId !== undefined) {
- assertStringOrNull(accountPatch.chatgptAccountId, 'Codex config metadata patch chatgptAccountId');
+ assertStringOrNull(accountPatch.chatgptAccountId, 'ChatGPT config metadata patch chatgptAccountId');
if (accountPatch.chatgptAccountId !== current.accounts[0].chatgptAccountId) {
- throw new TypeError('Codex ChatGPT account ID can only be changed by re-importing credentials');
+ throw new TypeError('ChatGPT account ID can only be changed by re-importing credentials');
}
}
@@ -61,24 +61,24 @@ export const patchCodexIdentityMetadata = (
for (const key of ['email', 'chatgptUserId', 'planType'] as const) {
const value = accountPatch[key];
if (value === undefined) continue;
- assertStringOrNull(value, `Codex config metadata patch ${key}`);
+ assertStringOrNull(value, `ChatGPT config metadata patch ${key}`);
next[key] = value;
}
return { accounts: [next] };
};
-function assertCodexUpstreamConfig(value: unknown): asserts value is CodexUpstreamConfig {
+function assertChatGPTUpstreamConfig(value: unknown): asserts value is ChatGPTUpstreamConfig {
// config_json round-trips through canonical serialization, so any surviving
// key is persisted. Reject unknown keys to keep the on-disk shape closed.
- const obj = assertAllowedObjectKeys(value, 'CodexUpstreamConfig', CONFIG_KEYS_SET);
+ const obj = assertAllowedObjectKeys(value, 'ChatGPTUpstreamConfig', CONFIG_KEYS_SET);
if (!Array.isArray(obj.accounts)) {
- throw new TypeError('CodexUpstreamConfig.accounts must be an array');
+ throw new TypeError('ChatGPTUpstreamConfig.accounts must be an array');
}
if (obj.accounts.length !== 1) {
- throw new TypeError(`CodexUpstreamConfig.accounts must hold exactly one account (got ${obj.accounts.length})`);
+ throw new TypeError(`ChatGPTUpstreamConfig.accounts must hold exactly one account (got ${obj.accounts.length})`);
}
for (let i = 0; i < obj.accounts.length; i++) {
- const where = `CodexUpstreamConfig.accounts[${i}]`;
+ const where = `ChatGPTUpstreamConfig.accounts[${i}]`;
const acc = assertAllowedObjectKeys(obj.accounts[i], where, IDENTITY_KEYS_SET);
for (const key of IDENTITY_KEYS) {
assertStringOrNull(acc[key], `${where}.${key}`);
@@ -86,9 +86,9 @@ function assertCodexUpstreamConfig(value: unknown): asserts value is CodexUpstre
}
}
-export function assertCodexUpstreamRecord(record: UpstreamRecord): asserts record is CodexUpstreamRecord {
- if (record.kind !== 'codex') {
- throw new TypeError(`Expected provider 'codex', got '${record.kind}'`);
+export function assertChatGPTUpstreamRecord(record: UpstreamRecord): asserts record is ChatGPTUpstreamRecord {
+ if (record.kind !== 'chatgpt') {
+ throw new TypeError(`Expected provider 'chatgpt', got '${record.kind}'`);
}
- assertCodexUpstreamConfig(record.config);
+ assertChatGPTUpstreamConfig(record.config);
}
diff --git a/packages/provider-codex/src/constants.ts b/packages/provider-chatgpt/src/constants.ts
similarity index 73%
rename from packages/provider-codex/src/constants.ts
rename to packages/provider-chatgpt/src/constants.ts
index 1245ba246d..93ca2cfdf7 100644
--- a/packages/provider-codex/src/constants.ts
+++ b/packages/provider-chatgpt/src/constants.ts
@@ -7,34 +7,34 @@
// independent reimplementation surveyed on GitHub:
// https://github.com/openai/codex/blob/87b808bb570f01f4b6fc8485c5459052fac0e320/codex-rs/login/src/auth/manager.rs
// https://github.com/170-carry/codex-tools/blob/0b0910b2b5351372e9ece1a82b3d5ea2ce7c3da5/src-tauri/src/auth.rs
-export const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
+export const CHATGPT_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
-export const CODEX_AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
-export const CODEX_OAUTH_TOKEN_URL = 'https://auth.openai.com/oauth/token';
+export const CHATGPT_AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
+export const CHATGPT_OAUTH_TOKEN_URL = 'https://auth.openai.com/oauth/token';
-// Fixed redirect URI registered against CODEX_CLIENT_ID at OpenAI.
+// Fixed redirect URI registered against CHATGPT_CLIENT_ID at OpenAI.
// Cannot be changed without re-registering the OAuth client.
-export const CODEX_REDIRECT_URI = 'http://localhost:1455/auth/callback';
+export const CHATGPT_REDIRECT_URI = 'http://localhost:1455/auth/callback';
// OAuth scope minimum-confirmed across implementations. We do NOT request the
// connector scopes (api.connectors.read / .invoke) — they are only needed for
// the MCP-connector feature and unrelated to /codex/responses.
-export const CODEX_OAUTH_SCOPE = 'openid profile email offline_access';
+export const CHATGPT_OAUTH_SCOPE = 'openid profile email offline_access';
-// OAuth User-Agent. Pinned independently of the data-plane CODEX_CLI_VERSION:
+// OAuth User-Agent. Pinned independently of the data-plane CHATGPT_CLI_VERSION:
// `0.91.0` is the version captured by OpenAI when the codex-cli OAuth client
// was first registered, and the auth.openai.com /token + /authorize endpoints
// continue to accept it across CLI revisions (cross-checked against
// sub2api/backend, which is in continuous production use against the same
// endpoints with this exact UA). Note the hyphen-lowercase product name —
// distinct from the underscore form used on the data plane below.
-export const CODEX_OAUTH_USER_AGENT = 'codex-cli/0.91.0';
+export const CHATGPT_OAUTH_USER_AGENT = 'codex-cli/0.91.0';
-export const CODEX_BACKEND_BASE = 'https://chatgpt.com/backend-api';
-export const CODEX_OPENAI_RESPONSES_PATH = '/codex/responses';
+export const CHATGPT_BACKEND_BASE = 'https://chatgpt.com/backend-api';
+export const CHATGPT_OPENAI_RESPONSES_PATH = '/codex/responses';
// Codex appends `alpha/search` to its ChatGPT model-provider base.
// https://github.com/openai/codex/blob/2e1607ee2fa8099a233df7437adee5f16a741905/codex-rs/codex-api/src/endpoint/search.rs#L31-L47
-export const CODEX_ALPHA_SEARCH_PATH = '/codex/alpha/search';
+export const CHATGPT_ALPHA_SEARCH_PATH = '/codex/alpha/search';
// Native unary compaction endpoint. The Codex CLI defaults to a client-side
// `RemoteCompactionV2` path that re-uses `/codex/responses` with an appended
// `compaction_trigger` item, but the server still serves this canonical
@@ -44,25 +44,25 @@ export const CODEX_ALPHA_SEARCH_PATH = '/codex/alpha/search';
// endpoint so the provider behaves identically to every other
// `/responses/compact` upstream and skips the SSE drain entirely.
// Reference: https://github.com/openai/codex/blob/f5f812389ee49ab4c9ef1237781ea1013e733fdc/codex-rs/core/src/client.rs#L155
-export const CODEX_OPENAI_RESPONSES_COMPACT_PATH = '/codex/responses/compact';
-export const CODEX_MODELS_PATH = '/codex/models';
+export const CHATGPT_OPENAI_RESPONSES_COMPACT_PATH = '/codex/responses/compact';
+export const CHATGPT_MODELS_PATH = '/codex/models';
// Earned ChatGPT subscription resets are a separate account surface from the
// ordinary Codex data plane. The official Codex backend client selects these
// WHAM paths for chatgpt.com and posts a caller-stable redemption id beside an
// optional opaque credit id:
// https://github.com/openai/codex/blob/ac7634b9f73ec1bf96466be7a5869f0949d20b30/codex-rs/backend-client/src/client/rate_limit_resets.rs#L112-L171
-export const CODEX_RATE_LIMIT_RESET_CREDITS_PATH = '/wham/rate-limit-reset-credits';
-export const CODEX_RATE_LIMIT_RESET_CREDITS_CONSUME_PATH = '/wham/rate-limit-reset-credits/consume';
+export const CHATGPT_RATE_LIMIT_RESET_CREDITS_PATH = '/wham/rate-limit-reset-credits';
+export const CHATGPT_RATE_LIMIT_RESET_CREDITS_CONSUME_PATH = '/wham/rate-limit-reset-credits/consume';
// Codex's image extension does not discover this model through /codex/models.
// It owns the capability and sends the fixed model id to these provider-
// relative endpoints instead.
// https://github.com/openai/codex/blob/646f7c0a91b8e327d263335da68ae8ef212895ce/codex-rs/ext/image-generation/src/tool.rs#L51-L59
// https://github.com/openai/codex/blob/646f7c0a91b8e327d263335da68ae8ef212895ce/codex-rs/codex-api/src/endpoint/images.rs#L33-L70
-export const CODEX_IMAGE_MODEL_ID = 'gpt-image-2';
-export const CODEX_OPENAI_IMAGES_GENERATIONS_PATH = '/codex/images/generations';
-export const CODEX_OPENAI_IMAGES_EDITS_PATH = '/codex/images/edits';
+export const CHATGPT_IMAGE_MODEL_ID = 'gpt-image-2';
+export const CHATGPT_OPENAI_IMAGES_GENERATIONS_PATH = '/codex/images/generations';
+export const CHATGPT_OPENAI_IMAGES_EDITS_PATH = '/codex/images/edits';
// codex_cli_rs version we impersonate on the data plane. Bumped against the
// stable release at https://github.com/openai/codex/releases/tag/rust-v0.159.3.
@@ -72,19 +72,19 @@ export const CODEX_OPENAI_IMAGES_EDITS_PATH = '/codex/images/edits';
// `version` header, and the User-Agent so the upstream sees a consistent client.
// https://github.com/openai/codex/blob/fe74a774532af67b5a4a3dec03ce9469e17f89af/codex-rs/model-provider-info/src/lib.rs#L386-L400
// https://github.com/openai/codex/blob/49e95cc73f4eb2999b1d14f863c009168df6122b/codex-rs/models-manager/models.json
-export const CODEX_CLI_VERSION = '0.159.3';
+export const CHATGPT_CLI_VERSION = '0.159.3';
// Official Codex HTTP/WS markers. Only private catalog metadata may select
// the outbound HTTP marker; caller markers never select a model's wire format.
// https://github.com/openai/codex/blob/3d2ee51ca2d5db578f328aa75e20aa22c0197c9a/codex-rs/core/src/client.rs#L163-L169
-export const CODEX_RESPONSES_LITE_HEADER =
+export const CHATGPT_RESPONSES_LITE_HEADER =
'x-openai-internal-codex-responses-lite';
-export const CODEX_RESPONSES_LITE_CLIENT_METADATA_KEY =
+export const CHATGPT_RESPONSES_LITE_CLIENT_METADATA_KEY =
'ws_request_header_x_openai_internal_codex_responses_lite';
// Shared official Codex data-plane identity for /codex/models and
// /codex/responses. The User-Agent intentionally includes Codex's normal
// OS/arch/terminal segment; do not append Floway/operator/MCP attribution.
-export const CODEX_ORIGINATOR = 'codex_cli_rs';
-export const CODEX_USER_AGENT =
- `codex_cli_rs/${CODEX_CLI_VERSION} (Mac OS 26.5.0; arm64) iTerm.app/3.6.10`;
+export const CHATGPT_ORIGINATOR = 'codex_cli_rs';
+export const CHATGPT_USER_AGENT =
+ `codex_cli_rs/${CHATGPT_CLI_VERSION} (Mac OS 26.5.0; arm64) iTerm.app/3.6.10`;
diff --git a/packages/provider-codex/src/defaults.ts b/packages/provider-chatgpt/src/defaults.ts
similarity index 96%
rename from packages/provider-codex/src/defaults.ts
rename to packages/provider-chatgpt/src/defaults.ts
index 5d705f4cf7..7e0011a66e 100644
--- a/packages/provider-codex/src/defaults.ts
+++ b/packages/provider-chatgpt/src/defaults.ts
@@ -1,6 +1,6 @@
import type { FlagDefaults } from '@floway-dev/provider';
-export const CODEX_DEFAULT_FLAGS: FlagDefaults = {
+export const CHATGPT_DEFAULT_FLAGS: FlagDefaults = {
'vendor-deepseek': false,
'vendor-qwen': false,
'vendor-kimi': false,
diff --git a/packages/provider-codex/src/fetch.ts b/packages/provider-chatgpt/src/fetch.ts
similarity index 69%
rename from packages/provider-codex/src/fetch.ts
rename to packages/provider-chatgpt/src/fetch.ts
index 7b94f23485..ec65c6b55c 100644
--- a/packages/provider-codex/src/fetch.ts
+++ b/packages/provider-chatgpt/src/fetch.ts
@@ -1,34 +1,34 @@
-import { CodexAccessOnlyCredentialError, codexPlanObservation, ensureCodexAccessToken, invalidateCodexAccessToken, mintCodexAccessToken, type CodexPlanObservation } from './access-token.ts';
+import { ChatGPTAccessOnlyCredentialError, chatgptPlanObservation, ensureChatGPTAccessToken, invalidateChatGPTAccessToken, mintChatGPTAccessToken, type ChatGPTPlanObservation } from './access-token.ts';
import { isObject } from './auth/guards.ts';
-import { CodexOAuthSessionTerminatedError } from './auth/oauth.ts';
+import { ChatGPTOAuthSessionTerminatedError } from './auth/oauth.ts';
import {
- CODEX_BACKEND_BASE,
- CODEX_CLI_VERSION,
- CODEX_ALPHA_SEARCH_PATH,
- CODEX_OPENAI_IMAGES_EDITS_PATH,
- CODEX_OPENAI_IMAGES_GENERATIONS_PATH,
- CODEX_ORIGINATOR,
- CODEX_OPENAI_RESPONSES_COMPACT_PATH,
- CODEX_OPENAI_RESPONSES_PATH,
- CODEX_RESPONSES_LITE_CLIENT_METADATA_KEY,
- CODEX_RESPONSES_LITE_HEADER,
- CODEX_USER_AGENT,
+ CHATGPT_BACKEND_BASE,
+ CHATGPT_CLI_VERSION,
+ CHATGPT_ALPHA_SEARCH_PATH,
+ CHATGPT_OPENAI_IMAGES_EDITS_PATH,
+ CHATGPT_OPENAI_IMAGES_GENERATIONS_PATH,
+ CHATGPT_ORIGINATOR,
+ CHATGPT_OPENAI_RESPONSES_COMPACT_PATH,
+ CHATGPT_OPENAI_RESPONSES_PATH,
+ CHATGPT_RESPONSES_LITE_CLIENT_METADATA_KEY,
+ CHATGPT_RESPONSES_LITE_HEADER,
+ CHATGPT_USER_AGENT,
} from './constants.ts';
import { sha256JsonUuid, uuidV7 } from './ids.ts';
-import { codexModelUsesResponsesLite, codexPlanSupportsImages } from './models.ts';
+import { chatgptModelUsesResponsesLite, chatgptPlanSupportsImages } from './models.ts';
import {
- hasCodexQuotaReading,
- parseCodexQuotaHeaders,
- putCodexQuota,
+ hasChatGPTQuotaReading,
+ parseChatGPTQuotaHeaders,
+ putChatGPTQuota,
} from './quota.ts';
import {
- encodeCodexResponsesLiteRequest,
- projectMovedCodexResponsesFields,
- type CodexResponsesBody,
- type CodexResponsesLiteRequest,
+ encodeChatGPTResponsesLiteRequest,
+ projectMovedChatGPTResponsesFields,
+ type ChatGPTResponsesBody,
+ type ChatGPTResponsesLiteRequest,
} from './responses-lite.ts';
-import { restoreCodexResponsesOutput } from './responses-output.ts';
-import type { CodexAccessTokenEntry, CodexAccountCredential } from './state.ts';
+import { restoreChatGPTResponsesOutput } from './responses-output.ts';
+import type { ChatGPTAccessTokenEntry, ChatGPTAccountCredential } from './state.ts';
import { isEventStreamMediaType } from '@floway-dev/protocols/common';
import type { OpenAIImagesGenerationsPayload } from '@floway-dev/protocols/openai-images';
import type { CanonicalOpenAIResponsesPayload, OpenAIResponsesCompactionResultEx, CanonicalOpenAIResponsesInputItem, OpenAIResponsesStreamEventEx } from '@floway-dev/protocols/openai-responses';
@@ -43,124 +43,124 @@ export type ProviderCompactionResult =
// terminal-state transitions go through the repo; access-token and quota
// persistence are handled inside their own helpers, which write the same
// state_json row the same way.
-export interface CodexCallEffects {
+export interface ChatGPTCallEffects {
persistRefreshTokenRotation(newRefreshToken: string): Promise;
persistTerminalState(state: 'session_terminated' | 'refresh_failed', message: string): Promise;
}
-// Account selection shared by Codex backend calls. Every surface uses the same
+// Account selection shared by ChatGPT backend calls. Every surface uses the same
// OAuth credential, quota state, terminal-session classification, and refresh
// retry contract; each operation owns its wire body and response decoding.
-interface CodexBackendCallBase {
+interface ChatGPTBackendCallBase {
upstreamId: string;
- account: CodexAccountCredential;
+ account: ChatGPTAccountCredential;
model: ProviderModel;
headers: Headers;
signal?: AbortSignal;
- effects: CodexCallEffects;
+ effects: ChatGPTCallEffects;
call: UpstreamCallOptions;
}
-export interface CallCodexOpenAIResponsesOptions extends CodexBackendCallBase {
+export interface CallChatGPTOpenAIResponsesOptions extends ChatGPTBackendCallBase {
body: Omit;
}
-export interface CallCodexOpenAIResponsesCompactOptions extends CodexBackendCallBase {
+export interface CallChatGPTOpenAIResponsesCompactOptions extends ChatGPTBackendCallBase {
body: Omit;
}
-export interface CallCodexAlphaSearchOptions extends CodexBackendCallBase {
+export interface CallChatGPTAlphaSearchOptions extends ChatGPTBackendCallBase {
body: Record;
}
-export interface CallCodexOpenAIImagesGenerationsOptions extends CodexBackendCallBase {
+export interface CallChatGPTOpenAIImagesGenerationsOptions extends ChatGPTBackendCallBase {
body: Omit;
// Null when the account identity carries no plan claim; the image gate
// treats an unknown plan as allowed (fail open).
fallbackPlanType: string | undefined;
}
-export interface CallCodexOpenAIImagesEditsOptions extends CodexBackendCallBase {
+export interface CallChatGPTOpenAIImagesEditsOptions extends ChatGPTBackendCallBase {
request: OpenAIImagesEditsRequest;
fallbackPlanType: string | undefined;
}
-type CodexOpenAIResponsesBody = CodexResponsesBody;
+type ChatGPTOpenAIResponsesBody = ChatGPTResponsesBody;
-export const callCodexOpenAIResponses = async (opts: CallCodexOpenAIResponsesOptions): Promise> => {
- const prepared = prepareCodexResponsesRequest(opts, 'generate');
- const ready = await prepareCodexCall(opts);
+export const callChatGPTOpenAIResponses = async (opts: CallChatGPTOpenAIResponsesOptions): Promise> => {
+ const prepared = prepareChatGPTResponsesRequest(opts, 'generate');
+ const ready = await prepareChatGPTCall(opts);
if (!ready.ok) return { ok: false, modelKey: opts.model.id, response: ready.response };
return await performStreamingOpenAIResponsesCall(opts, prepared, ready.accessToken, false);
};
-export const callCodexOpenAIResponsesCompact = async (opts: CallCodexOpenAIResponsesCompactOptions): Promise => {
- const prepared = prepareCodexResponsesRequest(opts, 'compact');
- const ready = await prepareCodexCall(opts);
+export const callChatGPTOpenAIResponsesCompact = async (opts: CallChatGPTOpenAIResponsesCompactOptions): Promise => {
+ const prepared = prepareChatGPTResponsesRequest(opts, 'compact');
+ const ready = await prepareChatGPTCall(opts);
if (!ready.ok) return { ok: false, modelKey: opts.model.id, response: ready.response };
return await performUnaryCompactCall(opts, prepared, ready.accessToken, false);
};
-export const callCodexAlphaSearch = async (opts: CallCodexAlphaSearchOptions): Promise => {
+export const callChatGPTAlphaSearch = async (opts: CallChatGPTAlphaSearchOptions): Promise => {
const requestId = stringField(opts.body, 'id') ?? uuidV7();
const normalized = { ...opts, body: { ...opts.body, id: requestId } };
- const ready = await prepareCodexCall(normalized);
+ const ready = await prepareChatGPTCall(normalized);
if (!ready.ok) return { modelKey: normalized.model.id, response: ready.response };
return await performAlphaSearchCall(normalized, ready.accessToken, false);
};
-const prepareCodexImageCall = async (opts: CodexBackendCallBase & { fallbackPlanType: string | undefined }): Promise<{ ok: true; accessToken: CodexAccessTokenEntry; effectivePlan: CodexPlanObservation; turnId: string } | { ok: false; response: Response }> => {
- const ready = await prepareCodexCall(opts);
+const prepareChatGPTImageCall = async (opts: ChatGPTBackendCallBase & { fallbackPlanType: string | undefined }): Promise<{ ok: true; accessToken: ChatGPTAccessTokenEntry; effectivePlan: ChatGPTPlanObservation; turnId: string } | { ok: false; response: Response }> => {
+ const ready = await prepareChatGPTCall(opts);
if (!ready.ok) return { ok: false, response: ready.response };
// An account whose plan is unknown falls back to a fail-open sentinel: the
// upstream gate only withholds images from an explicitly `free` plan.
- const effectivePlan = codexPlanObservation(ready.accessToken)
+ const effectivePlan = chatgptPlanObservation(ready.accessToken)
?? (opts.fallbackPlanType === undefined ? { planType: '' } : { planType: opts.fallbackPlanType });
- if (!codexPlanSupportsImages(effectivePlan.planType)) return { ok: false, response: imageUnavailableResult(opts.model.id).response };
+ if (!chatgptPlanSupportsImages(effectivePlan.planType)) return { ok: false, response: imageUnavailableResult(opts.model.id).response };
const turnId = trimHeader(opts.headers, 'x-codex-image-turn-id') ?? uuidV7();
return { ok: true, accessToken: ready.accessToken, effectivePlan, turnId };
};
-export const callCodexOpenAIImagesGenerations = async (opts: CallCodexOpenAIImagesGenerationsOptions): Promise => {
- const prepared = await prepareCodexImageCall(opts);
+export const callChatGPTOpenAIImagesGenerations = async (opts: CallChatGPTOpenAIImagesGenerationsOptions): Promise => {
+ const prepared = await prepareChatGPTImageCall(opts);
if (!prepared.ok) return { modelKey: opts.model.id, response: prepared.response };
const body = { ...opts.body, model: opts.model.id };
- const request: CodexImageCallRequest = { path: CODEX_OPENAI_IMAGES_GENERATIONS_PATH, body, turnId: prepared.turnId };
+ const request: ChatGPTImageCallRequest = { path: CHATGPT_OPENAI_IMAGES_GENERATIONS_PATH, body, turnId: prepared.turnId };
return await performImageCall(opts, request, prepared.accessToken, prepared.effectivePlan, false);
};
-export const callCodexOpenAIImagesEdits = async (opts: CallCodexOpenAIImagesEditsOptions): Promise => {
- const prepared = await prepareCodexImageCall(opts);
+export const callChatGPTOpenAIImagesEdits = async (opts: CallChatGPTOpenAIImagesEditsOptions): Promise => {
+ const prepared = await prepareChatGPTImageCall(opts);
if (!prepared.ok) return { modelKey: opts.model.id, response: prepared.response };
const body = await serializeOpenAIImagesEditsJsonPayload(opts.request, opts.model.id);
- const request: CodexImageCallRequest = { path: CODEX_OPENAI_IMAGES_EDITS_PATH, body, turnId: prepared.turnId };
+ const request: ChatGPTImageCallRequest = { path: CHATGPT_OPENAI_IMAGES_EDITS_PATH, body, turnId: prepared.turnId };
return await performImageCall(opts, request, prepared.accessToken, prepared.effectivePlan, false);
};
-const prepareCodexCall = async (opts: CodexBackendCallBase): Promise<{ ok: true; accessToken: CodexAccessTokenEntry } | { ok: false; response: Response }> => {
+const prepareChatGPTCall = async (opts: ChatGPTBackendCallBase): Promise<{ ok: true; accessToken: ChatGPTAccessTokenEntry } | { ok: false; response: Response }> => {
if (opts.account.state !== 'active') {
- return { ok: false, response: synthetic503(`Codex upstream is ${opts.account.state}`) };
+ return { ok: false, response: synthetic503(`ChatGPT upstream is ${opts.account.state}`) };
}
try {
- const entry = await ensureCodexAccessToken(opts.upstreamId, opts.account.chatgptAccountId, refresh => mintAccessToken(opts, refresh));
+ const entry = await ensureChatGPTAccessToken(opts.upstreamId, opts.account.chatgptAccountId, refresh => mintAccessToken(opts, refresh));
return { ok: true, accessToken: entry };
} catch (err) {
- if (err instanceof CodexOAuthSessionTerminatedError) return await codexRefreshFailed(opts, err);
+ if (err instanceof ChatGPTOAuthSessionTerminatedError) return await chatgptRefreshFailed(opts, err);
// An access-only credential with nothing usable left is a configuration
// problem, not an upstream one, so it reaches the client as our 503 with
// the re-import instruction rather than as a bare failure.
- if (err instanceof CodexAccessOnlyCredentialError) {
+ if (err instanceof ChatGPTAccessOnlyCredentialError) {
return { ok: false, response: synthetic503(err.message) };
}
throw err;
}
};
-const mintAccessToken = (opts: CodexBackendCallBase, refreshToken: string) =>
- mintCodexAccessToken(refreshToken, opts.call.fetcher, opts.effects.persistRefreshTokenRotation);
+const mintAccessToken = (opts: ChatGPTBackendCallBase, refreshToken: string) =>
+ mintChatGPTAccessToken(refreshToken, opts.call.fetcher, opts.effects.persistRefreshTokenRotation);
-interface CodexRequestIdentity {
+interface ChatGPTRequestIdentity {
installationId: string;
sessionId: string;
threadId: string;
@@ -169,7 +169,7 @@ interface CodexRequestIdentity {
windowId: string;
}
-export interface CodexCompactionTurnMetadata {
+export interface ChatGPTCompactionTurnMetadata {
trigger: 'manual' | 'auto';
reason: 'user_requested' | 'context_limit';
implementation: 'responses_compact' | 'responses_compaction_v2';
@@ -177,12 +177,12 @@ export interface CodexCompactionTurnMetadata {
strategy: 'memento';
}
-export interface CodexTurnMetadataOptions {
+export interface ChatGPTTurnMetadataOptions {
requestKind: 'turn' | 'compaction';
- compaction?: CodexCompactionTurnMetadata;
+ compaction?: ChatGPTCompactionTurnMetadata;
}
-export const CODEX_OPENAI_RESPONSES_COMPACTION_V2_TURN_METADATA: CodexTurnMetadataOptions = {
+export const CHATGPT_OPENAI_RESPONSES_COMPACTION_V2_TURN_METADATA: ChatGPTTurnMetadataOptions = {
requestKind: 'compaction',
compaction: {
trigger: 'manual',
@@ -206,7 +206,7 @@ const stringField = (record: Record | null, key: string): strin
return trimmed.length > 0 ? trimmed : null;
};
-const clientCodexClientMetadata = (body: unknown): Record => {
+const clientChatGPTClientMetadata = (body: unknown): Record => {
if (!isObject(body)) return {};
const candidate = body.client_metadata;
return isObject(candidate) ? candidate : {};
@@ -236,7 +236,7 @@ const parseClientTurnMetadataJson = (raw: string | null): Record): Record | null =>
+const callerTurnMetadata = (opts: ChatGPTBackendCallBase, clientMetadata: Record): Record | null =>
parseClientTurnMetadataJson(stringField(clientMetadata, 'x-codex-turn-metadata'))
?? parseClientTurnMetadataJson(trimHeader(opts.headers, 'x-codex-turn-metadata'));
@@ -253,12 +253,12 @@ const IDENTITY_MIRRORED_CLIENT_METADATA_KEYS = new Set([
'x-codex-installation-id', 'session_id', 'thread_id', 'x-codex-window-id', 'turn_id', 'x-codex-turn-metadata',
]);
-const buildCodexRequestIdentity = (
- opts: CodexBackendCallBase,
- body: CodexOpenAIResponsesBody,
+const buildChatGPTRequestIdentity = (
+ opts: ChatGPTBackendCallBase,
+ body: ChatGPTOpenAIResponsesBody,
clientMetadata: Record