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 @@ -3,6 +3,7 @@
## 0.22 — Unreleased

### Providers & Usage
- Synthetic: parse live five-hour, weekly, and search quota payloads, including continuous reset/regeneration details (#732). Thanks @baanish!
- Gemini: discover OAuth config in fnm/Homebrew/bundled CLI layouts so expired-token refresh keeps working (#723). Thanks @Leechael!
- Copilot: open the complete device-login verification URL when available so the browser flow carries the user code (#739). Thanks @skhe!
- Alibaba: update the China mainland Coding Plan endpoint and browser-cookie domain while keeping older domains as fallbacks (#712). Thanks @hezhongtang!
Expand Down
88 changes: 87 additions & 1 deletion Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,18 @@ extension UsageMenuCardView.Model {
}
}
}
if input.provider == .synthetic,
let regen = Self.syntheticRollingRegenDetail(
window: primary,
now: input.now,
showUsed: input.usageBarsShowUsed)
{
primaryResetText = regen.resetText
primaryDetailLeft = regen.pace.leftLabel
primaryDetailRight = regen.pace.rightLabel
primaryPacePercent = regen.pace.pacePercent
primaryPaceOnTop = regen.pace.paceOnTop
}
return Metric(
id: "primary",
title: input.metadata.sessionLabel,
Expand All @@ -1105,7 +1117,7 @@ extension UsageMenuCardView.Model {
percentStyle: PercentStyle,
zaiTimeDetail: String?) -> Metric
{
let paceDetail = Self.weeklyPaceDetail(
var paceDetail = Self.weeklyPaceDetail(
window: weekly,
now: input.now,
pace: input.weeklyPace,
Expand Down Expand Up @@ -1141,6 +1153,16 @@ extension UsageMenuCardView.Model {
{
weeklyResetText = detail
}
if input.provider == .synthetic,
let regen = Self.syntheticRegenDetail(
weekly: weekly,
cost: input.snapshot?.providerCost,
now: input.now,
showUsed: input.usageBarsShowUsed)
{
weeklyResetText = regen.resetText
paceDetail = regen.pace
}
return Metric(
id: "secondary",
title: input.metadata.weeklyLabel,
Expand Down Expand Up @@ -1314,6 +1336,69 @@ extension UsageMenuCardView.Model {
paceOnTop: paceOnTop)
}

private static func syntheticRegenDetail(
weekly: RateWindow,
cost: ProviderCostSnapshot?,
now: Date,
showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
{
guard let cost,
cost.limit > 0,
let nextRegenAmount = cost.nextRegenAmount,
nextRegenAmount > 0,
let resetsAt = weekly.resetsAt
else { return nil }

let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
let resetText = "Regenerates \(countdown)"

let nextRegenPercent = (nextRegenAmount / cost.limit) * 100
let afterNextRegenRemaining = min(100, weekly.remainingPercent + nextRegenPercent)
let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
let suffix = showUsed ? "used after next regen" : "after next regen"
let ticksToFull = max(0, cost.used) / nextRegenAmount
let left = String(format: "%.0f%% %@", afterNextRegen, suffix)
let right = if ticksToFull <= 0.1 {
"Near full"
} else if ticksToFull < 1.5 {
"Full in ~1 regen"
} else {
String(format: "Full in ~%.0f regens", ceil(ticksToFull))
}
return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true))
}

private static func syntheticRollingRegenDetail(
window: RateWindow,
now: Date,
showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
{
guard let resetsAt = window.resetsAt,
let nextRegenPercent = window.nextRegenPercent,
nextRegenPercent > 0
else { return nil }

let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
let resetText = "Regenerates \(countdown)"

let afterNextRegenRemaining = min(100, window.remainingPercent + nextRegenPercent)
let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
let suffix = showUsed ? "used after next regen" : "after next regen"
let left = String(format: "%.0f%% %@", afterNextRegen, suffix)

let missingPercent = max(0, window.usedPercent)
let ticksToFull = missingPercent / nextRegenPercent
let right = if ticksToFull <= 0.1 {
"Near full"
} else if ticksToFull < 1.5 {
"Full in ~1 regen"
} else {
String(format: "Full in ~%.0f regens", ceil(ticksToFull))
}

return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true))
}

private static func creditsLine(
metadata: ProviderMetadata,
credits: CreditsSnapshot?,
Expand Down Expand Up @@ -1378,6 +1463,7 @@ extension UsageMenuCardView.Model {
{
guard let cost else { return nil }
guard cost.limit > 0 else { return nil }
guard provider != .synthetic else { return nil }

let used: String
let limit: String
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBarCore/ProviderCostSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public struct ProviderCostSnapshot: Equatable, Codable, Sendable {
public let period: String?
/// Optional renewal/reset timestamp for the period.
public let resetsAt: Date?
/// Optional amount restored on the next regeneration tick for providers with rolling credit recovery.
public let nextRegenAmount: Double?
public let updatedAt: Date

public init(
Expand All @@ -17,13 +19,15 @@ public struct ProviderCostSnapshot: Equatable, Codable, Sendable {
currencyCode: String,
period: String? = nil,
resetsAt: Date? = nil,
nextRegenAmount: Double? = nil,
updatedAt: Date)
{
self.used = used
self.limit = limit
self.currencyCode = currencyCode
self.period = period
self.resetsAt = resetsAt
self.nextRegenAmount = nextRegenAmount
self.updatedAt = updatedAt
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ public enum SyntheticProviderDescriptor {
metadata: ProviderMetadata(
id: .synthetic,
displayName: "Synthetic",
sessionLabel: "Quota",
weeklyLabel: "Usage",
opusLabel: nil,
supportsOpus: false,
sessionLabel: "Five-hour quota",
weeklyLabel: "Weekly tokens",
opusLabel: "Search hourly",
supportsOpus: true,
supportsCredits: false,
creditsHint: "",
creditsHint: "Weekly token quota regenerates continuously.",
toggleTitle: "Show Synthetic usage",
cliName: "synthetic",
defaultEnabled: false,
Expand Down
Loading