Skip to content

Commit 44fce9d

Browse files
committed
feat: unify codex account detail modules
1 parent f5acf9e commit 44fce9d

6 files changed

Lines changed: 549 additions & 130 deletions

File tree

112 KB
Loading
117 KB
Loading

frontend/src/features/codex/CodexAccountListFeature.tsx

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import useAccountsQuotaState from '../accounts/hooks/useAccountsQuotaState';
2929
import useAccountsRateLimitState from '../accounts/hooks/useAccountsRateLimitState';
3030
import useAccountsUsageState from '../accounts/hooks/useAccountsUsageState';
3131
import { getAccountsPreviewCodexAccounts } from '../accounts/previewData';
32+
import type { ApiKeyConfigDraft } from '../accounts/model/accountDetailConfig';
3233
import {
3334
publishAccountDisabledChange,
3435
readAccountDisabledOverrides,
@@ -113,7 +114,7 @@ export default function CodexAccountListFeature({ sidecarStatus }: CodexAccountL
113114
const suppressNextDetailClickRef = useRef(false);
114115
const { codexQuotaByName, loadCodexQuotas } = useAccountsQuotaState(trackRequest);
115116
const { accountUsageByID, loadAccountUsage } = useAccountsUsageState(trackRequest);
116-
const { accountRateLimitByID, loadAccountRateLimits } = useAccountsRateLimitState(trackRequest);
117+
const { accountRateLimitByID, rateLimitStrategies, loadAccountRateLimits } = useAccountsRateLimitState(trackRequest);
117118

118119
const detailRow = useMemo(
119120
() => orderedRows.find((row) => row.id === detailRowID) || null,
@@ -774,6 +775,90 @@ export default function CodexAccountListFeature({ sidecarStatus }: CodexAccountL
774775
}
775776
}
776777

778+
async function saveDetailConfig(row: CodexAccountRow, draft: ApiKeyConfigDraft, mappings: CodexModelMappingRow[]) {
779+
if (!canEditCodexModelMappings(row.sourceKind) || row.sourceKind === 'codex-auth-file') {
780+
return;
781+
}
782+
783+
const normalizedModels = normalizeCodexModelMappingsForProvider(mappings);
784+
const nextMappings = buildOpenAICompatibleModelMappings({ models: normalizedModels });
785+
const nextAPIKey = draft.apiKey.trim();
786+
const nextBaseURL = draft.baseUrl.trim();
787+
const nextPrefix = draft.prefix.trim();
788+
const nextProxyURL = draft.proxyUrl.trim();
789+
const nextQuotaCurl = draft.quotaCurl.trim();
790+
const nextBillingCurl = draft.billingCurl.trim();
791+
792+
if (browserMode) {
793+
setOrderedRows((prev) =>
794+
prev.map((item) =>
795+
item.id === row.id
796+
? {
797+
...item,
798+
apiKey: nextAPIKey,
799+
apiKeys: nextAPIKey ? [nextAPIKey] : item.apiKeys,
800+
baseUrl: nextBaseURL,
801+
prefix: nextPrefix,
802+
proxyUrl: nextProxyURL,
803+
quotaCurl: nextQuotaCurl,
804+
quotaEnabled: Boolean(draft.quotaEnabled && nextQuotaCurl),
805+
billingCurl: nextBillingCurl,
806+
billingEnabled: Boolean(draft.billingEnabled && nextBillingCurl),
807+
modelMappings: nextMappings,
808+
}
809+
: item,
810+
),
811+
);
812+
setMessage(t('codex.account_list_model_mapping_saved'));
813+
return;
814+
}
815+
816+
setPendingMappingID(row.id);
817+
try {
818+
if (row.sourceKind === 'openai-compatible') {
819+
await trackRequest('UpdateOpenAICompatibleProvider', { id: row.id, baseUrl: nextBaseURL, models: normalizedModels }, () =>
820+
UpdateOpenAICompatibleProvider(
821+
main.UpdateOpenAICompatibleProviderInput.createFrom({
822+
currentName: row.provider,
823+
name: row.provider,
824+
baseUrl: nextBaseURL,
825+
prefix: nextPrefix,
826+
apiKey: nextAPIKey,
827+
apiKeys: nextAPIKey ? [nextAPIKey] : [],
828+
proxyUrl: nextProxyURL,
829+
headers: row.headers || {},
830+
models: normalizedModels,
831+
}),
832+
),
833+
);
834+
} else {
835+
await trackRequest('UpdateCodexAPIKeyConfig', { id: row.id, baseUrl: nextBaseURL, models: normalizedModels }, () =>
836+
UpdateCodexAPIKeyConfig(
837+
main.UpdateCodexAPIKeyConfigInput.createFrom({
838+
id: row.id,
839+
apiKey: nextAPIKey,
840+
baseUrl: nextBaseURL,
841+
prefix: nextPrefix,
842+
proxyUrl: nextProxyURL,
843+
models: normalizedModels,
844+
quotaCurl: nextQuotaCurl,
845+
quotaEnabled: Boolean(draft.quotaEnabled && nextQuotaCurl),
846+
billingCurl: nextBillingCurl,
847+
billingEnabled: Boolean(draft.billingEnabled && nextBillingCurl),
848+
}),
849+
),
850+
);
851+
}
852+
await reload(t('codex.account_list_model_mapping_saved'));
853+
} catch (error) {
854+
console.error(error);
855+
setMessage(`${t('codex.account_list_model_mapping_save_failed')}: ${toErrorMessage(error)}`);
856+
throw error;
857+
} finally {
858+
setPendingMappingID(null);
859+
}
860+
}
861+
777862
async function saveModelMappings(row: CodexAccountRow, mappings: CodexModelMappingRow[]) {
778863
if (!canEditCodexModelMappings(row.sourceKind)) {
779864
return;
@@ -968,6 +1053,8 @@ export default function CodexAccountListFeature({ sidecarStatus }: CodexAccountL
9681053
t={t}
9691054
quotaState={detailRowWithModels.quotaKey ? codexQuotaByName[detailRowWithModels.quotaKey] : undefined}
9701055
usageSummary={accountUsageByID[detailRowWithModels.id]}
1056+
rateLimitStatus={accountRateLimitByID[detailRowWithModels.id]}
1057+
rateLimitStrategies={rateLimitStrategies}
9711058
savingMappings={pendingMappingID === detailRowWithModels.id}
9721059
loadingModelMappings={loadingAuthFileModelID === detailRowWithModels.id}
9731060
modelMappingError={authFileModelErrors[detailRowWithModels.id] || ''}
@@ -982,6 +1069,8 @@ export default function CodexAccountListFeature({ sidecarStatus }: CodexAccountL
9821069
loadingModelOptions={loadingOpenAICompatibleModelID === detailRowWithModels.id}
9831070
modelOptionError={openAICompatibleModelErrors[detailRowWithModels.id] || ''}
9841071
onClose={closeDetail}
1072+
onSaveConfig={(draft, mappings) => saveDetailConfig(detailRowWithModels, draft, mappings)}
1073+
onRateLimitRulesChanged={() => void loadAccountRateLimits(orderedRows.map(buildCodexQuotaSummaryAccount))}
9851074
onSaveModelMappings={(mappings) => saveModelMappings(detailRowWithModels, mappings)}
9861075
/>
9871076
) : null}

frontend/src/features/codex/codexAccountList.test.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises';
44

55
import {
66
applyCodexAccountPriorities,
7+
buildCodexAccountDetailModulePlan,
78
buildCodexAuthFileModelMappings,
89
buildCodexModelAliasOptionNames,
910
buildCodexModelOptionNames,
@@ -121,6 +122,21 @@ test('canEditCodexModelMappings allows codex api key mappings in detail modal ed
121122
assert.equal(canEditCodexModelMappings('codex-api-key'), true);
122123
});
123124

125+
test('buildCodexAccountDetailModulePlan merges account detail modules with model routing by account type', () => {
126+
assert.deepEqual(
127+
buildCodexAccountDetailModulePlan({ sourceKind: 'openai-compatible' }),
128+
['credentials', 'proxy-route', 'rate-limit', 'verify', 'quota', 'billing', 'model-routing'],
129+
);
130+
assert.deepEqual(
131+
buildCodexAccountDetailModulePlan({ sourceKind: 'codex-api-key' }),
132+
['credentials', 'proxy-route', 'rate-limit', 'verify', 'quota', 'billing', 'model-routing'],
133+
);
134+
assert.deepEqual(
135+
buildCodexAccountDetailModulePlan({ sourceKind: 'codex-auth-file' }),
136+
['auth-file-actions', 'models', 'rate-limit', 'model-routing'],
137+
);
138+
});
139+
124140
test('buildCodexAccountRows keeps disabled or errored accounts in order but marks them not requestable', () => {
125141
const rows = buildCodexAccountRows({
126142
accounts: [

0 commit comments

Comments
 (0)