Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -2,6 +2,7 @@

## 0.54.1 — Unreleased

- Fireworks: auto-discover account slugs from API keys and report invalid or ambiguous accounts instead of silently showing no spend (#3068).
- Fixed `codexbar cost` SIGSEGV on Linux: `Bundle.allBundles` crashes under swift-corelibs-foundation, so test detection now checks the main executable path instead (#3058, #3059). Thanks @Lucenx9!
- Codex: added a personal-access-token usage source — `personal_access_token` in `auth.json` gets its own PAT strategy (whoami then `/wham/usage`), Auto prefers a usable PAT and falls back to OAuth/CLI, and ambient-home PATs are found when a managed profile would hide them (#3060). Thanks @oakimov!
- Count every enabled provider in Overview spend instead of only the six displayed cards, and bucket Overview spend with the configured calendar so boundary days match the dashboard (#3063, #3064). Thanks @Chipagosfinest!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ struct FireworksProviderImplementation: ProviderImplementation {

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if FireworksSettingsReader.apiKey(environment: context.environment) != nil,
FireworksSettingsReader.accountSlug(environment: context.environment) != nil
{
if FireworksSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
return context.settings.hasFireworksCredentials
Expand All @@ -50,21 +48,20 @@ struct FireworksProviderImplementation: ProviderImplementation {
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.",
subtitle: "Optional when the API key can access one account; CodexBar discovers it automatically. "
+ "For multiple accounts, find the slug in the app.fireworks.ai home account switcher or run "
+ "firectl whoami.",
kind: .plain,
placeholder: "x0mh0x",
binding: context.stringBinding(\.fireworksAccountSlug),
actions: [
ProviderSettingsActionDescriptor(
id: "fireworks-open-billing",
title: "Open Fireworks billing",
title: "Open Fireworks",
style: .link,
isVisible: nil,
perform: {
NSWorkspace.shared.open(
FireworksURLs.billing(
accountSlug: context.settings.fireworksAccountSlug))
NSWorkspace.shared.open(FireworksURLs.home)
}),
],
isVisible: nil,
Expand All @@ -74,11 +71,5 @@ struct FireworksProviderImplementation: ProviderImplementation {
}

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")!
}
static let home = URL(string: "https://app.fireworks.ai")!
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ extension SettingsStore {

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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.
/// Account slug that owns `apiKey`. When omitted, CodexBar discovers it from the
/// accounts visible to the Fireworks API key.
public var accountSlug: String? {
get { self.extensionValue(forKey: "accountSlug") }
set { self.setExtensionValue(newValue, forKey: "accountSlug") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,7 @@ public enum FireworksProviderDescriptor {
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)."
})
resolve: FireworksSettingsReader.apiKey)

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
Expand Down Expand Up @@ -84,24 +67,46 @@ struct FireworksAPIFetchStrategy: ProviderFetchStrategy {

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,
accountSlug: FireworksSettingsReader.accountSlug(environment: context.env),
session: self.transport)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api")
var diagnostic: String?
if usage.accountSlugWasDiscovered {
do {
try Self.persistAccountSlug(usage.accountSlug)
} catch {
diagnostic = "Auto-discovered Fireworks account '\(usage.accountSlug)' but could not save it: "
+ error.localizedDescription
}
}
let sourceLabel = usage.accountSlugWasDiscovered
? "api · \(usage.accountSlug) (auto-discovered)"
: "api · \(usage.accountSlug)"
return self.makeResult(
usage: usage.toUsageSnapshot(),
sourceLabel: sourceLabel,
diagnostic: diagnostic)
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}

private static func persistAccountSlug(_ accountSlug: String) throws {
let store = CodexBarConfigStore()
var config = try store.load() ?? .makeDefault()
var providerConfig = config.providerConfig(for: UsageProvider.fireworks.instanceID)
?? ProviderConfig(id: UsageProvider.fireworks.instanceID)
Comment on lines +102 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist discovered slugs through the live settings owner

When discovery completes while SettingsStore.schedulePersistConfig() still has a debounced write pending—for example immediately after the user enters the API key—this independent read-modify-write starts from the older on-disk configuration. Its save can make the file watcher reload that stale configuration and discard the newly entered key or another concurrent setting; if the pending settings write wins instead, it drops the discovered slug and forces discovery again. Route this mutation through the live SettingsStore/fetch-context update mechanism, with the existing revision guard, rather than writing a separate disk snapshot.

Useful? React with 👍 / 👎.

guard providerConfig.sanitizedAccountSlug != accountSlug else { return }
providerConfig.accountSlug = accountSlug
config.setProviderConfig(providerConfig)
try store.save(config)
}
}
Loading