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 @@ -9,6 +9,7 @@
- Widgets: opt-in display of Claude model-scoped weekly quotas (for example Fable) projected from the shared usage snapshot, off by default (#2645). Thanks @alfredjbclaw!

### Fixed
- Menu: apply the cost summary display style to every provider's menu card, so Submenu only hides inline cost rows for z.ai and other providers (#2976). Thanks @ar0nbg!
- Codex: keep CLI-owned `auth.json` read-only during usage refresh, delegate stale native credentials to CLI recovery, and fail closed for stale external OAuth files (#2944). Thanks @Yuxin-Qiao!
- Usage & Spend: keep safely priced Codex totals visible after completed history scans when request-tier uncertainty leaves some days unpriced (#2948). Thanks @Atopoz for the report!
- Vertex AI: match Cloud Monitoring quota usage without a `limit_name` to its unambiguous same-metric, same-location limit, restoring quota percentages (#2958). Thanks @MachApple!
Expand Down
5 changes: 3 additions & 2 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extension UsageMenuCardView.Model {
switch menuCard.usageNotes(context: ProviderUsageNotesContext(
snapshot: input.snapshot,
isRefreshing: input.isRefreshing,
tokenCostInlineDashboardEnabled: input.tokenCostInlineDashboardEnabled,
costSummaryInlineEnabled: input.costSummaryInlineEnabled,

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 Gate OpenAI API usage notes with the inline setting

When OpenAI Admin API data is present and the Cost summary setting is Off or Submenu only, the new flag removes inlineUsageDashboard, but the OpenAI usageNotesResolver ignores costSummaryInlineEnabled and still returns notes containing Today/7d costs and token totals. Because the card renders usageNotes whenever the dashboard is absent, this change replaces the chart with a textual inline cost summary instead of hiding inline cost data; suppress the OpenAI notes when this flag is false as well.

Useful? React with 👍 / 👎.

showOptionalUsage: input.showOptionalCreditsAndExtraUsage))
{
case let .openAIAPI(usage):
Expand Down Expand Up @@ -94,6 +94,7 @@ extension UsageMenuCardView.Model {
private static func resolveInlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? {
let menuCard = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
if menuCard.usesProviderCostHistoryAsPrimaryDashboard,
input.costSummaryInlineEnabled,
let tokenSnapshot = primaryCostHistorySnapshot(input: input),
!tokenSnapshot.daily.isEmpty
{
Expand All @@ -104,7 +105,7 @@ extension UsageMenuCardView.Model {
preferredCurrencyCode: input.preferredCurrencyCode)
}
if menuCard.supportsInlineTokenCostDashboard,
input.tokenCostInlineDashboardEnabled,
input.costSummaryInlineEnabled,
let tokenSnapshot = input.tokenSnapshot,
!tokenSnapshot.daily.isEmpty || tokenSnapshot.meteredCostUSD != nil
{
Expand Down
19 changes: 19 additions & 0 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ extension UsageMenuCardView.Model.ProviderCostSection {
}

extension UsageMenuCardView.Model {
static func providerCostFollowsSummaryStyle(
cost: ProviderCostSnapshot?,
style: ProviderCostMenuCardStyle,
isClaudeAdminAPI: Bool) -> Bool
{
// Provider-specific by design: Claude Admin API spend is a summary, while Claude extra-usage balance is not.
switch style {
case .apiSpend, .payAsYouGoSpend:
true
case .claude:
isClaudeAdminAPI
case .clawRouter:
(cost?.limit ?? 0) <= 0
case .generic, .hidden, .extraUsageBalance, .zenBalance, .pointsBalance, .prepaidCredits,
.payAsYouGoBalance:
false
}
}

static func isRequiredOpenCodeZenBalance(_ snapshot: UsageSnapshot?) -> Bool {
snapshot?.primary == nil &&
snapshot?.secondary == nil &&
Expand Down
6 changes: 3 additions & 3 deletions Sources/CodexBar/MenuCardView+ModelInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension UsageMenuCardView.Model {
let tokenCostUsageEnabled: Bool
let tokenCostIsRefreshing: Bool
let codexLocalSessionCostLedgerEnabled: Bool
let tokenCostInlineDashboardEnabled: Bool
let costSummaryInlineEnabled: Bool
let tokenCostMenuSectionEnabled: Bool
let costComparisonPeriodsEnabled: Bool
let showOptionalCreditsAndExtraUsage: Bool
Expand Down Expand Up @@ -69,7 +69,7 @@ extension UsageMenuCardView.Model {
tokenCostUsageEnabled: Bool,
tokenCostIsRefreshing: Bool = false,
codexLocalSessionCostLedgerEnabled: Bool = false,
tokenCostInlineDashboardEnabled: Bool? = nil,
costSummaryInlineEnabled: Bool? = nil,
tokenCostMenuSectionEnabled: Bool? = nil,
costComparisonPeriodsEnabled: Bool = false,
showOptionalCreditsAndExtraUsage: Bool,
Expand Down Expand Up @@ -111,7 +111,7 @@ extension UsageMenuCardView.Model {
self.tokenCostUsageEnabled = tokenCostUsageEnabled
self.tokenCostIsRefreshing = tokenCostIsRefreshing
self.codexLocalSessionCostLedgerEnabled = codexLocalSessionCostLedgerEnabled
self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled ?? tokenCostUsageEnabled
self.costSummaryInlineEnabled = costSummaryInlineEnabled ?? tokenCostUsageEnabled
self.tokenCostMenuSectionEnabled = tokenCostMenuSectionEnabled ?? tokenCostUsageEnabled
self.costComparisonPeriodsEnabled = costComparisonPeriodsEnabled
self.showOptionalCreditsAndExtraUsage = showOptionalCreditsAndExtraUsage
Expand Down
15 changes: 13 additions & 2 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,13 @@ extension UsageMenuCardView.Model {
let providerCostStyle = input.snapshot.map {
presentation.cost(snapshot: $0).menuCardStyle
} ?? .generic
let providerCost: ProviderCostSection? = if !showsProviderCost {
let providerCostFollowsSummaryStyle = Self.providerCostFollowsSummaryStyle(
cost: input.snapshot?.providerCost,
style: providerCostStyle,
isClaudeAdminAPI: isClaudeAdminAPI)
let providerCost: ProviderCostSection? = if !showsProviderCost ||
(providerCostFollowsSummaryStyle && !input.costSummaryInlineEnabled)
{
nil
} else {
Self.providerCostSection(
Expand Down Expand Up @@ -1031,8 +1037,13 @@ extension UsageMenuCardView.Model {

private static func visibleProviderDetails(input: Input) -> [ProviderDetailSection] {
var details = input.snapshot?.details ?? []
let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.optionalDetails
if !input.costSummaryInlineEnabled, !policy.costSummaryTitles.isEmpty {
details.removeAll { section in
section.title.map(policy.costSummaryTitles.contains) == true
}
}
if !input.showOptionalCreditsAndExtraUsage {
let policy = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.optionalDetails
if policy.hidesAllWithoutOptionalUsage {
details = []
} else if !policy.hiddenTitlesWithoutOptionalUsage.isEmpty {
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,9 @@ struct ProvidersPane: View {
resetTimeDisplayStyle: self.settings.resetTimeDisplayStyle,
tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: provider),
codexLocalSessionCostLedgerEnabled: self.settings.codexLocalSessionCostLedgerEnabled,
tokenCostInlineDashboardEnabled: self.settings.costSummaryShowsInlineDashboard(for: provider),
// Display style only controls the main menu. Provider details always expose
// available cost data in their Usage section.
costSummaryInlineEnabled: true,
tokenCostMenuSectionEnabled: self.settings.isCostUsageEffectivelyEnabled(for: provider),
showOptionalCreditsAndExtraUsage: self.settings.showOptionalCreditsAndExtraUsage,
claudeDailyRoutinesUsageVisible: self.settings.claudeDailyRoutinesUsageVisible,
Expand Down
11 changes: 5 additions & 6 deletions Sources/CodexBar/SettingsStore+TokenCost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import CodexBarCore
import Foundation

extension SettingsStore {
func costSummaryShowsInlineDashboard(for provider: UsageProvider) -> Bool {
// Provider-specific by design: DeepSeek's API exposes a balance card but no token-cost submenu data.
if provider == .deepseek {
return self.costUsageEnabled
}
return self.isCostUsageEffectivelyEnabled(for: provider) &&
func costSummaryShowsInline(for provider: UsageProvider) -> Bool {
// Provider-specific by design: Codex's local ledger can enable its summary without the global scanner.
let isEnabled = self.costUsageEnabled ||
(provider == .codex && self.codexLocalSessionCostLedgerEnabled)
return isEnabled &&
self.costSummaryDisplayStyle.showsInlineSummary
}

Expand Down
10 changes: 5 additions & 5 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1483,13 +1483,13 @@ extension StatusItemController {
return self.makeUsageBreakdownSubmenu(width: width)
}
// Provider-specific by design: OpenAI and Mistral attach cost history to their provider usage row.
if provider == .openai {
if provider == .openai, self.settings.costSummaryShowsSubmenu(for: provider) {
return self.makeOpenAIAPIUsageSubmenu(provider: provider, width: width)
}
// Mistral's top usage pane has no rate-limit bars of its own, so its cost history always hangs
// off this row. Providers whose cards render an inline cost dashboard also gain cost history
// on the top card when the Cost Summary style permits it; Both still keeps the dedicated Cost row.
if provider == .mistral {
// Mistral's top usage pane has no rate-limit bars of its own, so its cost history hangs off this row
// when the Cost Summary style permits it. Other inline cost dashboards follow the same submenu policy;
// Both still keeps the dedicated Cost row.
if provider == .mistral, self.settings.costSummaryShowsSubmenu(for: provider) {
return self.makeCostHistorySubmenu(provider: provider, width: width)
}
if hasInlineCostDashboard, self.settings.costSummaryShowsSubmenu(for: provider) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/StatusItemController+MenuCardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ extension StatusItemController {
tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: target),
tokenCostIsRefreshing: self.store.tokenCostRefreshIsActive(for: target),
codexLocalSessionCostLedgerEnabled: self.settings.codexLocalSessionCostLedgerEnabled,
tokenCostInlineDashboardEnabled: self.settings.costSummaryShowsInlineDashboard(for: target),
costSummaryInlineEnabled: self.settings.costSummaryShowsInline(for: target),
// openai/mistral's cost history always surfaces via the inline dashboard or a
// dedicated top-pane submenu (see `makeUsageSubmenu`), so they skip the generic
// "Cost" row. This must stay an explicit provider check rather than reusing
Expand Down
9 changes: 5 additions & 4 deletions Sources/CodexBar/StatusItemController+OverviewSubmenus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ extension StatusItemController {
width: CGFloat) -> NSMenu?
{
if provider == .openai,
self.settings.costSummaryShowsSubmenu(for: provider),
let submenu = self.makeOpenAIAPIUsageSubmenu(provider: provider, width: width)
{
return submenu
}
// Mistral's top usage pane has no rate-limit bars of its own, so its Overview row always
// prioritizes cost history too. Other `tokenCostRequiresProviderSnapshot` providers (e.g.
// opencodego) show real rate-limit bars and should fall through to the settings-gated
// check below, same as Codex/Claude (see StatusItemController+Menu.swift's makeUsageSubmenu).
// Mistral's top usage pane has no rate-limit bars of its own, so its Overview row prioritizes
// cost history when the display style permits it. Other `tokenCostRequiresProviderSnapshot`
// providers (e.g. opencodego) show real rate-limit bars and fall through to the generic check.
if provider == .mistral,
self.settings.costSummaryShowsSubmenu(for: provider),
let submenu = self.makeCostHistorySubmenu(provider: provider, width: width)
{
return submenu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ public enum ClaudeProviderDescriptor {
costVisibilityResolver: { context in
context.showOptionalUsage || context.snapshot?.loginMethod(for: .claude) == "Admin API"
},
supportsInlineTokenCostDashboard: true)),
supportsInlineTokenCostDashboard: true),
optionalDetails: ProviderOptionalDetailsPresentation(
costSummaryTitles: ["Usage summary", "Cost items"])),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api, .web, .cli, .oauth],
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public enum DeepSeekProviderDescriptor {
return .localized(["Select a DeepSeek Chrome profile in Settings."])
}
}
guard context.tokenCostInlineDashboardEnabled, context.showOptionalUsage else {
guard context.costSummaryInlineEnabled, context.showOptionalUsage else {
return .unhandled
}
guard context.snapshot?.details.isEmpty == false else {
Expand All @@ -137,7 +137,9 @@ public enum DeepSeekProviderDescriptor {
showsPrimaryBalanceDescription: true,
hidesPrimaryResetWithoutDate: true,
movePrimaryDetailToStatus: { _ in true }),
menu: ProviderMenuDescriptorPresentation(primaryDescriptionIsDetail: { _ in true })),
menu: ProviderMenuDescriptorPresentation(primaryDescriptionIsDetail: { _ in true }),
optionalDetails: ProviderOptionalDetailsPresentation(
costSummaryTitles: ["Detailed usage"])),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api, .web],
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public enum GroqProviderDescriptor {
noDataMessage: { "Sign in at console.groq.com to show Groq spend and token usage." },
showsRequestHistory: false,
hintPlacement: .hidden),
presentation: ProviderUsagePresentation(
optionalDetails: ProviderOptionalDetailsPresentation(
costSummaryTitles: ["Usage summary"])),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .web, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: { context in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ public enum MiniMaxProviderDescriptor {
context.snapshot.tertiary))
},
optionalDetails: ProviderOptionalDetailsPresentation(
hiddenTitlesWithoutOptionalUsage: ["Billing history"])),
hiddenTitlesWithoutOptionalUsage: ["Billing history"],
costSummaryTitles: ["Billing history"])),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .web, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ public enum OpenAIAPIProviderDescriptor {
return projected
}
return snapshot == nil ? tokenSnapshot : nil
})),
}),
optionalDetails: ProviderOptionalDetailsPresentation(
costSummaryTitles: ["Usage summary"])),
fetchPlan: self.fetchPlan(),
cli: ProviderCLIConfig(
name: "openai",
Expand Down
11 changes: 7 additions & 4 deletions Sources/CodexBarCore/Providers/ProviderUsagePresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,16 @@ public struct ProviderCostPresentation: Sendable, Equatable {
public struct ProviderOptionalDetailsPresentation: Sendable, Equatable {
public let hidesAllWithoutOptionalUsage: Bool
public let hiddenTitlesWithoutOptionalUsage: Set<String>
public let costSummaryTitles: Set<String>

public init(
hidesAllWithoutOptionalUsage: Bool = false,
hiddenTitlesWithoutOptionalUsage: Set<String> = [])
hiddenTitlesWithoutOptionalUsage: Set<String> = [],
costSummaryTitles: Set<String> = [])
{
self.hidesAllWithoutOptionalUsage = hidesAllWithoutOptionalUsage
self.hiddenTitlesWithoutOptionalUsage = hiddenTitlesWithoutOptionalUsage
self.costSummaryTitles = costSummaryTitles
}
}

Expand Down Expand Up @@ -208,18 +211,18 @@ public enum ProviderWidgetFamily: Sendable {
public struct ProviderUsageNotesContext: Sendable {
public let snapshot: UsageSnapshot?
public let isRefreshing: Bool
public let tokenCostInlineDashboardEnabled: Bool
public let costSummaryInlineEnabled: Bool
public let showOptionalUsage: Bool

public init(
snapshot: UsageSnapshot?,
isRefreshing: Bool,
tokenCostInlineDashboardEnabled: Bool,
costSummaryInlineEnabled: Bool,
showOptionalUsage: Bool)
{
self.snapshot = snapshot
self.isRefreshing = isRefreshing
self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled
self.costSummaryInlineEnabled = costSummaryInlineEnabled
self.showOptionalUsage = showOptionalUsage
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ public enum XAIProviderDescriptor {
let showsFallback = snapshot.providerCost?.period != "Prepaid credits"
let style: ProviderCostMenuCardStyle = showsFallback ? .generic : .prepaidCredits
return ProviderCostPresentation(showsGenericFallback: showsFallback, menuCardStyle: style)
}),
},
optionalDetails: ProviderOptionalDetailsPresentation(
costSummaryTitles: ["Billing summary"])),
fetchPlan: self.fetchPlan(),
cli: ProviderCLIConfig(
name: "xai",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ public enum ZaiProviderDescriptor {
context.snapshot.primary,
context.snapshot.secondary))
},
primaryBindingQuotaLanes: [.secondary]),
primaryBindingQuotaLanes: [.secondary],
optionalDetails: ProviderOptionalDetailsPresentation(
costSummaryTitles: ["Hourly tokens", "Daily tokens"])),
fetchPlan: self.fetchPlan(),
cli: ProviderCLIConfig(
name: "zai",
Expand Down
1 change: 1 addition & 0 deletions Tests/CodexBarTests/AiAndProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ struct AiAndProviderTests {
usageBarsShowUsed: true,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
costSummaryInlineEnabled: true,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ struct ClaudeAdminAPIInlineDashboardModelTests {
usageBarsShowUsed: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
costSummaryInlineEnabled: true,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))
Expand Down
1 change: 1 addition & 0 deletions Tests/CodexBarTests/ClaudeMenuCardCostTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ struct ClaudeMenuCardCostTests {
usageBarsShowUsed: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
costSummaryInlineEnabled: true,
showOptionalCreditsAndExtraUsage: false,
hidePersonalInfo: false,
now: now))
Expand Down
Loading