From 6770a571317f6a922805995ee17c0bcb6bf4e7ee Mon Sep 17 00:00:00 2001
From: Diego Rodrigues de Sa e Souza
<8016841+diegosouzapw@users.noreply.github.com>
Date: Mon, 20 Jul 2026 10:08:55 -0300
Subject: [PATCH] feat(providers): expose an explicit tier override for any
provider connection (#7818) (#7838)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
classifyTier() already honored a DB-backed providerOverrides list keyed
by an arbitrary provider-id string (built-in or custom), but nothing
exposed it through the UI or API. Adds GET/PUT /api/settings/tier-config,
a generic Advanced Settings tier selector wired into EditConnectionModal,
and makes TierCoverageWidget consult the same override before falling
back to registry-membership classification.
Owner decision: scope is the 3 real ProviderTier machine values
(free/cheap/premium) — the enum is not extended to 4.
---
.../features/7818-custom-provider-tier.md | 1 +
config/quality/file-size-baseline.json | 3 +-
.../dashboard/TierCoverageWidget.tsx | 38 ++++-
.../components/modals/EditConnectionModal.tsx | 2 +
.../components/modals/ProviderTierField.tsx | 68 +++++++++
.../components/modals/providerTierField.ts | 49 +++++++
src/app/api/settings/tier-config/route.ts | 58 ++++++++
src/i18n/messages/en.json | 6 +
src/i18n/messages/pt-BR.json | 6 +
.../unit/provider-tier-field-helpers.test.ts | 101 ++++++++++++++
...ier-config-provider-override-route.test.ts | 131 ++++++++++++++++++
...-coverage-widget-provider-override.test.ts | 43 ++++++
.../tier-resolver-provider-override.test.ts | 91 ++++++++++++
13 files changed, 591 insertions(+), 6 deletions(-)
create mode 100644 changelog.d/features/7818-custom-provider-tier.md
create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/components/modals/ProviderTierField.tsx
create mode 100644 src/app/(dashboard)/dashboard/providers/[id]/components/modals/providerTierField.ts
create mode 100644 src/app/api/settings/tier-config/route.ts
create mode 100644 tests/unit/provider-tier-field-helpers.test.ts
create mode 100644 tests/unit/tier-config-provider-override-route.test.ts
create mode 100644 tests/unit/tier-coverage-widget-provider-override.test.ts
create mode 100644 tests/unit/tier-resolver-provider-override.test.ts
diff --git a/changelog.d/features/7818-custom-provider-tier.md b/changelog.d/features/7818-custom-provider-tier.md
new file mode 100644
index 0000000000..23d7d9a538
--- /dev/null
+++ b/changelog.d/features/7818-custom-provider-tier.md
@@ -0,0 +1 @@
+- feat(providers): let any provider connection — built-in or custom — be pinned to an explicit routing tier (free/cheap/premium) via a new `/api/settings/tier-config` route and an Advanced Settings tier selector; `TierCoverageWidget` now honors the override too (#7818)
diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json
index d011602064..d14fa8c549 100644
--- a/config/quality/file-size-baseline.json
+++ b/config/quality/file-size-baseline.json
@@ -1,4 +1,5 @@
{
+ "_rebaseline_2026_07_20_7818_provider_tier_field": "Issue #7818 (explicit tier override for any provider connection) own growth: EditConnectionModal.tsx 1285->1287 (+2 = import + a single render call, mirroring the m365Tier.ts precedent). All actual selector logic (fetch/save against the new /api/settings/tier-config route) lives in the new leaf src/app/(dashboard)/dashboard/providers/[id]/components/modals/ProviderTierField.tsx + providerTierField.ts (both well under cap). Covered by tests/unit/tier-config-provider-override-route.test.ts and tests/unit/tier-resolver-provider-override.test.ts.",
"_rebaseline_2026_07_19_7546_ghe_copilot_route": "PR #7546 (GHE Copilot OAuth provider) own growth: oauth/[provider]/[action]/route.ts 960->963 (gate units, +3 = ghe-copilot device-code wiring at the existing multi-provider device-code branch — reading + HTTPS-validating the gheUrl search param (isValidGheUrl guards at both raw entry points, security-review hardening, 963->970), adding ghe-copilot to the no-PKCE provider set, and building the provider config override / threading gheUrl through poll->postExchange extraData). Mirrors the existing kiro/amazon-q startUrl override pattern right above it in the same branch; cohesive with the existing device-code dispatch chokepoint, not separately extractable without splitting a single provider-switch mid-branch. Frozen so can only shrink; structural shrink tracked in #3501.",
"_rebaseline_2026_07_19_6846_nvidia_concurrency_gate": "Issue #6846 Phase 1 (nvidia NIM local RPM budget + per-model lockout + per-connection concurrency cap) own growth: open-sse/executors/default.ts 877->890 (+13 = the irreducible call-site wiring at DefaultExecutor.execute(), the only place nvidia requests dispatch through — the existing session-pool body was extracted verbatim into a new private executeWithSessionPool() so the outer execute() can wrap it in the nvidia concurrency-gate acquire/finally-release). All actual gating logic (semaphore key + cap resolution) lives in the new leaf open-sse/executors/default/nvidiaConcurrencyGate.ts (not frozen, well under cap). Covered by tests/unit/nvidia-quota-phase1.test.ts.",
"_rebaseline_2026_07_18_v3849_provider_detail_wiring": "Merge campaign R2/R3 (2026-07-18): three authorized PRs each add irreducible call-site wiring to ProviderDetailPageClient.tsx — #7360 +5 (ProviderQuotaVisibilityToggle render, component extracted), #7419 +4 (NoAuthProviderControls wiring), #7062 +3 (Dahl provider hook) = 786->798. All three follow the extracted-component pattern (AgentrouterConsoleFields precedent); the frozen file only takes the wiring. Structural shrink tracked in #3501.",
@@ -220,7 +221,7 @@
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 798,
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": 942,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 967,
- "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1286,
+ "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1288,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 954,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts": 155,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderSettings.ts": 264,
diff --git a/src/app/(dashboard)/dashboard/TierCoverageWidget.tsx b/src/app/(dashboard)/dashboard/TierCoverageWidget.tsx
index d356c6a578..ee665efdad 100644
--- a/src/app/(dashboard)/dashboard/TierCoverageWidget.tsx
+++ b/src/app/(dashboard)/dashboard/TierCoverageWidget.tsx
@@ -4,14 +4,34 @@ import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { NOAUTH_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constants/providers";
+import type { ProviderTier } from "@omniroute/open-sse/services/tierTypes";
type TierCount = { configured: number; active: number };
type Coverage = { tier1: TierCount; tier2: TierCount; tier3: TierCount };
+type TierBucket = "tier1" | "tier2" | "tier3";
const NOAUTH_IDS = new Set(Object.keys(NOAUTH_PROVIDERS));
const OAUTH_IDS = new Set(Object.keys(OAUTH_PROVIDERS));
-function classifyConnection(providerId: string): "tier1" | "tier2" | "tier3" {
+/**
+ * Maps the routing-level `ProviderTier` (free/cheap/premium) onto this
+ * widget's own tier1/tier2/tier3 bucket vocabulary (#7818). The two do not
+ * line up 1:1 by name — premium (highest routing priority) is the widget's
+ * "Subscription" tier1 bucket, cheap is tier2, free is tier3 — so this stays
+ * a local mapping rather than renaming either enum.
+ */
+const OVERRIDE_TIER_TO_BUCKET: Record = {
+ premium: "tier1",
+ cheap: "tier2",
+ free: "tier3",
+};
+
+export function classifyConnection(
+ providerId: string,
+ overrides: Record
+): TierBucket {
+ const override = overrides[providerId.toLowerCase()];
+ if (override) return OVERRIDE_TIER_TO_BUCKET[override];
if (NOAUTH_IDS.has(providerId)) return "tier3";
if (OAUTH_IDS.has(providerId)) return "tier1";
return "tier2";
@@ -33,17 +53,25 @@ export function TierCoverageWidget() {
const [coverage, setCoverage] = useState(null);
useEffect(() => {
- fetch("/api/providers")
- .then((r) => r.json())
- .then((data) => {
+ Promise.all([
+ fetch("/api/providers").then((r) => r.json()),
+ fetch("/api/settings/tier-config")
+ .then((r) => (r.ok ? r.json() : null))
+ .catch(() => null),
+ ])
+ .then(([data, tierConfig]) => {
const connections: { provider: string; isActive: boolean }[] = data.connections ?? [];
+ const overrides: Record = {};
+ for (const o of tierConfig?.providerOverrides ?? []) {
+ overrides[String(o.provider).toLowerCase()] = o.tier;
+ }
const counts: Coverage = {
tier1: { configured: 0, active: 0 },
tier2: { configured: 0, active: 0 },
tier3: { configured: 0, active: 0 },
};
for (const conn of connections) {
- const tier = classifyConnection(conn.provider);
+ const tier = classifyConnection(conn.provider, overrides);
counts[tier].configured++;
if (conn.isActive) counts[tier].active++;
}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx
index 30f97ab089..34d8ba6cb2 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx
@@ -52,6 +52,7 @@ import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
import { assignEditApiKeyProviderSpecificData } from "./connectionProviderSpecificData";
import { isM365TierCapableProvider, normalizeM365TierValue, type M365TierValue } from "./m365Tier";
+import ProviderTierField from "./ProviderTierField";
import AgentrouterConsoleFields from "./AgentrouterConsoleFields";
import QuotaScrapingFields, { EMPTY_QUOTA_SCRAPING_FIELDS } from "./QuotaScrapingFields";
import GlmTeamQuotaFields, { EMPTY_GLM_TEAM_QUOTA_FIELDS } from "./GlmTeamQuotaFields";
@@ -900,6 +901,7 @@ export default function EditConnectionModal({
placeholder="my-app/1.0"
hint={t("customUserAgentHint")}
/>
+
{isM365TierCapable && (