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
Original file line number Diff line number Diff line change
Expand Up @@ -84,39 +84,86 @@ struct OpenCodeGoLocalUsageFetchStrategy: ProviderFetchStrategy {
let id: String = "opencodego.local"
let kind: ProviderFetchKind = .localProbe

typealias LocalSnapshotLoader = @Sendable (ProviderFetchContext) throws -> OpenCodeGoUsageSnapshot
typealias WebUsageOverlayFetcher = @Sendable (ProviderFetchContext, String) async throws
-> OpenCodeGoUsageSnapshot?

private let localSnapshotLoader: LocalSnapshotLoader
private let webUsageOverlayFetcher: WebUsageOverlayFetcher

private struct OverlayCookie {
let header: String
let cachedEntry: CookieHeaderCache.Entry?
}

init(
localSnapshotLoader: @escaping LocalSnapshotLoader = { context in
try OpenCodeGoLocalUsageReader().fetch(historyDays: context.costUsageHistoryDays)
},
webUsageOverlayFetcher: @escaping WebUsageOverlayFetcher = Self.liveWebUsageOverlay)
{
self.localSnapshotLoader = localSnapshotLoader
self.webUsageOverlayFetcher = webUsageOverlayFetcher
}

func isAvailable(_: ProviderFetchContext) async -> Bool {
true
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let snapshot = try await self.snapshot(context: context)
let (snapshot, overlaid) = try await self.snapshot(context: context)
return self.makeResult(
usage: snapshot.toUsageSnapshot(),
sourceLabel: "local")
sourceLabel: overlaid ? "local+web" : "local")
}

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

private func snapshot(context: ProviderFetchContext) async throws -> OpenCodeGoUsageSnapshot {
let snapshot = try OpenCodeGoLocalUsageReader().fetch(historyDays: context.costUsageHistoryDays)
guard context.includeOptionalUsage,
context.settings?.opencodego?.cookieSource != .off
private func snapshot(context: ProviderFetchContext) async throws -> (OpenCodeGoUsageSnapshot, Bool) {
let snapshot = try self.localSnapshotLoader(context)
guard context.settings?.opencodego?.cookieSource != .off,
let cookie = Self.cachedOrManualCookie(context: context)
else {
return snapshot
return (snapshot, false)
}

guard let cookieHeader = Self.cachedOrManualCookieHeader(context: context) else {
return snapshot
// The server knows the real billing-cycle anchors; the local monthly window is only an
// estimate anchored at the earliest local row. Overlay the authoritative web windows
// whenever a session cookie is already available (never a fresh browser import here).
// URLSession reports task cancellation as URLError.cancelled, so normalize it here to
// keep a cancelled refresh from completing with a successful local-only result.
let webSnapshot: OpenCodeGoUsageSnapshot?
do {
webSnapshot = try await self.webUsageOverlayFetcher(context, cookie.header)
} catch OpenCodeGoUsageError.invalidCredentials {
#if os(macOS)
if let cached = cookie.cachedEntry {
_ = CookieHeaderCache.clearIfCurrent(provider: .opencodego, expected: cached)
}
#endif
return (snapshot, false)
} catch is CancellationError {
throw CancellationError()
} catch let error as URLError where error.code == .cancelled {
throw CancellationError()
} catch {
return (snapshot, false)
}
if let webSnapshot {
return (snapshot.applyingWebUsage(webSnapshot), true)
}

guard context.includeOptionalUsage else {
return (snapshot, false)
}
let workspaceOverride = context.settings?.opencodego?.workspaceID
?? context.env["CODEXBAR_OPENCODEGO_WORKSPACE_ID"]
let zenBalanceTask = Task<Double?, Error> {
do {
return try await OpenCodeGoUsageFetcher.fetchOptionalZenBalance(
cookieHeader: cookieHeader,
cookieHeader: cookie.header,
timeout: context.webTimeout,
workspaceIDOverride: workspaceOverride)
} catch is CancellationError {
Expand All @@ -126,17 +173,49 @@ struct OpenCodeGoLocalUsageFetchStrategy: ProviderFetchStrategy {
}
}
let zenBalance = try await OpenCodeGoUsageFetcher.completedOptionalZenBalance(from: zenBalanceTask)
return snapshot.withZenBalanceUSD(zenBalance)
return (snapshot.withZenBalanceUSD(zenBalance), false)
}

static func liveWebUsageOverlay(
context: ProviderFetchContext,
cookieHeader: String) async throws -> OpenCodeGoUsageSnapshot?
{
let workspaceOverride = context.settings?.opencodego?.workspaceID
?? context.env["CODEXBAR_OPENCODEGO_WORKSPACE_ID"]
do {
return try await OpenCodeGoUsageFetcher.fetchUsage(
cookieHeader: cookieHeader,
timeout: context.webTimeout,
workspaceIDOverride: workspaceOverride,
includeZenBalance: context.includeOptionalUsage)
Comment on lines +186 to +190

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 Bound best-effort web overlay latency

When opencode.ai is slow or unreachable but a cached cookie exists, this best-effort overlay uses the normal provider web timeout (60s in the app context) before returning nil, and snapshot(context:) awaits it before yielding the already-available local DB snapshot. That turns unscoped Auto/local refreshes into minute-long stalls whenever the web path is degraded; bound or race the overlay separately so local data can still be published promptly.

Useful? React with 👍 / 👎.

} catch OpenCodeGoUsageError.invalidCredentials {
throw OpenCodeGoUsageError.invalidCredentials
} catch is CancellationError {
throw CancellationError()
} catch let error as URLError where error.code == .cancelled {
throw CancellationError()
} catch {
if Task.isCancelled {
throw CancellationError()
}
return nil
}
}

private static func cachedOrManualCookieHeader(context: ProviderFetchContext) -> String? {
private static func cachedOrManualCookie(context: ProviderFetchContext) -> OverlayCookie? {
if let settings = context.settings?.opencodego, settings.cookieSource == .manual {
return OpenCodeWebCookieSupport.requestCookieHeader(from: settings.manualCookieHeader)
guard let header = OpenCodeWebCookieSupport.requestCookieHeader(from: settings.manualCookieHeader) else {
return nil
}
return OverlayCookie(header: header, cachedEntry: nil)
}

#if os(macOS)
guard let cached = CookieHeaderCache.load(provider: .opencodego) else { return nil }
return OpenCodeWebCookieSupport.requestCookieHeader(from: cached.cookieHeader)
let observation = CookieHeaderCache.observeForConditionalMutation(provider: .opencodego)
guard let cached = observation.entry,
let header = OpenCodeWebCookieSupport.requestCookieHeader(from: cached.cookieHeader)
else { return nil }
return OverlayCookie(header: header, cachedEntry: cached)
#else
return nil
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,31 @@ public struct OpenCodeGoUsageSnapshot: Sendable {
return copy
}

/// Replaces the locally estimated usage windows with the server-reported (authoritative)
/// ones while keeping local-only data such as the daily cost history. The local monthly
/// window is anchored at the earliest local row, which can drift far from the real billing
/// cycle (wrong percentage and reset countdown), so whenever web usage is available its
/// percentages and reset countdowns win. A balance-only web response carries no windows
/// and must not clobber the local estimate.
public func applyingWebUsage(_ web: OpenCodeGoUsageSnapshot) -> OpenCodeGoUsageSnapshot {
guard !web.isBalanceOnly else {
return self.withZenBalanceUSD(web.zenBalanceUSD ?? self.zenBalanceUSD)
}
return OpenCodeGoUsageSnapshot(
hasWeeklyUsage: web.hasWeeklyUsage,
hasMonthlyUsage: web.hasMonthlyUsage,
rollingUsagePercent: web.rollingUsagePercent,
weeklyUsagePercent: web.weeklyUsagePercent,
monthlyUsagePercent: web.monthlyUsagePercent,
rollingResetInSec: web.rollingResetInSec,
weeklyResetInSec: web.weeklyResetInSec,
monthlyResetInSec: web.monthlyResetInSec,
zenBalanceUSD: web.zenBalanceUSD ?? self.zenBalanceUSD,
renewsAt: web.renewsAt ?? self.renewsAt,
daily: self.daily,
updatedAt: self.updatedAt)
}

public func withDaily(_ daily: [CostUsageDailyReport.Entry]) -> OpenCodeGoUsageSnapshot {
var copy = self
copy.daily = daily
Expand Down
Loading