feat(dashboard): hide token/cost analytics behind config flag (default off) (#25438)

The Analytics page and the token/cost surfaces on the Models page show
local debug estimates only. They count input+output (and a bar viz adds
cache_read+reasoning, missing cache_write entirely) from successful
main-agent responses that returned a usable usage block.

Excluded silently:
- All auxiliary calls — context compression, title generation, vision,
  session search, web extract, smart approvals, MCP routing, plugin LLM
  access (13 production call sites bypass update_token_counts)
- Provider-side retries, fallback attempts
- Any call whose usage block didn't come back
- cache_write_tokens (column exists in sessions table but not returned
  by /api/analytics/models)

Real-world impact: a user on Kimi K2.6 saw 150K local vs 27M on the
OpenRouter side over the same window. Precise-looking numbers next to
provider billing create false confidence and support load.

This change adds dashboard.show_token_analytics (default False) to gate:
- The Analytics nav item (hidden from sidebar when off)
- The Analytics page (renders an explanation card instead of charts)
- Token bars, totals, cost figures, avg/api_calls on the Models page

The Models page keeps capability metadata (context window, vision,
tools, reasoning), the use-as-main/aux menu, sessions count, and
last-used timestamps when the flag is off.

Set dashboard.show_token_analytics: true in config.yaml to opt back in
to the local debug estimate. Fixing the underlying accounting (issue
#23270) is a separate, larger workstream.

Refs: #23270, #21705
This commit is contained in:
Teknium 2026-05-13 22:20:25 -07:00 committed by GitHub
parent e90508103c
commit f7ad2f1115
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 249 additions and 100 deletions

View file

@ -75,6 +75,7 @@ import { PluginPage, PluginSlot, usePlugins } from "@/plugins";
import type { PluginManifest } from "@/plugins";
import { useTheme } from "@/themes";
import { isDashboardEmbeddedChatEnabled } from "@/lib/dashboard-flags";
import { api } from "@/lib/api";
function RootRedirect() {
return <Navigate to="/sessions" replace />;
@ -316,6 +317,21 @@ export default function App() {
const isChatRoute = normalizedPath === "/chat";
const embeddedChat = isDashboardEmbeddedChatEnabled();
// `dashboard.show_token_analytics` gates the Analytics nav item. The
// page itself remains reachable by URL (it renders an explanation when
// the flag is off — see AnalyticsPage), but hiding the nav entry avoids
// surfacing misleading token/cost numbers in the sidebar. Default off.
const [showTokenAnalytics, setShowTokenAnalytics] = useState(false);
useEffect(() => {
api
.getConfig()
.then((cfg) => {
const dash = (cfg?.dashboard ?? {}) as { show_token_analytics?: unknown };
setShowTokenAnalytics(dash.show_token_analytics === true);
})
.catch(() => setShowTokenAnalytics(false));
}, []);
// A plugin can replace the built-in /chat page via `tab.override: "/chat"`
// in its manifest. When one does, `buildRoutes` already swaps the route
// element for <PluginPage /> — but we also have to suppress the
@ -346,11 +362,12 @@ export default function App() {
[embeddedChat],
);
const builtinNav = useMemo(
() =>
embeddedChat ? [CHAT_NAV_ITEM, ...BUILTIN_NAV_REST] : BUILTIN_NAV_REST,
[embeddedChat],
);
const builtinNav = useMemo(() => {
const base = embeddedChat
? [CHAT_NAV_ITEM, ...BUILTIN_NAV_REST]
: BUILTIN_NAV_REST;
return showTokenAnalytics ? base : base.filter((n) => n.path !== "/analytics");
}, [embeddedChat, showTokenAnalytics]);
const sidebarNav = useMemo(
() => partitionSidebarNav(builtinNav, manifests),