Merge nucleic/nimble-pebble-lynx-jfch into dev

This commit is contained in:
2026-08-01 05:36:45 -07:00
parent fe298fe3c8
commit 9b526f5bc0
9 changed files with 166 additions and 18 deletions
+9 -3
View File
@@ -154,7 +154,8 @@ private struct UsageWindowRow: View {
/// Shows, left to right: the session **context-window** usage (document icon) and then the
/// account windows of **the provider this chat actually runs on** each as a `%` colored
/// calm amber red. A Claude chat shows the 5-hour (clock) and weekly (calendar) windows
/// from `AppStore.subscriptionUsage` (real Pro/Max utilization via the OAuth usage endpoint),
/// plus Fable's separate weekly cap when present, from `AppStore.subscriptionUsage`
/// (real Pro/Max utilization via the OAuth usage endpoint),
/// falling back to the coarse `rate_limit_event` status; a Codex chat shows the ChatGPT weekly
/// window from `AppStore.codexUsage` instead, so the pill never reports Claude's quota next to
/// a GPT model. Providers that report no quota at all (Grok, the ACP wrappers) show only the
@@ -252,6 +253,9 @@ struct QuotaIndicator: View {
if let w = usage.sevenDay {
append("weekly", QuotaFormat.weeklyIcon, w.utilization(at: now))
}
if let fable = usage.sevenDayFable {
append("fableWeekly", "sparkles", fable.utilization(at: now))
}
} else if let rl = store.latestRateLimit {
// The coarse `rate_limit_event` fallback is Claude stream telemetry, so it only
// stands in for Claude's windows.
@@ -293,6 +297,7 @@ struct QuotaIndicator: View {
}
claudeLine("5-hour", usage.fiveHour)
claudeLine("Weekly", usage.sevenDay)
claudeLine("Weekly · Fable", usage.sevenDayFable)
claudeLine("Weekly · Opus", usage.sevenDayOpus)
claudeLine("Weekly · Sonnet", usage.sevenDaySonnet)
} else if let rl = store.latestRateLimit {
@@ -318,8 +323,8 @@ struct QuotaIndicator: View {
// MARK: - Dashboard card
/// The home-dashboard form of the quota indicator: a card (matching the stat cards)
/// with labeled progress bars for the 5-hour (clock) and weekly (calendar) windows and
/// reset countdowns. `matchHeight` lets the home view size it to the neighboring stats grid so
/// with labeled progress bars and reset countdowns for the 5-hour (clock), weekly (calendar),
/// and optional Fable-weekly windows. `matchHeight` lets the home view size it to the neighboring stats grid so
/// the two columns line up; it falls back to the `rate_limit_event` status, then a
/// "checking" placeholder, so it always shows something in the dashboard.
struct QuotaCard: View {
@@ -355,6 +360,7 @@ struct QuotaCard: View {
VStack(alignment: .leading, spacing: 14) {
windowRow("5-hour limit", icon: QuotaFormat.fiveHourIcon, usage.fiveHour, now: now)
windowRow("Weekly limit", icon: QuotaFormat.weeklyIcon, usage.sevenDay, now: now)
windowRow("Weekly · Fable", icon: "sparkles", usage.sevenDayFable, now: now)
}
} else if let rl = store.latestRateLimit {
HStack(spacing: 6) {
+1
View File
@@ -13588,6 +13588,7 @@ extension AppStore: SyncHostBridge {
}
return WireSubscriptionUsage(
fiveHour: window(usage.fiveHour), sevenDay: window(usage.sevenDay),
sevenDayFable: window(usage.sevenDayFable),
sevenDayOpus: window(usage.sevenDayOpus), sevenDaySonnet: window(usage.sevenDaySonnet))
}
@@ -135,6 +135,7 @@ public enum IntelligenceRouter {
var modelUtilization = [String: Double]()
let claudeWindows = [
claudeUsage?.fiveHour, claudeUsage?.sevenDay,
claudeUsage?.sevenDayFable,
claudeUsage?.sevenDayOpus, claudeUsage?.sevenDaySonnet,
]
let hasPreciseClaudeUsage = claudeWindows.contains { $0 != nil }
@@ -170,6 +171,9 @@ public enum IntelligenceRouter {
if reached(claudeUsage?.sevenDayOpus) {
models.formUnion(IntelligenceRouter.routedModels.filter { $0.hasPrefix("claude-opus") })
}
if reached(claudeUsage?.sevenDayFable) {
models.formUnion(IntelligenceRouter.routedModels.filter { $0.hasPrefix("claude-fable") })
}
if reached(claudeUsage?.sevenDaySonnet) {
models.formUnion(IntelligenceRouter.routedModels.filter { $0.hasPrefix("claude-sonnet") })
}
@@ -178,6 +182,11 @@ public enum IntelligenceRouter {
modelUtilization[sku] = opusUsage
}
}
if let fableUsage = utilization(claudeUsage?.sevenDayFable) {
for sku in IntelligenceRouter.routedModels where sku.hasPrefix("claude-fable") {
modelUtilization[sku] = fableUsage
}
}
if let sonnetUsage = utilization(claudeUsage?.sevenDaySonnet) {
for sku in IntelligenceRouter.routedModels where sku.hasPrefix("claude-sonnet") {
modelUtilization[sku] = sonnetUsage
+66 -8
View File
@@ -43,6 +43,8 @@ public struct SubscriptionUsage: Sendable, Equatable {
public let fiveHour: UsageWindow?
/// The rolling 7-day ("weekly") limit, across all models.
public let sevenDay: UsageWindow?
/// The weekly Fable sub-limit (nil when the plan has no included Fable cap).
public let sevenDayFable: UsageWindow?
/// The weekly Opus sub-limit (nil when the plan has no separate Opus cap).
public let sevenDayOpus: UsageWindow?
/// The weekly Sonnet sub-limit.
@@ -50,10 +52,12 @@ public struct SubscriptionUsage: Sendable, Equatable {
public init(
fiveHour: UsageWindow?, sevenDay: UsageWindow?,
sevenDayFable: UsageWindow? = nil,
sevenDayOpus: UsageWindow?, sevenDaySonnet: UsageWindow?
) {
self.fiveHour = fiveHour
self.sevenDay = sevenDay
self.sevenDayFable = sevenDayFable
self.sevenDayOpus = sevenDayOpus
self.sevenDaySonnet = sevenDaySonnet
}
@@ -61,7 +65,7 @@ public struct SubscriptionUsage: Sendable, Equatable {
/// The most-constrained window's utilization what a single headline indicator
/// should track, since hitting *any* window throttles you.
public var peakUtilization: Double? {
[fiveHour, sevenDay, sevenDayOpus, sevenDaySonnet]
[fiveHour, sevenDay, sevenDayFable, sevenDayOpus, sevenDaySonnet]
.compactMap { $0?.utilization }.max()
}
}
@@ -121,17 +125,71 @@ public enum SubscriptionUsageFetcher {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw SubscriptionUsageError.badResponse
}
func window(_ key: String) -> UsageWindow? {
guard let obj = json[key] as? [String: Any],
let util = obj["utilization"] as? Double else { return nil }
func window(_ obj: [String: Any]?, utilizationKeys: [String] = ["utilization"]) -> UsageWindow? {
guard let obj,
let util = utilizationKeys.lazy
.compactMap({ (obj[$0] as? NSNumber)?.doubleValue }).first else { return nil }
let resets = (obj["resets_at"] as? String).flatMap(parseDate)
return UsageWindow(utilization: util, resetsAt: resets)
}
// Anthropic's richer response shape (introduced in July 2026) carries the
// authoritative buckets in a generic `limits` array. In particular, Fable has no
// `seven_day_fable` field: it arrives as a `weekly_scoped` entry whose model id may
// be null and whose display name is "Fable". Prefer these entries over the legacy
// flat fields, which are null or can lag on migrated accounts.
struct LimitCandidate {
let window: UsageWindow
let isActive: Bool
}
var session: LimitCandidate?
var weekly: LimitCandidate?
var scoped: [String: LimitCandidate] = [:]
func prefer(_ candidate: LimitCandidate, over current: LimitCandidate?) -> Bool {
guard let current else { return true }
if candidate.isActive != current.isActive { return candidate.isActive }
return candidate.window.utilization > current.window.utilization
}
if let limits = json["limits"] as? [[String: Any]] {
for limit in limits {
guard let parsed = window(limit, utilizationKeys: ["percent", "utilization"]),
let kind = limit["kind"] as? String else { continue }
let candidate = LimitCandidate(
window: parsed, isActive: limit["is_active"] as? Bool ?? false)
switch kind {
case "session":
if prefer(candidate, over: session) { session = candidate }
case "weekly_all":
if prefer(candidate, over: weekly) { weekly = candidate }
case "weekly_scoped":
guard let scope = limit["scope"] as? [String: Any],
let model = scope["model"] as? [String: Any] else { continue }
// `id` is currently null for Fable, so display_name is the primary key.
let label = (model["display_name"] as? String)
?? (model["id"] as? String)
guard let label = label?.lowercased() else { continue }
// Collapse versioned labels ("Fable 5", "Claude Fable 5") into one
// family so duplicate migration-era entries can be ranked together.
let family = ["fable", "opus", "sonnet"].first(where: label.contains)
?? label
if prefer(candidate, over: scoped[family]) { scoped[family] = candidate }
default:
continue
}
}
}
func scopedWindow(containing family: String) -> UsageWindow? {
scoped.first { $0.key.contains(family) }?.value.window
}
func flatWindow(_ key: String) -> UsageWindow? {
window(json[key] as? [String: Any])
}
return SubscriptionUsage(
fiveHour: window("five_hour"),
sevenDay: window("seven_day"),
sevenDayOpus: window("seven_day_opus"),
sevenDaySonnet: window("seven_day_sonnet"))
fiveHour: session?.window ?? flatWindow("five_hour"),
sevenDay: weekly?.window ?? flatWindow("seven_day"),
sevenDayFable: scopedWindow(containing: "fable") ?? flatWindow("seven_day_fable"),
sevenDayOpus: scopedWindow(containing: "opus") ?? flatWindow("seven_day_opus"),
sevenDaySonnet: scopedWindow(containing: "sonnet") ?? flatWindow("seven_day_sonnet"))
}
/// The endpoint stamps microsecond fractional seconds (`851919+00:00`); try the
@@ -33,22 +33,26 @@ public struct WireUsageWindow: Sendable, Codable, Equatable {
public struct WireSubscriptionUsage: Sendable, Codable, Equatable {
public let fiveHour: WireUsageWindow?
public let sevenDay: WireUsageWindow?
public let sevenDayFable: WireUsageWindow?
public let sevenDayOpus: WireUsageWindow?
public let sevenDaySonnet: WireUsageWindow?
public init(
fiveHour: WireUsageWindow?, sevenDay: WireUsageWindow?,
sevenDayFable: WireUsageWindow? = nil,
sevenDayOpus: WireUsageWindow? = nil, sevenDaySonnet: WireUsageWindow? = nil
) {
self.fiveHour = fiveHour
self.sevenDay = sevenDay
self.sevenDayFable = sevenDayFable
self.sevenDayOpus = sevenDayOpus
self.sevenDaySonnet = sevenDaySonnet
}
/// The most-constrained window's utilization the single headline number.
public var peakUtilization: Double? {
[fiveHour, sevenDay, sevenDayOpus, sevenDaySonnet].compactMap { $0?.utilization }.max()
[fiveHour, sevenDay, sevenDayFable, sevenDayOpus, sevenDaySonnet]
.compactMap { $0?.utilization }.max()
}
}
@@ -230,6 +230,7 @@ import Testing
let claude = SubscriptionUsage(
fiveHour: UsageWindow(utilization: 100, resetsAt: future),
sevenDay: nil,
sevenDayFable: UsageWindow(utilization: 100, resetsAt: future),
sevenDayOpus: UsageWindow(utilization: 100, resetsAt: future),
sevenDaySonnet: nil)
let codex = CodexUsage(weekly: CodexUsageWindow(
@@ -240,6 +241,7 @@ import Testing
#expect(limits.providers == [.claudeCode, .codex])
#expect(limits.models.contains("claude-opus-5"))
#expect(limits.models.contains("claude-fable-5"))
// The same stale snapshots stop excluding routes as soon as their reset passes.
let reset = IntelligenceRouter.Limits(
claudeUsage: claude, codexUsage: codex,
@@ -268,6 +270,28 @@ import Testing
#expect(resolution.reason.contains("usage limit"))
}
@Test func reachedFableWindowFallsThroughWithoutBlockingClaude() {
let now = Date(timeIntervalSince1970: 1_000_000)
let future = now.addingTimeInterval(600)
let limits = IntelligenceRouter.Limits(
claudeUsage: SubscriptionUsage(
fiveHour: UsageWindow(utilization: 10, resetsAt: future),
sevenDay: UsageWindow(utilization: 20, resetsAt: future),
sevenDayFable: UsageWindow(utilization: 100, resetsAt: future),
sevenDayOpus: nil, sevenDaySonnet: nil),
codexUsage: nil, claudeRateLimit: nil, now: now)
#expect(!limits.providers.contains(.claudeCode))
#expect(limits.contains(model: "claude-fable-5"))
#expect(!limits.contains(model: "claude-opus-5"))
let resolution = IntelligenceRouter.route(
purpose: .planning, level: .deep,
connected: [.claudeCode, .codex], limits: limits,
fallback: fallback)
#expect(resolution.model == "gpt-5.6-sol")
#expect(resolution.reason.contains("usage limit"))
}
@Test func availableCapacityBreaksClosePerformanceCall() {
// Sol and Opus are close choices for balanced backend work. With Codex's window
// nearly consumed and ample Claude quota, preserve the scarce Codex capacity.
@@ -288,6 +312,7 @@ import Testing
let claude = SubscriptionUsage(
fiveHour: UsageWindow(utilization: 10, resetsAt: future),
sevenDay: UsageWindow(utilization: 5, resetsAt: future),
sevenDayFable: UsageWindow(utilization: 80, resetsAt: future),
sevenDayOpus: UsageWindow(utilization: 15, resetsAt: future),
sevenDaySonnet: nil)
let codex = CodexUsage(weekly: CodexUsageWindow(
@@ -301,6 +326,7 @@ import Testing
connected: [.claudeCode, .codex], limits: limits,
fallback: fallback)
#expect(resolution.model == "claude-opus-5")
#expect(limits.modelUtilization["claude-fable-5"] == 80)
}
@Test func performanceKeepsCloseCallUntilCapacityGapIsMaterial() {
@@ -4,7 +4,7 @@ import Testing
@testable import NucleicCore
/// Locks the parse of Anthropic's `/api/oauth/usage` response (subscription quota).
/// The body below is a real capture from the endpoint (2026-06-14, plan account);
/// The body below mirrors captures from before and after the July 2026 `limits[]` migration;
/// re-capture and diff if the indicator ever reads wrong after a CLI/API change.
struct SubscriptionUsageTests {
private static let sample = Data("""
@@ -14,6 +14,14 @@ struct SubscriptionUsageTests {
"seven_day_oauth_apps": null,
"seven_day_opus": null,
"seven_day_sonnet": { "utilization": 0.0, "resets_at": null },
"limits": [
{ "kind": "session", "percent": 38, "resets_at": "2026-06-15T04:29:59.851919+00:00", "is_active": false },
{ "kind": "weekly_all", "percent": 15, "resets_at": "2026-06-21T01:59:59.851939+00:00", "is_active": false },
{ "kind": "weekly_scoped", "percent": 68, "resets_at": "2026-06-21T01:59:59.851939+00:00",
"scope": { "model": { "id": null, "display_name": "Fable" } }, "is_active": true },
{ "kind": "weekly_scoped", "percent": 12, "resets_at": null,
"scope": { "model": { "display_name": "Sonnet" } }, "is_active": false }
],
"extra_usage": { "is_enabled": false, "monthly_limit": null }
}
""".utf8)
@@ -21,16 +29,18 @@ struct SubscriptionUsageTests {
@Test func parsesWindowsAndPeak() throws {
let usage = try SubscriptionUsageFetcher.parse(Self.sample)
#expect(usage.fiveHour?.utilization == 37.0)
// Generic limits[] is authoritative over legacy flat buckets.
#expect(usage.fiveHour?.utilization == 38.0)
#expect(usage.fiveHour?.resetsAt != nil)
#expect(usage.sevenDay?.utilization == 14.0)
#expect(usage.sevenDay?.utilization == 15.0)
#expect(usage.sevenDayFable?.utilization == 68.0)
// A null per-model window is absent, not zero.
#expect(usage.sevenDayOpus == nil)
// A present window with a null reset keeps the utilization but no date.
#expect(usage.sevenDaySonnet?.utilization == 0.0)
#expect(usage.sevenDaySonnet?.utilization == 12.0)
#expect(usage.sevenDaySonnet?.resetsAt == nil)
// The headline tracks the most-constrained window.
#expect(usage.peakUtilization == 37.0)
#expect(usage.peakUtilization == 68.0)
}
@Test func microsecondResetTimestampParses() throws {
@@ -46,6 +56,37 @@ struct SubscriptionUsageTests {
#expect(components.minute == 29)
}
@Test func legacyFlatWindowsRemainSupported() throws {
let usage = try SubscriptionUsageFetcher.parse(Data("""
{
"five_hour": { "utilization": 37.0, "resets_at": null },
"seven_day": { "utilization": 14.0, "resets_at": null },
"seven_day_opus": { "utilization": 9.0, "resets_at": null }
}
""".utf8))
#expect(usage.fiveHour?.utilization == 37)
#expect(usage.sevenDay?.utilization == 14)
#expect(usage.sevenDayFable == nil)
#expect(usage.sevenDayOpus?.utilization == 9)
}
@Test func malformedGenericEntriesAreIgnored() throws {
let usage = try SubscriptionUsageFetcher.parse(Data("""
{
"five_hour": { "utilization": 8.0, "resets_at": null },
"limits": [
{ "kind": "weekly_scoped", "percent": null,
"scope": { "model": { "display_name": "Fable" } } },
{ "kind": "weekly_scoped", "percent": 45, "scope": null }
]
}
""".utf8))
#expect(usage.fiveHour?.utilization == 8)
#expect(usage.sevenDayFable == nil)
}
@Test func lapsedWindowReadsAsEmptyUntilRefetched() {
let reset = Date(timeIntervalSince1970: 1_000_000)
let window = UsageWindow(utilization: 73.0, resetsAt: reset)
@@ -788,6 +788,7 @@ import Testing
usage: WireSubscriptionUsage(
fiveHour: WireUsageWindow(utilization: 42.5, resetsAt: Date(timeIntervalSince1970: 2)),
sevenDay: WireUsageWindow(utilization: 78, resetsAt: nil),
sevenDayFable: WireUsageWindow(utilization: 91, resetsAt: nil),
sevenDayOpus: nil,
sevenDaySonnet: WireUsageWindow(utilization: 12, resetsAt: nil)),
statusFeeds: [WireStatusFeed(
@@ -801,7 +802,8 @@ import Testing
guard case .dashboard(let decoded) = back else { Issue.record("not a dashboard"); return }
#expect(decoded.todos.first?.triage == "critical")
#expect(decoded.usage?.fiveHour?.utilization == 42.5)
#expect(decoded.usage?.peakUtilization == 78)
#expect(decoded.usage?.sevenDayFable?.utilization == 91)
#expect(decoded.usage?.peakUtilization == 91)
#expect(decoded.statusFeeds.first?.hasActiveIncident == true)
}
@@ -64,6 +64,7 @@ struct QuotaCard: View {
}
windowRow("5-hour limit", icon: QuotaFormat.fiveHourIcon, usage.fiveHour, now: context.date)
windowRow("Weekly limit", icon: QuotaFormat.weeklyIcon, usage.sevenDay, now: context.date)
windowRow("Weekly · Fable", icon: "sparkles", usage.sevenDayFable, now: context.date)
}
.frame(maxWidth: .infinity, alignment: .leading)
}