Skip to content

Enterprise/Organization Claude accounts always fail with "Invalid response from Claude API" #940

@clintandrewhall

Description

@clintandrewhall

Summary

Users with Enterprise or Team plan Claude accounts (credit-based billing) cannot use CodexBar's web quota source. The fetch pipeline resolves the org correctly but then crashes parsing the usage response, because five_hour is null for enterprise orgs. There is also a secondary UX gap: even if the parsing were fixed, there is currently no way to specify an organizationID through the settings UI, so users with multiple orgs linked to the same email have no path to selecting the correct one without hand-editing JSON.


Reproduction

  1. Have a Claude session token for an account that has both a personal Pro plan and an Enterprise/Team plan org linked to the same email.
  2. Add the Claude session token in CodexBar's Preferences → Claude.
  3. CodexBar resolves the organization, calls GET /api/organizations/{org_id}/usage, receives HTTP 200 — then displays "Invalid response from Claude API."

Debug log (codexbar usage --provider claude --source web --log-level debug):

Organizations API status: 200
Organization resolved
Usage API status: 200
[fetch error] invalidResponse

Root cause — Bug 1: parseUsageResponse crashes on enterprise usage response

File: Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift

Enterprise accounts on credit-based billing return this from GET /api/organizations/{org_id}/usage:

{
  "five_hour": null,
  "seven_day": null,
  "extra_usage": {
    "monthly_limit": 100000,
    "used_credits": 4132
  }
}

five_hour and seven_day are null because session/weekly quotas do not apply. Usage is tracked via extra_usage in cents (monthly_limit: 100000 = $1,000 limit; used_credits: 4132 = $41.32 spent).

The current parser (line ~491) throws immediately when five_hour is absent:

// ClaudeWebAPIFetcher.swift, parseUsageResponse(_:logger:) ~line 475
guard let sessionPercent else {
    // If we can't parse session utilization, treat this as a failure
    // so callers can fall back to the CLI.
    throw FetchError.invalidResponse   // ← always reached for enterprise orgs
}

WebUsageData.sessionPercentUsed is a non-optional Double, so the struct cannot be constructed without it. The fix must either:

  • Make sessionPercentUsed optional (Double?) and update all callers to treat nil as "not applicable", or
  • Default it to 0.0 for enterprise accounts (which have no session limit) and document that sessionResetsAt == nil signals "no session cap".

WebUsageData struct (line ~84) for reference:

public struct WebUsageData: Sendable {
    public let sessionPercentUsed: Double        // ← non-optional, must change or default
    public let sessionResetsAt: Date?
    public let weeklyPercentUsed: Double?
    public let weeklyResetsAt: Date?
    public let opusPercentUsed: Double?
    public let extraRateWindows: [NamedRateWindow]
    public let extraUsageCost: ProviderCostSnapshot?
    public let accountOrganization: String?
    public let accountEmail: String?
    public let loginMethod: String?
}

Proposed parseUsageResponse change (minimal, defaulting to 0 strategy):

// Replace the guard block (~line 491) with:
let resolvedSessionPercent = sessionPercent ?? 0.0
// sessionResetsAt remains nil — UI can use that to hide the "resets at" label

// Parse extra_usage credits if present (enterprise credit billing, values in cents)
var extraUsageCost: ProviderCostSnapshot? = nil
if let extraUsage = json["extra_usage"] as? [String: Any] {
    if let usedCents = extraUsage["used_credits"] as? Double,
       let limitCents = extraUsage["monthly_limit"] as? Double,
       limitCents > 0 {
        extraUsageCost = ProviderCostSnapshot(
            used: usedCents / 100.0,
            limit: limitCents / 100.0,
            currencyCode: "USD",
            period: "Monthly",
            resetsAt: nil,
            updatedAt: Date())
    }
}

return WebUsageData(
    sessionPercentUsed: resolvedSessionPercent,
    // ...
    extraUsageCost: extraUsageCost,
    // ...
)

Note on reset date: extra_usage does not include a reset timestamp. The existing fetchExtraUsageCost function calls GET /api/organizations/{org_id}/overage_spend_limit (line ~568) as a best-effort fallback. That endpoint also does not currently return a reset date in OverageSpendLimitResponse. For the initial fix, resetsAt: nil is acceptable.

The existing fetchExtraUsageCost guard if usage.extraUsageCost == nil (line ~210) means the overage_spend_limit call is skipped if extra_usage already provided data — that's correct behavior, no change needed there.


Root cause — Bug 2: No UI to set organizationID on a token account

Users with multiple Claude orgs on one email (e.g. personal Pro + Enterprise) need to specify which org to query. The organizationID field exists end-to-end in the data model but is not exposed in the add-account form.

Data model (Sources/CodexBarCore/TokenAccounts.swift, line ~14): Already present.

public let organizationID: String?  // JSON key: "organizationId"

Settings store (Sources/CodexBar/SettingsStore+TokenAccounts.swift): addTokenAccount(provider:label:token:organizationID:) already accepts the parameter — it just isn't called with a value from the UI.

What needs changing — three files:

1. Sources/CodexBar/Providers/Shared/ProviderSettingsDescriptors.swift, line ~137

// Current:
let addAccount: (_ label: String, _ token: String) -> Void

// Change to:
let addAccount: (_ label: String, _ token: String, _ organizationID: String?) -> Void

2. Sources/CodexBar/PreferencesProviderSettingsRows.swift, line ~319

Add @State var newOrgID = "" to the view; show an optional "Org ID" text field when descriptor.supportsOrganizationID is true:

HStack(spacing: 8) {
    TextField("Label", text: self.$newLabel)
    SecureField(self.descriptor.placeholder, text: self.$newToken)
    if self.descriptor.supportsOrganizationID {
        TextField("Org ID (optional)", text: self.$newOrgID)
            .textFieldStyle(.roundedBorder)
            .font(.footnote)
    }
    Button("Add") {
        let orgID = self.newOrgID.trimmingCharacters(in: .whitespacesAndNewlines)
        self.descriptor.addAccount(label, token, orgID.isEmpty ? nil : orgID)
        self.newOrgID = ""
    }
}

Add let supportsOrganizationID: Bool to ProviderSettingsTokenAccountsDescriptor.

3. Sources/CodexBar/PreferencesProvidersPane.swift, line ~359

// Current:
addAccount: { label, token in
    self.settings.addTokenAccount(provider: provider, label: label, token: token)
}

// Change to:
addAccount: { label, token, organizationID in
    self.settings.addTokenAccount(provider: provider, label: label, token: token, organizationID: organizationID)
}

Set supportsOrganizationID: provider == .claude when building the descriptor.


Workaround (until fixed)

  1. Add the Claude account token via the UI normally (without org ID).
  2. Locate the token accounts file: ~/Library/Application Support/CodexBar/token-accounts.json
  3. Add "organizationId": "<your-org-uuid>" to the correct account entry — the UUID must be lowercase.
  4. The correct UUID can be found from the Claude web app: open DevTools → Console → run fetch('/api/organizations').then(r=>r.json()).then(console.log) — use the uuid of the target org.

Important: After adding the account via UI, quit CodexBar before editing the JSON file. The app overwrites the file on changes and will reset your manual edit.


Test cases to add

ClaudeWebAPIFetcher.swift already exposes _parseUsageResponseForTesting in #if DEBUG. Suggested additions:

// Enterprise usage: five_hour and seven_day are null, extra_usage present
func testParseUsageResponse_enterpriseCreditBilling() throws {
    let json = """
    {"five_hour": null, "seven_day": null, "extra_usage": {"monthly_limit": 100000, "used_credits": 4132}}
    """.data(using: .utf8)!
    let result = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(json)
    XCTAssertEqual(result.sessionPercentUsed, 0.0)
    XCTAssertNil(result.sessionResetsAt)
    XCTAssertEqual(result.extraUsageCost?.used, 41.32, accuracy: 0.001)
    XCTAssertEqual(result.extraUsageCost?.limit, 1000.0, accuracy: 0.001)
}

// Personal Pro: five_hour present, extra_usage absent — existing behavior unchanged
func testParseUsageResponse_personalProUnchanged() throws {
    let json = """
    {"five_hour": {"utilization": 42, "resets_at": "2025-06-01T00:00:00Z"}, "seven_day": null}
    """.data(using: .utf8)!
    let result = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(json)
    XCTAssertEqual(result.sessionPercentUsed, 42.0)
    XCTAssertNotNil(result.sessionResetsAt)
    XCTAssertNil(result.extraUsageCost)
}

Summary of changes

File Change
Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift Remove throw FetchError.invalidResponse when five_hour is null; default sessionPercentUsed to 0.0; parse extra_usage credits inside parseUsageResponse
Sources/CodexBar/Providers/Shared/ProviderSettingsDescriptors.swift Add organizationID: String? param to addAccount closure; add supportsOrganizationID: Bool field
Sources/CodexBar/PreferencesProviderSettingsRows.swift Add optional "Org ID" text field to inline add-account form, shown when supportsOrganizationID is true
Sources/CodexBar/PreferencesProvidersPane.swift Pass organizationID from form through to settings.addTokenAccount

The largest change is in ClaudeWebAPIFetcher.swift. The UI changes are additive and isolated to Claude.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions