//! Alibaba Model Studio usage feed implementation. //! //! Mirrors prime-agent:packages/ai/src/utils/usage/providers/alibaba.ts field for field. const std = @import("std"); const usage = @import("../usage.zig"); pub const AlibabaRegion = enum { intl, cn, @"intl-personal", @"cn-personal", pub fn toString(self: AlibabaRegion) []const u8 { return switch (self) { .intl => "intl", .cn => "cn", .@"intl-personal" => "intl-personal", .@"cn-personal" => "cn-personal", }; } pub fn fromString(str: []const u8) ?AlibabaRegion { if (std.mem.eql(u8, str, "intl")) return .intl; if (std.mem.eql(u8, str, "cn")) return .cn; if (std.mem.eql(u8, str, "intl-personal")) return .@"intl-personal"; if (std.mem.eql(u8, str, "cn-personal")) return .@"cn-personal"; return null; } }; pub const RegionInfo = struct { currentRegionId: []const u8, cliConsoleSite: []const u8, quotaHost: []const u8, productCode: []const u8, }; pub fn getRegionInfo(region: AlibabaRegion) RegionInfo { return switch (region) { .intl => .{ .currentRegionId = "ap-southeast-1", .cliConsoleSite = "international", .quotaHost = "https://modelstudio.console.alibabacloud.com", .productCode = "sfm_tokenplanteams_dp_intl", }, .cn => .{ .currentRegionId = "cn-beijing", .cliConsoleSite = "domestic", .quotaHost = "https://bailian.console.aliyun.com", .productCode = "sfm_tokenplanteams_dp_cn", }, .@"intl-personal" => .{ .currentRegionId = "ap-southeast-1", .cliConsoleSite = "international", .quotaHost = "https://bailian-singapore-cs.alibabacloud.com", .productCode = "sfm_tokenplansolo_public_intl", }, .@"cn-personal" => .{ .currentRegionId = "cn-beijing", .cliConsoleSite = "domestic", .quotaHost = "https://bailian-cs.console.aliyun.com", .productCode = "sfm_tokenplansolo_public_cn", }, }; } pub const ALIBABA_CLI_TIMEOUT_MS: i64 = 15_000; pub fn alibabaCliArguments(alloc: std.mem.Allocator, region: AlibabaRegion) ![]const []const u8 { const info = getRegionInfo(region); const args = try alloc.alloc([]const u8, 8); args[0] = "usage"; args[1] = "token-plan"; args[2] = "--console-region"; args[3] = info.currentRegionId; args[4] = "--console-site"; args[5] = info.cliConsoleSite; args[6] = "--output"; args[7] = "json"; return args; } pub const ALIBABA_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage"; pub const ALIBABA_SUBSCRIPTION_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription"; pub const ALIBABA_QUOTA_CONFIG_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/quota-config"; pub const ALIBABA_RESET_CARD_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/reset-card/list"; /// Unwrap the console's envelope (`data.DataV2.data.data`) when present. The CLI answers with the /// inner payload directly, so one parser covers both. pub fn unwrapEnvelope(value: ?std.json.Value) ?std.json.Value { const root = usage.asObject(value) orelse return value; const data = usage.asObject(root.get("data")); if (data) |d| { const data_v2 = usage.asObject(d.get("DataV2")); if (data_v2) |v2| { const v2_data = usage.asObject(v2.get("data")); if (v2_data) |v2d| { if (v2d.get("data")) |inner| return inner; } } if (d.get("data")) |inner| return inner; } return value; } pub const ParseUsageResult = struct { limits: []const usage.UsageLimit = &[_]usage.UsageLimit{}, notes: []const []const u8 = &[_][]const u8{}, }; pub fn parseAlibabaUsage(alloc: std.mem.Allocator, payload: ?std.json.Value) !ParseUsageResult { const inner_val = unwrapEnvelope(payload); const inner = usage.asObject(inner_val); var limits = std.ArrayList(usage.UsageLimit).empty; if (inner) |ud| { // Session: per5HourPercentage (0..1 fraction) -> percent if (usage.asNumber(ud.get("per5HourPercentage"))) |sf| { const used_pct = sf * 100.0; const reset = usage.asNumber(ud.get("per5HourResetTime")); const reset_ms = usage.toInt(i64, reset); try limits.append(alloc, .{ .id = "five_hour", .label = "Session", .usedPercent = used_pct, .windowLabel = "5h window", .windowSeconds = 5 * 3600, .resetsAt = reset_ms, .status = usage.statusFor(used_pct), }); } // Weekly: per1WeekPercentage (0..1 fraction) -> percent if (usage.asNumber(ud.get("per1WeekPercentage"))) |wf| { const used_pct = wf * 100.0; const reset = usage.asNumber(ud.get("per1WeekResetTime")); const reset_ms = usage.toInt(i64, reset); try limits.append(alloc, .{ .id = "weekly", .label = "Weekly", .usedPercent = used_pct, .windowLabel = "7d window", .windowSeconds = 7 * 86_400, .resetsAt = reset_ms, .status = usage.statusFor(used_pct), }); } } return .{ .limits = limits.items, .notes = &[_][]const u8{}, }; } pub fn parseAlibabaQuotaConfig( alloc: std.mem.Allocator, payload: ?std.json.Value, plan_type: ?[]const u8, ) ![]const []const u8 { const inner_val = unwrapEnvelope(payload); const tiers = usage.asObject(inner_val) orelse return &[_][]const u8{}; const pt = plan_type orelse return &[_][]const u8{}; const lowered = try alloc.alloc(u8, pt.len); _ = std.ascii.lowerString(lowered, pt); const tier = usage.asObject(tiers.get(lowered)) orelse return &[_][]const u8{}; const weekly = usage.asNumber(tier.get("weekly")); const five_hour = usage.asNumber(tier.get("five_hour")); if (weekly == null and five_hour == null) return &[_][]const u8{}; var note_buf = std.ArrayList(u8).empty; try note_buf.appendSlice(alloc, "Token plan "); try note_buf.appendSlice(alloc, pt); try note_buf.appendSlice(alloc, ": "); var parts_wrote = false; if (usage.toInt(i64, weekly)) |w| { const w_str = try std.fmt.allocPrint(alloc, "weekly {d}", .{w}); try note_buf.appendSlice(alloc, w_str); parts_wrote = true; } if (usage.toInt(i64, five_hour)) |fh| { if (parts_wrote) try note_buf.appendSlice(alloc, ", "); const fh_str = try std.fmt.allocPrint(alloc, "5h {d}", .{fh}); try note_buf.appendSlice(alloc, fh_str); } try note_buf.appendSlice(alloc, " credits"); const notes = try alloc.alloc([]const u8, 1); notes[0] = note_buf.items; return notes; } pub const ConsoleSession = struct { host: []const u8, region: []const u8, secToken: ?[]const u8 = null, headers: ?std.json.ObjectMap = null, }; pub fn parseConsoleSession(session_val: ?std.json.Value, default_host: []const u8) ?ConsoleSession { const session = usage.asObject(session_val) orelse return null; const region = usage.stringOf(session.get("region")) orelse return null; const host = usage.stringOf(session.get("url")) orelse default_host; const secToken = usage.stringOf(session.get("secToken")); const headers = usage.asObject(session.get("headers")); return .{ .host = host, .region = region, .secToken = secToken, .headers = headers, }; } pub fn regionFor(cred_root: ?std.json.ObjectMap, session: ?ConsoleSession) AlibabaRegion { const root = cred_root orelse return .@"intl-personal"; const kind = usage.asString(root.get("kind")) orelse ""; if (std.mem.eql(u8, kind, "cli")) { const config = usage.asObject(root.get("config")); if (config) |c| { if (usage.stringOf(c.get("region"))) |r_str| { if (AlibabaRegion.fromString(r_str)) |r| return r; } } } if (std.mem.eql(u8, kind, "session") and session != null) { if (std.mem.startsWith(u8, session.?.region, "cn")) { return .@"cn-personal"; } else { return .@"intl-personal"; } } return .@"intl-personal"; } pub fn buildGatewayUrl(alloc: std.mem.Allocator, host: []const u8, api: []const u8) ![]const u8 { const base = if (std.mem.indexOf(u8, host, "/data/api.json") != null) host else try std.fmt.allocPrint(alloc, "{s}/data/api.json", .{host}); const clean_base = if (std.mem.indexOfScalar(u8, base, '?')) |idx| base[0..idx] else base; const enc_api = try usage.urlEncode(alloc, api); return try std.fmt.allocPrint(alloc, "{s}?action=IntlBroadScopeAspnGateway&product=sfm_bailian&api={s}&_v=", .{ clean_base, enc_api }); } pub fn buildFormBody( alloc: std.mem.Allocator, api: []const u8, region: AlibabaRegion, session: ConsoleSession, ) ![]const u8 { const info = getRegionInfo(region); var pw = usage.Writer.init(alloc); try pw.raw("{\"Api\":"); try pw.string(api); try pw.raw(",\"V\":\"1.0\",\"Data\":{\"cornerstoneParam\":{\"protocol\":\"V2\",\"console\":\"ONE_CONSOLE\",\"productCode\":\"p_efm\",\"domain\":\"modelstudio.console.alibabacloud.com\",\"consoleSite\":\"MODELSTUDIO_ALBABACLOUD\",\"xsp_lang\":\"en-US\",\"feUrl\":"); const fe_url = try std.fmt.allocPrint(alloc, "https://modelstudio.console.alibabacloud.com/{s}/subscription/token-plan/personal", .{info.currentRegionId}); try pw.string(fe_url); try pw.raw("}"); if (std.mem.eql(u8, api, ALIBABA_SUBSCRIPTION_API)) { try pw.raw(",\"queryInstanceInfoRequest\":{\"commodityCode\":"); try pw.string(info.productCode); try pw.raw("}"); } try pw.raw("}}"); const enc_params = try usage.urlEncode(alloc, pw.items()); const enc_region = try usage.urlEncode(alloc, session.region); if (session.secToken) |tok| { if (tok.len > 0) { const enc_tok = try usage.urlEncode(alloc, tok); return try std.fmt.allocPrint(alloc, "params={s}®ion={s}&sec_token={s}", .{ enc_params, enc_region, enc_tok }); } } return try std.fmt.allocPrint(alloc, "params={s}®ion={s}", .{ enc_params, enc_region }); } pub fn consoleRequests( alloc: std.mem.Allocator, session: ConsoleSession, region: AlibabaRegion, ) ![]const usage.UsageRequest { const apis = [_][]const u8{ ALIBABA_USAGE_API, ALIBABA_SUBSCRIPTION_API, ALIBABA_QUOTA_CONFIG_API, ALIBABA_RESET_CARD_API, }; var base_headers = std.ArrayList([2][]const u8).empty; if (session.headers) |sh| { var it = sh.iterator(); while (it.next()) |entry| { if (usage.asString(entry.value_ptr.*)) |val| { const key_lower = try alloc.alloc(u8, entry.key_ptr.*.len); _ = std.ascii.lowerString(key_lower, entry.key_ptr.*); if (!std.mem.eql(u8, key_lower, "content-type") and !std.mem.eql(u8, key_lower, "accept") and !std.mem.eql(u8, key_lower, "origin") and !std.mem.eql(u8, key_lower, "referer")) { try base_headers.append(alloc, .{ key_lower, val }); } } } } try base_headers.append(alloc, .{ "content-type", "application/x-www-form-urlencoded" }); try base_headers.append(alloc, .{ "accept", "*/*" }); try base_headers.append(alloc, .{ "origin", "https://modelstudio.console.alibabacloud.com" }); const referer = try std.fmt.allocPrint(alloc, "https://modelstudio.console.alibabacloud.com/{s}/subscription/token-plan/personal", .{session.region}); try base_headers.append(alloc, .{ "referer", referer }); const requests = try alloc.alloc(usage.UsageRequest, apis.len); for (apis, 0..) |api, i| { const url = try buildGatewayUrl(alloc, session.host, api); const body = try buildFormBody(alloc, api, region, session); requests[i] = .{ .kind = .http, .method = "POST", .url = url, .headers = base_headers.items, .body = body, }; } return requests; } fn getResetCardsCount(body_str: ?[]const u8, alloc: std.mem.Allocator) ?usize { const bs = body_str orelse return null; const parsed = std.json.parseFromSlice(std.json.Value, alloc, bs, .{}) catch return null; if (usage.asArray(parsed.value)) |arr| { return arr.items.len; } const unwrapped = unwrapEnvelope(parsed.value); if (usage.asArray(unwrapped)) |arr| { return arr.items.len; } return null; } pub fn reportFromConsole( alloc: std.mem.Allocator, cred_root: ?std.json.ObjectMap, resp_array: std.json.Array, ) !usage.Outcome { const cred_email = if (cred_root) |cr| usage.stringOf(cr.get("email")) else null; if (resp_array.items.len == 0) { return .{ .report = .{ .error_message = "console gateway HTTP no response" } }; } // Match responses: first try by api in envelope, otherwise positional var usage_resp: ?std.json.ObjectMap = null; var sub_resp: ?std.json.ObjectMap = null; var quota_resp: ?std.json.ObjectMap = null; var card_resp: ?std.json.ObjectMap = null; for (resp_array.items) |r_val| { const r_obj = usage.asObject(r_val) orelse continue; const b_str = usage.asString(r_obj.get("body")) orelse continue; if (std.json.parseFromSlice(std.json.Value, alloc, b_str, .{})) |parsed| { if (usage.asObject(parsed.value)) |root| { const data_obj = usage.asObject(root.get("data")); const api = if (data_obj) |d| usage.asString(d.get("api")) else usage.asString(root.get("api")); if (api) |a| { if (std.mem.eql(u8, a, ALIBABA_USAGE_API)) { usage_resp = r_obj; } else if (std.mem.eql(u8, a, ALIBABA_SUBSCRIPTION_API)) { sub_resp = r_obj; } else if (std.mem.eql(u8, a, ALIBABA_QUOTA_CONFIG_API)) { quota_resp = r_obj; } else if (std.mem.eql(u8, a, ALIBABA_RESET_CARD_API)) { card_resp = r_obj; } } } } else |_| {} } if (usage_resp == null and resp_array.items.len > 0) usage_resp = usage.asObject(resp_array.items[0]); if (sub_resp == null and resp_array.items.len > 1) sub_resp = usage.asObject(resp_array.items[1]); if (quota_resp == null and resp_array.items.len > 2) quota_resp = usage.asObject(resp_array.items[2]); if (card_resp == null and resp_array.items.len > 3) card_resp = usage.asObject(resp_array.items[3]); const status_num = if (usage_resp) |ro| usage.asNumber(ro.get("status")) else null; const status = usage.toInt(i64, status_num); if (status == null or status.? != 200) { const err_text = if (status) |st| try std.fmt.allocPrint(alloc, "console gateway HTTP {d}", .{st}) else "console gateway HTTP no response"; return .{ .report = .{ .error_message = err_text } }; } const usage_b_str = if (usage_resp) |ro| usage.asString(ro.get("body")) else null; if (usage_b_str == null) { return .{ .report = .{ .error_message = "console gateway HTTP no response" } }; } const usage_parsed = std.json.parseFromSlice(std.json.Value, alloc, usage_b_str.?, .{}) catch { return .{ .report = .{ .error_message = "console gateway HTTP no response" } }; }; const parsed = try parseAlibabaUsage(alloc, usage_parsed.value); // Subscription response var sub_val: ?std.json.Value = null; if (sub_resp) |ro| { if (usage.asString(ro.get("body"))) |bs| { if (std.json.parseFromSlice(std.json.Value, alloc, bs, .{})) |p| { sub_val = p.value; } else |_| {} } } const sub_unwrapped = unwrapEnvelope(sub_val); const sub_obj = usage.asObject(sub_unwrapped); const plan_type = if (sub_obj) |so| usage.stringOf(so.get("specCode")) else null; const instance_code = if (sub_obj) |so| usage.stringOf(so.get("instanceCode")) else null; const remaining_days = if (sub_obj) |so| usage.asNumber(so.get("remainingDays")) else null; // Quota config response var quota_val: ?std.json.Value = null; if (quota_resp) |ro| { if (usage.asString(ro.get("body"))) |bs| { if (std.json.parseFromSlice(std.json.Value, alloc, bs, .{})) |p| { quota_val = p.value; } else |_| {} } } const quota_notes = try parseAlibabaQuotaConfig(alloc, quota_val, plan_type); var notes = std.ArrayList([]const u8).empty; for (parsed.notes) |n| try notes.append(alloc, n); for (quota_notes) |n| try notes.append(alloc, n); if (usage.toInt(i64, remaining_days)) |rd| { const note = try std.fmt.allocPrint(alloc, "{d} days remaining in the plan", .{rd}); try notes.append(alloc, note); } if (card_resp) |ro| { const card_b_str = usage.asString(ro.get("body")); if (getResetCardsCount(card_b_str, alloc)) |count| { if (count > 0) { const note = try std.fmt.allocPrint(alloc, "{d} reset cards available", .{count}); try notes.append(alloc, note); } } } const err_msg: ?[]const u8 = if (parsed.limits.len == 0) "the token plan reported no windows" else null; return .{ .report = .{ .limits = parsed.limits, .planType = plan_type, .accountId = instance_code, .email = cred_email, .notes = notes.items, .error_message = err_msg, }, }; } pub const CliReportOutcome = struct { report: ?usage.ReportOutcome = null, reason: ?[]const u8 = null, }; /// The human part of a CLI failure. A JSON error body carries its own message and usually a hint — /// "No console access token found. Run `bl auth login --console`." — which is the action the user /// needs; anything else explains itself in its first non-empty line. JSON without a message says /// nothing useful, so the reason stays the bare exit code rather than quoting a brace. fn cliFailureDetail(alloc: std.mem.Allocator, text: []const u8) !?[]const u8 { if (std.json.parseFromSlice(std.json.Value, alloc, text, .{})) |parsed_val| { const root = usage.asObject(parsed_val.value); const err_obj = if (root) |r| usage.asObject(r.get("error")) else null; const message = if (err_obj) |e| usage.stringOf(e.get("message")) else null; if (message) |msg| { if (msg.len == 0) return null; if (err_obj) |e| { if (usage.stringOf(e.get("hint"))) |hint| { if (hint.len > 0) { const join: []const u8 = if (msg[msg.len - 1] == '.') " " else ". "; const joined: ?[]const u8 = try std.fmt.allocPrint(alloc, "{s}{s}{s}", .{ msg, join, hint }); return joined; } } } const bare: ?[]const u8 = msg; return bare; } return null; } else |_| {} var lines = std.mem.splitScalar(u8, text, '\n'); while (lines.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len > 0) { const found: ?[]const u8 = line[0..@min(line.len, 120)]; return found; } } return null; } /// A CLI that wrote to stderr before failing has already explained itself, and that explanation is /// more useful than the exit code: "bl is not on PATH" beats "exit 126". The body is provider-visible /// text, so it is trimmed to one bounded line before it goes into a report. fn cliFailureReason(alloc: std.mem.Allocator, status: i64, resp0: std.json.ObjectMap) ![]const u8 { if (usage.stringOf(resp0.get("body"))) |text| { if (try cliFailureDetail(alloc, text)) |detail| { const capped = detail[0..@min(detail.len, 160)]; return std.fmt.allocPrint(alloc, "bailian CLI failed (exit {d}): {s}", .{ status, capped }); } } return std.fmt.allocPrint(alloc, "bailian CLI failed (exit {d})", .{status}); } fn reportFromCli( alloc: std.mem.Allocator, cred_root: ?std.json.ObjectMap, resp0: std.json.ObjectMap, region: AlibabaRegion, ) !CliReportOutcome { const cred_account_id = if (cred_root) |cr| usage.stringOf(cr.get("accountId")) else null; const cred_email = if (cred_root) |cr| usage.stringOf(cr.get("email")) else null; const status = usage.toInt(i64, usage.asNumber(resp0.get("status"))) orelse -1; if (status != 0 and status != 200) { const reason = try cliFailureReason(alloc, status, resp0); return .{ .reason = reason }; } const body_str = usage.asString(resp0.get("body")); if (body_str) |bs| { if (std.json.parseFromSlice(std.json.Value, alloc, bs, .{})) |parsed_val| { const parsed = try parseAlibabaUsage(alloc, parsed_val.value); if (parsed.limits.len > 0) { const note = try std.fmt.allocPrint(alloc, "read through the bailian CLI ({s})", .{region.toString()}); const notes = try alloc.alloc([]const u8, 1); notes[0] = note; return .{ .report = .{ .limits = parsed.limits, .accountId = cred_account_id, .email = cred_email, .notes = notes, .error_message = null, }, }; } } else |_| {} } return .{ .reason = "the bailian CLI output carried no token-plan windows" }; } pub fn advance( alloc: std.mem.Allocator, credential_json: []const u8, responses_json: []const u8, _now_ms: i64, ) !usage.Outcome { _ = _now_ms; const cred_parsed = std.json.parseFromSlice(std.json.Value, alloc, credential_json, .{}) catch { return .{ .report = .{ .error_message = "invalid credential JSON" } }; }; const cred_root = usage.asObject(cred_parsed.value); const default_host = "https://bailian-singapore-cs.alibabacloud.com"; const session_val = if (cred_root) |cr| cr.get("session") else null; const session = parseConsoleSession(session_val, default_host); const region = regionFor(cred_root, session); const resp_parsed = std.json.parseFromSlice(std.json.Value, alloc, responses_json, .{}) catch { return .{ .report = .{ .error_message = "invalid responses JSON" } }; }; const resp_array = usage.asArray(resp_parsed.value) orelse { return .{ .report = .{ .error_message = "invalid responses JSON" } }; }; if (resp_array.items.len == 0) { // Round 1: Official CLI first const cli_args = try alibabaCliArguments(alloc, region); const requests = try alloc.alloc(usage.UsageRequest, 1); requests[0] = .{ .kind = .command, .command = "bl", .args = cli_args, .timeoutMs = ALIBABA_CLI_TIMEOUT_MS, }; return .{ .requests = requests }; } if (resp_array.items.len == 1) { const resp0_obj = usage.asObject(resp_array.items[0]); const status = if (resp0_obj) |ro| (usage.toInt(i64, usage.asNumber(ro.get("status"))) orelse -1) else -1; const unavailable = (status == 127); if (!unavailable) { if (resp0_obj) |ro| { const cli_outcome = try reportFromCli(alloc, cred_root, ro, region); if (cli_outcome.report) |rep| { return .{ .report = rep }; } if (session == null) { const notes = try alloc.alloc([]const u8, 1); notes[0] = "a captured console session would be the fallback"; return .{ .report = .{ .limits = &[_]usage.UsageLimit{}, .error_message = cli_outcome.reason orelse "the bailian CLI gave no usable output", .notes = notes, }, }; } } else if (session == null) { const notes = try alloc.alloc([]const u8, 1); notes[0] = "a captured console session would be the fallback"; return .{ .report = .{ .limits = &[_]usage.UsageLimit{}, .error_message = "the bailian CLI gave no usable output", .notes = notes, }, }; } } else if (session == null) { return .{ .report = .{ .limits = &[_]usage.UsageLimit{}, .error_message = "needs a session credential when the host runs no commands; capture a console HAR with prime-agent's scripts/capture-alibaba-session.mjs", }, }; } return .{ .requests = try consoleRequests(alloc, session.?, region) }; } if (session == null) { return .{ .report = .{ .limits = &[_]usage.UsageLimit{}, .error_message = "alibaba needs a cli or session credential", }, }; } return try reportFromConsole(alloc, cred_root, resp_array); }