fix(ui): prevent silent overwrite of existing API key connections on re-add (#6499)

Unique default connection name prevents silent overwrite of existing API-key connections. Integrated into release/v3.8.46 with a unit-tested helper; remaining file-size reds are pre-existing base-red drift.
This commit is contained in:
Dilnei
2026-07-06 23:59:01 -03:00
committed by GitHub
parent 9a33cdac1d
commit 526048da5e
7 changed files with 54 additions and 4 deletions

View File

@@ -31,6 +31,7 @@
### 🐛 Bug Fixes
- **fix(dashboard):** adding a second API key connection for the same provider no longer silently overwrites the first — the Add-API-key modal now derives a unique default connection name (`main`, then `main-2`, `main-3`, …) so the backend name-based upsert can't collide (#6499 — thanks @dilneiss).
- **fix(compression):** the session-dedup engine now also deduplicates a large multi-line block repeated **within a single message** (intra-message dedup), not just across turns; the compression-preview API surfaces a `fallbackReason`, and the fusion panel reports how many models were rate-limited vs failed on total-panel failure (#6501 — thanks @chirag127).
- **fix(compression):** a stacked-pipeline step naming an unregistered engine now surfaces a `validationErrors` entry instead of silently no-op'ing, so misconfigured pipelines are visible in the preview API (#6506 — thanks @chirag127).
- **feat(usage):** add a **Codex reset-credit redemption** flow to the Provider Limits UI — a `useCodexResetCreditRedemption` hook + `/api/usage/codex-reset-credit` route + `codexResetCredits` lib let you redeem banked Codex reset credits from the quota card. Regression guard: `tests/unit/codex-reset-credits.test.ts`. (thanks @JxnLexn)

View File

@@ -201,7 +201,7 @@
"src/app/(dashboard)/dashboard/playground/components/tabs/ApiTab.tsx": 847,
"src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx": 784,
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow.tsx": 942,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 952,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx": 959,
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1278,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 954,
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderModels.ts": 155,
@@ -378,5 +378,6 @@
"_rebaseline_2026_07_05_6235_doubao_dola": "PR #6235 own growth: tests/unit/web-cookie-providers-new.test.ts 850->890 (+40 = doubao-web -> Dola global provider switch regression cases: new host/cookie-domain/token-source assertions for www.dola.com). Cohesive test growth alongside the provider switch; contributor backryun. Fast-path PR->release skips check:file-size, so this bump lands with the PR.",
"_rebaseline_2026_07_06_v3845_release_close": "Release v3.8.45 cycle-close rebaseline (captain, sess ce897453): 13 files grown by the cycle's merged fix/feature PRs (#6216 streaming fixes + request-logger UI grew RequestLoggerV2/chat/chatHelpers/auth/stream/response-sanitizer.test; #6251/#6253 dashboard UX grew combos page/modals/wizard/ComboDefaultsTab/ProxyRegistryManager/providerPageHelpers). Growth is legitimate merged-feature code, absorbed at release per Phase 0 drift policy; all remain frozen (cannot grow further).",
"_rebaseline_2026_07_06_6118_zed_oauthmodal": "PR #6118 own growth: OAuthModal.tsx 989->993 (+4 = Zed hosted native-app sign-in modal branch). Cohesive UI growth for the zed-hosted OAuth provider; not extractable. The prior 6118 comment set the note but left the frozen value at 989.",
"_rebaseline_2026_07_06_6351_glm_team_quota": "PR #6351 own growth (GLM team-plan quota fields threaded through the connection modals; new GlmTeamQuotaFields.tsx extracted): AddApiKeyModal.tsx ->951 (+9), EditConnectionModal.tsx ->1277 (+18). Absorbs the pre-existing session base-red on these frozen modals; release captain rebaseline-at-release supersedes."
"_rebaseline_2026_07_06_6351_glm_team_quota": "PR #6351 own growth (GLM team-plan quota fields threaded through the connection modals; new GlmTeamQuotaFields.tsx extracted): AddApiKeyModal.tsx ->951 (+9), EditConnectionModal.tsx ->1277 (+18). Absorbs the pre-existing session base-red on these frozen modals; release captain rebaseline-at-release supersedes.",
"_rebaseline_2026_07_06_6499_unique_default_name": "PR #6499 own growth: AddApiKeyModal.tsx 952->959 (+7 = a unique default connection name so a second API key for the same provider does not reuse 'main' and trigger the backend name-based upsert that silently overwrote the first connection). The pure name derivation was extracted to computeConnectionDefaultName.ts (unit-tested) to keep the growth minimal; the contributor's original full-form-reset rewrite was trimmed to a spread reset to avoid dropping the GLM team-quota fields #6351 added and to hold the frozen god-file growth down. Release captain rebaseline-at-release supersedes."
}

View File

@@ -709,6 +709,7 @@ export default function ProviderDetailPageClient() {
isCommandCode={isCommandCode}
isUpstreamProxyProvider={isUpstreamProxyProvider}
subscriptionRisk={subscriptionRisk}
existingConnectionCount={connections.length}
showRiskNoticeModal={showRiskNoticeModal}
handleConfirmRiskNotice={handleConfirmRiskNotice}
handleCancelRiskNotice={handleCancelRiskNotice}

View File

@@ -50,6 +50,7 @@ interface ProviderModalsPanelProps {
isCommandCode: boolean;
isUpstreamProxyProvider: boolean;
subscriptionRisk: boolean;
existingConnectionCount?: number;
// Risk notice
showRiskNoticeModal: boolean;
handleConfirmRiskNotice: () => void;
@@ -146,6 +147,7 @@ export default function ProviderModalsPanel({
isCcCompatible,
isUpstreamProxyProvider,
subscriptionRisk,
existingConnectionCount,
showRiskNoticeModal,
handleConfirmRiskNotice,
handleCancelRiskNotice,
@@ -280,6 +282,7 @@ export default function ProviderModalsPanel({
providerName={providerInfo.name}
providerWebsite={providerInfo.website}
initialBaseUrl={siliconFlowInitialBaseUrl}
existingConnectionCount={existingConnectionCount}
isCompatible={isCompatible}
isAnthropic={isAnthropicProtocolCompatible}
isCcCompatible={isCcCompatible}

View File

@@ -33,6 +33,7 @@ import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
import { buildAddProviderSpecificData } from "./connectionProviderSpecificData";
import { computeConnectionDefaultName } from "./computeConnectionDefaultName";
import QuotaScrapingFields, { EMPTY_QUOTA_SCRAPING_FIELDS } from "./QuotaScrapingFields";
import GlmTeamQuotaFields, { EMPTY_GLM_TEAM_QUOTA_FIELDS } from "./GlmTeamQuotaFields";
export interface AddApiKeyModalProps {
@@ -41,6 +42,7 @@ export interface AddApiKeyModalProps {
providerName?: string;
providerWebsite?: string;
initialBaseUrl?: string;
existingConnectionCount?: number;
isCompatible?: boolean;
isAnthropic?: boolean;
isCcCompatible?: boolean;
@@ -64,6 +66,7 @@ export default function AddApiKeyModal({
providerName,
providerWebsite,
initialBaseUrl,
existingConnectionCount = 0,
isCompatible,
isAnthropic,
isCcCompatible,
@@ -114,7 +117,7 @@ export default function AddApiKeyModal({
}[commandCodeAuthState.phase]
: null;
const [formData, setFormData] = useState({
name: "main", // #5421: required field; default resists autofill garbage (was "" → "wiw")
name: computeConnectionDefaultName(existingConnectionCount),
apiKey: "",
tokenSecret: "", // #5446 — Modal Token Secret (joined with apiKey as id:secret)
defaultModel: "",
@@ -148,11 +151,15 @@ export default function AddApiKeyModal({
const wasOpen = wasOpenRef.current;
wasOpenRef.current = isOpen;
if (!isOpen || wasOpen) return;
// On open, reset baseUrl and assign a unique default name so a second API key
// for the same provider doesn't reuse "main" and trigger the backend
// name-based upsert that would silently overwrite the first connection (#6499).
setFormData((current) => ({
...current,
name: computeConnectionDefaultName(existingConnectionCount),
baseUrl: initialBaseUrl || defaultBaseUrl,
}));
}, [defaultBaseUrl, initialBaseUrl, isOpen]);
}, [defaultBaseUrl, initialBaseUrl, isOpen, existingConnectionCount]);
const bulkSupported = supportsBulkApiKey(provider);
const [mode, setMode] = useState<"single" | "bulk">("single");
const [bulkText, setBulkText] = useState("");

View File

@@ -0,0 +1,10 @@
// #6499 — a fresh API-key connection defaults its name to "main". The backend
// upserts connections by (provider, name), so opening the modal a second time for
// the same provider with the same "main" name silently OVERWRITES the first
// connection. Deriving a unique default from the existing connection count keeps
// the first connection ("main") backward-compatible while giving each subsequent
// one a distinct name ("main-2", "main-3", …).
export function computeConnectionDefaultName(existingConnectionCount?: number): string {
const count = existingConnectionCount ?? 0;
return count <= 0 ? "main" : `main-${count + 1}`;
}

View File

@@ -0,0 +1,27 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { computeConnectionDefaultName } from "@/app/(dashboard)/dashboard/providers/[id]/components/modals/computeConnectionDefaultName";
// #6499 — the backend upserts API-key connections by (provider, name), so a second
// connection defaulting to "main" silently overwrites the first. The default name
// must stay "main" for the first connection (backward compatible) and get a unique
// numeric suffix afterwards.
test("first connection defaults to 'main'", () => {
assert.equal(computeConnectionDefaultName(0), "main");
});
test("undefined count is treated as zero → 'main'", () => {
assert.equal(computeConnectionDefaultName(undefined), "main");
});
test("subsequent connections get a unique numeric suffix", () => {
assert.equal(computeConnectionDefaultName(1), "main-2");
assert.equal(computeConnectionDefaultName(2), "main-3");
assert.equal(computeConnectionDefaultName(9), "main-10");
});
test("negative counts are clamped to 'main' (never a broken 'main-0')", () => {
assert.equal(computeConnectionDefaultName(-1), "main");
});