feat(dashboard): show Codex plan label in provider and quota views (#7210)

* feat(dashboard): show Codex plan label in provider and quota views

ConnectionRow on the provider-detail page never surfaced the Codex
subscription plan captured at OAuth import time
(providerSpecificData.chatgptPlanType, src/lib/oauth/services/codexImport.ts)
anywhere in the row UI. Added a small pure helper, getCodexPlanLabel, and a
Badge in ConnectionRow gated on isCodex.

Separately, the quota view's plan-badge machinery (resolvePlanValue /
tierByConnection / QuotaCardHeader) already existed for all providers, but
its persisted-metadata fallback list omitted chatgptPlanType. When the live
Codex usage endpoint has no plan_type/planType field, the usage service
reports the literal string "unknown" (open-sse/services/usage/codex.ts),
which resolvePlanValue's normalizePlanCandidate() filters out — so the quota
badge fell through to "Unknown" instead of the plan captured at login.
Added chatgptPlanType to the persisted candidate list.

Co-authored-by: Carmelo Campos <carmelogunsroses@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2570

* chore(changelog): fragment for #7210

* fix(dashboard): extract getCodexPlanLabel to unfreeze providerPageHelpers.ts

The Fast Quality Gates file-size ratchet froze
providerPageHelpers.ts at 1053 lines; adding getCodexPlanLabel
inline pushed it to 1067. Move the self-contained helper into its
own codexPlanLabel.ts module instead of growing the frozen file,
and repoint ConnectionRow.tsx + the regression test at the new
location. No behavior change.

---------

Co-authored-by: Carmelo Campos <carmelogunsroses@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-17 10:39:46 -03:00
committed by GitHub
parent 2dc4a92be7
commit 97f993013d
5 changed files with 88 additions and 1 deletions

View File

@@ -0,0 +1 @@
- **feat(dashboard):** show the Codex subscription plan label in provider connection rows and the quota view, falling back to the plan captured at OAuth import when the live usage endpoint doesn't report one. (thanks @CarmeloCampos)

View File

@@ -0,0 +1,19 @@
/**
* Codex subscription plan label (e.g. "Plus", "Pro", "Team"), persisted on the
* connection's providerSpecificData.chatgptPlanType at OAuth import time (see
* src/lib/oauth/services/codexImport.ts). Returns "" when the connection is
* not Codex or the value is missing/blank — callers gate rendering on that.
*
* Kept in its own module (not providerPageHelpers.ts) because that file is
* frozen at its file-size ratchet cap (config/quality/file-size-baseline.json)
* and this helper is fully self-contained.
*/
export function getCodexPlanLabel(isCodex: boolean, providerSpecificData: unknown): string {
if (!isCodex) return "";
const record =
providerSpecificData && typeof providerSpecificData === "object"
? (providerSpecificData as Record<string, unknown>)
: {};
const raw = record.chatgptPlanType;
return typeof raw === "string" ? raw.trim() : "";
}

View File

@@ -15,7 +15,12 @@ import {
getCodexEffectiveServiceTier,
type CodexGlobalServiceMode,
} from "@/lib/providers/codexFastTier";
import { normalizeCodexLimitPolicy, providerText, ERROR_TYPE_LABELS } from "../providerPageHelpers";
import {
normalizeCodexLimitPolicy,
providerText,
ERROR_TYPE_LABELS,
} from "../providerPageHelpers";
import { getCodexPlanLabel } from "../codexPlanLabel";
// ---------------------------------------------------------------------------
// Types (exported so the client can reference them without re-importing)
@@ -499,6 +504,7 @@ export default function ConnectionRow({
const claudeBlockExtraUsageEnabled = isClaude
? isClaudeExtraUsageBlockEnabled("claude", connection.providerSpecificData)
: false;
const codexPlanLabel = getCodexPlanLabel(!!isCodex, connection.providerSpecificData);
const cliproxyapiDeepMode = !!cliproxyapiEnabled;
return (
@@ -540,6 +546,11 @@ export default function ConnectionRow({
<Badge variant={statusPresentation.statusVariant as any} size="sm" dot>
{statusPresentation.statusLabel}
</Badge>
{codexPlanLabel && (
<Badge variant="primary" size="sm" className="capitalize">
{codexPlanLabel}
</Badge>
)}
{/* T12: Token expiry status indicator (state-driven, no Date.now in render) */}
{/* #5836: the red "Token Expired" badge is TERMINAL-only — for OAuth
refresh-capable providers (Antigravity/Gemini) the access token lapses

View File

@@ -193,6 +193,12 @@ export function resolvePlanValue(plan, providerSpecificData) {
psd.organizationRateLimitTier,
psd.rateLimitTier,
psd.organizationType,
// Codex OAuth bootstrap: chatgpt_plan_type is captured at import time
// (src/lib/oauth/services/codexImport.ts) and is the only source of the
// plan when the live Codex usage endpoint omits plan_type/planType (the
// usage service then reports the literal string "unknown" — see
// open-sse/services/usage/codex.ts).
psd.chatgptPlanType,
];
if (livePlan && normalizePlanTier(livePlan).key !== "free") {

View File

@@ -0,0 +1,50 @@
// Port of upstream decolua/9router PR #2570 (feat(ui): show Codex plan labels
// in provider and quota views).
//
// Two independent gaps this closes:
//
// 1. providerPageHelpers.getCodexPlanLabel — the provider-detail ConnectionRow
// never surfaced the Codex subscription plan (persisted at OAuth import
// time in providerSpecificData.chatgptPlanType — see
// src/lib/oauth/services/codexImport.ts) anywhere in the row UI.
//
// 2. ProviderLimits/utils.resolvePlanValue — the quota-view plan badge
// machinery already existed (tierByConnection / QuotaCardHeader), but its
// persisted-metadata fallback list did not include chatgptPlanType. When
// the live Codex usage endpoint does not return a plan_type field (usage
// service falls back to the literal string "unknown" — see
// open-sse/services/usage/codex.ts), the badge fell through to "Unknown"
// instead of the plan captured at login.
import { test } from "node:test";
import assert from "node:assert/strict";
import { getCodexPlanLabel } from "@/app/(dashboard)/dashboard/providers/[id]/codexPlanLabel";
import { resolvePlanValue } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils";
test("getCodexPlanLabel returns the trimmed chatgptPlanType for codex connections", () => {
assert.equal(getCodexPlanLabel(true, { chatgptPlanType: " Pro " }), "Pro");
});
test("getCodexPlanLabel returns empty string when not a codex connection", () => {
assert.equal(getCodexPlanLabel(false, { chatgptPlanType: "Pro" }), "");
});
test("getCodexPlanLabel returns empty string when chatgptPlanType is missing/blank", () => {
assert.equal(getCodexPlanLabel(true, {}), "");
assert.equal(getCodexPlanLabel(true, { chatgptPlanType: " " }), "");
assert.equal(getCodexPlanLabel(true, undefined), "");
});
test("resolvePlanValue falls back to the persisted Codex chatgptPlanType when the live plan is unknown", () => {
// Reproduces the exact shape open-sse/services/usage/codex.ts returns when
// the upstream Codex usage endpoint omits plan_type/planType.
assert.equal(resolvePlanValue("unknown", { chatgptPlanType: "Pro" }), "Pro");
});
test("resolvePlanValue still prefers a real live plan over the persisted Codex fallback", () => {
assert.equal(resolvePlanValue("Team", { chatgptPlanType: "Pro" }), "Team");
});
test("resolvePlanValue returns null when neither live nor persisted Codex plan is available", () => {
assert.equal(resolvePlanValue("unknown", {}), null);
assert.equal(resolvePlanValue(null, null), null);
});