This commit is contained in:
decolua
2026-06-15 18:18:04 +07:00
parent 8ab5af0052
commit b282f05549
66 changed files with 2328 additions and 213 deletions
@@ -347,10 +347,10 @@ export default function ClaudeToolCard({
<label className="flex items-center gap-1.5 cursor-pointer select-none">
<input type="checkbox" checked={ccFilterNaming} onChange={handleCcFilterNamingToggle} className="w-3.5 h-3.5 accent-primary cursor-pointer" />
<span className="text-xs text-text-muted">Filter naming requests</span>
<Tooltip text="Intercepts Claude Code's topic-naming requests and returns a fake response locally, saving API tokens.">
<span className="material-symbols-outlined text-text-muted text-[14px] cursor-help">info</span>
</Tooltip>
</label>
<Tooltip text="Intercepts Claude Code's topic-naming requests and returns a fake response locally, saving API tokens.">
<span className="material-symbols-outlined text-text-muted text-[14px] cursor-help">info</span>
</Tooltip>
</div>
</div>
+17 -6
View File
@@ -5,7 +5,7 @@ import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, us
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers";
import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, Toggle, ConfirmModal } from "@/shared/components";
import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, Toggle, ConfirmModal, CapacityBadges } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
@@ -19,6 +19,7 @@ export default function CombosPage() {
const [editingCombo, setEditingCombo] = useState(null);
const [activeProviders, setActiveProviders] = useState([]);
const [comboStrategies, setComboStrategies] = useState({});
const [modelCaps, setModelCaps] = useState({});
const [confirmState, setConfirmState] = useState(null);
const { copied, copy } = useCopyToClipboard();
@@ -28,10 +29,11 @@ export default function CombosPage() {
const fetchData = async () => {
try {
const [combosRes, providersRes, settingsRes] = await Promise.all([
const [combosRes, providersRes, settingsRes, modelsRes] = await Promise.all([
fetch("/api/combos"),
fetch("/api/providers"),
fetch("/api/settings"),
fetch("/api/models"),
]);
const combosData = await combosRes.json();
const providersData = await providersRes.json();
@@ -42,6 +44,13 @@ export default function CombosPage() {
if (providersRes.ok) {
setActiveProviders(providersData.connections || []);
}
if (modelsRes.ok) {
const md = await modelsRes.json();
// Build fullModel -> caps map for badge lookup
const map = {};
for (const m of md.models || []) if (m.caps) map[m.fullModel] = m.caps;
setModelCaps(map);
}
setComboStrategies(settingsData.comboStrategies || {});
} catch (error) {
console.log("Error fetching data:", error);
@@ -143,7 +152,7 @@ export default function CombosPage() {
<div className="min-w-0">
<h1 className="text-2xl font-semibold">Combos</h1>
<p className="text-sm text-text-muted mt-1">
Create model combos with fallback support
Create model combos with fallback support — auto-adapts per request: routes images to vision models and web search to search-capable models.
</p>
</div>
<Button icon="add" onClick={() => setShowCreateModal(true)} className="w-full sm:w-auto">
@@ -171,6 +180,7 @@ export default function CombosPage() {
<ComboCard
key={combo.id}
combo={combo}
modelCaps={modelCaps}
copied={copied}
onCopy={copy}
onEdit={() => setEditingCombo(combo)}
@@ -214,7 +224,7 @@ export default function CombosPage() {
);
}
function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled, onToggleRoundRobin }) {
function ComboCard({ combo, modelCaps = {}, copied, onCopy, onEdit, onDelete, roundRobinEnabled, onToggleRoundRobin }) {
return (
<Card padding="sm" className="group">
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
@@ -229,8 +239,9 @@ function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled,
<span className="text-xs text-text-muted italic">No models</span>
) : (
combo.models.slice(0, 3).map((model, index) => (
<code key={index} className="max-w-full truncate rounded bg-black/5 px-1.5 py-0.5 font-mono text-[10px] text-text-muted dark:bg-white/5 sm:max-w-[220px]">
{model}
<code key={index} className="inline-flex items-center gap-1 rounded bg-black/5 px-1.5 py-0.5 font-mono text-xs text-text-muted dark:bg-white/5">
<span>{model}</span>
<CapacityBadges caps={modelCaps[model]} />
</code>
))
)}
@@ -1,6 +1,7 @@
import PropTypes from "prop-types";
import { CapacityBadges } from "@/shared/components";
export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting, onDisable }) {
export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting, onDisable, caps }) {
const borderColor = testStatus === "ok"
? "border-green-500/40"
: testStatus === "error"
@@ -24,7 +25,10 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
</span>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<code className="max-w-[72vw] truncate rounded bg-sidebar px-1.5 py-0.5 font-mono text-xs text-text-muted sm:max-w-[360px]">{fullModel}</code>
{model.name && <span className="truncate pl-1 text-[9px] italic text-text-muted/70">{model.name}</span>}
<span className="flex min-w-0 items-center text-[9px] gap-1 pl-1">
{model.name && <span className="truncate text-[9px] italic text-text-muted/70">{model.name}</span>}
<CapacityBadges caps={caps} colorOverride="text-text-muted/70" size={12} />
</span>
</div>
{onTest && (
<div className="relative shrink-0 group/btn">
@@ -92,4 +96,5 @@ ModelRow.propTypes = {
onTest: PropTypes.func,
isTesting: PropTypes.bool,
onDisable: PropTypes.func,
caps: PropTypes.object,
};
@@ -8,6 +8,7 @@ import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthW
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers";
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { useModelCaps } from "@/shared/hooks/useModelCaps";
import { translate } from "@/i18n/runtime";
import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher";
import ModelRow from "./ModelRow";
@@ -29,6 +30,7 @@ export default function ProviderDetailPage() {
const params = useParams();
const router = useRouter();
const providerId = params.id;
const { getCaps } = useModelCaps();
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(true);
const [providerNode, setProviderNode] = useState(null);
@@ -953,6 +955,7 @@ export default function ProviderDetailPage() {
isTesting={testingModelId === model.id}
isCustom
isFree={false}
caps={getCaps(`${providerId}/${model.id}`)}
/>
))}
@@ -977,6 +980,7 @@ export default function ProviderDetailPage() {
isTesting={testingModelId === model.id}
isFree={model.isFree}
onDisable={() => handleDisableModel(model.id)}
caps={getCaps(`${providerId}/${model.id}`)}
/>
);
})}
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import ProviderIcon from "@/shared/components/ProviderIcon";
import QuotaTable from "./QuotaTable";
import Toggle from "@/shared/components/Toggle";
import Tooltip from "@/shared/components/Tooltip";
import {
parseQuotaData,
calculatePercentage,
@@ -34,9 +35,15 @@ import {
QUOTA_SORT_OPTIONS,
} from "./utils";
import Card from "@/shared/components/Card";
import { EditConnectionModal } from "@/shared/components";
import { ConfirmModal, EditConnectionModal } from "@/shared/components";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
function getCodexResetCreditCount(quota) {
const value = quota?.raw?.resetCredits?.availableCount;
const count = typeof value === "number" ? value : Number(value);
return Number.isFinite(count) ? Math.max(0, count) : 0;
}
export default function ProviderLimits() {
const [connections, setConnections] = useState([]);
const [quotaData, setQuotaData] = useState({});
@@ -50,6 +57,8 @@ export default function ProviderLimits() {
const [connectionsLoading, setConnectionsLoading] = useState(true);
const [deletingId, setDeletingId] = useState(null);
const [togglingId, setTogglingId] = useState(null);
const [resettingLimitId, setResettingLimitId] = useState(null);
const [resetConfirmState, setResetConfirmState] = useState(null);
const [showEditModal, setShowEditModal] = useState(false);
const [selectedConnection, setSelectedConnection] = useState(null);
const [proxyPools, setProxyPools] = useState([]);
@@ -207,6 +216,32 @@ export default function ProviderLimits() {
[fetchQuota],
);
const handleResetCodexLimit = useCallback(
async (connectionId, provider) => {
if (provider !== "codex" || resettingLimitId) return;
setResettingLimitId(connectionId);
setErrors((prev) => ({ ...prev, [connectionId]: null }));
try {
const response = await fetch(`/api/usage/${connectionId}/codex-reset-credits`, { method: "POST" });
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.message || result.error || result.code || "Failed to reset Codex limit");
}
await fetchQuota(connectionId, provider);
setLastUpdated(new Date());
} catch (error) {
setErrors((prev) => ({ ...prev, [connectionId]: error.message || "Failed to reset Codex limit" }));
} finally {
setResettingLimitId(null);
}
},
[fetchQuota, resettingLimitId],
);
const handleDeleteConnection = useCallback(
async (id) => {
if (!confirm("Delete this connection?")) return;
@@ -791,7 +826,10 @@ export default function ProviderLimits() {
// Use table layout for all providers
const isInactive = conn.isActive === false;
const rowBusy = deletingId === conn.id || togglingId === conn.id;
const isCodex = conn.provider === "codex";
const resetCreditCount = getCodexResetCreditCount(quota);
const isResettingLimit = resettingLimitId === conn.id;
const rowBusy = deletingId === conn.id || togglingId === conn.id || isResettingLimit;
return (
<Card
@@ -822,53 +860,90 @@ export default function ProviderLimits() {
{getConnectionLabel(conn)}
</p>
) : null}
{isCodex && (
<p className="text-[11px] text-text-muted truncate">
Reset eligible: {resetCreditCount}
</p>
)}
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
type="button"
onClick={() => refreshProvider(conn.id, conn.provider)}
disabled={isLoading || rowBusy}
aria-label="Refresh quota"
className="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
title="Refresh quota"
>
<span
className={`material-symbols-outlined text-[18px] text-text-muted ${isLoading ? "animate-spin" : ""}`}
{isCodex && (
<Tooltip text={`Codex reset credits remaining: ${resetCreditCount}`}>
<div
className={`hidden h-8 items-center gap-1 rounded-lg border px-2 text-[11px] sm:flex ${
resetCreditCount > 0
? "border-primary/30 bg-primary/5 text-primary"
: "border-black/10 bg-black/[0.02] text-text-muted dark:border-white/10 dark:bg-white/[0.03]"
}`}
>
<span className="material-symbols-outlined text-[14px]">restart_alt</span>
<span className="tabular-nums">{resetCreditCount}</span>
</div>
</Tooltip>
)}
{isCodex && resetCreditCount > 0 && (
<Tooltip text={`Use one Codex reset credit. Available: ${resetCreditCount}`}>
<button
type="button"
onClick={() => setResetConfirmState({ connection: conn, resetCreditCount })}
disabled={isLoading || rowBusy}
className="flex h-8 items-center gap-1 rounded-lg border border-primary/30 px-2 text-[11px] text-primary transition-colors hover:bg-primary/10 disabled:opacity-50"
>
<span className={`material-symbols-outlined text-[15px] ${isResettingLimit ? "animate-spin" : ""}`}>
{isResettingLimit ? "progress_activity" : "bolt"}
</span>
<span className="hidden lg:inline">Reset limit</span>
</button>
</Tooltip>
)}
<Tooltip text="Refresh quota">
<button
type="button"
onClick={() => refreshProvider(conn.id, conn.provider)}
disabled={isLoading || rowBusy}
aria-label="Refresh quota"
className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
>
refresh
</span>
</button>
<button
type="button"
onClick={() => {
setSelectedConnection(conn);
setShowEditModal(true);
}}
disabled={rowBusy}
aria-label="Edit connection"
className="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary transition-colors disabled:opacity-50"
title="Edit connection"
>
<span className="material-symbols-outlined text-[18px]">
edit
</span>
</button>
<button
type="button"
onClick={() => handleDeleteConnection(conn.id)}
disabled={rowBusy}
aria-label="Delete connection"
className="p-1.5 rounded-lg hover:bg-red-500/10 text-red-500 transition-colors disabled:opacity-50"
title="Delete connection"
>
<span
className={`material-symbols-outlined text-[18px] ${deletingId === conn.id ? "animate-pulse" : ""}`}
<span
className={`material-symbols-outlined text-[18px] text-text-muted ${isLoading ? "animate-spin" : ""}`}
>
refresh
</span>
</button>
</Tooltip>
<Tooltip text="Edit connection">
<button
type="button"
onClick={() => {
setSelectedConnection(conn);
setShowEditModal(true);
}}
disabled={rowBusy}
aria-label="Edit connection"
className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary transition-colors disabled:opacity-50"
>
delete
</span>
</button>
<span className="material-symbols-outlined text-[18px]">
edit
</span>
</button>
</Tooltip>
<Tooltip text="Delete connection">
<button
type="button"
onClick={() => handleDeleteConnection(conn.id)}
disabled={rowBusy}
aria-label="Delete connection"
className="flex h-8 w-8 items-center justify-center rounded-lg hover:bg-red-500/10 text-red-500 transition-colors disabled:opacity-50"
>
<span
className={`material-symbols-outlined text-[18px] ${deletingId === conn.id ? "animate-pulse" : ""}`}
>
delete
</span>
</button>
</Tooltip>
<div
className="inline-flex items-center pl-0.5"
title={
@@ -1047,6 +1122,25 @@ export default function ProviderLimits() {
</div>
</div>
<ConfirmModal
isOpen={Boolean(resetConfirmState)}
onClose={() => {
if (!resettingLimitId) setResetConfirmState(null);
}}
onConfirm={async () => {
const connection = resetConfirmState?.connection;
if (!connection) return;
await handleResetCodexLimit(connection.id, connection.provider);
setResetConfirmState(null);
}}
title="Reset Codex limit?"
message={`Use 1 Codex reset credit for ${getConnectionLabel(resetConfirmState?.connection || {}) || "this account"}. This cannot be undone. Remaining credits: ${resetConfirmState?.resetCreditCount ?? 0}.`}
confirmText="Reset limit"
cancelText="Cancel"
variant="danger"
loading={Boolean(resettingLimitId)}
/>
<EditConnectionModal
isOpen={showEditModal}
connection={selectedConnection}