mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
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:
committed by
GitHub
parent
2dc4a92be7
commit
97f993013d
1
changelog.d/features/7210-codex-plan-labels.md
Normal file
1
changelog.d/features/7210-codex-plan-labels.md
Normal 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)
|
||||
@@ -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() : "";
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
50
tests/unit/codex-plan-label-2570.test.ts
Normal file
50
tests/unit/codex-plan-label-2570.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user