From 90eb5c96ba7af1974062304ba252f395c6931780 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Fri, 22 May 2026 18:25:45 -0400 Subject: [PATCH] Use live limits for fast-mode forecasts --- .../ContextPanelCore/FastModeForecast.swift | 2 +- .../ContextPanelPreviewApp.swift | 2 +- .../FastModeForecastTests.swift | 41 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Sources/ContextPanelCore/FastModeForecast.swift b/Sources/ContextPanelCore/FastModeForecast.swift index 2ca5e81..d4a64c3 100644 --- a/Sources/ContextPanelCore/FastModeForecast.swift +++ b/Sources/ContextPanelCore/FastModeForecast.swift @@ -609,7 +609,7 @@ public extension Sequence where Element == MainLimitSummary { return FastModeCapacityForecast( limitID: summary.id, accountName: "\(summary.provider.displayName) \(summary.window.displayName) pool", - providerLimits: summary.limits, + providerLimits: summary.liveLimits, now: now, standardBurnRate: standardRate.map { BurnRate(mode: .standard, unitsPerHour: $0) }, fastBurnRate: standardRate.map { BurnRate(mode: .fast, unitsPerHour: $0 * fastModeMultiplier) }, diff --git a/Sources/ContextPanelPreview/ContextPanelPreviewApp.swift b/Sources/ContextPanelPreview/ContextPanelPreviewApp.swift index 6e3fc9a..8cf5623 100644 --- a/Sources/ContextPanelPreview/ContextPanelPreviewApp.swift +++ b/Sources/ContextPanelPreview/ContextPanelPreviewApp.swift @@ -1828,7 +1828,7 @@ struct MainLimitDetail: View { return FastModeCapacityForecast( limitID: summary.id, accountName: limit.accountName, - providerLimits: summary.limits, + providerLimits: summary.liveLimits, now: Date(), standardBurnRate: standardBurnRate.map { BurnRate(mode: .standard, unitsPerHour: $0) diff --git a/Tests/ContextPanelCoreTests/FastModeForecastTests.swift b/Tests/ContextPanelCoreTests/FastModeForecastTests.swift index 191ac28..8f3b3fa 100644 --- a/Tests/ContextPanelCoreTests/FastModeForecastTests.swift +++ b/Tests/ContextPanelCoreTests/FastModeForecastTests.swift @@ -692,6 +692,47 @@ private let now = Date(timeIntervalSinceReferenceDate: 900_000_000) #expect(portfolio.detailCopy.contains("5h guardrail:")) } +@Test func openAIFastModeForecastIgnoresFiveHourBucketsBlockedByWeeklyLimit() throws { + let activeReset = now.addingTimeInterval(5 * 3_600) + let weeklyReset = now.addingTimeInterval(7 * 24 * 3_600) + let current = storedOpenAIFiveHourWithWeeklyGate( + savedAt: now, + activeUsed: 100, + fullUsed: 10, + activeWeeklyUsed: 20, + fullWeeklyUsed: 100, + activeReset: activeReset, + weeklyReset: weeklyReset + ).snapshot + + let portfolio = current.mainLimitSummaries.openAIFastModeCapacityForecast( + now: now, + observedBurnRates: [ + "openai:fiveHour": ObservedBurnRate( + limitID: "openai:fiveHour", + unitsPerHour: 2, + observedDurationHours: 2, + sampleCount: 3 + ), + "openai:weekly": ObservedBurnRate( + limitID: "openai:weekly", + unitsPerHour: 2, + observedDurationHours: 2, + sampleCount: 3 + ), + ], + defaultStandardBurnRateUnitsPerHour: 2, + fastModeMultiplier: 1, + reserveUnits: 6, + minimumSafeHours: 1 + ) + let fiveHour = try #require(portfolio.forecasts.first { $0.window == .fiveHour }) + + #expect(fiveHour.remainingUnits == 0) + #expect(fiveHour.recommendation == .limited) + #expect(portfolio.detailCopy.contains("5h guardrail:")) +} + @Test func observedBurnRateAllowsConsistentlyUnknownResets() throws { let reset = now.addingTimeInterval(96 * 3_600) let history = [