-
Notifications
You must be signed in to change notification settings - Fork 1.4k
MiniMax: recharge credits, usage dashboard, and quota utilization #1821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
88dfac6
972cffa
2431a49
d58793c
8d40b54
227ed49
94b454e
6c49ef2
6a3451d
30cf613
adbc2c2
a3137c8
1883738
17f2e21
204dde1
2baec7b
e5c0443
c446b1a
8c248c1
bc0b960
4be8e94
c8541d9
1b14fa1
b7470c9
b447e20
98d35cd
ee3547c
0a20638
0728e28
2185751
669f6fd
c8a6722
143ac2e
e25a28f
8119ca1
712ca75
873b7b5
1104f76
b889140
3d5b18f
edfc384
b2bc5da
50bbe01
110248f
36c4ab2
44d373b
2b66a46
df28ac0
048f9b6
ec7ae80
e95fe89
7c3f534
93ede2a
222e39c
bfdcce8
5d1661d
ebe1d6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -711,6 +711,16 @@ extension UsageStore { | |
| self.settings.stepfunToken = token | ||
| } | ||
| }, | ||
| providerRegionUpdater: { [weak self] provider, region in | ||
| await MainActor.run { | ||
| guard let self, | ||
| provider == .minimax, | ||
| let resolved = MiniMaxAPIRegion(rawValue: region), | ||
| self.settings.minimaxAPIRegion != resolved | ||
| else { return } | ||
| self.settings.minimaxAPIRegion = resolved | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a selected MiniMax token account only succeeds after the global→China retry, this writes Useful? React with 👍 / 👎. |
||
| } | ||
| }, | ||
| costUsageHistoryDays: self.settings.costUsageHistoryDays, | ||
| persistsCLISessions: true, | ||
| persistentCLISessionIdleWindow: ProviderRegistry.persistentCLISessionIdleWindow( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -324,6 +324,26 @@ struct TokenAccountCLIContext { | |
| } | ||
| } | ||
|
|
||
| func regionUpdater() -> ProviderFetchContext.ProviderRegionUpdater { | ||
| { provider, region in | ||
| try? Self.updateStoredRegion(provider: provider, region: region) | ||
| } | ||
| } | ||
|
|
||
| private static func updateStoredRegion(provider: UsageProvider, region: String) throws { | ||
| guard provider == .minimax else { return } | ||
| let trimmed = region.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !trimmed.isEmpty else { return } | ||
|
|
||
| let store = CodexBarConfigStore() | ||
| var config = try store.load() ?? .makeDefault() | ||
| var providerConfig = config.providerConfig(for: provider) ?? ProviderConfig(id: provider) | ||
| guard providerConfig.region != trimmed else { return } | ||
| providerConfig.region = trimmed | ||
| config.setProviderConfig(providerConfig) | ||
| try store.save(config) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| private static func updateStoredManualToken(provider: UsageProvider, token: String) throws { | ||
| guard provider == .stepfun else { return } | ||
| let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -271,4 +271,34 @@ extension MiniMaxUsageFetcher { | |
| return snapshot | ||
| } | ||
| } | ||
|
|
||
| static func attachingTokenPlanCreditIfAvailable( | ||
| to snapshot: MiniMaxUsageSnapshot, | ||
| context: WebFetchContext, | ||
| groupID: String?) async throws -> MiniMaxUsageSnapshot | ||
| { | ||
| guard snapshot.pointsBalance == nil, | ||
| MiniMaxCookieHeader.normalized(from: context.cookie) != nil | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the normal token-plan remains response already carries Useful? React with 👍 / 👎. |
||
| else { | ||
| return snapshot | ||
| } | ||
|
|
||
| let resolvedGroupID = groupID ?? MiniMaxCookieHeader.override(from: context.cookie)?.groupID | ||
| do { | ||
| let balance = try await MiniMaxTokenPlanCreditFetcher.fetch( | ||
| cookieHeader: context.cookie, | ||
| groupID: resolvedGroupID, | ||
| region: context.region, | ||
| environment: context.environment, | ||
| transport: context.transport) | ||
| return snapshot.withPointsBalanceIfMissing(balance) | ||
| } catch is CancellationError { | ||
| throw CancellationError() | ||
| } catch let error as URLError where error.code == .cancelled { | ||
| throw CancellationError() | ||
| } catch { | ||
| Self.log.debug("MiniMax token plan credit unavailable: \(error.localizedDescription)") | ||
| return snapshot | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import Foundation | ||
| #if canImport(FoundationNetworking) | ||
| import FoundationNetworking | ||
| #endif | ||
|
|
||
| enum MiniMaxTokenPlanCreditFetcher { | ||
| static func fetch( | ||
| cookieHeader: String, | ||
| groupID: String?, | ||
| region: MiniMaxAPIRegion, | ||
| environment: [String: String], | ||
| transport: any ProviderHTTPTransport) async throws -> Double? | ||
| { | ||
| let url = try self.resolveCreditURL(region: region, environment: environment) | ||
| var request = URLRequest(url: url) | ||
| request.httpMethod = "GET" | ||
| request.setValue(cookieHeader, forHTTPHeaderField: "Cookie") | ||
| if let groupID = groupID?.trimmingCharacters(in: .whitespacesAndNewlines), !groupID.isEmpty { | ||
| request.setValue(groupID, forHTTPHeaderField: "x-group-id") | ||
| } | ||
| request.setValue("application/json, text/plain, */*", forHTTPHeaderField: "accept") | ||
| request.setValue("XMLHttpRequest", forHTTPHeaderField: "x-requested-with") | ||
| let userAgent = | ||
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + | ||
| "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" | ||
| request.setValue(userAgent, forHTTPHeaderField: "user-agent") | ||
| request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "accept-language") | ||
| let origin = MiniMaxSubscriptionMetadataFetcher.platformOriginURL(region: region) | ||
| request.setValue(origin.absoluteString, forHTTPHeaderField: "origin") | ||
| request.setValue(origin.absoluteString + "/", forHTTPHeaderField: "referer") | ||
|
|
||
| let response = try await transport.response(for: request) | ||
| guard response.statusCode == 200 else { | ||
| if response.statusCode == 401 || response.statusCode == 403 { | ||
| throw MiniMaxUsageError.invalidCredentials | ||
| } | ||
| throw MiniMaxUsageError.apiError("HTTP \(response.statusCode)") | ||
| } | ||
| return try self.parseBalance(data: response.data) | ||
| } | ||
|
|
||
| static func parseBalance(data: Data) throws -> Double? { | ||
| let object = try JSONSerialization.jsonObject(with: data, options: []) | ||
| guard let payload = object as? [String: Any] else { | ||
| throw MiniMaxUsageError.parseFailed("MiniMax token plan credit payload was not an object.") | ||
| } | ||
| try self.validateBaseResponse(in: payload) | ||
| return self.balance(from: payload) | ||
| } | ||
|
|
||
| static func resolveCreditURL(region: MiniMaxAPIRegion, environment: [String: String]) throws -> URL { | ||
| if let rejectedKey = MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: environment) { | ||
| throw ProviderEndpointOverrideError.minimax(rejectedKey) | ||
| } | ||
| if let override = MiniMaxSettingsReader.tokenPlanCreditURL(environment: environment) { | ||
| return override | ||
| } | ||
| return region.tokenPlanCreditURL | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When MiniMax is configured only through the documented Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| private static func validateBaseResponse(in payload: [String: Any]) throws { | ||
| guard let baseResp = payload["base_resp"] as? [String: Any] else { return } | ||
| let status = self.intValue(baseResp["status_code"]) ?? 0 | ||
| guard status != 0 else { return } | ||
| let message = (baseResp["status_msg"] as? String) ?? "MiniMax token plan credit error \(status)" | ||
| if status == 1004 || message.lowercased().contains("cookie") || message.lowercased().contains("login") { | ||
| throw MiniMaxUsageError.invalidCredentials | ||
| } | ||
| throw MiniMaxUsageError.apiError(message) | ||
| } | ||
|
|
||
| private static func balance(from payload: [String: Any]) -> Double? { | ||
| if let balance = self.doubleValue(payload["remaining_credits"]), balance >= 0 { | ||
| return balance | ||
| } | ||
| if let breakdown = payload["balance_breakdown"] as? [String: Any], | ||
| let balance = self.doubleValue(breakdown["total_balance"]), | ||
| balance >= 0 | ||
| { | ||
| return balance | ||
| } | ||
| if let balance = self.doubleValue(payload["points_balance"]), balance >= 0 { | ||
| return balance | ||
| } | ||
| if let balance = self.doubleValue(payload["total_credits"]), | ||
| let used = self.doubleValue(payload["used_credits"]), | ||
| balance >= used | ||
| { | ||
| return balance - used | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| private static func doubleValue(_ value: Any?) -> Double? { | ||
| if let number = value as? Double { return number } | ||
| if let number = value as? Int { return Double(number) } | ||
| if let string = value as? String { | ||
| return Double(string.trimmingCharacters(in: .whitespacesAndNewlines)) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| private static func intValue(_ value: Any?) -> Int? { | ||
| if let number = value as? Int { return number } | ||
| if let number = value as? Double { return Int(number) } | ||
| if let string = value as? String { | ||
| return Int(string.trimmingCharacters(in: .whitespacesAndNewlines)) | ||
| } | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| extension MiniMaxSubscriptionMetadataFetcher { | ||
| static func platformOriginURL(region: MiniMaxAPIRegion) -> URL { | ||
| switch region { | ||
| case .global: URL(string: "https://platform.minimax.io")! | ||
| case .chinaMainland: URL(string: "https://platform.minimaxi.com")! | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation no longer persists the fallback China region;
fetchAPITokenUsageonly returnsresolvedRegionfor the current fetch and the credit enrichment uses that transient value. Leaving this changelog entry will tell users that later refreshes skip the global probe automatically, which is no longer true unless they set the region manually.Useful? React with 👍 / 👎.