Skip to content
Open
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
33 changes: 31 additions & 2 deletions Sources/CodexBarCore/Providers/Claude/ClaudePlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ public enum ClaudePlan: String, CaseIterable, Sendable {
}

public static func fromWebAccount(rateLimitTier: String?, billingType: String?) -> Self? {
self.fromWebAccount(rateLimitTier: rateLimitTier, billingType: billingType, seatTier: nil)
}

public static func fromWebAccount(rateLimitTier: String?, billingType: String?, seatTier: String?) -> Self? {
if self.teamSeatLoginMethod(seatTier: seatTier) != nil {
return .team
}

if let plan = self.fromRateLimitTier(rateLimitTier) {
return plan
}
Expand Down Expand Up @@ -110,9 +118,18 @@ public enum ClaudePlan: String, CaseIterable, Sendable {
}

public static func webLoginMethod(rateLimitTier: String?, billingType: String?) -> String? {
self.fromWebAccount(
self.webLoginMethod(rateLimitTier: rateLimitTier, billingType: billingType, seatTier: nil)
}

public static func webLoginMethod(rateLimitTier: String?, billingType: String?, seatTier: String?) -> String? {
if let teamSeat = self.teamSeatLoginMethod(seatTier: seatTier) {
return teamSeat
}

return self.fromWebAccount(
rateLimitTier: rateLimitTier,
billingType: billingType)?.brandedLoginMethod(rateLimitTier: rateLimitTier)
billingType: billingType,
seatTier: seatTier)?.brandedLoginMethod(rateLimitTier: rateLimitTier)
}

public static func cliCompatibilityLoginMethod(_ loginMethod: String?) -> String? {
Expand Down Expand Up @@ -150,6 +167,18 @@ public enum ClaudePlan: String, CaseIterable, Sendable {
return nil
}

private static func teamSeatLoginMethod(seatTier: String?) -> String? {
let tier = Self.normalized(seatTier)
guard tier.hasPrefix("team_") else { return nil }
if tier.contains("standard") {
return "Claude Team Standard"
}
if tier.contains("premium") || tier.contains("tier_1") {
return "Claude Team Premium"
}
return "Claude Team"
}

private static func maxUsageMultiplier(rateLimitTier: String?) -> String? {
let words = Self.normalizedWords(rateLimitTier)
guard let maxIndex = words.firstIndex(of: Self.max.rawValue),
Expand Down
61 changes: 58 additions & 3 deletions Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1374,11 +1374,20 @@ extension ClaudeUsageFetcher {
Self.log.debug(msg)
}
}
// Only merge usage/cost extras; keep identity fields from the primary data source.
// Only merge usage/cost extras by default; allow web account metadata to refine a generic
// Team label with the exact Standard/Premium seat tier when available.
let mergedExtraRateWindows = snapshot.extraRateWindows.isEmpty ? webData.extraRateWindows : snapshot
.extraRateWindows
let mergedProviderCost = snapshot.providerCost ?? webData.extraUsageCost
if mergedProviderCost != snapshot.providerCost || mergedExtraRateWindows != snapshot.extraRateWindows {
let mergedLoginMethod = Self.refinedLoginMethod(
current: snapshot.loginMethod,
currentAccountEmail: snapshot.accountEmail,
web: webData.loginMethod,
webAccountEmail: webData.accountEmail)
if mergedProviderCost != snapshot.providerCost ||
mergedExtraRateWindows != snapshot.extraRateWindows ||
mergedLoginMethod != snapshot.loginMethod
{
return ClaudeUsageSnapshot(
primary: snapshot.primary,
primaryWindowKind: snapshot.primaryWindowKind,
Expand All @@ -1389,7 +1398,7 @@ extension ClaudeUsageFetcher {
updatedAt: snapshot.updatedAt,
accountEmail: snapshot.accountEmail,
accountOrganization: snapshot.accountOrganization,
loginMethod: snapshot.loginMethod,
loginMethod: mergedLoginMethod,
rawText: snapshot.rawText,
oauthKeychainPersistentRefHash: snapshot.oauthKeychainPersistentRefHash,
oauthHistoryOwnerIdentifier: snapshot.oauthHistoryOwnerIdentifier,
Expand All @@ -1403,6 +1412,39 @@ extension ClaudeUsageFetcher {
return snapshot
}

private static func refinedLoginMethod(
current: String?,
currentAccountEmail: String?,
web: String?,
webAccountEmail: String?) -> String?
{
let webText = web?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let webText, !webText.isEmpty else { return current }
guard self.isGenericTeamLoginMethod(current) else { return current }
guard self.isSameAccountEmail(currentAccountEmail, webAccountEmail) else { return current }

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 Include organization in Team tier refinement

Fresh evidence: this version now verifies only the email before accepting the web seat label. For a Claude user who belongs to multiple Team orgs under the same email, the web extras path can be pointed at or auto-select one organization while the primary CLI snapshot still carries another accountOrganization; this guard then accepts the web tier and the merge keeps the primary organization, producing an Org A label with a Standard/Premium tier sourced from Org B. Compare the primary/web organization as well, or skip refinement when the primary org is known and differs.

Useful? React with 👍 / 👎.

let normalized = webText.lowercased()
guard normalized.contains("premium") || normalized.contains("standard") else { return current }
return webText
}

private static func isSameAccountEmail(_ current: String?, _ web: String?) -> Bool {
guard let current = self.normalizedAccountEmail(current),
let web = self.normalizedAccountEmail(web)
else { return false }
return current == web
}

private static func normalizedAccountEmail(_ email: String?) -> String? {
let normalized = email?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return (normalized?.isEmpty ?? true) ? nil : normalized
}

private static func isGenericTeamLoginMethod(_ loginMethod: String?) -> Bool {
guard ClaudePlan.fromCompatibilityLoginMethod(loginMethod) == .team else { return false }
let normalized = loginMethod?.lowercased() ?? ""
return !normalized.contains("premium") && !normalized.contains("standard")
}

// MARK: - Process helpers

private static func which(_ tool: String) -> String? {
Expand Down Expand Up @@ -1468,6 +1510,19 @@ extension ClaudeUsageFetcher {

#if DEBUG
extension ClaudeUsageFetcher {
public static func _refinedLoginMethodForTesting(
current: String?,
currentAccountEmail: String?,
web: String?,
webAccountEmail: String?) -> String?
{
self.refinedLoginMethod(
current: current,
currentAccountEmail: currentAccountEmail,
web: web,
webAccountEmail: webAccountEmail)
}

public static func _mapOAuthUsageForTesting(
_ data: Data,
rateLimitTier: String? = nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,12 @@ public enum ClaudeWebAPIFetcher {

struct Membership: Decodable {
let organization: Organization
let seatTier: String?

enum CodingKeys: String, CodingKey {
case organization
case seatTier = "seat_tier"
}

struct Organization: Decodable {
let uuid: String?
Expand Down Expand Up @@ -806,7 +812,8 @@ public enum ClaudeWebAPIFetcher {
let membership = Self.selectMembership(response.memberships, orgId: orgId)
let plan = ClaudePlan.webLoginMethod(
rateLimitTier: membership?.organization.rateLimitTier,
billingType: membership?.organization.billingType)
billingType: membership?.organization.billingType,
seatTier: membership?.seatTier)
return WebAccountInfo(email: email, loginMethod: plan)
}

Expand Down Expand Up @@ -968,7 +975,9 @@ public enum ClaudeWebAPIFetcher {
targetOrganizationID: String? = nil,
logger: ((String) -> Void)? = nil) async throws -> WebUsageData
{
_ = sessionKeyInfo
_ = targetOrganizationID
_ = logger
throw FetchError.notSupportedOnThisPlatform
}

Expand Down
22 changes: 22 additions & 0 deletions Tests/CodexBarTests/ClaudePlanResolverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ struct ClaudePlanResolverTests {
== "Claude Pro")
}

@Test
func `web account seat tier maps Team Standard and Premium seats`() {
#expect(
ClaudePlan.webLoginMethod(
rateLimitTier: "default_raven",
billingType: "stripe_subscription",
seatTier: "team_standard")
== "Claude Team Standard")
#expect(
ClaudePlan.webLoginMethod(
rateLimitTier: "default_raven",
billingType: "stripe_subscription",
seatTier: "team_tier_1")
== "Claude Team Premium")
#expect(
ClaudePlan.webLoginMethod(
rateLimitTier: "default_raven",
billingType: "stripe_subscription",
seatTier: "team_premium")
== "Claude Team Premium")
}

@Test
func `compatibility parser understands current labels`() {
#expect(ClaudePlan.fromCompatibilityLoginMethod("Claude Max") == .max)
Expand Down
98 changes: 98 additions & 0 deletions Tests/CodexBarTests/ClaudeTeamTierRefinementTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import Foundation
import Testing
@testable import CodexBar
@testable import CodexBarCore

struct ClaudeTeamTierRefinementTests {
private static func makeOAuthUsageResponse() throws -> OAuthUsageResponse {
let json = """

{
"five_hour": {
"utilization": 7,
"resets_at": "2025-12-23T16:00:00.000Z"
},
"seven_day": {
"utilization": 21,
"resets_at": "2025-12-29T23:00:00.000Z"
}
}

"""
return try ClaudeOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
}

@Test
func `oauth team login method is not refined when web extras are disabled`() async throws {
let usageResponse = try Self.makeOAuthUsageResponse()
let fetcher = ClaudeUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
environment: [:],
dataSource: .oauth,
useWebExtras: false)

let fetchOverride: @Sendable (String, Bool) async throws -> OAuthUsageResponse = { _, _ in
usageResponse
}
let loadCredsOverride: @Sendable (
[String: String],
Bool,
Bool) async throws -> ClaudeOAuthCredentials = { _, _, _ in
ClaudeOAuthCredentials(
accessToken: "access-token",
refreshToken: nil,
expiresAt: Date(timeIntervalSinceNow: 3600),
scopes: ["user:profile"],
rateLimitTier: "claude_team",
subscriptionType: nil)
}
let transport = ProviderHTTPTransportHandler { _ in
Issue.record("Claude web account lookup should not run when web extras are disabled")
throw URLError(.badServerResponse)
}

let snapshot = try await ClaudeWebHTTPTransport.$overrideForTesting.withValue(transport) {
try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride) {
try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(loadCredsOverride) {
try await fetcher.loadLatestUsage(model: "sonnet")
}
}
}

#expect(snapshot.loginMethod == "Claude Team")
#expect(snapshot.accountEmail == nil)
}

@Test
func `generic team login method refines only when web account email matches`() {
let refined = ClaudeUsageFetcher._refinedLoginMethodForTesting(
current: "Claude Team",
currentAccountEmail: " Primary@Example.com ",
web: "Claude Team Premium",
webAccountEmail: "primary@example.com")

#expect(refined == "Claude Team Premium")
}

@Test
func `generic team login method ignores web tier from a different account`() {
let refined = ClaudeUsageFetcher._refinedLoginMethodForTesting(
current: "Claude Team",
currentAccountEmail: "primary@example.com",
web: "Claude Team Premium",
webAccountEmail: "other@example.com")

#expect(refined == "Claude Team")
}

@Test
func `generic team login method ignores web tier when account cannot be verified`() {
let refined = ClaudeUsageFetcher._refinedLoginMethodForTesting(
current: "Claude Team",
currentAccountEmail: nil,
web: "Claude Team Standard",
webAccountEmail: "primary@example.com")

#expect(refined == "Claude Team")
}
}
43 changes: 43 additions & 0 deletions Tests/CodexBarTests/ClaudeWebUsageExtraWindowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,49 @@ import Testing
@testable import CodexBarCore

struct ClaudeWebUsageExtraWindowTests {
@Test
func `parses Team Premium seat tier from account memberships`() throws {
let json = """
{
"email_address": "person@example.com",
"memberships": [
{
"seat_tier": "team_tier_1",
"organization": {
"uuid": "org-team",
"name": "Team Org",
"rate_limit_tier": "default_raven",
"billing_type": "stripe_subscription"
}
}
]
}
"""
let account = try #require(ClaudeWebAPIFetcher._parseAccountInfoForTesting(Data(json.utf8), orgId: "org-team"))
#expect(account.email == "person@example.com")
#expect(account.loginMethod == "Claude Team Premium")
}

@Test
func `parses Team Standard seat tier from account memberships`() throws {
let json = """
{
"memberships": [
{
"seat_tier": "team_standard",
"organization": {
"uuid": "org-team",
"rate_limit_tier": "default_raven",
"billing_type": "stripe_subscription"
}
}
]
}
"""
let account = try #require(ClaudeWebAPIFetcher._parseAccountInfoForTesting(Data(json.utf8), orgId: "org-team"))
#expect(account.loginMethod == "Claude Team Standard")
}

@Test
func `parses claude web API sonnet usage response`() throws {
let json = """
Expand Down