Merge remote-tracking branch 'upstream/master'
# Conflicts: # .gitignore # open-sse/handlers/chatCore.js
This commit is contained in:
@@ -41,6 +41,10 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const bulkPlaceholder = isCloudflareAi
|
||||
? `name1|sk-key1|acc123456\nname2|sk-key2|def789012\nsk-key-only-auto-named`
|
||||
: BULK_PLACEHOLDER;
|
||||
|
||||
const [mode, setMode] = useState("single"); // "single" | "bulk"
|
||||
const [bulkText, setBulkText] = useState("");
|
||||
const [bulkResult, setBulkResult] = useState(null); // { success, failed }
|
||||
@@ -135,14 +139,31 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
|
||||
let failed = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const parts = lines[i].split("|");
|
||||
const apiKey = parts.length >= 2 ? parts.slice(1).join("|").trim() : parts[0].trim();
|
||||
const baseName = parts.length >= 2 ? parts[0].trim() : "Key";
|
||||
const name = `${baseName} ${i + 1}`;
|
||||
|
||||
let apiKey;
|
||||
let providerSpecificData;
|
||||
if (isCloudflareAi && parts.length >= 3) {
|
||||
// Format: name|apiKey|accountId
|
||||
apiKey = parts.slice(1, -1).join("|").trim();
|
||||
providerSpecificData = { accountId: parts[parts.length - 1].trim() };
|
||||
} else {
|
||||
apiKey = parts.length >= 2 ? parts.slice(1).join("|").trim() : parts[0].trim();
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/providers", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ provider, apiKey, name, priority: 1, testStatus: "unknown" }),
|
||||
body: JSON.stringify({
|
||||
provider,
|
||||
apiKey,
|
||||
name,
|
||||
priority: 1,
|
||||
testStatus: "unknown",
|
||||
...(providerSpecificData ? { providerSpecificData } : {}),
|
||||
}),
|
||||
});
|
||||
if (res.ok) success++;
|
||||
else failed++;
|
||||
@@ -168,10 +189,15 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
|
||||
|
||||
{mode === "bulk" && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-xs text-text-muted">One key per line. Format: <code>name|apiKey</code> or just <code>apiKey</code> (auto-named by index).</p>
|
||||
<p className="text-xs text-text-muted">
|
||||
{isCloudflareAi
|
||||
? <>One key per line. Format: <code>name|apiKey|accountId</code> or just <code>apiKey</code> (auto-named by index).</>
|
||||
: <>One key per line. Format: <code>name|apiKey</code> or just <code>apiKey</code> (auto-named by index).</>
|
||||
}
|
||||
</p>
|
||||
<textarea
|
||||
className="w-full rounded border border-accent/30 bg-sidebar p-2 text-sm font-mono resize-y min-h-[140px] focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
placeholder={BULK_PLACEHOLDER}
|
||||
placeholder={bulkPlaceholder}
|
||||
value={bulkText}
|
||||
onChange={(e) => setBulkText(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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, caps }) {
|
||||
export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting, onDisable, caps, thinkingSuffix }) {
|
||||
const displayModel = thinkingSuffix ? `${fullModel}(${thinkingSuffix})` : fullModel;
|
||||
const borderColor = testStatus === "ok"
|
||||
? "border-green-500/40"
|
||||
: testStatus === "error"
|
||||
@@ -24,7 +25,7 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
|
||||
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
|
||||
</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>
|
||||
<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]">{displayModel}</code>
|
||||
<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} />
|
||||
@@ -48,7 +49,7 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
|
||||
)}
|
||||
<div className="relative shrink-0 group/btn">
|
||||
<button
|
||||
onClick={() => onCopy(fullModel, `model-${model.id}`)}
|
||||
onClick={() => onCopy(displayModel, `model-${model.id}`)}
|
||||
className="rounded p-0.5 text-text-muted hover:bg-sidebar hover:text-primary"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">
|
||||
@@ -97,4 +98,5 @@ ModelRow.propTypes = {
|
||||
isTesting: PropTypes.bool,
|
||||
onDisable: PropTypes.func,
|
||||
caps: PropTypes.object,
|
||||
thinkingSuffix: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -5,8 +5,9 @@ import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard, ConfirmModal } from "@/shared/components";
|
||||
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 { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { getModelsByProviderId, getModelKind } from "@/shared/constants/models";
|
||||
import { getThinkingLevels } from "open-sse/providers/thinkingLevels.js";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { useModelCaps } from "@/shared/hooks/useModelCaps";
|
||||
import { translate } from "@/i18n/runtime";
|
||||
@@ -147,11 +148,38 @@ export default function ProviderDetailPage() {
|
||||
const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId);
|
||||
const isCompatible = isOpenAICompatible || isAnthropicCompatible;
|
||||
const hasDualAuthModes = !isCompatible && isOAuth && supportsApiKeyAuth;
|
||||
const oauthConnectionLabel = providerId === "xai" ? "Grok Build OAuth" : "OAuth";
|
||||
const oauthConnectionLabel =
|
||||
providerId === "xai" ? "Grok Build OAuth"
|
||||
: providerId === "grok-cli" ? "Grok CLI Device Login"
|
||||
: "OAuth";
|
||||
const apiKeyConnectionLabel = providerId === "xai" ? "xAI API Key" : "API Key";
|
||||
const thinkingConfig = AI_PROVIDERS[providerId]?.thinkingConfig || THINKING_CONFIG.extended;
|
||||
|
||||
// Resolve suffix "(level)" for a model when a thinking level is picked and the model supports it.
|
||||
const resolveThinkingSuffix = (modelId) => {
|
||||
if (!thinkingMode || thinkingMode === "auto") return null;
|
||||
const levels = getThinkingLevels(providerId, modelId);
|
||||
return levels && levels.includes(thinkingMode) ? thinkingMode : null;
|
||||
};
|
||||
const providerStorageAlias = isCompatible ? providerId : providerAlias;
|
||||
// Union of levels across this provider's reasoning models — drives the level picker options.
|
||||
// Include custom models too (e.g. manually added gpt-5.6-sol → max).
|
||||
const providerThinkingLevels = (() => {
|
||||
const set = new Set();
|
||||
const seen = new Set();
|
||||
const addLevels = (modelId) => {
|
||||
if (!modelId || seen.has(modelId)) return;
|
||||
seen.add(modelId);
|
||||
const lv = getThinkingLevels(providerId, modelId);
|
||||
if (lv) lv.forEach((l) => { if (l !== "none") set.add(l); });
|
||||
};
|
||||
for (const m of models) addLevels(m.id);
|
||||
for (const m of kiloFreeModels) addLevels(m.id);
|
||||
for (const entry of customModels) {
|
||||
if (entry.providerAlias !== providerStorageAlias) continue;
|
||||
if ((entry.kind || entry.type || "llm") !== "llm") continue;
|
||||
addLevels(entry.id);
|
||||
}
|
||||
return set.size ? ["auto", ...[...set]] : null;
|
||||
})();
|
||||
const providerDisplayAlias = isCompatible
|
||||
? (providerNode?.prefix || providerId)
|
||||
: providerAlias;
|
||||
@@ -1065,6 +1093,7 @@ export default function ProviderDetailPage() {
|
||||
isCustom
|
||||
isFree={false}
|
||||
caps={getCaps(`${providerId}/${model.id}`)}
|
||||
thinkingSuffix={resolveThinkingSuffix(model.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1090,6 +1119,7 @@ export default function ProviderDetailPage() {
|
||||
isFree={model.isFree}
|
||||
onDisable={() => handleDisableModel(model.id)}
|
||||
caps={getCaps(`${providerId}/${model.id}`)}
|
||||
thinkingSuffix={resolveThinkingSuffix(model.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -1395,21 +1425,6 @@ export default function ProviderDetailPage() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Thinking config */}
|
||||
{/* {thinkingConfig && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-text-muted font-medium">Thinking</span>
|
||||
<select
|
||||
value={thinkingMode}
|
||||
onChange={(e) => handleThinkingModeChange(e.target.value)}
|
||||
className="text-xs px-2 py-1 border border-border rounded-md bg-background focus:outline-none focus:border-primary"
|
||||
>
|
||||
{thinkingConfig.options.map((opt) => (
|
||||
<option key={opt} value={opt}>{opt.charAt(0).toUpperCase() + opt.slice(1)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)} */}
|
||||
{/* Round Robin toggle */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
||||
@@ -1580,9 +1595,23 @@ export default function ProviderDetailPage() {
|
||||
{/* Models */}
|
||||
<Card>
|
||||
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{"Available Models"}
|
||||
</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{"Available Models"}
|
||||
</h2>
|
||||
{providerThinkingLevels && (
|
||||
<select
|
||||
value={thinkingMode}
|
||||
onChange={(e) => handleThinkingModeChange(e.target.value)}
|
||||
title="Appends (level) suffix to copied model names"
|
||||
className="rounded-md border border-border bg-background px-2 py-1 text-xs focus:border-primary focus:outline-none"
|
||||
>
|
||||
{providerThinkingLevels.map((opt) => (
|
||||
<option key={opt} value={opt}>{`Thinking: ${opt.charAt(0).toUpperCase() + opt.slice(1)}`}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
{!isCompatible && (() => {
|
||||
const allIds = [
|
||||
...models,
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Card, Button } from "@/shared/components";
|
||||
|
||||
const fmtTokens = (n) => {
|
||||
if (n >= 1000000) return `${(n / 1000000).toFixed(2)}M`;
|
||||
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;
|
||||
return String(n || 0);
|
||||
};
|
||||
|
||||
const fmtUptime = (ms) => {
|
||||
if (!ms || ms <= 0) return "—";
|
||||
const m = Math.floor(ms / 60000);
|
||||
const h = Math.floor(m / 60);
|
||||
return h > 0 ? `${h}h${String(m % 60).padStart(2, "0")}m` : `${m}m`;
|
||||
};
|
||||
|
||||
const WINDOW_TABS = [
|
||||
{ id: "today", label: "Today" },
|
||||
{ id: "yesterday", label: "Yesterday" },
|
||||
{ id: "last7d", label: "7 days" },
|
||||
{ id: "last30d", label: "30 days" },
|
||||
{ id: "all", label: "All time" },
|
||||
];
|
||||
|
||||
const REASON_LABELS = {
|
||||
applied: "Prompt exceeded threshold",
|
||||
below_threshold: "Below size threshold",
|
||||
not_profitable: "Compression not profitable",
|
||||
below_min_chars: "Below minimum chars",
|
||||
below_min_tokens: "Below minimum tokens",
|
||||
unsupported_model: "Model not in allowlist",
|
||||
unsupported_format: "Non-Claude request format",
|
||||
timeout: "Compression timed out",
|
||||
transform_error: "Transform error",
|
||||
passthrough: "Passthrough",
|
||||
disabled: "Disabled",
|
||||
not_installed: "Not installed",
|
||||
};
|
||||
|
||||
function SummaryCard({ label, value, sub, tone }) {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">{label}</p>
|
||||
<p className={`text-xl font-semibold mt-1 ${tone || ""}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-text-muted mt-0.5">{sub}</p>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PxpipeClient() {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [health, setHealth] = useState(null);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [logs, setLogs] = useState(null);
|
||||
const [windowId, setWindowId] = useState("last7d");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statusRes, statsRes, logsRes] = await Promise.all([
|
||||
fetch("/api/pxpipe/status", { headers: { "Cache-Control": "no-store" } }),
|
||||
fetch("/api/pxpipe/stats"),
|
||||
fetch("/api/pxpipe/logs?limit=50"),
|
||||
]);
|
||||
setStatus(await statusRes.json());
|
||||
setStats(await statsRes.json());
|
||||
setLogs(await logsRes.json());
|
||||
const healthRes = await fetch("/api/pxpipe/health", { method: "POST" });
|
||||
setHealth(await healthRes.json());
|
||||
} catch {
|
||||
/* sections render placeholders */
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const w = stats?.windows?.[windowId];
|
||||
const statusLabel = !status
|
||||
? "—"
|
||||
: !status.installed
|
||||
? "Not installed"
|
||||
: health?.healthy
|
||||
? "Healthy"
|
||||
: status.running
|
||||
? "Running"
|
||||
: "Stopped";
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary">image</span>
|
||||
PXPIPE Dashboard
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="/dashboard/token-saver" className="text-xs text-primary underline hover:opacity-80">
|
||||
Token Saver settings
|
||||
</a>
|
||||
<Button size="sm" variant="ghost" onClick={refresh} disabled={loading}>
|
||||
{loading ? "Refreshing…" : "Refresh"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
<SummaryCard
|
||||
label="Status"
|
||||
value={statusLabel}
|
||||
tone={health?.healthy ? "text-success" : status?.installed ? "text-warning" : "text-text-muted"}
|
||||
sub={status?.enabled ? "Enabled in pipeline" : "Disabled in pipeline"}
|
||||
/>
|
||||
<SummaryCard label="Version" value={status?.version ? `v${status.version}` : "—"} sub="pxpipe-proxy" />
|
||||
<SummaryCard label="Uptime" value={fmtUptime(status?.uptimeMs)} sub="module loaded" />
|
||||
<SummaryCard label="Requests" value={w ? w.requests.toLocaleString() : "—"} />
|
||||
<SummaryCard label="Compressed" value={w ? w.compressed.toLocaleString() : "—"} tone="text-success" />
|
||||
<SummaryCard label="Bypassed" value={w ? w.bypassed.toLocaleString() : "—"} />
|
||||
</div>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3 mb-4">
|
||||
<h3 className="font-medium">Token savings (estimated)</h3>
|
||||
<div className="flex items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1">
|
||||
{WINDOW_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setWindowId(tab.id)}
|
||||
className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${
|
||||
windowId === tab.id
|
||||
? "bg-primary text-white shadow-sm"
|
||||
: "text-text-muted hover:text-text hover:bg-bg-hover"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
|
||||
<div>
|
||||
<p className="text-xs text-text-muted">Original tokens</p>
|
||||
<p className="text-lg font-semibold">{w ? fmtTokens(w.tokensBeforeEst) : "—"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted">After PXPIPE</p>
|
||||
<p className="text-lg font-semibold">{w ? fmtTokens(w.tokensAfterEst) : "—"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted">Saved</p>
|
||||
<p className="text-lg font-semibold text-success">{w ? fmtTokens(w.tokensSavedEst) : "—"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted">Reduction</p>
|
||||
<p className="text-lg font-semibold text-success">{w ? `${w.savedPct}%` : "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-3">
|
||||
Estimates from body size before/after imaging; billed usage per request
|
||||
(recorded on the Usage page) remains the ground truth. Images generated:{" "}
|
||||
{w ? w.imagesGenerated.toLocaleString() : "—"} · avg compression time:{" "}
|
||||
{w ? `${w.avgCompressionMs}ms` : "—"} · errors: {w ? w.errors : "—"}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-medium mb-3">Tokens saved — last 30 days</h3>
|
||||
{stats?.timeline?.some((d) => d.tokensSavedEst > 0) ? (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<AreaChart data={stats.timeline} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="gradPxpipe" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.25} />
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" strokeOpacity={0.2} />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 11 }} tickFormatter={(d) => d.slice(5)} />
|
||||
<YAxis tick={{ fontSize: 11 }} tickFormatter={fmtTokens} width={48} />
|
||||
<Tooltip formatter={(v) => [fmtTokens(v), "Tokens saved"]} labelFormatter={(d) => d} />
|
||||
<Area type="monotone" dataKey="tokensSavedEst" stroke="#10b981" fill="url(#gradPxpipe)" strokeWidth={2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-32 flex items-center justify-center text-text-muted text-sm">
|
||||
No savings recorded yet — enable PXPIPE in the Token Saver and route a large Claude-format request.
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="font-medium mb-3">History</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-xs text-text-muted border-b border-border">
|
||||
<th className="py-2 pr-3">Time</th>
|
||||
<th className="py-2 pr-3">Model</th>
|
||||
<th className="py-2 pr-3 text-right">Original</th>
|
||||
<th className="py-2 pr-3 text-right">Compressed</th>
|
||||
<th className="py-2 pr-3 text-right">Saved</th>
|
||||
<th className="py-2 pr-3 text-right">%</th>
|
||||
<th className="py-2 pr-3 text-right">Duration</th>
|
||||
<th className="py-2">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(stats?.recent || []).slice(0, 50).map((ev, i) => (
|
||||
<tr key={`${ev.ts}-${i}`} className="border-b border-border/50">
|
||||
<td className="py-1.5 pr-3 whitespace-nowrap text-text-muted">
|
||||
{new Date(ev.ts).toLocaleString()}
|
||||
</td>
|
||||
<td className="py-1.5 pr-3 font-mono text-xs">{ev.provider ? `${ev.provider}/${ev.model}` : ev.model || "—"}</td>
|
||||
<td className="py-1.5 pr-3 text-right font-mono text-xs">
|
||||
{ev.applied ? fmtTokens(ev.tokensBeforeEst) : "—"}
|
||||
</td>
|
||||
<td className="py-1.5 pr-3 text-right font-mono text-xs">
|
||||
{ev.applied ? fmtTokens(ev.tokensAfterEst) : "—"}
|
||||
</td>
|
||||
<td className="py-1.5 pr-3 text-right font-mono text-xs text-success">
|
||||
{ev.applied ? fmtTokens(ev.tokensSavedEst) : "—"}
|
||||
</td>
|
||||
<td className="py-1.5 pr-3 text-right font-mono text-xs">
|
||||
{ev.applied ? `${ev.savedPct}%` : "—"}
|
||||
</td>
|
||||
<td className="py-1.5 pr-3 text-right font-mono text-xs">
|
||||
{ev.durationMs != null ? `${ev.durationMs}ms` : "—"}
|
||||
</td>
|
||||
<td className="py-1.5">
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded ${
|
||||
ev.applied
|
||||
? "bg-success/15 text-success"
|
||||
: ev.reason === "transform_error" || ev.reason === "timeout"
|
||||
? "bg-danger/15 text-danger"
|
||||
: "bg-warning/15 text-warning"
|
||||
}`}
|
||||
title={ev.detail || ""}
|
||||
>
|
||||
{ev.applied ? "Compressed" : REASON_LABELS[ev.reason] || ev.reason}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!stats?.recent || stats.recent.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={8} className="py-6 text-center text-text-muted text-sm">
|
||||
No PXPIPE activity yet
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4" id="logs">
|
||||
<h3 className="font-medium mb-3">PXPIPE Logs</h3>
|
||||
{logs?.installLog ? (
|
||||
<pre className="rounded bg-black/5 dark:bg-white/5 p-3 text-xs font-mono overflow-x-auto max-h-64 overflow-y-auto whitespace-pre-wrap">
|
||||
{logs.installLog}
|
||||
</pre>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">No install log yet.</p>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import PxpipeClient from "./PxpipeClient";
|
||||
|
||||
export default function PxpipePage() {
|
||||
return <PxpipeClient />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, Button, Input, Modal, Toggle } from "@/shared/components";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { Card, Button, Input, Modal, Toggle, ConfirmModal } from "@/shared/components";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime";
|
||||
import {
|
||||
@@ -24,10 +24,39 @@ export default function TokenSaverClient() {
|
||||
useState(false);
|
||||
const [headroomActionLoading, setHeadroomActionLoading] = useState(false);
|
||||
const [headroomActionError, setHeadroomActionError] = useState("");
|
||||
const [headroomExtras, setHeadroomExtras] = useState({
|
||||
version: null,
|
||||
extras: { code: false, ml: false },
|
||||
available: ["code", "ml"],
|
||||
loading: false,
|
||||
});
|
||||
const [pendingExtras, setPendingExtras] = useState([]);
|
||||
const [extrasActionLoading, setExtrasActionLoading] = useState(false);
|
||||
const [extrasActionError, setExtrasActionError] = useState("");
|
||||
const [removingExtra, setRemovingExtra] = useState(null);
|
||||
const [installLog, setInstallLog] = useState("");
|
||||
const [extrasConfirm, setExtrasConfirm] = useState(null);
|
||||
const [codeAware, setCodeAware] = useState(false);
|
||||
const [kompress, setKompress] = useState(true);
|
||||
const [restartingProxy, setRestartingProxy] = useState(false);
|
||||
const logPollRef = useRef(null);
|
||||
const [cavemanEnabled, setCavemanEnabled] = useState(false);
|
||||
const [cavemanLevel, setCavemanLevel] = useState("full");
|
||||
const [ponytailEnabled, setPonytailEnabled] = useState(false);
|
||||
const [ponytailLevel, setPonytailLevel] = useState("full");
|
||||
const [pxpipeEnabled, setPxpipeEnabled] = useState(false);
|
||||
const [pxpipeMinChars, setPxpipeMinChars] = useState(25000);
|
||||
const [pxpipeStatus, setPxpipeStatus] = useState({
|
||||
installed: false,
|
||||
installing: false,
|
||||
running: false,
|
||||
version: null,
|
||||
loading: true,
|
||||
});
|
||||
const [pxpipeHealth, setPxpipeHealth] = useState(null);
|
||||
const [showPxpipeModal, setShowPxpipeModal] = useState(false);
|
||||
const [pxpipeActionLoading, setPxpipeActionLoading] = useState(false);
|
||||
const [pxpipeActionError, setPxpipeActionError] = useState("");
|
||||
const [locale, setLocale] = useState("en");
|
||||
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
@@ -102,6 +131,39 @@ export default function TokenSaverClient() {
|
||||
});
|
||||
const data = await res.json();
|
||||
setHeadroomStatus({ ...data, loading: false });
|
||||
if (!data?.installed) {
|
||||
setHeadroomExtras({
|
||||
version: null,
|
||||
extras: { code: false, ml: false },
|
||||
available: ["code", "ml"],
|
||||
loading: false,
|
||||
});
|
||||
setPendingExtras([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const er = await fetch("/api/headroom/extras", {
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
if (!er.ok) throw new Error("extras status failed");
|
||||
const ed = await er.json();
|
||||
setHeadroomExtras((s) => ({
|
||||
...s,
|
||||
version: ed.version ?? null,
|
||||
extras: ed.extras || { code: false, ml: false },
|
||||
available: ed.available || ["code", "ml"],
|
||||
loading: false,
|
||||
}));
|
||||
setPendingExtras([]);
|
||||
} catch {
|
||||
setHeadroomExtras({
|
||||
version: null,
|
||||
extras: { code: false, ml: false },
|
||||
available: ["code", "ml"],
|
||||
loading: false,
|
||||
});
|
||||
setPendingExtras([]);
|
||||
}
|
||||
} catch {
|
||||
setHeadroomStatus({
|
||||
installed: false,
|
||||
@@ -109,6 +171,13 @@ export default function TokenSaverClient() {
|
||||
python: null,
|
||||
loading: false,
|
||||
});
|
||||
setHeadroomExtras({
|
||||
version: null,
|
||||
extras: { code: false, ml: false },
|
||||
available: ["code", "ml"],
|
||||
loading: false,
|
||||
});
|
||||
setPendingExtras([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -137,6 +206,138 @@ export default function TokenSaverClient() {
|
||||
}
|
||||
}, [refreshHeadroomStatus]);
|
||||
|
||||
const togglePendingExtra = (extra) => {
|
||||
setPendingExtras((cur) =>
|
||||
cur.includes(extra) ? cur.filter((e) => e !== extra) : [...cur, extra]
|
||||
);
|
||||
};
|
||||
|
||||
// Poll the install log tail while a pip install/uninstall is running.
|
||||
const startLogPolling = useCallback(() => {
|
||||
setInstallLog("");
|
||||
if (logPollRef.current) clearInterval(logPollRef.current);
|
||||
const tick = async () => {
|
||||
try {
|
||||
const r = await fetch("/api/headroom/extras?log=1", {
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (typeof d.log === "string") setInstallLog(d.log);
|
||||
} catch { /* ignore transient poll errors */ }
|
||||
};
|
||||
tick();
|
||||
logPollRef.current = setInterval(tick, 1500);
|
||||
}, []);
|
||||
|
||||
const stopLogPolling = useCallback(() => {
|
||||
if (logPollRef.current) {
|
||||
clearInterval(logPollRef.current);
|
||||
logPollRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => stopLogPolling(), [stopLogPolling]);
|
||||
|
||||
const installExtrasConfirmed = useCallback(async () => {
|
||||
if (pendingExtras.length === 0) return;
|
||||
setExtrasActionLoading(true);
|
||||
setExtrasActionError("");
|
||||
startLogPolling();
|
||||
try {
|
||||
const res = await fetch("/api/headroom/extras", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ extras: pendingExtras }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "Install failed");
|
||||
setHeadroomExtras((s) => ({
|
||||
...s,
|
||||
version: data.version ?? s.version,
|
||||
extras: data.extras || s.extras,
|
||||
}));
|
||||
setPendingExtras([]);
|
||||
} catch (e) {
|
||||
setExtrasActionError(e.message);
|
||||
} finally {
|
||||
stopLogPolling();
|
||||
setExtrasActionLoading(false);
|
||||
}
|
||||
}, [pendingExtras, startLogPolling, stopLogPolling]);
|
||||
|
||||
const removeExtraConfirmed = useCallback(async (extra) => {
|
||||
setRemovingExtra(extra);
|
||||
setExtrasActionError("");
|
||||
startLogPolling();
|
||||
try {
|
||||
const res = await fetch("/api/headroom/extras", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ extras: [extra] }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "Remove failed");
|
||||
setHeadroomExtras((s) => ({
|
||||
...s,
|
||||
version: data.version ?? s.version,
|
||||
extras: data.extras || s.extras,
|
||||
}));
|
||||
} catch (e) {
|
||||
setExtrasActionError(e.message);
|
||||
} finally {
|
||||
stopLogPolling();
|
||||
setRemovingExtra(null);
|
||||
}
|
||||
}, [startLogPolling, stopLogPolling]);
|
||||
|
||||
const handleInstallExtras = useCallback(() => {
|
||||
if (pendingExtras.length === 0) return;
|
||||
// Warn about the heavy ~1GB torch download before installing [ml].
|
||||
if (pendingExtras.includes("ml")) {
|
||||
setExtrasConfirm({
|
||||
title: "Install [ml]",
|
||||
message: "[ml] downloads ~1 GB (torch + huggingface-hub). Continue?",
|
||||
confirmText: "Install",
|
||||
variant: "primary",
|
||||
onConfirm: installExtrasConfirmed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
installExtrasConfirmed();
|
||||
}, [pendingExtras, installExtrasConfirmed]);
|
||||
|
||||
const handleRemoveExtra = useCallback((extra) => {
|
||||
setExtrasConfirm({
|
||||
title: `Remove [${extra}]`,
|
||||
message: `Remove [${extra}] and its packages?`,
|
||||
confirmText: "Remove",
|
||||
variant: "danger",
|
||||
onConfirm: () => removeExtraConfirmed(extra),
|
||||
});
|
||||
}, [removeExtraConfirmed]);
|
||||
|
||||
// Toggle an extra's active state (persist setting), then restart the proxy so
|
||||
// the new --code-aware / --disable-kompress flags take effect.
|
||||
const toggleExtraActive = useCallback(async (extra, value) => {
|
||||
setExtrasActionError("");
|
||||
if (extra === "code") setCodeAware(value);
|
||||
if (extra === "ml") setKompress(value);
|
||||
const key = extra === "code" ? "headroomCodeAware" : "headroomKompress";
|
||||
await patchSetting({ [key]: value });
|
||||
if (!headroomStatus.running) return;
|
||||
setRestartingProxy(true);
|
||||
try {
|
||||
const res = await fetch("/api/headroom/restart", { method: "POST" });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "Restart failed");
|
||||
await refreshHeadroomStatus();
|
||||
} catch (e) {
|
||||
setExtrasActionError(e.message);
|
||||
} finally {
|
||||
setRestartingProxy(false);
|
||||
}
|
||||
}, [headroomStatus.running, refreshHeadroomStatus]);
|
||||
|
||||
const handleCavemanLevel = (level) => {
|
||||
setCavemanLevel(level);
|
||||
patchSetting({ cavemanLevel: level });
|
||||
@@ -152,6 +353,59 @@ export default function TokenSaverClient() {
|
||||
patchSetting({ ponytailLevel: level });
|
||||
};
|
||||
|
||||
const refreshPxpipeStatus = useCallback(async () => {
|
||||
setPxpipeStatus((s) => ({ ...s, loading: true }));
|
||||
try {
|
||||
const res = await fetch("/api/pxpipe/status", {
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
const data = await res.json();
|
||||
setPxpipeStatus({ ...data, loading: false });
|
||||
if (typeof data.minChars === "number") setPxpipeMinChars(data.minChars);
|
||||
} catch {
|
||||
setPxpipeStatus({ installed: false, installing: false, running: false, version: null, loading: false });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const runPxpipeHealth = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/pxpipe/health", { method: "POST" });
|
||||
setPxpipeHealth(await res.json());
|
||||
} catch (e) {
|
||||
setPxpipeHealth({ healthy: false, checks: [], error: e.message });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const pxpipeAction = useCallback(
|
||||
async (endpoint) => {
|
||||
setPxpipeActionError("");
|
||||
setPxpipeActionLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/pxpipe/${endpoint}`, { method: "POST" });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || `PXPIPE ${endpoint} failed`);
|
||||
await refreshPxpipeStatus();
|
||||
await runPxpipeHealth();
|
||||
} catch (e) {
|
||||
setPxpipeActionError(e.message);
|
||||
} finally {
|
||||
setPxpipeActionLoading(false);
|
||||
}
|
||||
},
|
||||
[refreshPxpipeStatus, runPxpipeHealth]
|
||||
);
|
||||
|
||||
const handlePxpipeEnabled = (value) => {
|
||||
setPxpipeEnabled(value);
|
||||
patchSetting({ pxpipeEnabled: value });
|
||||
};
|
||||
|
||||
const handlePxpipeMinCharsBlur = () => {
|
||||
const next = Math.max(0, Number(pxpipeMinChars) || 25000);
|
||||
setPxpipeMinChars(next);
|
||||
patchSetting({ pxpipeMinChars: next });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
@@ -161,16 +415,22 @@ export default function TokenSaverClient() {
|
||||
setRtkEnabledState(data.rtkEnabled !== false);
|
||||
setHeadroomEnabled(!!data.headroomEnabled);
|
||||
setHeadroomUrl(data.headroomUrl || "http://localhost:8787");
|
||||
setCodeAware(data.headroomCodeAware === true);
|
||||
setKompress(data.headroomKompress !== false);
|
||||
setCavemanEnabled(!!data.cavemanEnabled);
|
||||
setCavemanLevel(data.cavemanLevel || "full");
|
||||
setPonytailEnabled(!!data.ponytailEnabled);
|
||||
setPonytailLevel(data.ponytailLevel || "full");
|
||||
setPxpipeEnabled(!!data.pxpipeEnabled);
|
||||
if (typeof data.pxpipeMinChars === "number") setPxpipeMinChars(data.pxpipeMinChars);
|
||||
refreshHeadroomStatus();
|
||||
// PRD: run the PXPIPE health check automatically when the page opens
|
||||
refreshPxpipeStatus().then(runPxpipeHealth);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
loadSettings();
|
||||
}, [refreshHeadroomStatus]);
|
||||
}, [refreshHeadroomStatus, refreshPxpipeStatus, runPxpipeHealth]);
|
||||
|
||||
const headroomRunning = !!headroomStatus.running;
|
||||
const headroomStatusLabel = headroomStatus.loading
|
||||
@@ -187,6 +447,23 @@ export default function TokenSaverClient() {
|
||||
const headroomManaged =
|
||||
headroomLocalUrl && !!headroomStatus.managedPid;
|
||||
|
||||
const pxpipeHealthy = pxpipeHealth?.healthy === true;
|
||||
const pxpipeStatusLabel = pxpipeStatus.loading
|
||||
? "Checking…"
|
||||
: pxpipeStatus.installing
|
||||
? "Installing…"
|
||||
: !pxpipeStatus.installed
|
||||
? "Not installed"
|
||||
: pxpipeHealthy
|
||||
? "Healthy"
|
||||
: pxpipeStatus.running
|
||||
? "Running"
|
||||
: "Stopped";
|
||||
const pxpipeChipClass =
|
||||
pxpipeHealthy || pxpipeStatus.running
|
||||
? "bg-success/15 text-success"
|
||||
: "bg-warning/15 text-warning";
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<Card id="rtk">
|
||||
@@ -220,7 +497,7 @@ export default function TokenSaverClient() {
|
||||
onChange={() => handleRtkEnabled(!rtkEnabled)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-4 border-b border-border gap-4 flex-wrap">
|
||||
<div className="flex items-center justify-between py-4 gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<p className="font-medium">
|
||||
@@ -257,7 +534,105 @@ export default function TokenSaverClient() {
|
||||
onChange={() => handleHeadroomEnabled(!headroomEnabled)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-4 gap-4 flex-wrap">
|
||||
{headroomStatus.installed && (
|
||||
<div className="mb-3 ml-1 pl-3 pb-4 border-l-2 border-border">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-text-muted">
|
||||
Compression extras
|
||||
{headroomExtras.version ? ` · v${headroomExtras.version}` : ""}:
|
||||
</span>
|
||||
{headroomExtras.available.map((extra) => {
|
||||
const installed = !!headroomExtras.extras[extra];
|
||||
const pending = pendingExtras.includes(extra);
|
||||
const extraTitle =
|
||||
extra === "code"
|
||||
? "tree-sitter AST compression for code responses"
|
||||
: "Kompress-v2 HF model for prose/agentic traces (~+1GB)";
|
||||
|
||||
if (installed) {
|
||||
const active = extra === "code" ? codeAware : kompress;
|
||||
return (
|
||||
<div
|
||||
key={extra}
|
||||
className="flex items-center gap-1.5 text-xs px-2 py-1 rounded border border-success/40 bg-success/5 text-text"
|
||||
title={extraTitle}
|
||||
>
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={active}
|
||||
disabled={restartingProxy}
|
||||
onChange={() => toggleExtraActive(extra, !active)}
|
||||
/>
|
||||
<span className="font-medium">[{extra}]</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveExtra(extra)}
|
||||
disabled={removingExtra === extra}
|
||||
className="ml-1 text-error underline hover:opacity-80 disabled:opacity-50"
|
||||
title={`Uninstall [${extra}]`}
|
||||
>
|
||||
{removingExtra === extra ? "Uninstalling…" : "Uninstall"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<label
|
||||
key={extra}
|
||||
className={`flex items-center gap-1.5 text-xs px-2 py-1 rounded border cursor-pointer transition-colors ${
|
||||
pending
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-border text-text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
title={extraTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-3 h-3"
|
||||
checked={pending}
|
||||
onChange={() => togglePendingExtra(extra)}
|
||||
/>
|
||||
<span className="font-medium">[{extra}]</span>
|
||||
<span className="opacity-70">not installed</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{pendingExtras.length > 0 && (
|
||||
<button
|
||||
onClick={handleInstallExtras}
|
||||
disabled={extrasActionLoading}
|
||||
className="text-xs px-2.5 py-1 rounded bg-primary text-white hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{extrasActionLoading
|
||||
? "Installing…"
|
||||
: `Install [proxy,${pendingExtras.join(",")}]`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{extrasActionError && (
|
||||
<p className="text-xs text-error mt-1">{extrasActionError}</p>
|
||||
)}
|
||||
{restartingProxy && (
|
||||
<p className="text-xs text-text-muted mt-1">Restarting proxy…</p>
|
||||
)}
|
||||
{(extrasActionLoading || removingExtra) && installLog && (
|
||||
<pre className="mt-2 max-h-32 overflow-auto rounded bg-surface-2 p-2 text-[10px] leading-tight text-text-muted whitespace-pre-wrap">
|
||||
{installLog}
|
||||
</pre>
|
||||
)}
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Installing adds the package; use <code>on</code>/<code>off</code>{" "}
|
||||
to activate it (restarts the proxy). Default install is{" "}
|
||||
<code>[proxy]</code> only (SmartCrusher for JSON). Adding{" "}
|
||||
<code>[code]</code> enables AST compression
|
||||
(Python/JS/TS/Go/Rust/Java/C/C++/Perl). Adding <code>[ml]</code>{" "}
|
||||
enables the Kompress-v2 HF model for prose/agentic traces but
|
||||
adds ~1 GB (torch + huggingface-hub).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-border gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">
|
||||
Compress LLM output{" "}
|
||||
@@ -358,6 +733,52 @@ export default function TokenSaverClient() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* PXPIPE hidden from UI — experimental, not exposed to users yet */}
|
||||
{false && (
|
||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-border gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<p className="font-medium">
|
||||
Compress prompts as images{" "}
|
||||
<a
|
||||
href="https://github.com/teamchong/pxpipe"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs font-normal text-primary underline hover:opacity-80"
|
||||
>
|
||||
(PXPIPE)
|
||||
</a>
|
||||
</p>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${pxpipeChipClass}`}>
|
||||
{pxpipeStatusLabel}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPxpipeModal(true)}
|
||||
className="text-xs text-primary underline hover:opacity-80"
|
||||
>
|
||||
{pxpipeStatus.installed ? "Manage" : "Setup"}
|
||||
</button>
|
||||
<a
|
||||
href="/dashboard/pxpipe"
|
||||
className="text-xs text-primary underline hover:opacity-80"
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Transforms large textual context into optimized images before
|
||||
sending to the LLM. Ideal for huge prompts, tool outputs and long
|
||||
conversations.
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={pxpipeEnabled}
|
||||
disabled={!pxpipeStatus.installed}
|
||||
onChange={() => handlePxpipeEnabled(!pxpipeEnabled)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
@@ -374,6 +795,16 @@ export default function TokenSaverClient() {
|
||||
{headroomStatusLabel}
|
||||
</span>
|
||||
</div>
|
||||
{headroomRunning && (
|
||||
<a
|
||||
href="/api/headroom/proxy/dashboard"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full rounded border border-border px-4 py-2 text-center text-sm hover:bg-surface-2"
|
||||
>
|
||||
Open Headroom Dashboard
|
||||
</a>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium">Proxy URL</p>
|
||||
<Input
|
||||
@@ -457,6 +888,128 @@ export default function TokenSaverClient() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={false}
|
||||
title={pxpipeStatus.installed ? "PXPIPE" : "Setup PXPIPE"}
|
||||
onClose={() => setShowPxpipeModal(false)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-text-muted">
|
||||
Compress prompts using multimodal encoding. Runs in-process — no
|
||||
extra server or environment variables required.
|
||||
</p>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>Status</span>
|
||||
<span className={pxpipeHealthy || pxpipeStatus.running ? "text-success" : "text-warning"}>
|
||||
{pxpipeStatusLabel}
|
||||
{pxpipeStatus.version ? ` · v${pxpipeStatus.version}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{pxpipeHealth?.checks?.length > 0 && (
|
||||
<div className="flex flex-col gap-1 rounded border border-border p-3">
|
||||
<p className="text-sm font-medium mb-1">Health check</p>
|
||||
{pxpipeHealth.checks.map((check) => (
|
||||
<div key={check.id} className="flex items-center justify-between text-xs">
|
||||
<span className={check.ok ? "text-success" : "text-warning"}>
|
||||
{check.ok ? "●" : "○"} {check.label}
|
||||
</span>
|
||||
{check.detail && (
|
||||
<span className="text-text-muted font-mono truncate max-w-[50%]">{check.detail}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{pxpipeHealth.error && (
|
||||
<p className="text-xs text-warning mt-1">{pxpipeHealth.error}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!pxpipeStatus.installed ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm text-warning">PXPIPE is not installed.</p>
|
||||
<Button
|
||||
onClick={() => pxpipeAction("install")}
|
||||
fullWidth
|
||||
disabled={pxpipeActionLoading || pxpipeStatus.installing}
|
||||
>
|
||||
{pxpipeActionLoading || pxpipeStatus.installing ? "Installing…" : "Install"}
|
||||
</Button>
|
||||
<p className="text-xs text-text-muted">
|
||||
Installs the npm package <code className="font-mono">pxpipe-proxy</code> into
|
||||
the 9Router data directory. May take a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{pxpipeStatus.running ? (
|
||||
<>
|
||||
<Button onClick={() => pxpipeAction("restart")} variant="ghost" disabled={pxpipeActionLoading}>
|
||||
Restart
|
||||
</Button>
|
||||
<Button onClick={() => pxpipeAction("stop")} variant="ghost" disabled={pxpipeActionLoading}>
|
||||
Stop
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={() => pxpipeAction("start")} disabled={pxpipeActionLoading}>
|
||||
{pxpipeActionLoading ? "Starting…" : "Start"}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => pxpipeAction("install")} variant="ghost" disabled={pxpipeActionLoading}>
|
||||
Repair
|
||||
</Button>
|
||||
<a
|
||||
href="/dashboard/pxpipe#logs"
|
||||
className="col-span-2 rounded border border-border px-4 py-2 text-center text-sm hover:bg-surface-2"
|
||||
>
|
||||
Open Logs
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium">Minimum prompt size (chars)</p>
|
||||
<Input
|
||||
value={String(pxpipeMinChars)}
|
||||
onChange={(e) => setPxpipeMinChars(e.target.value)}
|
||||
onBlur={handlePxpipeMinCharsBlur}
|
||||
placeholder="25000"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-text-muted">
|
||||
Requests smaller than this bypass PXPIPE and are sent as-is.
|
||||
</p>
|
||||
</div>
|
||||
{pxpipeActionError && (
|
||||
<p className="text-sm text-warning">{pxpipeActionError}</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => refreshPxpipeStatus().then(runPxpipeHealth)}
|
||||
variant="ghost"
|
||||
fullWidth
|
||||
>
|
||||
Recheck
|
||||
</Button>
|
||||
<Button onClick={() => setShowPxpipeModal(false)} fullWidth>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={!!extrasConfirm}
|
||||
onClose={() => setExtrasConfirm(null)}
|
||||
onConfirm={() => {
|
||||
const fn = extrasConfirm?.onConfirm;
|
||||
setExtrasConfirm(null);
|
||||
fn?.();
|
||||
}}
|
||||
title={extrasConfirm?.title}
|
||||
message={extrasConfirm?.message}
|
||||
confirmText={extrasConfirm?.confirmText}
|
||||
variant={extrasConfirm?.variant}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const fmtCost = (n) => `$${(n || 0).toFixed(2)}`;
|
||||
|
||||
export default function OverviewCards({ stats }) {
|
||||
return (
|
||||
<div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 sm:gap-4">
|
||||
<div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 sm:gap-4">
|
||||
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Total Requests</span>
|
||||
<span className="truncate text-2xl font-bold">{fmt(stats.totalRequests)}</span>
|
||||
@@ -17,12 +17,10 @@ export default function OverviewCards({ stats }) {
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Total Input Tokens</span>
|
||||
<span className="truncate text-2xl font-bold text-primary">{fmt(stats.totalPromptTokens)}</span>
|
||||
</Card>
|
||||
{/* Temporarily hidden: Cached Tokens card
|
||||
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Cached Tokens</span>
|
||||
<span className="truncate text-2xl font-bold text-info">{fmt(stats.totalCachedTokens)}</span>
|
||||
</Card>
|
||||
*/}
|
||||
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Output Tokens</span>
|
||||
<span className="truncate text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span>
|
||||
|
||||
@@ -475,6 +475,23 @@ export function parseQuotaData(provider, data) {
|
||||
}
|
||||
break;
|
||||
|
||||
case "grok-cli":
|
||||
// Grok Build credits (on-demand window + prepaid balance).
|
||||
// Do NOT forward absolute `remaining` — getRemainingPercentage treats
|
||||
// it as a 0–100 percentage (same as Qoder). Use remainingPercentage.
|
||||
if (data.quotas) {
|
||||
Object.entries(data.quotas).forEach(([name, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Generic fallback for unknown providers
|
||||
if (data.quotas) {
|
||||
|
||||
@@ -412,7 +412,49 @@ export default function RequestDetailsTab() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{selectedDetail.pxpipe && (
|
||||
<div className="rounded-lg border border-black/5 dark:border-white/5 p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="material-symbols-outlined text-[18px] text-text-muted">image</span>
|
||||
<span className="font-semibold text-sm text-text-main">PXPIPE</span>
|
||||
<span className={cn(
|
||||
"text-xs px-2 py-0.5 rounded",
|
||||
selectedDetail.pxpipe.applied
|
||||
? "bg-green-500/15 text-green-600"
|
||||
: "bg-amber-500/15 text-amber-600"
|
||||
)}>
|
||||
{selectedDetail.pxpipe.applied ? "Activated" : "Skipped"}
|
||||
</span>
|
||||
</div>
|
||||
{selectedDetail.pxpipe.applied ? (
|
||||
<div className="grid grid-cols-2 gap-2 text-sm sm:grid-cols-4">
|
||||
<div>
|
||||
<span className="text-text-muted block text-xs">Original (est.)</span>
|
||||
<span className="font-mono">{(selectedDetail.pxpipe.tokensBeforeEst || 0).toLocaleString()} tokens</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted block text-xs">Compressed (est.)</span>
|
||||
<span className="font-mono">{(selectedDetail.pxpipe.tokensAfterEst || 0).toLocaleString()} tokens</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted block text-xs">Saved</span>
|
||||
<span className="font-mono text-green-600">{selectedDetail.pxpipe.savedPct || 0}%</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted block text-xs">Images</span>
|
||||
<span className="font-mono">{selectedDetail.pxpipe.imageCount || 0} ({selectedDetail.pxpipe.durationMs || 0}ms)</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">
|
||||
Reason: <span className="font-mono">{selectedDetail.pxpipe.reason}</span>
|
||||
{selectedDetail.pxpipe.detail ? ` — ${selectedDetail.pxpipe.detail}` : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<CollapsibleSection title="1. Client Request (Input)" defaultOpen={true} icon="input">
|
||||
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
|
||||
|
||||
Reference in New Issue
Block a user