Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ struct MiniMaxProviderImplementation: ProviderImplementation {
subtitle: "Choose the MiniMax host (global .io or China mainland .com).",
binding: regionBinding,
options: regionOptions,
isVisible: { authMode().allowsCookies },
isVisible: nil,
onChange: nil),
]
}
Expand Down
35 changes: 34 additions & 1 deletion Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,42 @@ public struct MiniMaxUsageFetcher: Sendable {
throw MiniMaxUsageError.invalidCredentials
}

let regionsToTry: [MiniMaxAPIRegion] = {
// Historically, MiniMax API token fetching used a China endpoint by default in some configurations.
// If the user has no persisted region and we default to `.global`, retry the China endpoint when the
// global host rejects the token so upgrades don't regress existing setups.
if region == .global { return [.global, .chinaMainland] }
return [region]
}()

var lastError: Error?
for (index, attemptRegion) in regionsToTry.enumerated() {
do {
return try await self.fetchUsageOnce(apiToken: cleaned, region: attemptRegion, now: now)
} catch let error as MiniMaxUsageError {
lastError = error
if index == 0, regionsToTry.count > 1, case .invalidCredentials = error {
Self.log.debug("MiniMax API token rejected for global host, retrying China mainland host")
continue
}
throw error
} catch {
lastError = error
throw error
}
}

throw lastError ?? MiniMaxUsageError.invalidCredentials
}

private static func fetchUsageOnce(
apiToken: String,
region: MiniMaxAPIRegion,
now: Date) async throws -> MiniMaxUsageSnapshot
{
var request = URLRequest(url: region.apiRemainsURL)
request.httpMethod = "GET"
request.setValue("Bearer \(cleaned)", forHTTPHeaderField: "Authorization")
request.setValue("Bearer \(apiToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("CodexBar", forHTTPHeaderField: "MM-API-Source")
Expand Down
144 changes: 144 additions & 0 deletions Tests/CodexBarTests/MiniMaxAPITokenFetchTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import CodexBarCore
import Foundation
import Testing

@Suite(.serialized)
struct MiniMaxAPITokenFetchTests {
@Test
func retriesChinaHostWhenGlobalRejectsToken() async throws {
let registered = URLProtocol.registerClass(MiniMaxAPITokenStubURLProtocol.self)
defer {
if registered {
URLProtocol.unregisterClass(MiniMaxAPITokenStubURLProtocol.self)
}
MiniMaxAPITokenStubURLProtocol.handler = nil
MiniMaxAPITokenStubURLProtocol.requests = []
}

MiniMaxAPITokenStubURLProtocol.handler = { request in
guard let url = request.url else { throw URLError(.badURL) }
let host = url.host ?? ""
if host == "api.minimax.io" {
return Self.makeResponse(url: url, body: "{}", statusCode: 401)
}
if host == "api.minimaxi.com" {
let start = 1_700_000_000_000
let end = start + 5 * 60 * 60 * 1000
let body = """
{
"base_resp": { "status_code": 0 },
"current_subscribe_title": "Max",
"model_remains": [
{
"current_interval_total_count": 1000,
"current_interval_usage_count": 250,
"start_time": \(start),
"end_time": \(end),
"remains_time": 240000
}
]
}
"""
return Self.makeResponse(url: url, body: body)
}
return Self.makeResponse(url: url, body: "{}", statusCode: 404)
}

let now = Date(timeIntervalSince1970: 1_700_000_000)
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(apiToken: "sk-cp-test", region: .global, now: now)

#expect(snapshot.planName == "Max")
#expect(MiniMaxAPITokenStubURLProtocol.requests.count == 2)
#expect(MiniMaxAPITokenStubURLProtocol.requests.first?.url?.host == "api.minimax.io")
#expect(MiniMaxAPITokenStubURLProtocol.requests.last?.url?.host == "api.minimaxi.com")
}

@Test
func doesNotRetryWhenRegionIsChinaMainland() async throws {
let registered = URLProtocol.registerClass(MiniMaxAPITokenStubURLProtocol.self)
defer {
if registered {
URLProtocol.unregisterClass(MiniMaxAPITokenStubURLProtocol.self)
}
MiniMaxAPITokenStubURLProtocol.handler = nil
MiniMaxAPITokenStubURLProtocol.requests = []
}

MiniMaxAPITokenStubURLProtocol.handler = { request in
guard let url = request.url else { throw URLError(.badURL) }
let host = url.host ?? ""
if host == "api.minimaxi.com" {
let start = 1_700_000_000_000
let end = start + 5 * 60 * 60 * 1000
let body = """
{
"base_resp": { "status_code": 0 },
"current_subscribe_title": "Max",
"model_remains": [
{
"current_interval_total_count": 1000,
"current_interval_usage_count": 250,
"start_time": \(start),
"end_time": \(end),
"remains_time": 240000
}
]
}
"""
return Self.makeResponse(url: url, body: body)
}
return Self.makeResponse(url: url, body: "{}", statusCode: 401)
}

let now = Date(timeIntervalSince1970: 1_700_000_000)
_ = try await MiniMaxUsageFetcher.fetchUsage(apiToken: "sk-cp-test", region: .chinaMainland, now: now)

#expect(MiniMaxAPITokenStubURLProtocol.requests.count == 1)
#expect(MiniMaxAPITokenStubURLProtocol.requests.first?.url?.host == "api.minimaxi.com")
}

private static func makeResponse(
url: URL,
body: String,
statusCode: Int = 200) -> (HTTPURLResponse, Data)
{
let response = HTTPURLResponse(
url: url,
statusCode: statusCode,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": "application/json"])!
return (response, Data(body.utf8))
}
}

final class MiniMaxAPITokenStubURLProtocol: URLProtocol {
nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
nonisolated(unsafe) static var requests: [URLRequest] = []

override static func canInit(with request: URLRequest) -> Bool {
guard let host = request.url?.host else { return false }
return host == "api.minimax.io" || host == "api.minimaxi.com"
}

override static func canonicalRequest(for request: URLRequest) -> URLRequest {
request
}

override func startLoading() {
Self.requests.append(self.request)
guard let handler = Self.handler else {
self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
return
}
do {
let (response, data) = try handler(self.request)
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
self.client?.urlProtocol(self, didLoad: data)
self.client?.urlProtocolDidFinishLoading(self)
} catch {
self.client?.urlProtocol(self, didFailWithError: error)
}
}

override func stopLoading() {}
}