Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0bae571
feat(sakana): surface pay-as-you-go credit balance
ss251 Jul 1, 2026
e90750b
fix(sakana): address review feedback on PAYG fetch
ss251 Jul 1, 2026
1b9dbe1
fix(sakana): address clawsweeper re-review findings
ss251 Jul 1, 2026
3fb2c32
Merge remote-tracking branch 'upstream/main' into feat/sakana-pay-as-…
ss251 Jul 2, 2026
9cc408d
Merge remote-tracking branch 'origin/main' into codex/sakana-payg
steipete Jul 4, 2026
1af0cea
fix: harden Sakana pay as you go refresh
steipete Jul 4, 2026
c1d7afe
fix: make Gemini helper deadlines reliable
steipete Jul 4, 2026
741f145
ci: allow macOS shards to finish
steipete Jul 4, 2026
153f6d5
Merge remote-tracking branch 'origin/main' into codex/sakana-payg
steipete Jul 4, 2026
38c7bab
Merge branch 'codex/gemini-process-timeout' into codex/sakana-payg
steipete Jul 4, 2026
7a67ad0
fix: keep Sakana quota refresh responsive
steipete Jul 4, 2026
a1571ae
fix: bound Sakana enrichment latency
steipete Jul 4, 2026
d9b7d88
fix(sakana): gate PAYG menu rows on optional-usage setting at render
ss251 Jul 4, 2026
023c69a
Merge remote-tracking branch 'origin/main' into codex/sakana-payg
steipete Jul 4, 2026
28297f0
Merge remote-tracking branch 'origin/main' into codex/sakana-payg
steipete Jul 4, 2026
56a01cc
Merge remote-tracking branch 'origin/main' into codex/sakana-payg
steipete Jul 4, 2026
8b4cf0d
fix: render Sakana balance in live menu
steipete Jul 4, 2026
bf0875d
Merge remote-tracking branch 'origin/main' into codex/sakana-payg
steipete Jul 4, 2026
89bcd85
Merge remote-tracking branch 'origin/main' into codex/sakana-payg
steipete Jul 4, 2026
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.38.1 — Unreleased

### Added
- Sakana AI: show best-effort pay-as-you-go credit balance and recent usage without delaying subscription quota refreshes. Thanks @ss251!
- Kimi: show monthly subscription usage alongside weekly and five-hour limits with a short total budget for the optional membership request. Thanks @zhiyue!
- Localization: add complete Russian coverage for the app and redesigned website. Thanks @Kirchberg!
- Localization: add Galician app translations and language selection. Thanks @B1NAR10!
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ extension UsageMenuCardView.Model.ProviderCostSection {
}

extension UsageMenuCardView.Model {
static func sakanaPayAsYouGoSection(_ usage: SakanaPayAsYouGoSnapshot?) -> ProviderCostSection? {
guard let usage else { return nil }
return ProviderCostSection(
title: L("Extra usage"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(usage.balanceDetail)",
percentLine: usage.periodUsageTotal.map { "\(L("Usage")): \(UsageFormatter.usdString($0))" })
}

static func isRequiredOpenCodeZenBalance(_ snapshot: UsageSnapshot?) -> Bool {
snapshot?.primary == nil &&
snapshot?.secondary == nil &&
Expand Down
6 changes: 5 additions & 1 deletion Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,11 @@ extension UsageMenuCardView.Model {
input.provider == .factory ||
(input.provider == .opencodego && !isRequiredOpenCodeZenBalance)) &&
!input.showOptionalCreditsAndExtraUsage
let providerCost: ProviderCostSection? = if hidesOptionalProviderCost ||
let providerCost: ProviderCostSection? = if input.provider == .sakana {
input.showOptionalCreditsAndExtraUsage
? Self.sakanaPayAsYouGoSection(input.snapshot?.sakanaPayAsYouGo)
: nil
} else if hidesOptionalProviderCost ||
(input.provider == .openai && openAIAPIUsage != nil)
{
nil
Expand Down
20 changes: 18 additions & 2 deletions Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,10 @@ struct MenuDescriptor {
resetOverride: opusResetOverride)
}

Self.appendProviderUsageSummaries(entries: &entries, snapshot: snap)
Self.appendProviderUsageSummaries(
entries: &entries,
snapshot: snap,
showOptionalUsage: settings.showOptionalCreditsAndExtraUsage)
if snap.rateLimitsUnavailable(for: provider) {
entries.append(.text(L("Limits not available"), .secondary))
}
Expand All @@ -264,7 +267,8 @@ struct MenuDescriptor {

private static func appendProviderUsageSummaries(
entries: inout [Entry],
snapshot: UsageSnapshot)
snapshot: UsageSnapshot,
showOptionalUsage: Bool)
{
if let cost = snapshot.providerCost {
if cost.currencyCode == "Quota" {
Expand Down Expand Up @@ -303,6 +307,18 @@ struct MenuDescriptor {
if let mimoUsage = snapshot.mimoUsage {
entries.append(.text("\(L("Balance")): \(mimoUsage.balanceDetail)", .primary))
}
// Sakana pay-as-you-go is optional data gated by "Show optional credits and extra usage".
// Gate the render on the setting too, not just the fetch: toggling the setting off only
// rebuilds the menu, it does not immediately refetch, so a previously-populated
// sakanaPayAsYouGo would otherwise linger in the cached snapshot until the next refresh.
if showOptionalUsage, let sakanaPayAsYouGo = snapshot.sakanaPayAsYouGo {
entries.append(.text("\(L("Balance")): \(sakanaPayAsYouGo.balanceDetail)", .primary))
Comment thread
steipete marked this conversation as resolved.
if let periodUsageTotal = sakanaPayAsYouGo.periodUsageTotal {
entries.append(.text(
"\(L("Usage")): \(UsageFormatter.usdString(periodUsageTotal))",
.secondary))
}
}
}

private static func appendOpenAIAPIUsageSummary(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ public struct ProviderDiagnosticUsageSummary: Codable, Sendable {
if snapshot.minimaxUsage != nil { providerSpecificData.append("minimaxUsage") }
if snapshot.deepseekUsage != nil { providerSpecificData.append("deepseekUsage") }
if snapshot.openRouterUsage != nil { providerSpecificData.append("openRouterUsage") }
if snapshot.sakanaPayAsYouGo != nil { providerSpecificData.append("sakanaPayAsYouGo") }
if snapshot.openAIAPIUsage != nil { providerSpecificData.append("openAIAPIUsage") }
if snapshot.claudeAdminAPIUsage != nil { providerSpecificData.append("claudeAdminAPIUsage") }
if snapshot.mistralUsage != nil { providerSpecificData.append("mistralUsage") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ struct SakanaWebFetchStrategy: ProviderFetchStrategy {
}
let usage = try await SakanaUsageFetcher.fetchUsage(
cookieHeader: cookieHeader,
timeout: context.webTimeout)
timeout: context.webTimeout,
includeOptionalUsage: context.includeOptionalUsage)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "web")
}

Expand Down
235 changes: 223 additions & 12 deletions Sources/CodexBarCore/Providers/Sakana/SakanaUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,22 @@ public struct SakanaUsageSnapshot: Sendable {
public let priceLabel: String?
public let fiveHour: QuotaWindow?
public let weekly: QuotaWindow?
public let payAsYouGo: SakanaPayAsYouGoSnapshot?
public let updatedAt: Date

public init(
planName: String?,
priceLabel: String?,
fiveHour: QuotaWindow?,
weekly: QuotaWindow?,
payAsYouGo: SakanaPayAsYouGoSnapshot? = nil,
updatedAt: Date = Date())
{
self.planName = planName
self.priceLabel = priceLabel
self.fiveHour = fiveHour
self.weekly = weekly
self.payAsYouGo = payAsYouGo
self.updatedAt = updatedAt
}

Expand Down Expand Up @@ -64,11 +67,36 @@ public struct SakanaUsageSnapshot: Sendable {
secondary: secondary,
tertiary: nil,
providerCost: nil,
sakanaPayAsYouGo: self.payAsYouGo,
updatedAt: self.updatedAt,
identity: identity)
}
}

/// Sakana "Pay as you go" tab data (prepaid credit balance + a rolling usage total for the
/// console's selected date range). Fetched best-effort alongside the subscription quota windows;
/// absence never fails the primary Sakana fetch.
public struct SakanaPayAsYouGoSnapshot: Codable, Equatable, Sendable {
public let creditBalance: Double
public let periodUsageTotal: Double?
/// Raw label from the console's date-range picker (e.g. "Jun 02, 2026 - Jul 01, 2026").
public let periodLabel: String?

public init(
creditBalance: Double,
periodUsageTotal: Double? = nil,
periodLabel: String? = nil)
{
self.creditBalance = creditBalance
self.periodUsageTotal = periodUsageTotal
self.periodLabel = periodLabel
}

public var balanceDetail: String {
UsageFormatter.usdString(self.creditBalance)
}
}

public enum SakanaUsageError: LocalizedError, Sendable, Equatable {
case missingCookie
case loginRequired
Expand All @@ -89,8 +117,27 @@ public enum SakanaUsageError: LocalizedError, Sendable, Equatable {
}
}

private final class SakanaPayAsYouGoResult: @unchecked Sendable {
private let lock = NSLock()
private var result: SakanaPayAsYouGoSnapshot?

func complete(with result: SakanaPayAsYouGoSnapshot?) {
self.lock.withLock {
self.result = result
}
}

func valueIfCompleted() -> SakanaPayAsYouGoSnapshot? {
self.lock.withLock { self.result }
}
}

public enum SakanaUsageFetcher {
private static let billingURL = URL(string: "https://console.sakana.ai/billing")!
private static let payAsYouGoURL = URL(string: "https://console.sakana.ai/billing?tab=payAsYouGo")!
/// Optional enrichment gets a small shared budget from the start of the primary request. A slow
/// primary therefore never waits, while a fast primary can briefly collect an in-flight result.
private static let payAsYouGoEnrichmentBudget: Duration = .milliseconds(200)
private static let defaultTransport: ProviderHTTPClient = {
let configuration = URLSessionConfiguration.ephemeral
configuration.httpCookieStorage = nil
Expand All @@ -103,7 +150,8 @@ public enum SakanaUsageFetcher {
cookieHeader: String,
session transportOverride: (any ProviderHTTPTransport)? = nil,
timeout: TimeInterval = 15,
now: Date = Date()) async throws -> SakanaUsageSnapshot
now: Date = Date(),
includeOptionalUsage: Bool = true) async throws -> SakanaUsageSnapshot
{
guard let cookieHeader = CookieHeaderNormalizer.normalize(cookieHeader) else {
throw SakanaUsageError.missingCookie
Expand All @@ -117,22 +165,185 @@ public enum SakanaUsageFetcher {
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")

let transport = transportOverride ?? self.defaultTransport
let response = try await transport.response(for: request)
if response.statusCode == 401 || response.statusCode == 403 || (300..<400).contains(response.statusCode) {
throw SakanaUsageError.loginRequired
let fetchStartedAt = ContinuousClock.now
let payAsYouGoResult = includeOptionalUsage ? SakanaPayAsYouGoResult() : nil
let payAsYouGoTask: Task<Void, Error>? = if let payAsYouGoResult {
Task {
let result = await self.boundedFetchPayAsYouGo(
cookieHeader: cookieHeader,
transport: transport,
timeout: timeout)
payAsYouGoResult.complete(with: result)
}
} else {
nil
}

return try await withTaskCancellationHandler {
do {
let response = try await transport.response(for: request)
if response.statusCode == 401 || response.statusCode == 403 ||
(300..<400).contains(response.statusCode)
{
throw SakanaUsageError.loginRequired
}
guard response.response.url?.scheme?.lowercased() == "https",
response.response.url?.host?.lowercased() == self.billingURL.host?.lowercased()
else {
throw SakanaUsageError.loginRequired
}
guard response.statusCode == 200 else {
throw SakanaUsageError.apiError(response.statusCode)
}
guard let html = String(data: response.data, encoding: .utf8), !html.isEmpty else {
throw SakanaUsageError.parseFailed("Billing page response was empty.")
}
let snapshot = try self.parseBillingHTML(html, now: now)
let payAsYouGo = await self.collectPayAsYouGo(
task: payAsYouGoTask,
result: payAsYouGoResult,
fetchStartedAt: fetchStartedAt)
try Task.checkCancellation()
return SakanaUsageSnapshot(
planName: snapshot.planName,
priceLabel: snapshot.priceLabel,
fiveHour: snapshot.fiveHour,
weekly: snapshot.weekly,
payAsYouGo: payAsYouGo,
updatedAt: snapshot.updatedAt)
} catch {
payAsYouGoTask?.cancel()
throw error
}
} onCancel: {
payAsYouGoTask?.cancel()
}
}

private static func collectPayAsYouGo(
task: Task<Void, Error>?,
result: SakanaPayAsYouGoResult?,
fetchStartedAt: ContinuousClock.Instant) async -> SakanaPayAsYouGoSnapshot?
{
guard let task, let result else { return nil }
let elapsed = fetchStartedAt.duration(to: .now)
let remainingBudget = elapsed < self.payAsYouGoEnrichmentBudget
? self.payAsYouGoEnrichmentBudget - elapsed
: .zero
if remainingBudget > .zero {
let join = BoundedTaskJoin(sourceTask: task)
_ = await join.value(joinGrace: remainingBudget)
}
task.cancel()
return result.valueIfCompleted()
}

/// Caps the lifetime of the optional Pay-as-you-go fetch. The primary fetch only consumes an
/// already-completed or shared-budget result and cancels this task otherwise.
private static let payAsYouGoJoinGrace: Duration = .seconds(5)

private static func boundedFetchPayAsYouGo(
cookieHeader: String,
transport: any ProviderHTTPTransport,
timeout: TimeInterval) async -> SakanaPayAsYouGoSnapshot?
{
await self.boundedFetch(timeout: self.payAsYouGoJoinGrace) {
await self.fetchPayAsYouGo(cookieHeader: cookieHeader, transport: transport, timeout: timeout)
}
}

static func _boundedFetchPayAsYouGoForTesting(
timeout: Duration,
operation: @escaping @Sendable () async -> SakanaPayAsYouGoSnapshot?) async -> SakanaPayAsYouGoSnapshot?
{
await self.boundedFetch(timeout: timeout, operation: operation)
}

private static func boundedFetch(
timeout: Duration,
operation: @escaping @Sendable () async -> SakanaPayAsYouGoSnapshot?) async -> SakanaPayAsYouGoSnapshot?
{
let sourceTask = Task<SakanaPayAsYouGoSnapshot?, Error> {
await operation()
}
guard response.response.url?.scheme?.lowercased() == "https",
response.response.url?.host?.lowercased() == self.billingURL.host?.lowercased()
let race = BoundedTaskJoin(sourceTask: sourceTask)
switch await race.value(joinGrace: timeout) {
case let .value(result):
return result
case .timedOut, .failure:
return nil
}
}

/// Best-effort fetch of the Pay-as-you-go tab. Never throws: subscription quota windows are
/// the primary, historically-supported contract of this fetcher, and an account without PAYG
/// credit (or a console change that breaks this parser) must not regress that core behavior.
private static func fetchPayAsYouGo(
cookieHeader: String,
transport: any ProviderHTTPTransport,
timeout: TimeInterval) async -> SakanaPayAsYouGoSnapshot?
{
var request = URLRequest(url: self.payAsYouGoURL)
request.httpMethod = "GET"
request.timeoutInterval = timeout
request.setValue("text/html,application/xhtml+xml", forHTTPHeaderField: "Accept")
request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")

guard let response = try? await transport.response(for: request),
response.statusCode == 200,
response.response.url?.scheme?.lowercased() == "https",
response.response.url?.host?.lowercased() == self.billingURL.host?.lowercased(),
let html = String(data: response.data, encoding: .utf8), !html.isEmpty
else {
throw SakanaUsageError.loginRequired
return nil
}
guard response.statusCode == 200 else {
throw SakanaUsageError.apiError(response.statusCode)
return self.parsePayAsYouGoHTML(html)
}

static func parsePayAsYouGoHTML(_ html: String) -> SakanaPayAsYouGoSnapshot? {
guard let balanceText = self.capture(
pattern: #"<h2[^>]*>\s*Credit balance\s*</h2>[\s\S]{0,900}?<p[^>]*tabular-nums[^"]*"[^>]*>"# +
#"\$?([0-9][0-9,]*(?:\.[0-9]+)?)</p>"#,
in: html),
let creditBalance = self.parseAmount(balanceText)
else {
return nil
}
guard let html = String(data: response.data, encoding: .utf8), !html.isEmpty else {
throw SakanaUsageError.parseFailed("Billing page response was empty.")

let usageTotalText = self.capture(
pattern: #"<h2[^>]*>\s*Usage\s*</h2>\s*<span[^>]*>\s*Total(?:<!--\s*-->)?:\s*"# +
#"(?:<!--\s*-->)?\$?([0-9][0-9,]*(?:\.[0-9]+)?)\s*</span>"#,
in: html)
let periodUsageTotal = usageTotalText.flatMap(self.parseAmount)

let periodLabel = self.capture(
pattern: #"aria-label="Usage date range"[^>]*>([\s\S]*?)</button>"#,
in: html).map(self.stripHTMLComments)

return SakanaPayAsYouGoSnapshot(
creditBalance: creditBalance,
periodUsageTotal: periodUsageTotal,
periodLabel: periodLabel)
}

private static func parseAmount(_ text: String) -> Double? {
guard let value = Double(text.replacingOccurrences(of: ",", with: "")), value.isFinite else {
return nil
}
return try self.parseBillingHTML(html, now: now)
return value
}

/// Strips React's `<!-- -->` hydration boundary comments (inserted between separately
/// interpolated JSX text nodes) and collapses the remaining whitespace.
private static func stripHTMLComments(_ text: String) -> String {
let stripped = text.replacingOccurrences(
of: #"<!--.*?-->"#,
with: "",
options: .regularExpression)
return stripped
.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
}

static func parseBillingHTML(
Expand Down
Loading