Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.47.1 — Unreleased

### Added
- Fireworks: new provider showing 30-day spend from the account billing summary API, with Settings fields for the API key and account slug (Fireworks has no whoami endpoint and no public balance API).
- CLI: expose Codex cost-history completeness in JSON and add an experimental provider-native-only scan mode (#2520). Thanks @NickGuAI!
- Usage & Spend: add an accessible daily, weekly, and cumulative token-activity heatmap backed by the existing persistent cost scan cache (#2548). Thanks @Yuxin-Qiao!
- Settings: the sidebar is now resizable — drag the divider between 200–380pt; the width persists across launches.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [Abacus AI](docs/abacus.md) — Browser cookie auth for ChatLLM/RouteLLM compute credit tracking.
- [Mistral](docs/mistral.md) — Browser cookies for API spend, credit balance, and monthly-plan usage.
- [DeepSeek](docs/deepseek.md) — API key for credit balance tracking (paid vs. granted breakdown).
- [Fireworks](docs/fireworks.md) — API key + account slug for 30-day spend from the billing summary API.
- [DeepInfra](docs/deepinfra.md) — API key for prepaid balance, current-month spend, and spending-limit tracking.
- [Moonshot / Kimi API](docs/moonshot.md) — API key for Moonshot/Kimi API account balance tracking.
- [Venice](docs/venice.md) — API key for DIEM or USD balance tracking.
Expand Down
1 change: 1 addition & 0 deletions Scripts/regenerate-provider-manifests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ PROVIDERS=(
alibabatokenplan
qwencloud
factory
fireworks
gemini
antigravity
copilot
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import AppKit
import CodexBarCore
import Foundation
import SwiftUI

struct FireworksProviderImplementation: ProviderImplementation {
let id: UsageProvider = .fireworks

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "api" }
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.fireworksAPIToken
_ = settings.fireworksAccountSlug
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext)
-> ProviderSettingsSnapshotContribution?
{
.fireworks(context.settings.fireworksSettingsSnapshot())
}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if FireworksSettingsReader.apiKey(environment: context.environment) != nil,
FireworksSettingsReader.accountSlug(environment: context.environment) != nil
{
return true
}
return context.settings.hasFireworksCredentials
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "fireworks-api-key",
title: "API key",
subtitle: "Create a key at app.fireworks.ai/settings. The same key authorizes billing reads.",
kind: .secure,
placeholder: "fw_...",
binding: context.stringBinding(\.fireworksAPIToken),
actions: [],
isVisible: nil,
onActivate: nil),
ProviderSettingsFieldDescriptor(
id: "fireworks-account-slug",
title: "Account slug",
subtitle: "The segment after /accounts/ in your app.fireworks.ai URLs, e.g. x0mh0x for "
+ "app.fireworks.ai/accounts/x0mh0x. Required because Fireworks has no whoami endpoint.",
kind: .plain,
placeholder: "x0mh0x",
binding: context.stringBinding(\.fireworksAccountSlug),
actions: [
ProviderSettingsActionDescriptor(
id: "fireworks-open-billing",
title: "Open Fireworks billing",
style: .link,
isVisible: nil,
perform: {
NSWorkspace.shared.open(
FireworksURLs.billing(
accountSlug: context.settings.fireworksAccountSlug))
}),
],
isVisible: nil,
onActivate: nil),
]
}
}

enum FireworksURLs {
static func billing(accountSlug: String) -> URL {
let slug = accountSlug.trimmingCharacters(in: .whitespacesAndNewlines)
if slug.isEmpty {
return URL(string: "https://app.fireworks.ai")!
}
return URL(string: "https://app.fireworks.ai/accounts/\(slug)/settings/billing")!
}
}
34 changes: 34 additions & 0 deletions Sources/CodexBar/Providers/Fireworks/FireworksSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var fireworksAPIToken: String {
get { self.configSnapshot.providerConfig(for: .fireworks)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .fireworks) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .fireworks, field: "apiKey", value: newValue)
}
}

var fireworksAccountSlug: String {
get { self.configSnapshot.providerConfig(for: .fireworks)?.sanitizedAccountSlug ?? "" }
set {
self.updateProviderConfig(provider: .fireworks) { entry in
entry.accountSlug = self.normalizedConfigValue(newValue)
}
}
}

var hasFireworksCredentials: Bool {
guard let config = self.configSnapshot.providerConfig(for: .fireworks) else { return false }
return config.sanitizedAPIKey != nil && config.sanitizedAccountSlug != nil
}
}

extension SettingsStore {
func fireworksSettingsSnapshot() -> ProviderSettingsSnapshot.FireworksProviderSettings {
ProviderSettingsSnapshot.FireworksProviderSettings(accountSlug: self.fireworksAccountSlug)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum ProviderImplementationManifest {
{ AlibabaTokenPlanProviderImplementation() },
{ QwenCloudProviderImplementation() },
{ FactoryProviderImplementation() },
{ FireworksProviderImplementation() },
{ GeminiProviderImplementation() },
{ AntigravityProviderImplementation() },
{ CopilotProviderImplementation() },
Expand Down
11 changes: 11 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-fireworks.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public enum LogCategories {
public static let devin = Self.provider(.devin)
public static let doubaoUsage = Self.provider(.doubao, scope: "usage")
public static let elevenLabsUsage = Self.provider(.elevenlabs, scope: "usage")
public static let fireworksUsage = Self.provider(.fireworks, scope: "usage")
public static let geminiProbe = Self.provider(.gemini, scope: "probe")
public static let grok = Self.provider(.grok)
public static let hooks = "hooks"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

extension ProviderConfig {
/// Account slug (the segment after `/accounts/` in console URLs) that owns `apiKey`.
/// Fireworks does not expose a whoami endpoint, so the slug cannot be derived from the key.
public var accountSlug: String? {
get { self.extensionValue(forKey: "accountSlug") }
set { self.setExtensionValue(newValue, forKey: "accountSlug") }
}

public var sanitizedAccountSlug: String? {
Self.clean(self.accountSlug)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Foundation

public enum FireworksProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
private static let credentials = ProviderCredentialAdapter.apiKey(
environmentKey: FireworksSettingsReader.configAPIKeyEnvironmentKey,
additionalProjections: [
ProviderCredentialEnvironmentProjection(
key: FireworksSettingsReader.configAccountSlugEnvironmentKey,
value: { $0.sanitizedAccountSlug }),
],
resolve: FireworksSettingsReader.apiKey,
configValidator: { config in
guard config.sanitizedAPIKey != nil, config.sanitizedAccountSlug == nil else {
return []
}
return [CodexBarConfigIssue(
severity: .error,
provider: .fireworks,
field: "accountSlug",
code: "missing_account_slug",
message: "Fireworks needs the account slug from app.fireworks.ai/accounts/<slug> to read billing.")]
},
missingCredentialMessage: { environment in
guard FireworksSettingsReader.apiKey(environment: environment) != nil else {
return nil
}
return "Fireworks needs the account slug (set FIREWORKS_ACCOUNT_SLUG or the slug field in Settings)."
})

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .fireworks,
credentials: self.credentials,
metadata: ProviderMetadata(
id: .fireworks,
displayName: "Fireworks",
sessionLabel: "Spend",
weeklyLabel: "Spend",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Fireworks usage",
cliName: "fireworks",
defaultEnabled: false,
widgetSelectable: false,
isPrimaryProvider: false,
usesAccountFallback: false,
balanceOnly: false,
dashboardURL: "https://app.fireworks.ai",
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .init(provider: .fireworks),
iconResourceName: "ProviderIcon-fireworks",
color: ProviderColor(red: 242 / 255, green: 91 / 255, blue: 28 / 255),
confettiPalette: [
ProviderColor(hex: 0xE65618),
ProviderColor(hex: 0xFF9A3C),
ProviderColor(hex: 0x2B2B2E),
]),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Fireworks spend comes from the billing summary API; cost history is not tracked." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [FireworksAPIFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "fireworks",
aliases: ["fw"],
versionDetector: nil))
}
}

struct FireworksAPIFetchStrategy: ProviderFetchStrategy {
let id = "fireworks.api"
let kind: ProviderFetchKind = .apiToken
private let transport: any ProviderHTTPTransport

init(transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) {
self.transport = transport
}

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
FireworksSettingsReader.apiKey(environment: context.env) != nil
&& FireworksSettingsReader.accountSlug(environment: context.env) != nil
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let apiKey = FireworksSettingsReader.apiKey(environment: context.env) else {
throw FireworksUsageError.missingCredentials
}
guard let accountSlug = FireworksSettingsReader.accountSlug(environment: context.env) else {
throw FireworksUsageError.missingAccountSlug
}
let usage = try await FireworksUsageFetcher.fetchUsage(
apiKey: apiKey,
accountSlug: accountSlug,
session: self.transport)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

public struct FireworksProviderSettings: Sendable {
public let accountSlug: String?

public init(accountSlug: String? = nil) {
self.accountSlug = accountSlug
}
}

public enum FireworksProviderSettingsKey: ProviderSettingsSectionKey {
public static let providerID = ProviderInstanceID.fireworks
public typealias Section = FireworksProviderSettings
}

extension ProviderSettingsSnapshot {
public typealias FireworksProviderSettings = CodexBarCore.FireworksProviderSettings
public var fireworks: FireworksProviderSettings? {
self[FireworksProviderSettingsKey.self]
}

public static func make(fireworks: FireworksProviderSettings?) -> Self {
self.make(fireworks, for: FireworksProviderSettingsKey.self)
}
}

extension ProviderSettingsSnapshotContribution {
public static func fireworks(_ section: FireworksProviderSettings) -> Self {
Self(section, for: FireworksProviderSettingsKey.self)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation

public struct FireworksSettingsReader: Sendable {
public static let apiKeyEnvironmentKeys = [
"FIREWORKS_API_KEY",
"FIREWORKS_KEY",
]
public static let accountSlugEnvironmentKey = "FIREWORKS_ACCOUNT_SLUG"
public static let configAPIKeyEnvironmentKey = "CODEXBAR_FIREWORKS_API_KEY"
public static let configAccountSlugEnvironmentKey = "CODEXBAR_FIREWORKS_ACCOUNT_SLUG"

public static func apiKey(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
for key in [self.configAPIKeyEnvironmentKey] + self.apiKeyEnvironmentKeys {
guard let raw = environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
continue
}
let cleaned = Self.cleaned(raw)
if !cleaned.isEmpty {
return cleaned
}
}
return nil
}

public static func accountSlug(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
for key in [self.configAccountSlugEnvironmentKey, self.accountSlugEnvironmentKey] {
guard let raw = environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
continue
}
let cleaned = Self.cleaned(raw)
if !cleaned.isEmpty {
return cleaned
}
}
return nil
}

private static func cleaned(_ raw: String) -> String {
var value = raw
if (value.hasPrefix("\"") && value.hasSuffix("\""))
|| (value.hasPrefix("'") && value.hasSuffix("'"))
{
value = String(value.dropFirst().dropLast())
}
return value.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
Loading