//! OpenCode Go usage feed implementation.
//!
//! Mirrors prime-agent:packages/ai/src/utils/usage/providers/opencode-go.ts field for field.
const std = @import("std");
const usage = @import("../usage.zig");
const iso8601 = @import("../iso8601.zig");
pub const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
const WindowDef = struct {
key: []const u8,
label: []const u8,
windowLabel: []const u8,
windowSeconds: i64,
};
const OPENCODE_WINDOWS = [_]WindowDef{
.{ .key = "rolling", .label = "Session", .windowLabel = "5h window", .windowSeconds = 5 * 3600 },
.{ .key = "weekly", .label = "Weekly", .windowLabel = "7d window", .windowSeconds = 7 * 86_400 },
.{ .key = "monthly", .label = "Monthly", .windowLabel = "30d window", .windowSeconds = 30 * 86_400 },
};
pub fn parseOpenCodeGoUsage(alloc: std.mem.Allocator, payload: ?std.json.Value) ![]const usage.UsageLimit {
const root = usage.asObject(payload);
const usage_obj = if (root) |r| usage.asObject(r.get("usage")) else null;
if (usage_obj == null) return &[_]usage.UsageLimit{};
var limits = std.ArrayList(usage.UsageLimit).empty;
for (OPENCODE_WINDOWS) |win| {
const raw_window = usage.asObject(usage_obj.?.get(win.key)) orelse continue;
const used_percent = usage.asNumber(raw_window.get("percent"));
const resets_at_str = usage.asString(raw_window.get("resetsAt"));
const resets_at: ?i64 = if (resets_at_str) |s| iso8601.parseIso8601(s) else null;
const raw_status = usage.asString(raw_window.get("status"));
const is_rate_limited = if (raw_status) |rs| blk: {
const lower = try alloc.alloc(u8, rs.len);
_ = std.ascii.lowerString(lower, rs);
break :blk std.mem.eql(u8, lower, "rate-limited") or std.mem.eql(u8, lower, "exhausted");
} else false;
const status: []const u8 = if (is_rate_limited or (used_percent != null and used_percent.? >= 100.0))
"exhausted"
else
usage.statusFor(used_percent);
try limits.append(alloc, .{
.id = win.key,
.label = win.label,
.usedPercent = used_percent,
.windowLabel = win.windowLabel,
.windowSeconds = win.windowSeconds,
.resetsAt = resets_at,
.status = status,
});
}
return limits.items;
}
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) orelse {
return .{ .report = .{ .error_message = "invalid credential JSON" } };
};
const kind = usage.asString(cred_root.get("kind")) orelse "";
const is_oauth = std.mem.eql(u8, kind, "oauth");
const is_api_key = std.mem.eql(u8, kind, "apiKey");
if (!is_oauth and !is_api_key) {
return .{ .report = .{ .error_message = "opencode-go needs an apiKey or OAuth credential" } };
}
const api_key = if (is_oauth)
usage.asString(cred_root.get("access")) orelse ""
else
usage.asString(cred_root.get("apiKey")) orelse "";
const cred_account_id = usage.asString(cred_root.get("accountId"));
const cred_email = usage.asString(cred_root.get("email"));
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) {
var headers = std.ArrayList([2][]const u8).empty;
const auth_hdr = try std.fmt.allocPrint(alloc, "Bearer {s}", .{api_key});
try headers.append(alloc, .{ "Authorization", auth_hdr });
try headers.append(alloc, .{ "Accept", "application/json" });
try headers.append(alloc, .{ "User-Agent", "OpenCode-Status-Plugin/1.0" });
const requests = try alloc.alloc(usage.UsageRequest, 1);
requests[0] = .{
.method = "GET",
.url = OPENCODE_GO_USAGE_URL,
.headers = headers.items,
};
return .{ .requests = requests };
}
const first = switch (try usage.parseFirstResponse(alloc, resp_array, "usage body is not JSON")) {
.ok => |f| f,
.err => |msg| return .{ .report = .{ .error_message = msg } },
};
const body_parsed = first.parsed;
const limits = try parseOpenCodeGoUsage(alloc, body_parsed.value);
return .{
.report = .{
.planType = "Go",
.accountId = cred_account_id,
.email = cred_email,
.limits = limits,
.notes = &[_][]const u8{"OpenCode Go unified models"},
.error_message = null,
},
};
}