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
11 changes: 10 additions & 1 deletion Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -692,8 +692,17 @@ public struct CursorStatusProbe: Sendable {
// Convert cents to USD (plan percent derives from raw values to avoid percent unit mismatches).
// Use breakdown.total if available (includes bonus credits), otherwise fall back to limit.
let planUsedRaw = Double(summary.individualUsage?.plan?.used ?? 0)
let planLimitRaw = Double(summary.individualUsage?.plan?.breakdown?.total ?? summary.individualUsage?.plan?
var planLimitRaw = Double(summary.individualUsage?.plan?.breakdown?.total ?? summary.individualUsage?.plan?
.limit ?? 0)

// Fix for plans where the API returns an incorrect limit (e.g., "Cursor Pro_Student" returning $2000 instead of $20)
if let membershipType = summary.membershipType,
membershipType.contains("Pro_Student"),
planLimitRaw > 2000
{
planLimitRaw = 2000
}

let planUsed = planUsedRaw / 100.0
let planLimit = planLimitRaw / 100.0
let planPercentUsed: Double = if planLimitRaw > 0 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ struct MiniMaxAPIFetchStrategy: ProviderFetchStrategy {
guard let apiToken = ProviderTokenResolver.minimaxToken(environment: context.env) else {
throw MiniMaxAPISettingsError.missingToken
}
let usage = try await MiniMaxUsageFetcher.fetchUsage(apiToken: apiToken)
let region = context.settings?.minimax?.apiRegion ?? .global
let usage = try await MiniMaxUsageFetcher.fetchUsage(apiToken: apiToken, region: region)
return self.makeResult(
usage: usage.toUsageSnapshot(),
sourceLabel: "api")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ public struct MiniMaxUsageFetcher: Sendable {

public static func fetchUsage(
apiToken: String,
region: MiniMaxAPIRegion = .global,
now: Date = Date()) async throws -> MiniMaxUsageSnapshot
{
let cleaned = apiToken.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else {
throw MiniMaxUsageError.invalidCredentials
}

var request = URLRequest(url: self.apiRemainsURL)
var request = URLRequest(url: region.remainsURL)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use API token endpoint, not platform remains URL

In MiniMaxUsageFetcher.fetchUsage(apiToken:) the request now targets region.remainsURL, which resolves to the platform host (platform.minimax.io/platform.minimaxi.com) and the cookie “openplatform” remains path. The MiniMax docs in this repo specify the API token endpoint as https://api.minimax.io/v1/coding_plan/remains (different host and path) and reserve the platform host for cookie-based fetches (GET {host}/v1/api/openplatform/coding_plan/remains). This change will cause API-token-only users (especially global region) to hit the cookie endpoint and likely receive 404/401 responses, so usage fetch will fail for the API-token flow.

Useful? React with 👍 / 👎.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

request.httpMethod = "GET"
request.setValue("Bearer \(cleaned)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "accept")
Expand Down
15 changes: 13 additions & 2 deletions Sources/CodexBarCore/Providers/OpenCode/OpenCodeUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -409,18 +409,29 @@ public struct OpenCodeUsageFetcher: Sendable {

private static func extractServerErrorMessage(from text: String) -> String? {
guard let data = text.data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data, options: []),
let dict = object as? [String: Any]
let object = try? JSONSerialization.jsonObject(with: data, options: [])
else {
// If it's not JSON, try to extract error from HTML if possible
if text.contains("<title>"),
let match = text.range(of: #"(?i)<title>([^<]+)</title>"#, options: .regularExpression)
{
return String(text[match].dropFirst(7).dropLast(8)).trimmingCharacters(in: .whitespacesAndNewlines)
}
return nil
}

guard let dict = object as? [String: Any] else { return nil }

if let message = dict["message"] as? String, !message.isEmpty {
return message
}
if let error = dict["error"] as? String, !error.isEmpty {
return error
}
// Check for common error fields in some frameworks
if let detail = dict["detail"] as? String, !detail.isEmpty {
return detail
}
return nil
}

Expand Down