{
  "format" : 1,
  "manifest" : {
    "accent" : "pink",
    "capabilities" : [
      {
        "userServer" : {
          "secret" : "LITELLM_PROXY_URL"
        }
      },
      {
        "secret" : {
          "name" : "LITELLM_API_KEY"
        }
      }
    ],
    "homepage" : "https://github.com/navbytes/stackbar-plugins/tree/main/Monitoring/LiteLLM",
    "id" : "io.navbytes.stackbar.plugins.litellm",
    "minimumStackbar" : "0.17.0",
    "name" : "LiteLLM",
    "summary" : "Tokens, spend, budget and model mix from your LiteLLM proxy, with charts.",
    "surfaces" : [
      "menuBar",
      "panel",
      "window",
      "widget",
      "settings"
    ],
    "symbol" : "sparkles",
    "triggers" : [
      {
        "launch" : {

        }
      },
      {
        "every" : {
          "seconds" : 120
        }
      },
      {
        "wake" : {

        }
      }
    ],
    "version" : "1.3.0"
  },
  "publisher" : {
    "name" : "navbytes",
    "publicKey" : "4dpLtXEpFXdi+6cviY/KdYuUulJPbEmCxxIK0EEN2/w="
  },
  "signature" : "fjaSUJ/GT2sXO2Q/7MX3PjgiVaVp36uZu/Fi7iWvwrJeBmo/7DXIPVzPEi5xWk25jxRTt8hLK0nzAxM0egsyAQ==",
  "source" : "import StackbarKit\nimport Foundation\n\n/// Tokens and spend from your own LiteLLM proxy: today, the last week and\n/// month as tiles, half a year of activity as a heatmap, a trend you can\n/// switch between a week, a month and everything, and the model mix.\n///\n/// Nothing is contacted until you enter the proxy's URL as\n/// LITELLM_PROXY_URL, and the plugin may reach that host and no other. The\n/// key is LITELLM_API_KEY, kept in the Keychain. The last good numbers stay\n/// on screen while the proxy can't be reached.\n@main\nfinal class LiteLLM: StackbarPlugin {\n    static let manifest = Manifest(\n        id: \"io.navbytes.stackbar.plugins.litellm\",\n        name: \"LiteLLM\",\n        version: \"1.3.0\",\n        summary: \"Tokens, spend, budget and model mix from your LiteLLM proxy, with charts.\",\n        surfaces: [.menuBar, .panel, .window, .widget, .settings],\n        triggers: [.launch, .minutes(2), .wake],\n        capabilities: [.userServer(secret: \"LITELLM_PROXY_URL\"), .secret(name: \"LITELLM_API_KEY\")],\n        minimumStackbar: \"0.17.0\",\n        homepage: \"https://github.com/navbytes/stackbar-plugins/tree/main/Monitoring/LiteLLM\",\n        accent: .pink,\n        symbol: \"sparkles\"\n    )\n\n    // MARK: Previews\n\n    static let previews: [PreviewScene<LiteLLM>] = [\n        PreviewScene(\"hero\", \"Spend, tokens and budget for your LLM proxy\") { plugin in sample(plugin) },\n        PreviewScene(\"dashboard\", \"Half a year of usage, by model\", surface: .window) { plugin in sample(plugin) },\n        PreviewScene(\"widget\", \"Today's spend on the desktop\", surface: .widget) { plugin in sample(plugin) },\n    ]\n\n    /// Half a year of a small team's usage: weekdays busy, weekends quiet,\n    /// growing slowly.\n    static func sample(_ plugin: LiteLLM) {\n        plugin.proxy = \"https://llm.example.com\"\n        var days: [String: Day] = [:]\n        for offset in -179...0 {\n            let weekday = Calendar.current.component(.weekday, from: Date().addingTimeInterval(Double(offset) * 86400))\n            let weekend = weekday == 1 || weekday == 7\n            let growth = 1 + Double(offset + 180) / 180\n            let wobble = 0.8 + 0.4 * abs(sin(Double(offset) * 1.7))\n            let tokens = (weekend ? 0.6e6 : 3.2e6) * growth * wobble\n            let requests = (tokens / 2600).rounded()\n            days[day(offset)] = Day(tokens: tokens, spend: tokens / 1e6 * 4.1, requests: requests, failed: (requests * 0.012).rounded())\n        }\n        plugin.days = days\n        plugin.models = [\n            Model(name: \"claude-sonnet\", tokens: 212e6, spend: 868),\n            Model(name: \"gpt-5-mini\", tokens: 140e6, spend: 214),\n            Model(name: \"claude-haiku\", tokens: 96e6, spend: 118),\n            Model(name: \"text-embedding-3-small\", tokens: 61e6, spend: 6),\n            Model(name: \"llama-3.3-70b\", tokens: 22e6, spend: 19),\n        ]\n        plugin.budget = Budget(spend: 612, max: 1000, resetAt: Calendar.current.date(byAdding: .day, value: 12, to: Date()))\n        plugin.todayDetail = Today(date: day(0), requests: 1490, failed: 17, promptTokens: 2.9e6, cacheReadTokens: 1.1e6, cacheSavings: 3.4)\n        plugin.historyFetched = Date()\n        plugin.lastSuccess = Date()\n    }\n\n    /// One day's totals, kept between runs for the heatmap and the trend.\n    struct Day: Codable, Equatable {\n        var tokens: Double\n        var spend: Double\n        var requests: Double = 0\n        var failed: Double = 0\n    }\n\n    struct Model: Codable, Equatable {\n        var name: String\n        var tokens: Double\n        var spend: Double\n    }\n\n    struct Budget: Codable, Equatable {\n        var spend: Double\n        var max: Double?\n        var resetAt: Date?\n    }\n\n    enum Range: String, CaseIterable {\n        case week = \"Week\", month = \"Month\", halfYear = \"6M\"\n    }\n\n    enum Measure: String, CaseIterable {\n        case tokens = \"Tokens\", spend = \"Spend\"\n    }\n\n    enum MenuBarShows: String, CaseIterable {\n        case spend = \"Spend\", tokens = \"Tokens\", budget = \"Budget\"\n    }\n\n    @Stored(\"days\") var days: [String: Day] = [:]\n    @Stored(\"models\") var models: [Model] = []\n    @Stored(\"budget\") var budget: Budget? = nil\n    @Stored(\"today\") var todayDetail: Today? = nil\n    @Stored(\"historyFetched\") var historyFetched: Date? = nil\n    @Stored(\"lastSuccess\") var lastSuccess: Date? = nil\n    @Stored(\"range\") var range = Range.month.rawValue\n    @Stored(\"measure\") var measure = Measure.tokens.rawValue\n    @Stored(\"menuBarShows\") var menuBarShows = MenuBarShows.spend.rawValue\n    @Stored(\"notifyBudget\") var notifyBudget = true\n    @Stored(\"notified\") var notified: [String: Double] = [:]\n\n    /// Today's health, from the latest fetch.\n    struct Today: Codable, Equatable {\n        var date: String\n        var requests: Double\n        var failed: Double\n        var promptTokens: Double\n        var cacheReadTokens: Double\n        var cacheSavings: Double\n    }\n\n    var problem: String?\n    var proxy: String?\n\n    // MARK: - Fetching\n\n    func refresh() async throws {\n        guard let url = try await Stackbar.secret(\"LITELLM_PROXY_URL\")?.trimmingCharacters(in: .whitespacesAndNewlines), !url.isEmpty else {\n            proxy = nil\n            problem = \"Enter your proxy's URL as LITELLM_PROXY_URL.\"\n            return\n        }\n        let base = url.hasSuffix(\"/\") ? String(url.dropLast()) : url\n        proxy = base\n        guard let key = try await Stackbar.secret(\"LITELLM_API_KEY\")?.trimmingCharacters(in: .whitespacesAndNewlines), !key.isEmpty else {\n            problem = \"Enter a LiteLLM key as LITELLM_API_KEY.\"\n            return\n        }\n        do {\n            async let info = fetchBudget(base: base, key: key)\n            async let month = fetchDays(base: base, key: key, from: Self.day(-30), to: Self.day(0))\n            let (newBudget, recent) = try await (info, month)\n            budget = newBudget\n            merge(recent.days)\n            models = recent.models\n            todayDetail = recent.today\n            if historyFetched.map({ Date().timeIntervalSince($0) > 6 * 3600 }) ?? true {\n                let history = try await fetchDays(base: base, key: key, from: Self.day(-182), to: Self.day(-31))\n                merge(history.days)\n                historyFetched = Date()\n            }\n            lastSuccess = Date()\n            problem = nil\n            notifyAboutBudget()\n        } catch {\n            problem = \"\\(error)\"\n        }\n    }\n\n    private func headers(_ key: String) -> [String: String] {\n        [\"Authorization\": \"Bearer \\(key)\", \"Accept\": \"application/json\"]\n    }\n\n    private func fetchBudget(base: String, key: String) async throws -> Budget? {\n        let response = try await Stackbar.get(base + \"/user/info\", headers: headers(key))\n        try check(response, what: \"/user/info\")\n        let info = try JSONDecoder().decode(UserInfoResponse.self, from: response.body).user_info\n        guard let info else { return nil }\n        let max = info.max_budget?.value\n        return Budget(spend: info.spend?.value ?? 0, max: max.flatMap { $0 > 0 ? $0 : nil }, resetAt: info.budget_reset_at.flatMap(Self.parseDate))\n    }\n\n    struct Fetched {\n        var days: [String: Day] = [:]\n        var models: [Model] = []\n        var today: Today?\n    }\n\n    /// `/user/daily/activity` for a span of days, page by page.\n    private func fetchDays(base: String, key: String, from: String, to: String) async throws -> Fetched {\n        var fetched = Fetched()\n        var byModel: [String: Model] = [:]\n        var page = 1\n        while page <= 20 {\n            let response = try await Stackbar.get(base + \"/user/daily/activity?start_date=\\(from)&end_date=\\(to)&page=\\(page)&page_size=100\", headers: headers(key))\n            try check(response, what: \"/user/daily/activity\")\n            let activity = try JSONDecoder().decode(ActivityResponse.self, from: response.body)\n            for result in activity.results ?? [] {\n                let metrics = result.metrics ?? Metrics()\n                fetched.days[result.date] = Day(tokens: metrics.tokens, spend: metrics.spend?.value ?? 0, requests: metrics.requests, failed: metrics.failed_requests ?? 0)\n                if result.date == Self.day(0) {\n                    fetched.today = Today(date: result.date, requests: metrics.requests, failed: metrics.failed_requests ?? 0,\n                                          promptTokens: metrics.prompt_tokens ?? 0, cacheReadTokens: metrics.cache_read_input_tokens ?? 0,\n                                          cacheSavings: metrics.prompt_caching_savings_spend ?? 0)\n                }\n                for (name, breakdown) in result.breakdown?.models ?? [:] {\n                    let model = breakdown?.metrics ?? Metrics()\n                    let short = Self.shortName(name)\n                    var total = byModel[short] ?? Model(name: short, tokens: 0, spend: 0)\n                    total.tokens += model.tokens\n                    total.spend += model.spend?.value ?? 0\n                    byModel[short] = total\n                }\n            }\n            let pages = activity.metadata?.total_pages ?? 1\n            guard activity.metadata?.has_more == true || page < pages else { break }\n            page += 1\n        }\n        fetched.models = byModel.values.filter { $0.tokens > 0 || $0.spend > 0 }.sorted { $0.tokens > $1.tokens }\n        return fetched\n    }\n\n    private func check(_ response: HTTPResponse, what: String) throws {\n        switch response.status {\n        case 200..<300: return\n        case 401, 403: throw HostError(\"The proxy refused the key (\\(response.status)). Check LITELLM_API_KEY.\")\n        case 404: throw HostError(\"The proxy has no \\(what). Is LITELLM_PROXY_URL the proxy's base URL?\")\n        default: throw HostError(\"The proxy answered \\(response.status) for \\(what).\")\n        }\n    }\n\n    private func merge(_ fetched: [String: Day]) {\n        var all = days\n        for (date, day) in fetched { all[date] = day }\n        let oldest = Self.day(-200)\n        days = all.filter { $0.key >= oldest }\n    }\n\n    private func notifyAboutBudget() {\n        guard notifyBudget, let budget, let max = budget.max else { return }\n        let used = budget.spend / max * 100\n        let today = Self.day(0)\n        for threshold in [100.0, 90.0] where used >= threshold {\n            guard (notified[today] ?? 0) < threshold else { return }\n            notified = [today: threshold]\n            Stackbar.notify(threshold >= 100 ? \"LiteLLM budget used up\" : \"LiteLLM budget at \\(Int(used))%\",\n                            body: \"\\(Self.money(budget.spend)) of \\(Self.money(max))\" + (budget.resetAt.map { \", resets \\(Self.relative($0))\" } ?? \"\"))\n            return\n        }\n    }\n\n    // MARK: - Surfaces\n\n    let theme = Theme.pink\n\n    var menuBar: ViewContent {\n        if proxy == nil || (days.isEmpty && budget == nil) {\n            Label(\"\", symbol: \"gauge.with.dots.needle.0percent\").role(.secondary)\n        } else {\n            Label(menuBarText, symbol: budgetUsed.map { $0 >= 90 ? \"exclamationmark.triangle.fill\" : $0 >= 50 ? \"gauge.with.dots.needle.50percent\" : \"gauge.with.dots.needle.33percent\" } ?? \"gauge.with.dots.needle.33percent\")\n                .role(budgetUsed.map { $0 >= 90 ? .critical : $0 >= 75 ? .warning : .primary } ?? .primary)\n        }\n    }\n\n    var panel: ViewContent {\n        VStack(spacing: Space.m) {\n            if proxy == nil {\n                EmptyState(\"Connect your LiteLLM proxy\", symbol: \"server.rack\",\n                           message: \"Paste its URL as LITELLM_PROXY_URL and a key as LITELLM_API_KEY. Nothing is contacted until you do.\", theme: theme)\n            } else if days.isEmpty, let problem {\n                EmptyState(\"Can't reach the proxy\", symbol: \"exclamationmark.triangle\", message: problem, theme: .warning) {\n                    ActionButton(\"Try Again\", symbol: \"arrow.clockwise\", theme: theme, prominent: true) { try await self.refresh() }\n                }\n            } else if days.isEmpty {\n                loading\n            } else {\n                header\n                staleBanner\n                tiles\n                budgetCard\n                trendCard\n            }\n        }\n        .frame(fillWidth: true)\n    }\n\n    /// The full dashboard: the panel, then the half year, the model mix and today's request health.\n    var window: ViewContent {\n        VStack(spacing: Space.m) {\n            if days.isEmpty {\n                panel\n            } else {\n                header\n                staleBanner\n                tiles\n                budgetCard\n                trendCard\n                Card(\"Activity\", detail: \"\\(activeDays(0...181)) active days in 6 months\") {\n                    Chart(values: series(days: 182, measure: .tokens), kind: .heatmap, labels: dayLabels(182, format: \"EEE d MMM\"), unit: \" tokens\")\n                        .foreground(theme.solidPaint)\n                }\n                if !models.isEmpty {\n                    Card(\"Models\", detail: \"last 30 days, by \\(currentMeasure == .spend ? \"spend\" : \"tokens\")\") {\n                        Chart(values: modelSlices.map(\\.value), kind: .donut, labels: modelSlices.map(\\.name), unit: currentMeasure == .spend ? \"$\" : nil)\n                    }\n                }\n                if let today = todayDetail, today.requests > 0 {\n                    Card(\"Today's requests\") {\n                        HStack(spacing: Space.s) {\n                            StatTile(\"Requests\", value: Self.compact(today.requests), theme: theme)\n                            StatTile(\"Succeeded\", value: String(format: \"%.1f%%\", (today.requests - today.failed) / today.requests * 100),\n                                     caption: today.failed > 0 ? \"\\(Self.compact(today.failed)) failed\" : nil, theme: today.failed > 0 ? .warning : .success)\n                            if today.promptTokens > 0 {\n                                StatTile(\"From cache\", value: \"\\(Int((today.cacheReadTokens / today.promptTokens * 100).rounded()))%\",\n                                         caption: today.cacheSavings > 0 ? \"saved \\(Self.money(today.cacheSavings))\" : \"of input\", theme: theme)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        .frame(fillWidth: true)\n    }\n\n    @ViewBuilder var header: ViewContent {\n        let today = total(0...0)\n        let title: String = switch MenuBarShows(rawValue: menuBarShows) ?? .spend {\n        case .tokens: \"\\(Self.compact(today.tokens)) tokens today\"\n        case .budget: budgetUsed.map { \"\\(Int($0.rounded()))% of budget\" } ?? \"\\(Self.money(today.spend)) today\"\n        case .spend: \"\\(Self.money(today.spend)) today\"\n        }\n        let other = MenuBarShows(rawValue: menuBarShows) == .tokens ? Self.money(today.spend) : \"\\(Self.compact(today.tokens)) tokens\"\n        PanelHeader(title, subtitle: \"\\(other) · updated \\(lastSuccess.map(Self.relative) ?? \"never\")\",\n                    symbol: \"gauge.with.dots.needle.67percent\", theme: theme) {\n            IconButton(\"arrow.up.right.square\", label: \"Open Dashboard\", theme: theme) { if let proxy = self.proxy { Stackbar.open(proxy + \"/ui\") } }\n        }\n    }\n\n    @ViewBuilder var staleBanner: ViewContent {\n        if let problem {\n            Banner(\"Showing numbers from \\(lastSuccess.map(Self.relative) ?? \"earlier\"). \\(problem)\")\n        }\n    }\n\n    @ViewBuilder var tiles: ViewContent {\n        let week = total(0...6), month = total(0...29)\n        HeroCard(theme: theme, look: .glow, spacing: Space.s) {\n            HStack(spacing: Space.l) {\n                HeroStat(\"Today\", value: Self.compact(total(0...0).tokens), theme: theme, live: total(0...0).tokens > 0)\n                HeroStat(\"7 days\", value: Self.compact(week.tokens), theme: theme, symbol: \"calendar\")\n                HeroStat(\"30 days\", value: Self.compact(month.tokens), theme: theme, symbol: \"chart.bar.fill\")\n                Spacer()\n            }\n            Text(\"\\(Self.money(total(0...0).spend)) today · \\(Self.money(week.spend)) this week · ~\\(Self.compact(month.tokens / Double(max(1, activeDays(0...29)))))/day\").role(.secondary).typography(.captionNumber)\n        }\n    }\n\n    @ViewBuilder var budgetCard: ViewContent {\n        if let budget, let max = budget.max {\n            let used = budget.spend / max\n            Card(spacing: Space.xs) {\n                Meter(\"Budget\", reading: \"\\(Self.money(budget.spend)) of \\(Self.money(max))\", value: min(used, 1),\n                      theme: used >= 0.9 ? .critical : used >= 0.75 ? .warning : theme)\n                if let reset = budget.resetAt {\n                    Text(\"Resets \\(Self.relative(reset))\").role(.secondary).typography(.caption)\n                }\n            }\n        }\n    }\n\n    @ViewBuilder var trendCard: ViewContent {\n        Card(spacing: Space.s) {\n            HStack(spacing: Space.s) {\n                Picker(\"Range\", options: Range.allCases.map(\\.rawValue), selection: range, style: .segmented) { self.range = $0 }\n                Spacer()\n                Picker(\"Show\", options: Measure.allCases.map(\\.rawValue), selection: measure, style: .segmented) { self.measure = $0 }\n                    .frame(width: 130)\n            }\n            trendChart.foreground(theme.solidPaint)\n        }\n    }\n\n    @ViewBuilder var loading: ViewContent {\n        PanelHeader(\"Checking…\", subtitle: \"Fetching your usage from \\(proxy ?? \"the proxy\")\", symbol: \"gauge.with.dots.needle.67percent\", theme: theme)\n        HStack(spacing: Space.s) {\n            ForEach([\"Today\", \"7 days\", \"30 days\"]) { name in\n                StatTile(name, value: \"–\", theme: .neutral)\n            }\n        }\n        Rectangle().fill(.surface).frame(height: 160).clipShape(.roundedRectangle(cornerRadius: Radius.large))\n    }\n\n    @ViewBuilder var trendChart: ViewContent {\n        let measure = currentMeasure\n        let unit = measure == .tokens ? nil : \"$\"\n        switch Range(rawValue: range) ?? .month {\n        case .week:\n            Chart(values: series(days: 7, measure: measure), kind: .bar, labels: dayLabels(7, format: \"EEE\"), unit: unit, highlight: 6)\n        case .month:\n            Chart(values: series(days: 30, measure: measure), kind: .area, labels: sparseLabels(dayLabels(30, format: \"MMM d\"), every: 7), unit: unit, highlight: 29)\n        case .halfYear:\n            let weeks = weekly(measure: measure)\n            Chart(values: weeks.values, kind: .area, labels: weeks.labels, unit: unit)\n        }\n    }\n\n    var widget: ViewContent {\n        if proxy == nil || days.isEmpty {\n            VStack(alignment: .center, spacing: Space.xs) {\n                IconTile(\"server.rack\", theme: theme)\n                Text(\"Not connected\").role(.secondary).typography(.caption)\n            }\n            .frame(fillWidth: true, fillHeight: true)\n        } else {\n            VStack(spacing: Space.s) {\n                HStack(spacing: Space.m) {\n                    Metric(Self.money(total(0...0).spend), caption: \"today · \\(Self.compact(total(0...0).tokens)) tokens\", type: .metric)\n                    Spacer()\n                    Chart(values: series(days: 14, measure: .tokens), kind: .area).foreground(theme.solidPaint).frame(width: 120, height: 48)\n                }\n                if let budget, let max = budget.max {\n                    let used = budget.spend / max\n                    Meter(\"Budget\", reading: \"\\(Int((used * 100).rounded()))%\", value: min(used, 1), theme: used >= 0.9 ? .critical : used >= 0.75 ? .warning : theme)\n                }\n            }\n            .frame(fillWidth: true, fillHeight: true)\n        }\n    }\n\n    var settings: ViewContent {\n        Section(\"Proxy\") {\n            LabeledContent(\"URL\", value: proxy ?? \"Not set: enter LITELLM_PROXY_URL\")\n            LabeledContent(\"Last update\", value: lastSuccess.map(Self.relative) ?? \"Never\")\n            if let problem { Text(problem).style(.caption).role(.warning) }\n        }\n        Section(\"Menu bar\") {\n            Picker(\"Shows\", options: MenuBarShows.allCases.map(\\.rawValue), selection: menuBarShows, style: .segmented) { self.menuBarShows = $0 }\n        }\n        Section(\"Notifications\") {\n            Toggle(\"Tell me at 90% and 100% of the budget\", isOn: notifyBudget) { self.notifyBudget = $0 }\n        }\n    }\n\n    var tools: [AgentTool] {\n        AgentTool(\"usage_summary\", description: \"LiteLLM tokens and spend for today, 7 and 30 days, the budget and the top models\") { _ in\n            self.summaryText\n        }\n    }\n\n    // MARK: - Numbers\n\n    var currentMeasure: Measure { Measure(rawValue: measure) ?? .tokens }\n\n    var budgetUsed: Double? {\n        guard let budget, let max = budget.max else { return nil }\n        return budget.spend / max * 100\n    }\n\n    var menuBarText: String {\n        switch MenuBarShows(rawValue: menuBarShows) ?? .spend {\n        case .spend: return Self.money(total(0...0).spend, cents: true)\n        case .tokens: return Self.compact(total(0...0).tokens)\n        case .budget: return budgetUsed.map { \"\\(Int($0.rounded()))%\" } ?? Self.money(total(0...0).spend, cents: true)\n        }\n    }\n\n    /// Totals over days ago, 0 being today.\n    func total(_ ago: ClosedRange<Int>) -> Day {\n        ago.reduce(into: Day(tokens: 0, spend: 0)) { sum, offset in\n            let day = days[Self.day(-offset)] ?? Day(tokens: 0, spend: 0)\n            sum.tokens += day.tokens\n            sum.spend += day.spend\n            sum.requests += day.requests\n            sum.failed += day.failed\n        }\n    }\n\n    func activeDays(_ ago: ClosedRange<Int>) -> Int {\n        ago.filter { (days[Self.day(-$0)]?.tokens ?? 0) > 0 }.count\n    }\n\n    /// The last `count` days, oldest first.\n    func series(days count: Int, measure: Measure) -> [Double] {\n        (0..<count).reversed().map { offset in\n            let day = days[Self.day(-offset)]\n            return measure == .tokens ? day?.tokens ?? 0 : day?.spend ?? 0\n        }\n    }\n\n    func dayLabels(_ count: Int, format: String) -> [String] {\n        let formatter = DateFormatter()\n        formatter.locale = Locale(identifier: \"en_US_POSIX\")\n        formatter.dateFormat = format\n        return (0..<count).reversed().map { formatter.string(from: Calendar.current.date(byAdding: .day, value: -$0, to: Date()) ?? Date()) }\n    }\n\n    /// Keeps every `every`th label, counted back from the last, and blanks the\n    /// rest so a month's axis stays readable.\n    func sparseLabels(_ labels: [String], every: Int) -> [String] {\n        labels.enumerated().map { index, label in (labels.count - 1 - index) % every == 0 ? label : \"\" }\n    }\n\n    func weekly(measure: Measure) -> (values: [Double], labels: [String]) {\n        let daily = series(days: 182, measure: measure)\n        let names = dayLabels(182, format: \"MMM d\")\n        var values: [Double] = []\n        var labels: [String] = []\n        for start in stride(from: daily.count % 7, to: daily.count, by: 7) {\n            values.append(daily[start..<min(start + 7, daily.count)].reduce(0, +))\n            labels.append(values.count % 4 == 1 ? names[start] : \"\")\n        }\n        return (values, labels)\n    }\n\n    /// The top five models and the rest as \"Other\", by the chosen measure.\n    var modelSlices: [(name: String, value: Double)] {\n        let measure = currentMeasure\n        let sorted = models.map { (name: $0.name, value: measure == .tokens ? $0.tokens : $0.spend) }.sorted { $0.value > $1.value }\n        let rest = sorted.dropFirst(5).reduce(0) { $0 + $1.value }\n        return Array(sorted.prefix(5)) + (rest > 0 ? [(name: \"Other\", value: rest)] : [])\n    }\n\n    var summaryText: String {\n        guard proxy != nil else { return \"LiteLLM is not connected: LITELLM_PROXY_URL is not set.\" }\n        var lines = [\n            \"Today: \\(Self.compact(total(0...0).tokens)) tokens, \\(Self.money(total(0...0).spend, cents: true))\",\n            \"7 days: \\(Self.compact(total(0...6).tokens)) tokens, \\(Self.money(total(0...6).spend, cents: true))\",\n            \"30 days: \\(Self.compact(total(0...29).tokens)) tokens, \\(Self.money(total(0...29).spend, cents: true))\",\n        ]\n        if let budget, let max = budget.max {\n            lines.append(\"Budget: \\(Self.money(budget.spend, cents: true)) of \\(Self.money(max))\" + (budget.resetAt.map { \", resets \\(Self.relative($0))\" } ?? \"\"))\n        }\n        for model in models.prefix(5) {\n            lines.append(\"\\(model.name): \\(Self.compact(model.tokens)) tokens, \\(Self.money(model.spend, cents: true))\")\n        }\n        if let problem { lines.append(\"Problem: \\(problem)\") }\n        return lines.joined(separator: \"\\n\")\n    }\n\n    // MARK: - Formatting\n\n    static func day(_ offset: Int) -> String {\n        let formatter = DateFormatter()\n        formatter.locale = Locale(identifier: \"en_US_POSIX\")\n        formatter.dateFormat = \"yyyy-MM-dd\"\n        return formatter.string(from: Calendar.current.date(byAdding: .day, value: offset, to: Date()) ?? Date())\n    }\n\n    static func compact(_ value: Double) -> String {\n        for (limit, suffix) in [(1e12, \"T\"), (1e9, \"B\"), (1e6, \"M\"), (1e3, \"K\")] where abs(value) >= limit {\n            let scaled = value / limit\n            return (scaled >= 100 ? String(format: \"%.0f\", scaled) : String(format: \"%.1f\", scaled)) + suffix\n        }\n        return String(format: \"%.0f\", value)\n    }\n\n    static func money(_ value: Double, cents: Bool = true) -> String {\n        value >= 1000 ? \"$\" + compact(value) : String(format: cents ? \"$%.2f\" : \"$%.0f\", value)\n    }\n\n    static func relative(_ date: Date) -> String {\n        let seconds = date.timeIntervalSinceNow\n        let minutes = Int((abs(seconds) / 60).rounded())\n        let amount = minutes < 1 ? \"now\" : minutes < 60 ? \"\\(minutes) min\" : minutes < 48 * 60 ? \"\\(minutes / 60) h\" : \"\\(minutes / 1440) days\"\n        if amount == \"now\" { return \"just now\" }\n        return seconds > 0 ? \"in \\(amount)\" : \"\\(amount) ago\"\n    }\n\n    static func parseDate(_ text: String) -> Date? {\n        let full = ISO8601DateFormatter()\n        full.formatOptions = [.withInternetDateTime, .withFractionalSeconds]\n        let plain = ISO8601DateFormatter()\n        return full.date(from: text) ?? plain.date(from: text) ?? plain.date(from: text + \"Z\")\n    }\n\n    /// `anthropic/claude-sonnet-4` reads as `claude-sonnet-4`.\n    static func shortName(_ name: String) -> String {\n        name.split(separator: \"/\").last.map(String.init) ?? name\n    }\n\n    // MARK: - LiteLLM's JSON\n\n    /// A number LiteLLM sends either as a number or as a string.\n    struct Flexible: Decodable {\n        var value: Double\n        init(from decoder: Decoder) throws {\n            let container = try decoder.singleValueContainer()\n            if let number = try? container.decode(Double.self) {\n                value = number\n            } else {\n                value = Double(try container.decode(String.self)) ?? 0\n            }\n        }\n    }\n\n    struct UserInfoResponse: Decodable {\n        struct Info: Decodable {\n            var spend: Flexible?\n            var max_budget: Flexible?\n            var budget_reset_at: String?\n        }\n        var user_info: Info?\n    }\n\n    struct Metrics: Decodable {\n        var spend: Flexible?\n        var prompt_tokens: Double?\n        var completion_tokens: Double?\n        var total_tokens: Double?\n        var successful_requests: Double?\n        var failed_requests: Double?\n        var api_requests: Double?\n        var cache_read_input_tokens: Double?\n        var prompt_caching_savings_spend: Double?\n\n        var tokens: Double { total_tokens ?? (prompt_tokens ?? 0) + (completion_tokens ?? 0) }\n        var requests: Double { api_requests ?? (successful_requests ?? 0) + (failed_requests ?? 0) }\n    }\n\n    struct ActivityResponse: Decodable {\n        struct Result: Decodable {\n            struct Breakdown: Decodable {\n                struct Entry: Decodable { var metrics: Metrics? }\n                var models: [String: Entry?]?\n            }\n            var date: String\n            var metrics: Metrics?\n            var breakdown: Breakdown?\n        }\n        struct Metadata: Decodable {\n            var total_pages: Int?\n            var has_more: Bool?\n        }\n        var results: [Result]?\n        var metadata: Metadata?\n    }\n}\n",
  "sourceDigest" : "2d558b76705fbdcecf9b9d7b9f2778df1ea5d766bb4bec21c3b4219967779df3"
}