mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 04:32:31 +03:00
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
This commit is contained in:
@@ -15,7 +15,12 @@ import {
|
||||
getCodexEffectiveServiceTier,
|
||||
type CodexGlobalServiceMode,
|
||||
} from "@/lib/providers/codexFastTier";
|
||||
import { normalizeCodexLimitPolicy, providerText, ERROR_TYPE_LABELS } from "../providerPageHelpers";
|
||||
import {
|
||||
normalizeCodexLimitPolicy,
|
||||
getCodexPlanLabel,
|
||||
providerText,
|
||||
ERROR_TYPE_LABELS,
|
||||
} from "../providerPageHelpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
|
||||
@@ -735,6 +735,22 @@ export function normalizeCodexLimitPolicy(policy: unknown): { use5h: boolean; us
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* UI adapter around the canonical getCodexRequestDefaults from requestDefaults.ts.
|
||||
* Adds the "medium" fallback for reasoningEffort required by the connection form.
|
||||
|
||||
@@ -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]/providerPageHelpers";
|
||||
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