From 94827370f6746fb4a4599897d536a981af86ea83 Mon Sep 17 00:00:00 2001 From: Rohit Sabu <13933510+rohitsabu@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:24:03 +0400 Subject: [PATCH] fix(alibaba): restore Token Plan rate windows --- .../Alibaba/AlibabaTokenPlanAPIRegion.swift | 70 ++++++++ .../AlibabaTokenPlanUsageFetcher.swift | 167 ++++++++++++++++++ .../AlibabaTokenPlanUsageSnapshot.swift | 35 +++- .../AlibabaTokenPlanProviderTests.swift | 113 ++++++++++++ 4 files changed, 382 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanAPIRegion.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanAPIRegion.swift index ae7453f901..60c57d0056 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanAPIRegion.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanAPIRegion.swift @@ -36,6 +36,76 @@ public enum AlibabaTokenPlanAPIRegion: String, CaseIterable, Sendable { } } + public var personalDashboardURL: URL { + switch self { + case .international: + URL( + string: "https://modelstudio.console.alibabacloud.com/ap-southeast-1/" + + "?tab=plan#/efm/subscription/token-plan/personal")! + case .chinaMainland: + URL( + string: "https://bailian.console.aliyun.com/cn-beijing" + + "?tab=plan#/efm/subscription/token-plan/personal")! + } + } + + public var consoleRPCBaseURLString: String { + switch self { + case .international: + "https://bailian-singapore-cs.alibabacloud.com" + case .chinaMainland: + "https://bailian-cs.console.aliyun.com" + } + } + + public var consoleRPCAction: String { + switch self { + case .international: + "IntlBroadScopeAspnGateway" + case .chinaMainland: + "BroadScopeAspnGateway" + } + } + + public var consoleRPCProduct: String { + "sfm_bailian" + } + + public var rateLimitAPIName: String { + "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage" + } + + public var rateLimitURL: URL { + var components = URLComponents(string: self.consoleRPCBaseURLString)! + components.path = "/data/api.json" + components.queryItems = [ + URLQueryItem(name: "action", value: self.consoleRPCAction), + URLQueryItem(name: "product", value: self.consoleRPCProduct), + URLQueryItem(name: "api", value: self.rateLimitAPIName), + URLQueryItem(name: "_v", value: "undefined"), + ] + return components.url! + } + + public var consoleDomain: String { + switch self { + case .international: + "modelstudio.console.alibabacloud.com" + case .chinaMainland: + "bailian.console.aliyun.com" + } + } + + public var consoleSite: String { + switch self { + case .international: + // This is Alibaba's live console contract, including its historical spelling. + "MODELSTUDIO_ALBABACLOUD" + case .chinaMainland: + "BAILIAN_ALIYUN" + } + } + public var currentRegionID: String { switch self { case .international: diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanUsageFetcher.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanUsageFetcher.swift index 27aee5a635..5ce8730e03 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanUsageFetcher.swift @@ -123,6 +123,20 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable { region: region, environment: environment, session: dashboardSession) + if let secToken { + do { + return try await self.fetchRateLimitUsage( + cookieHeader: normalizedAPIHeader, + secToken: secToken, + region: region, + now: now, + session: apiSession) + } catch { + Self.log.info( + "Alibaba Token Plan rate-limit request failed; using subscription summary", + metadata: ["error": error.localizedDescription]) + } + } Self.log.info( "Fetching Alibaba Token Plan usage", metadata: [ @@ -205,6 +219,100 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable { return try self.parseUsageSnapshot(from: data, now: now) } + private static func fetchRateLimitUsage( + cookieHeader: String, + secToken: String, + region: AlibabaTokenPlanAPIRegion, + now: Date, + session: URLSession) async throws -> AlibabaTokenPlanUsageSnapshot + { + let url = region.rateLimitURL + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.timeoutInterval = 20 + request.httpBody = self.rateLimitRequestBody( + cookieHeader: cookieHeader, + secToken: secToken, + region: region) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.setValue("*/*", forHTTPHeaderField: "Accept") + request.setValue(cookieHeader, forHTTPHeaderField: "Cookie") + if let csrf = self.extractCookieValue(name: "login_aliyunid_csrf", from: cookieHeader) ?? + self.extractCookieValue(name: "csrf", from: cookieHeader) + { + request.setValue(csrf, forHTTPHeaderField: "x-xsrf-token") + request.setValue(csrf, forHTTPHeaderField: "x-csrf-token") + } + request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With") + request.setValue(Self.browserLikeUserAgent, forHTTPHeaderField: "User-Agent") + request.setValue(region.gatewayBaseURLString, forHTTPHeaderField: "Origin") + request.setValue(region.personalDashboardURL.absoluteString, forHTTPHeaderField: "Referer") + + let data: Data + let response: URLResponse + do { + (data, response) = try await session.data(for: request) + } catch { + throw AlibabaTokenPlanUsageError.networkError(error.localizedDescription) + } + guard let httpResponse = response as? HTTPURLResponse else { + throw AlibabaTokenPlanUsageError.networkError("Invalid rate-limit response") + } + guard httpResponse.statusCode == 200 else { + if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { + throw AlibabaTokenPlanUsageError.loginRequired + } + throw AlibabaTokenPlanUsageError.apiError("HTTP \(httpResponse.statusCode)") + } + return try self.parseRateLimitUsageSnapshot(from: data, now: now) + } + + private static func rateLimitRequestBody( + cookieHeader: String, + secToken: String, + region: AlibabaTokenPlanAPIRegion) -> Data + { + let traceID = UUID().uuidString.lowercased() + var cornerstoneParam: [String: Any] = [ + "feTraceId": traceID, + "feURL": region.personalDashboardURL.absoluteString, + "protocol": "V2", + "console": "ONE_CONSOLE", + "productCode": "p_efm", + "switchAgent": 1_233_135, + "switchUserType": 3, + "domain": region.consoleDomain, + "consoleSite": region.consoleSite, + "userNickName": "", + "userPrincipalName": "", + "xsp_lang": "en-US", + ] + if let anonymousID = self.extractCookieValue(name: "cna", from: cookieHeader), + !anonymousID.isEmpty + { + cornerstoneParam["X-Anonymous-Id"] = anonymousID + } + let paramsObject: [String: Any] = [ + "Api": region.rateLimitAPIName, + "V": "1.0", + "Data": [ + "cornerstoneParam": cornerstoneParam, + ], + ] + guard let paramsData = try? JSONSerialization.data(withJSONObject: paramsObject, options: []), + let paramsString = String(data: paramsData, encoding: .utf8) + else { + return Data() + } + var components = URLComponents() + components.queryItems = [ + URLQueryItem(name: "params", value: paramsString), + URLQueryItem(name: "region", value: region.currentRegionID), + URLQueryItem(name: "sec_token", value: secToken), + ] + return Data((components.percentEncodedQuery ?? "").utf8) + } + static func resolveQuotaURL( region: AlibabaTokenPlanAPIRegion, environment: [String: String]) -> URL @@ -280,6 +388,65 @@ public struct AlibabaTokenPlanUsageFetcher: Sendable { updatedAt: now) } + static func parseRateLimitUsageSnapshot( + from data: Data, + now: Date = Date()) throws -> AlibabaTokenPlanUsageSnapshot + { + guard !data.isEmpty else { + throw AlibabaTokenPlanUsageError.parseFailed("Empty response body") + } + let object: Any + do { + object = try JSONSerialization.jsonObject(with: data, options: []) + } catch { + if self.isLikelyLoginHTML(data) { + throw AlibabaTokenPlanUsageError.loginRequired + } + throw AlibabaTokenPlanUsageError.parseFailed("Invalid JSON response") + } + let expanded = self.expandedJSON(object) + guard let dictionary = expanded as? [String: Any] else { + throw AlibabaTokenPlanUsageError.parseFailed("Unexpected payload") + } + try self.throwIfErrorPayload(dictionary) + + let rateKeys = [ + "per5HourPercentage", + "per1WeekPercentage", + "per5HourResetTime", + "per1WeekResetTime", + ] + guard let usage = self.findFirstDictionary(matchingAnyKey: rateKeys, in: dictionary) else { + throw AlibabaTokenPlanUsageError.parseFailed("Missing rate-limit usage") + } + let fiveHour = self.normalizedUsedPercent(self.parseDouble(usage["per5HourPercentage"])) + let sevenDay = self.normalizedUsedPercent(self.parseDouble(usage["per1WeekPercentage"])) + guard fiveHour != nil || sevenDay != nil else { + throw AlibabaTokenPlanUsageError.parseFailed("Missing rate-limit usage") + } + + return AlibabaTokenPlanUsageSnapshot( + planName: "TOKEN PLAN", + usedQuota: nil, + totalQuota: nil, + remainingQuota: nil, + resetsAt: nil, + fiveHourUsedPercent: fiveHour, + fiveHourResetsAt: self.parseDate(usage["per5HourResetTime"]), + sevenDayUsedPercent: sevenDay, + sevenDayResetsAt: self.parseDate(usage["per1WeekResetTime"]), + updatedAt: now) + } + + private static func normalizedUsedPercent(_ raw: Double?) -> Double? { + guard let raw, raw >= 0 else { return nil } + if raw <= 1 { + return raw * 100 + } + guard raw <= 100 else { return nil } + return raw + } + private static func subscriptionSummaryRequestBody(region: AlibabaTokenPlanAPIRegion, secToken: String?) -> Data { let paramsObject = ["ProductCode": region.tokenPlanProductCode] guard let paramsData = try? JSONSerialization.data(withJSONObject: paramsObject, options: []), diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanUsageSnapshot.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanUsageSnapshot.swift index 36cee2a2b2..df7b2d4ad8 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanUsageSnapshot.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanUsageSnapshot.swift @@ -6,6 +6,10 @@ public struct AlibabaTokenPlanUsageSnapshot: Sendable { public let totalQuota: Double? public let remainingQuota: Double? public let resetsAt: Date? + public let fiveHourUsedPercent: Double? + public let fiveHourResetsAt: Date? + public let sevenDayUsedPercent: Double? + public let sevenDayResetsAt: Date? public let updatedAt: Date public init( @@ -14,6 +18,10 @@ public struct AlibabaTokenPlanUsageSnapshot: Sendable { totalQuota: Double?, remainingQuota: Double?, resetsAt: Date?, + fiveHourUsedPercent: Double? = nil, + fiveHourResetsAt: Date? = nil, + sevenDayUsedPercent: Double? = nil, + sevenDayResetsAt: Date? = nil, updatedAt: Date) { self.planName = planName @@ -21,13 +29,17 @@ public struct AlibabaTokenPlanUsageSnapshot: Sendable { self.totalQuota = totalQuota self.remainingQuota = remainingQuota self.resetsAt = resetsAt + self.fiveHourUsedPercent = fiveHourUsedPercent + self.fiveHourResetsAt = fiveHourResetsAt + self.sevenDayUsedPercent = sevenDayUsedPercent + self.sevenDayResetsAt = sevenDayResetsAt self.updatedAt = updatedAt } } extension AlibabaTokenPlanUsageSnapshot { public func toUsageSnapshot() -> UsageSnapshot { - let primary: RateWindow? = Self.usedPercent( + let monthlyCredits: RateWindow? = Self.usedPercent( used: self.usedQuota, total: self.totalQuota, remaining: self.remainingQuota).map { @@ -40,6 +52,23 @@ extension AlibabaTokenPlanUsageSnapshot { total: self.totalQuota, remaining: self.remainingQuota)) } + let fiveHour = self.fiveHourUsedPercent.map { + RateWindow( + usedPercent: $0, + windowMinutes: 5 * 60, + resetsAt: self.fiveHourResetsAt, + resetDescription: nil) + } + let sevenDay = self.sevenDayUsedPercent.map { + RateWindow( + usedPercent: $0, + windowMinutes: 7 * 24 * 60, + resetsAt: self.sevenDayResetsAt, + resetDescription: nil) + } + let primary = fiveHour ?? monthlyCredits + let secondary = fiveHour == nil ? nil : sevenDay + let tertiary = fiveHour == nil ? nil : monthlyCredits let planName = self.planName?.trimmingCharacters(in: .whitespacesAndNewlines) let loginMethod = (planName?.isEmpty ?? true) ? nil : planName @@ -51,8 +80,8 @@ extension AlibabaTokenPlanUsageSnapshot { return UsageSnapshot( primary: primary, - secondary: nil, - tertiary: nil, + secondary: secondary, + tertiary: tertiary, providerCost: nil, updatedAt: self.updatedAt, identity: identity) diff --git a/Tests/CodexBarTests/AlibabaTokenPlanProviderTests.swift b/Tests/CodexBarTests/AlibabaTokenPlanProviderTests.swift index 1db85ef8a0..8f89562ce0 100644 --- a/Tests/CodexBarTests/AlibabaTokenPlanProviderTests.swift +++ b/Tests/CodexBarTests/AlibabaTokenPlanProviderTests.swift @@ -208,6 +208,45 @@ struct AlibabaTokenPlanUsageSnapshotTests { @Suite(.serialized) struct AlibabaTokenPlanUsageParsingTests { + @Test + func `parses token plan rate windows with millisecond resets`() throws { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let json = """ + { + "code": "200", + "data": { + "DataV2": { + "data": { + "success": true, + "data": { + "per5HourPercentage": 0.0769, + "per5HourResetTime": 1700100000000, + "per1WeekPercentage": 0.0261, + "per1WeekResetTime": 1700200000000 + } + }, + "success": true, + "httpStatus": 200 + } + }, + "successResponse": true + } + """ + + let snapshot = try AlibabaTokenPlanUsageFetcher.parseRateLimitUsageSnapshot( + from: Data(json.utf8), + now: now) + let usage = snapshot.toUsageSnapshot() + + #expect(abs((usage.primary?.usedPercent ?? -.infinity) - 7.69) < 0.000_001) + #expect(usage.primary?.windowMinutes == 5 * 60) + #expect(usage.primary?.resetsAt == Date(timeIntervalSince1970: 1_700_100_000)) + #expect(abs((usage.secondary?.usedPercent ?? -.infinity) - 2.61) < 0.000_001) + #expect(usage.secondary?.windowMinutes == 7 * 24 * 60) + #expect(usage.secondary?.resetsAt == Date(timeIntervalSince1970: 1_700_200_000)) + #expect(usage.loginMethod(for: .alibabatokenplan) == "TOKEN PLAN") + } + @Test func `parses subscription summary payload`() throws { let now = Date(timeIntervalSince1970: 1_700_000_000) @@ -453,6 +492,78 @@ struct AlibabaTokenPlanUsageParsingTests { #expect(snapshot.planName == "TOKEN PLAN") } + @Test + func `fetches authenticated token plan rate windows before credit fallback`() async throws { + defer { + AlibabaTokenPlanStubURLProtocol.handler = nil + } + + AlibabaTokenPlanStubURLProtocol.handler = { request in + guard let url = request.url else { throw URLError(.badURL) } + + if url.host == "rate-limit.test", request.httpMethod == "GET" { + return Self.makeResponse( + url: url, + body: "", + statusCode: 200) + } + + if url.host == "bailian-singapore-cs.alibabacloud.com", + request.httpMethod == "POST", + URLComponents(url: url, resolvingAgainstBaseURL: false)? + .queryItems? + .first(where: { $0.name == "api" })? + .value == "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage" + { + #expect(request.value(forHTTPHeaderField: "Origin") == + "https://modelstudio.console.alibabacloud.com") + let body = Self.requestBodyString(from: request) + #expect(body.contains("sec_token=session-token")) + #expect(body.contains("params=")) + #expect(body.contains("region=ap-southeast-1")) + let json = """ + { + "code": "200", + "data": { + "DataV2": { + "data": { + "success": true, + "data": { + "per5HourPercentage": 0.0769, + "per5HourResetTime": 1700100000000, + "per1WeekPercentage": 0.0261, + "per1WeekResetTime": 1700200000000 + } + }, + "success": true, + "httpStatus": 200 + } + }, + "successResponse": true + } + """ + return Self.makeResponse(url: url, body: json, statusCode: 200) + } + + throw URLError(.unsupportedURL) + } + + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [AlibabaTokenPlanStubURLProtocol.self] + let session = URLSession(configuration: configuration) + let snapshot = try await AlibabaTokenPlanUsageFetcher.fetchUsage( + apiCookieHeader: "login_aliyunid_ticket=ticket", + dashboardCookieHeader: "login_aliyunid_ticket=ticket", + environment: [AlibabaTokenPlanSettingsReader.hostKey: "https://rate-limit.test"], + session: session) + let usage = snapshot.toUsageSnapshot() + + #expect(abs((usage.primary?.usedPercent ?? -.infinity) - 7.69) < 0.000_001) + #expect(usage.primary?.windowMinutes == 300) + #expect(abs((usage.secondary?.usedPercent ?? -.infinity) - 2.61) < 0.000_001) + #expect(usage.secondary?.windowMinutes == 10080) + } + @Test func `SEC token preflight uses injected session`() async throws { AlibabaTokenPlanStubURLProtocol.handler = { request in @@ -921,7 +1032,9 @@ final class AlibabaTokenPlanStubURLProtocol: URLProtocol { override static func canInit(with request: URLRequest) -> Bool { guard let host = request.url?.host else { return false } return host == "bailian.console.aliyun.com" || + host == "bailian-singapore-cs.alibabacloud.com" || host == "alibaba-token-plan.test" || + host == "rate-limit.test" || host == "session-token.test" }