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
4 changes: 2 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.5</string>
<string>0.2.6</string>
<key>CFBundleVersion</key>
<string>6</string>
<string>7</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
Expand Down
175 changes: 125 additions & 50 deletions Sources/CodexLimits/AnalyticsWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ enum AnalyticsTimeRange: String, CaseIterable, Codable, Identifiable, Sendable {

func interval(
within bounds: DateInterval,
endingAt proposedEnd: Date
now: Date
) -> DateInterval {
let duration: TimeInterval
switch self {
Expand All @@ -65,63 +65,138 @@ enum AnalyticsTimeRange: String, CaseIterable, Codable, Identifiable, Sendable {
case .twelveWeeks:
duration = 84 * 86_400
}
let end = min(max(proposedEnd, bounds.start), bounds.end)
let end = max(now, bounds.start)
return DateInterval(
start: max(bounds.start, end.addingTimeInterval(-duration)),
start: end.addingTimeInterval(-duration),
end: end
)
}
}

struct AccountTokenActivityRange: Equatable, Sendable {
let days: [TokenDay]
let completeDayCount: Int
let completeTokens: Int64?

init(days: [TokenDay], interval: DateInterval) {
let day: TimeInterval = 86_400
self.days = days
.filter {
$0.date >= interval.start
&& $0.date.addingTimeInterval(day) <= interval.end
}
.sorted { $0.date < $1.date }

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)
?? calendar.timeZone
let startOfDay = calendar.startOfDay(for: interval.start)
var expected = startOfDay < interval.start
? startOfDay.addingTimeInterval(day)
: startOfDay
var expectedDates: [Date] = []
while expected.addingTimeInterval(day) <= interval.end {
expectedDates.append(expected)
expected = expected.addingTimeInterval(day)
}
completeDayCount = expectedDates.count
func accountTokenInterval(
at date: Date,
in intervals: [AccountTokenActivityInterval],
within range: DateInterval
) -> AccountTokenActivityInterval? {
intervals.first {
$0.start >= range.start
&& $0.end <= range.end
&& $0.start <= date
&& date <= $0.end
}
}

func steppedAccountTokenInterval(
in intervals: [AccountTokenActivityInterval],
from selected: AccountTokenActivityInterval?,
by offset: Int
) -> AccountTokenActivityInterval? {
let ordered = intervals.sorted {
$0.start == $1.start ? $0.end < $1.end : $0.start < $1.start
}
guard !ordered.isEmpty else { return nil }
guard let selected,
let index = ordered.firstIndex(of: selected) else {
return offset < 0 ? ordered.last : ordered.first
}
return ordered[min(max(index + offset, 0), ordered.count - 1)]
}

func retainedAccountTokenInterval(
_ selected: AccountTokenActivityInterval?,
in intervals: [AccountTokenActivityInterval],
range: DateInterval
) -> AccountTokenActivityInterval? {
guard let selected,
selected.start >= range.start,
selected.end <= range.end else { return nil }
return intervals.first { $0 == selected }
}

struct AccountTokenActivityDisplayInterval: Equatable, Hashable, Identifiable,
Sendable {
let sourceIntervals: [AccountTokenActivityInterval]

init(sourceIntervals: [AccountTokenActivityInterval]) {
precondition(!sourceIntervals.isEmpty)
self.sourceIntervals = sourceIntervals
}

guard !expectedDates.isEmpty else {
completeTokens = nil
return
var id: Self { self }
var start: Date { sourceIntervals[0].start }
var end: Date { sourceIntervals[sourceIntervals.count - 1].end }
var tokenDelta: Int64 {
sourceIntervals.reduce(0) { $0 + $1.tokenDelta }
}
var method: AccountTokenActivityMethod { sourceIntervals[0].method }
var isAggregated: Bool { sourceIntervals.count > 1 }
}

func displayedAccountTokenIntervals(
_ intervals: [AccountTokenActivityInterval],
breaks: [AccountTokenActivityBreak],
maximumMarks: Int
) -> [AccountTokenActivityDisplayInterval] {
let ordered = intervals.sorted {
$0.start == $1.start ? $0.end < $1.end : $0.start < $1.start
}
guard !ordered.isEmpty else { return [] }
let groupLimit = max(
Int(ceil(Double(ordered.count) / Double(max(maximumMarks, 1)))),
1
)
var result: [AccountTokenActivityDisplayInterval] = []
var group = [ordered[0]]
for interval in ordered.dropFirst() {
let previous = group[group.count - 1]
let hasBreak = breaks.contains {
$0.timestamp == previous.end
&& $0.timestamp == interval.start
}
var total: Int64 = 0
for date in expectedDates {
guard let bucket = self.days.first(where: { $0.date == date }),
bucket.completeness == .complete,
bucket.tokens >= 0 else {
completeTokens = nil
return
}
let result = total.addingReportingOverflow(bucket.tokens)
guard !result.overflow else {
completeTokens = nil
return
}
total = result.partialValue
let compatible = previous.end == interval.start
&& previous.method == interval.method
&& previous.accountPartitionID == interval.accountPartitionID
&& previous.limitID == interval.limitID
&& previous.allowanceReset == interval.allowanceReset
&& (previous.tokenDelta == 0) == (interval.tokenDelta == 0)
&& !hasBreak
if compatible, group.count < groupLimit {
group.append(interval)
} else {
result.append(AccountTokenActivityDisplayInterval(
sourceIntervals: group
))
group = [interval]
}
completeTokens = total
}
result.append(AccountTokenActivityDisplayInterval(sourceIntervals: group))
return result
}

func accountTokenDisplayInterval(
at date: Date,
in intervals: [AccountTokenActivityDisplayInterval],
within range: DateInterval
) -> AccountTokenActivityDisplayInterval? {
intervals.first {
$0.start >= range.start
&& $0.end <= range.end
&& $0.start <= date
&& date <= $0.end
}
}

func steppedAccountTokenDisplayInterval(
in intervals: [AccountTokenActivityDisplayInterval],
from selected: AccountTokenActivityDisplayInterval?,
by offset: Int
) -> AccountTokenActivityDisplayInterval? {
guard !intervals.isEmpty else { return nil }
guard let selected,
let index = intervals.firstIndex(of: selected) else {
return offset < 0 ? intervals.last : intervals.first
}
return intervals[min(max(index + offset, 0), intervals.count - 1)]
}

struct WorkspaceFilters: Codable, Equatable, Sendable {
Expand Down Expand Up @@ -312,7 +387,7 @@ final class AnalyticsWorkspaceStore: ObservableObject {

func effectiveRange(
within bounds: DateInterval,
endingAt latestObserved: Date
now: Date
) -> DateInterval {
if state.timeRange == .selected,
let visibleRange = state.visibleRange,
Expand All @@ -321,7 +396,7 @@ final class AnalyticsWorkspaceStore: ObservableObject {
}
return state.timeRange.interval(
within: bounds,
endingAt: latestObserved
now: now
)
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexLimits/CodexAssistedInsights.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ struct CodexMetadataAnalysisPayload: Codable, Equatable, Sendable {
) -> CodexMetadataAnalysisPayload {
let deterministicInput = DeterministicInsightInput(
reader: reader,
exploration: exploration
exploration: exploration,
now: now
)
let selectedUsage = deterministicInput.usagePerToken
let range = deterministicInput.selectedRange ?? reader.interval.map {
Expand Down
15 changes: 9 additions & 6 deletions Sources/CodexLimits/DeterministicInsights.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ struct DeterministicInsightInput: Equatable, Sendable {

init(
reader: UsageReaderSnapshot,
exploration: AnalyticsExplorationState
exploration: AnalyticsExplorationState,
now: Date = Date()
) {
sourceState = reader.sourceState
freshness = reader.freshness
Expand All @@ -133,7 +134,7 @@ struct DeterministicInsightInput: Equatable, Sendable {
selectedRange = Self.effectiveRange(
usagePerToken: selectedUsage,
observedInterval: reader.interval,
fetchedAt: reader.fetchedAt,
now: now,
exploration: exploration
)
filters = exploration.filters
Expand Down Expand Up @@ -166,7 +167,7 @@ struct DeterministicInsightInput: Equatable, Sendable {
static func effectiveRange(
usagePerToken: UsagePerTokenSnapshot,
observedInterval: UsageObservedInterval?,
fetchedAt: Date?,
now: Date,
exploration: AnalyticsExplorationState
) -> DateInterval? {
let evidence = usagePerToken.history
Expand All @@ -183,7 +184,7 @@ struct DeterministicInsightInput: Equatable, Sendable {
? exploration.visibleRange
: nil
}
let bounds = DateInterval(start: start, end: end)
let bounds = DateInterval(start: start, end: max(end, now))
if exploration.timeRange == .currentWindow {
return usagePerToken.current?.interval ?? observedRange
}
Expand All @@ -195,7 +196,7 @@ struct DeterministicInsightInput: Equatable, Sendable {
}
return exploration.timeRange.interval(
within: bounds,
endingAt: fetchedAt ?? evidenceEnd ?? end
now: now
)
}
}
Expand All @@ -207,12 +208,14 @@ enum DeterministicInsightEngine {
static func evaluate(
reader: UsageReaderSnapshot,
exploration: AnalyticsExplorationState,
now: Date = Date(),
dispositions: [String: InsightDisposition]
) -> DeterministicInsightsSnapshot {
evaluate(
DeterministicInsightInput(
reader: reader,
exploration: exploration
exploration: exploration,
now: now
),
dispositions: dispositions
)
Expand Down
41 changes: 30 additions & 11 deletions Sources/CodexLimits/ForecastEngine.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import Foundation

struct RecentAccountMovement: Equatable, Sendable {
let latest: UsageSample
let percentPerDay: Double
}

enum ForecastEngine {
static func evaluate(
window: UsageWindow,
samples: [UsageSample],
tokenHistory: [TokenDay],
safetyBuffer: Double,
now: Date,
previousStatus: PaceStatus?
previousStatus: PaceStatus?,
recentMovement: RecentAccountMovement? = nil
) -> Forecast {
let daysLeft = max(window.resetsAt.timeIntervalSince(now) / 86_400, 0)
let latestDate = recentMovement?.latest.observedAt ?? now
let latestRemaining = recentMovement?.latest.remainingPercent
?? window.remainingPercent
let daysLeft = max(
window.resetsAt.timeIntervalSince(latestDate) / 86_400,
0
)
let currentSamples = samples
.filter { $0.resetsAt == window.resetsAt && $0.observedAt <= now }
.sorted { $0.observedAt < $1.observedAt }
Expand All @@ -27,9 +39,10 @@ enum ForecastEngine {
recentRate = windowRate
}

let currentRate = currentIntervalSamples.count > 1
? 0.7 * recentRate + 0.3 * windowRate
: windowRate
let currentRate = recentMovement?.percentPerDay
?? (currentIntervalSamples.count > 1
? 0.7 * recentRate + 0.3 * windowRate
: windowRate)
let historicalRates = comparableHistoricalRates(
samples: samples,
excluding: window.resetsAt
Expand All @@ -49,11 +62,17 @@ enum ForecastEngine {
historicalRate = median(Array(historicalRates.prefix(4)))
historicalReferenceSource = .accountHistory
}
let expectedRate = 0.75 * currentRate + 0.25 * historicalRate
let safetyRate = max(currentRate, historicalRate) * 1.2
let expected = max(window.remainingPercent - expectedRate * daysLeft, 0)
let safety = max(window.remainingPercent - safetyRate * daysLeft, 0)
let historical = max(window.remainingPercent - historicalRate * daysLeft, 0)
let expectedRate = recentMovement == nil
? 0.75 * currentRate + 0.25 * historicalRate
: currentRate
let safetyRate = (
recentMovement == nil
? max(currentRate, historicalRate)
: currentRate
) * 1.2
let expected = max(latestRemaining - expectedRate * daysLeft, 0)
let safety = max(latestRemaining - safetyRate * daysLeft, 0)
let historical = max(latestRemaining - historicalRate * daysLeft, 0)
let historicalReference = historicalReferenceSource.map {
UsageForecastReference(
source: $0,
Expand All @@ -62,7 +81,7 @@ enum ForecastEngine {
)
}
let allowanceRate = daysLeft > 0
? max(window.remainingPercent - safetyBuffer, 0) / daysLeft
? max(latestRemaining - safetyBuffer, 0) / daysLeft
: 0
let recommended = historicalReferenceSource == .accountHistory
? min(allowanceRate, historicalRate * 1.2)
Expand Down
Loading