diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e5bea5fd..887c91f7f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 6ffd2b05e0..2988ff4d08 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -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." } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx index efbb574fe2..e460af15d7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/ProviderDetailPageClient.tsx @@ -709,6 +709,7 @@ export default function ProviderDetailPageClient() { isCommandCode={isCommandCode} isUpstreamProxyProvider={isUpstreamProxyProvider} subscriptionRisk={subscriptionRisk} + existingConnectionCount={connections.length} showRiskNoticeModal={showRiskNoticeModal} handleConfirmRiskNotice={handleConfirmRiskNotice} handleCancelRiskNotice={handleCancelRiskNotice} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx index 8c7df58b9c..732ff15c19 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx @@ -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} diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx index d46fbd51d2..eaeeef2931 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx @@ -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(""); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/computeConnectionDefaultName.ts b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/computeConnectionDefaultName.ts new file mode 100644 index 0000000000..7e0bf4cf45 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/computeConnectionDefaultName.ts @@ -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}`; +} diff --git a/tests/unit/compute-connection-default-name-6499.test.ts b/tests/unit/compute-connection-default-name-6499.test.ts new file mode 100644 index 0000000000..9c0bda08af --- /dev/null +++ b/tests/unit/compute-connection-default-name-6499.test.ts @@ -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"); +});