//! 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);
}