Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -14,20 +18,28 @@ 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
self.usedQuota = usedQuota
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 {
Expand All @@ -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
Comment on lines +69 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Label the restored rate windows by their durations

When the new rate-limit response succeeds, these assignments make the primary bar the 5-hour percentage and the secondary bar the 7-day percentage. MenuCardView+ModelHelpers.rateWindowLabels uses the Token Plan descriptor labels verbatim, but that descriptor still says Credits and Usage (rather than 5-hour and Weekly), so every successful rate-limit fetch presents the new percentage quotas under misleading labels. Update the Token Plan metadata alongside this remapping.

Useful? React with 👍 / 👎.

let tertiary = fiveHour == nil ? nil : monthlyCredits

let planName = self.planName?.trimmingCharacters(in: .whitespacesAndNewlines)
let loginMethod = (planName?.isEmpty ?? true) ? nil : planName
Expand All @@ -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)
Expand Down
Loading