diff --git a/package.json b/package.json index 66d2523652dd..e6d0daa45b62 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.5", + "packageManager": "bun@1.3.6", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "typecheck": "bun turbo typecheck", diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 53e578214b9d..0fc0fa038a02 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -1,4 +1,4 @@ -import { Match, Show, Switch, createMemo } from "solid-js" +import { For, Match, Show, Switch, createMemo } from "solid-js" import { Tooltip } from "@opencode-ai/ui/tooltip" import { ProgressCircle } from "@opencode-ai/ui/progress-circle" import { Button } from "@opencode-ai/ui/button" @@ -12,6 +12,74 @@ interface SessionContextUsageProps { variant?: "button" | "indicator" } +interface ProviderUsage { + provider: string + tokens: number + cost: number + requests: number + percentage: number +} + +interface ModelUsage { + model: string + provider: string + input: number + output: number + cost: number + requests: number + total: number +} + +function getProviderEmoji(provider: string): string { + const p = provider.toLowerCase() + if (p.includes("anthropic")) return "🟣" + if (p.includes("google")) return "🔵" + if (p.includes("openai")) return "🟢" + if (p.includes("antigravity")) return "🌌" + if (p.includes("xai") || p.includes("grok")) return "⚡" + if (p.includes("groq")) return "🟠" + if (p.includes("mistral")) return "🔶" + if (p.includes("deepseek")) return "🐋" + return "⚪" +} + +function shortenModelName(model: string): string { + return model + .replace("claude-", "") + .replace("gpt-", "") + .replace("gemini-", "") + .replace("-latest", "") + .replace("-20250514", "") + .replace("-20250219", "") + .replace("-20241022", "") + .replace("-20240620", "") +} + +function formatCost(n: number): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 4, + maximumFractionDigits: 4, + }).format(n) +} + +function getProviderLabel(provider: string): string { + const p = provider.toLowerCase() + if (p.includes("anthropic")) return "Anthropic" + if (p.includes("google")) return "Google" + if (p.includes("openai")) return "OpenAI" + if (p.includes("antigravity")) return "Antigravity" + if (p.includes("xai") || p.includes("grok")) return "xAI" + return provider +} + +function formatTokens(tokens: number): string { + if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M` + if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K` + return tokens.toString() +} + export function SessionContextUsage(props: SessionContextUsageProps) { const sync = useSync() const params = useParams() @@ -30,6 +98,83 @@ export function SessionContextUsage(props: SessionContextUsageProps) { }).format(total) }) + // Per-provider usage breakdown + const providerUsage = createMemo((): ProviderUsage[] => { + const byProvider = new Map() + + for (const msg of messages()) { + if (msg.role !== "assistant") continue + const assistant = msg as AssistantMessage + const providerId = assistant.providerID + if (!providerId) continue + + const tokens = assistant.tokens.input + assistant.tokens.output + + assistant.tokens.reasoning + assistant.tokens.cache.read + + assistant.tokens.cache.write + + const existing = byProvider.get(providerId) || { tokens: 0, cost: 0, requests: 0 } + existing.tokens += tokens + existing.cost += assistant.cost || 0 + existing.requests += 1 + byProvider.set(providerId, existing) + } + + const totalTokens = Array.from(byProvider.values()).reduce((sum, x) => sum + x.tokens, 0) + + const result: ProviderUsage[] = [] + for (const [provider, usage] of byProvider) { + result.push({ + provider, + tokens: usage.tokens, + cost: usage.cost, + requests: usage.requests, + percentage: totalTokens > 0 ? (usage.tokens / totalTokens) * 100 : 0, + }) + } + + return result.sort((a, b) => b.tokens - a.tokens) + }) + + // Per-model usage breakdown + const modelUsage = createMemo((): ModelUsage[] => { + const byModel = new Map() + + for (const msg of messages()) { + if (msg.role !== "assistant") continue + const assistant = msg as AssistantMessage + const providerId = assistant.providerID || "unknown" + const modelId = assistant.modelID || "unknown" + const key = `${providerId}:${modelId}` + + const existing = byModel.get(key) || { provider: providerId, input: 0, output: 0, cost: 0, requests: 0 } + existing.input += assistant.tokens.input + assistant.tokens.cache.read + existing.output += assistant.tokens.output + assistant.tokens.reasoning + existing.cost += assistant.cost || 0 + existing.requests += 1 + byModel.set(key, existing) + } + + const result: ModelUsage[] = [] + for (const [key, usage] of byModel) { + const modelId = key.split(":").slice(1).join(":") + result.push({ + model: modelId, + provider: usage.provider, + input: usage.input, + output: usage.output, + cost: usage.cost, + requests: usage.requests, + total: usage.input + usage.output, + }) + } + + return result.sort((a, b) => b.total - a.total) + }) + + const totalTokens = createMemo(() => { + return providerUsage().reduce((sum, x) => sum + x.tokens, 0) + }) + const context = createMemo(() => { const last = messages().findLast((x) => { if (x.role !== "assistant") return false @@ -60,27 +205,80 @@ export function SessionContextUsage(props: SessionContextUsageProps) { ) const tooltipValue = () => ( -
+
+ {/* Provider Usage Breakdown */} + 0}> +
+
Provider Usage
+ + {(usage) => ( +
+
+ {getProviderEmoji(usage.provider)} + {getProviderLabel(usage.provider)} +
+
+ {formatTokens(usage.tokens)} + {Math.round(usage.percentage)}% +
+
+ )} +
+
+ Total + {formatTokens(totalTokens())} +
+
+
+ + {/* Model Usage Breakdown */} + 0}> +
+
Model Usage
+ + {(usage) => ( +
+
+
+ {getProviderEmoji(usage.provider)} + {shortenModelName(usage.model)} +
+ {usage.requests}x +
+
+ ↑{formatTokens(usage.input)} ↓{formatTokens(usage.output)} + {formatCost(usage.cost)} +
+
+ )} +
+
+
+ + {/* Context Window Usage */} {(ctx) => ( - <> +
{ctx().tokens} - Tokens + Context Tokens
{ctx().percentage ?? 0}% - Usage + Window Usage
- +
)}
+ + {/* Cost */}
{cost()} - Cost + Total Cost
+ -
Click to view context
+
Click to view context
) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 4958ad2c353b..89aec7770db5 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -19,6 +19,7 @@ import { TextField } from "@opencode-ai/ui/text-field" import { DialogSelectServer } from "@/components/dialog-select-server" import { SessionLspIndicator } from "@/components/session-lsp-indicator" import { SessionMcpIndicator } from "@/components/session-mcp-indicator" +import { SessionContextUsage } from "@/components/session-context-usage" import type { Session } from "@opencode-ai/sdk/v2/client" import { same } from "@/utils/same" @@ -161,6 +162,7 @@ export function SessionHeader() { {server.name} +
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 0561e5eacd49..3c96ae997443 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -887,7 +887,7 @@ export function Session() { - +
- -