{
  "format" : 1,
  "manifest" : {
    "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.11.0",
    "name" : "LiteLLM",
    "summary" : "Tokens, spend, budget and model mix from your LiteLLM proxy, with charts.",
    "surfaces" : [
      "menuBar",
      "panel",
      "window",
      "widget",
      "settings"
    ],
    "triggers" : [
      {
        "launch" : {

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

        }
      }
    ],
    "version" : "1.0.0"
  },
  "publisher" : {
    "name" : "navbytes",
    "publicKey" : "4dpLtXEpFXdi+6cviY/KdYuUulJPbEmCxxIK0EEN2/w="
  },
  "signature" : "P4KRLeBQAqhZiGxzqgM/PfATEMLSx7kFVXaHdE/FujKvqYywmE7WLq8w8VGSuL0lQd9/7XnyAkJ3H5+IoKAoBw==",
  "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.0.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.11.0\",\n        homepage: \"https://github.com/navbytes/stackbar-plugins/tree/main/Monitoring/LiteLLM\"\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 = \"6 Months\"\n    }\n\n    enum Measure: String, CaseIterable {\n        case tokens = \"Tokens\", spend = \"Spend\"\n    }\n\n    enum MenuBarShows: String, CaseIterable {\n        case spend = \"Spend today\", tokens = \"Tokens today\", budget = \"Budget used\"\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    var menuBar: ViewContent {\n        if proxy == nil || (days.isEmpty && budget == nil) {\n            Label(\"LiteLLM\", symbol: \"gauge.with.dots.needle.0percent\")\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        if proxy == nil {\n            ContentUnavailableView(\"Connect your LiteLLM proxy\", symbol: \"server.rack\",\n                                   message: \"Enter its URL as LITELLM_PROXY_URL and a key as LITELLM_API_KEY. Nothing is contacted until you do.\")\n        } else if days.isEmpty, let problem {\n            ContentUnavailableView(\"Can't reach the proxy\", symbol: \"exclamationmark.triangle\", message: problem)\n        } else if days.isEmpty {\n            ContentUnavailableView(\"Loading\", symbol: \"hourglass\", message: \"Fetching your usage from \\(proxy ?? \"the proxy\").\")\n        } else {\n            dashboard\n        }\n    }\n\n    @ViewBuilder var dashboard: ViewContent {\n        if let problem {\n            Label(\"Showing numbers from \\(lastSuccess.map(Self.relative) ?? \"earlier\"): \\(problem)\", symbol: \"exclamationmark.triangle\").role(.warning)\n        }\n        HStack {\n            Section(\"Today\") {\n                Text(Self.compact(total(0...0).tokens)).style(.title)\n                Text(Self.money(total(0...0).spend)).style(.caption).role(.secondary)\n            }\n            Section(\"7 days\") {\n                Text(Self.compact(total(0...6).tokens)).style(.title)\n                Text(\"\\(Self.money(total(0...6).spend)) · \\(activeDays(0...6)) active days\").style(.caption).role(.secondary)\n            }\n            Section(\"30 days\") {\n                Text(Self.compact(total(0...29).tokens)).style(.title)\n                Text(\"~\\(Self.compact(total(0...29).tokens / Double(max(1, activeDays(0...29)))))/day\").style(.caption).role(.secondary)\n            }\n        }\n        if let budget, let max = budget.max {\n            Section(\"Budget\") {\n                ProgressBar(\"\\(Self.money(budget.spend)) of \\(Self.money(max))\" + (budget.resetAt.map { \" · resets \\(Self.relative($0))\" } ?? \"\"),\n                            value: budget.spend, total: max, role: budget.spend >= max * 0.9 ? .critical : budget.spend >= max * 0.75 ? .warning : .accent)\n            }\n        }\n        Section(\"Activity\") {\n            Chart(\"\\(activeDays(0...181)) active days in 6 months\", values: series(days: 182, measure: .tokens), kind: .heatmap, labels: dayLabels(182, format: \"EEE d MMM\"), unit: \" tokens\")\n        }\n        Section(\"Trend\") {\n            HStack {\n                Picker(\"Range\", options: Range.allCases.map(\\.rawValue), selection: range) { self.range = $0 }\n                Picker(\"Show\", options: Measure.allCases.map(\\.rawValue), selection: measure) { self.measure = $0 }\n            }\n            trendChart\n        }\n        if !models.isEmpty {\n            Section(\"Models, last 30 days\") {\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            Section(\"Today's requests\") {\n                LabeledContent(\"Requests\", value: Self.compact(today.requests))\n                LabeledContent(\"Succeeded\", value: String(format: \"%.1f%%\", (today.requests - today.failed) / today.requests * 100))\n                if today.promptTokens > 0 {\n                    LabeledContent(\"Read from cache\", value: \"\\(Int((today.cacheReadTokens / today.promptTokens * 100).rounded()))% of input\")\n                }\n                if today.cacheSavings > 0 { LabeledContent(\"Saved by caching\", value: Self.money(today.cacheSavings)) }\n            }\n        }\n        HStack {\n            Button(\"Open Dashboard\", symbol: \"rectangle.on.rectangle\") { if let proxy = self.proxy { Stackbar.open(proxy + \"/ui\") } }\n            Button(\"Refresh\", symbol: \"arrow.clockwise\") { try await self.refresh() }\n        }\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            Label(\"LiteLLM: not connected\", symbol: \"server.rack\")\n        } else {\n            LabeledContent(\"Today\", value: \"\\(Self.compact(total(0...0).tokens)) · \\(Self.money(total(0...0).spend))\")\n            if let budget, let max = budget.max {\n                ProgressBar(\"Budget \\(Int((budget.spend / max * 100).rounded()))%\", value: budget.spend, total: max)\n            }\n            Chart(values: series(days: 14, measure: .tokens), kind: .area, unit: nil)\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) { 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: Date().addingTimeInterval(-Double($0) * 86400)) }\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: Date().addingTimeInterval(Double(offset) * 86400))\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" : "b5971bcf4a882abcd088d955e8f027d3af796f5bbe555d1328b172f91c21ddb6"
}