diff --git a/gui/src/components/codex-account-pool-cards.tsx b/gui/src/components/codex-account-pool-cards.tsx
index 8accb3d87d..3fc37b8d59 100644
--- a/gui/src/components/codex-account-pool-cards.tsx
+++ b/gui/src/components/codex-account-pool-cards.tsx
@@ -1,4 +1,4 @@
-import { useT } from "../i18n/shared";
+import { useT, useI18n } from "../i18n/shared";
import { IconAlert, IconPause, IconPlay, IconX } from "../icons";
import { displayAccountId } from "../lib/privacy";
import type { CodexAccountEntry } from "./codex-account-pool-types";
@@ -49,6 +49,7 @@ export function CodexAccountPoolCards({
doctorCopyOutcomeFor?: (accountId: string) => "copied" | "unavailable" | null;
}) {
const t = useT();
+ const { locale } = useI18n();
const isNext = (account: CodexAccountEntry) => !account.paused && activeId === account.id;
return (
@@ -125,7 +126,16 @@ export function CodexAccountPoolCards({
-
{a.email}{a.plan ? ` · ${a.plan}` : ""} · {t("prov.accountId")}: {displayAccountId(a.id)}
+ {(() => {
+ const parts = [a.email, a.plan ? a.plan : "", `${t("prov.accountId")}: ${displayAccountId(a.id)}`].filter(Boolean);
+ if (typeof a.expiresAt === "number" && a.expiresAt > 0) {
+ const expDate = new Date(a.expiresAt);
+ if (!Number.isNaN(expDate.getTime())) {
+ parts.push(t("prov.expiresAt", { date: expDate.toLocaleDateString(locale, { month: "short", day: "numeric", year: "numeric" }) }));
+ }
+ }
+ return parts.join(" · ");
+ })()}
{healthSummary && (
{healthSummary}
)}
@@ -156,6 +166,7 @@ export function CodexAccountPoolReauthBanner({
onReauth: () => void;
}) {
const t = useT();
+ const { locale } = useI18n();
return (
{t("codexAuth.tokenExpired")}
diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx
index 6a19c5f0d2..fb1d8b42ce 100644
--- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx
+++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx
@@ -217,7 +217,16 @@ export default function ProviderAuthPanel({
{label}
- {[account.email, `${t("prov.accountId")}: ${maskedId}`].filter(Boolean).join(" · ")}
+ {(() => {
+ const parts = [account.email, `${t("prov.accountId")}: ${maskedId}`];
+ if (typeof account.expiresAt === "number" && account.expiresAt > 0) {
+ const expDate = new Date(account.expiresAt);
+ if (!Number.isNaN(expDate.getTime())) {
+ parts.push(t("prov.expiresAt", { date: expDate.toLocaleDateString(locale, { month: "short", day: "numeric", year: "numeric" }) }));
+ }
+ }
+ return parts.filter(Boolean).join(" · ");
+ })()}
{healthSummary && (
{healthSummary}
)}
diff --git a/gui/src/components/provider-workspace/types.ts b/gui/src/components/provider-workspace/types.ts
index 97e6f1969d..adb8c339df 100644
--- a/gui/src/components/provider-workspace/types.ts
+++ b/gui/src/components/provider-workspace/types.ts
@@ -46,6 +46,7 @@ export type OAuthAccountRow = {
email?: string;
active: boolean;
needsReauth?: boolean;
+ expiresAt?: number;
health?: { status: OAuthAccountHealthStatus; reason?: string; until?: string };
healthLabel?: string;
healthSummary?: string;
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts
index 46d78faceb..9f6d0a5f6e 100644
--- a/gui/src/i18n/de.ts
+++ b/gui/src/i18n/de.ts
@@ -328,7 +328,8 @@ export const de: Record = {
"prov.removeConfirm": "Anbieter \"{name}\" entfernen? Seine Modelle verschwinden aus Codex’ Auswahl.",
"prov.hasApiKey": "API-Schlüssel konfiguriert",
"prov.hasHeaders": "benutzerdefinierte Header konfiguriert",
- "prov.accounts": "Konten ({n})",
+ "prov.accounts":
+ "prov.expiresAt": "Läuft ab: {date}", "Konten ({n})",
"prov.accountsAria": "{name}-Konten umschalten",
"prov.accountActive": "Aktiv",
"prov.accountReauth": "Erneut anmelden",
diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts
index 8389a237a2..1b4c13b0ef 100644
--- a/gui/src/i18n/en.ts
+++ b/gui/src/i18n/en.ts
@@ -345,7 +345,8 @@ export const en = {
"prov.removeConfirm": "Remove provider \"{name}\"? Its models disappear from Codex's picker.",
"prov.hasApiKey": "api key configured",
"prov.hasHeaders": "custom headers configured",
- "prov.accounts": "Accounts ({n})",
+ "prov.accounts":
+ "prov.expiresAt": "Expires: {date}", "Accounts ({n})",
"prov.accountsAria": "Toggle {name} accounts",
"prov.accountActive": "Active",
"prov.accountReauth": "Re-login",
diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts
index 77aaa0dd82..3cac2debdb 100644
--- a/gui/src/i18n/ja.ts
+++ b/gui/src/i18n/ja.ts
@@ -334,7 +334,8 @@ export const ja: Record = {
"prov.removeConfirm": "プロバイダー \"{name}\" を削除しますか? そのモデルは Codex のピッカーから消えます。",
"prov.hasApiKey": "API キー設定済み",
"prov.hasHeaders": "カスタムヘッダー設定済み",
- "prov.accounts": "アカウント ({n})",
+ "prov.accounts":
+ "prov.expiresAt": "有効期限: {date}", "アカウント ({n})",
"prov.accountsAria": "{name} のアカウントを切り替え",
"prov.accountActive": "アクティブ",
"prov.accountReauth": "再ログイン",
diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts
index 93e03755ec..92dc244b86 100644
--- a/gui/src/i18n/ko.ts
+++ b/gui/src/i18n/ko.ts
@@ -337,7 +337,8 @@ export const ko: Record = {
"prov.removeConfirm": "프로바이더 \"{name}\" 을(를) 삭제할까요? 해당 모델이 Codex 선택기에서 사라집니다.",
"prov.hasApiKey": "API 키 설정됨",
"prov.hasHeaders": "커스텀 헤더 설정됨",
- "prov.accounts": "계정 ({n})",
+ "prov.accounts":
+ "prov.expiresAt": "만료일: {date}", "계정 ({n})",
"prov.accountsAria": "{name} 계정 목록 열기/닫기",
"prov.accountActive": "활성",
"prov.accountReauth": "재로그인",
diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts
index 48e9149b63..4fe8d862bf 100644
--- a/gui/src/i18n/ru.ts
+++ b/gui/src/i18n/ru.ts
@@ -339,7 +339,8 @@ export const ru: Record = {
"prov.removeConfirm": "Удалить провайдера \"{name}\"? Его модели исчезнут из селектора моделей Codex.",
"prov.hasApiKey": "API-ключ настроен",
"prov.hasHeaders": "настроены пользовательские заголовки",
- "prov.accounts": "Аккаунты ({n})",
+ "prov.accounts":
+ "prov.expiresAt": "Истекает: {date}", "Аккаунты ({n})",
"prov.accountsAria": "Показать или скрыть аккаунты {name}",
"prov.accountActive": "Активен",
"prov.accountReauth": "Повторный вход",
diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts
index 15e6053b33..265d8b4d04 100644
--- a/gui/src/i18n/zh.ts
+++ b/gui/src/i18n/zh.ts
@@ -334,7 +334,8 @@ export const zh: Record = {
"prov.removeConfirm": "移除提供方 \"{name}\"?其模型将从 Codex 选择器中消失。",
"prov.hasApiKey": "已配置 API 密钥",
"prov.hasHeaders": "已配置自定义请求头",
- "prov.accounts": "账户({n})",
+ "prov.accounts":
+ "prov.expiresAt": "到期时间: {date}", "账户({n})",
"prov.accountsAria": "展开/收起 {name} 账户",
"prov.accountActive": "使用中",
"prov.accountReauth": "需重新登录",
diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts
index 3bb6577292..8eea47c320 100644
--- a/src/codex/auth-api.ts
+++ b/src/codex/auth-api.ts
@@ -195,6 +195,7 @@ function poolAccountDto(
const quota = quotaForPlan(quotaResult.quota, account.plan);
const needsReauth = !hasCredential || quotaResult.needsReauth || isAccountNeedsReauth(account.id);
const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
+ const cred = getCodexAccountCredential(account.id);
return {
id: account.id,
email: maskEmail(account.email) ?? account.email,
@@ -206,6 +207,7 @@ function poolAccountDto(
quota: quota ? { ...quota } : null,
needsReauth,
hasCredential,
+ ...(cred?.expiresAt ? { expiresAt: cred.expiresAt } : {}),
...(quotaResult.quotaProbeSkipped ? { quotaProbeSkipped: true as const } : {}),
...oauthAccountHealthFields("codex", account.id, health),
};
@@ -647,6 +649,7 @@ export interface CodexAuthAccountDto {
quota: (StoredAccountQuota | (Omit & { updatedAt: number })) | null;
needsReauth?: boolean;
hasCredential: boolean;
+ expiresAt?: number;
health: OAuthAccountHealth;
healthLabel: OAuthHealthLabel;
healthSummary: string;
diff --git a/tests/oauth-accounts-api.test.ts b/tests/oauth-accounts-api.test.ts
index 7d50b81dc7..8956e0f8ee 100644
--- a/tests/oauth-accounts-api.test.ts
+++ b/tests/oauth-accounts-api.test.ts
@@ -64,6 +64,7 @@ describe("multiauth accounts API", () => {
const emails = body.accounts.map(a => a.email ?? "");
expect(emails.some(e => e.includes("first@example.com"))).toBe(false); // masked
expect(body.accounts.find(a => a.id === "aaaa1111")?.active).toBe(true);
+ expect(body.accounts.find(a => a.id === "aaaa1111")?.expiresAt).toBe(9999999999999);
const raw = JSON.stringify(body);
expect(raw.includes("t1")).toBe(false); // no tokens
} finally {