1import type { ServerProviderUsageWindow } from "@t3tools/contracts";
2import {
3 clampPercent,
4 makeUnavailableUsageLimits,
5 makeUsageLimits,
6} from "@t3tools/provider-core/server/usageLimits";
7import * as DateTime from "effect/DateTime";
8import * as Effect from "effect/Effect";
9import * as Schema from "effect/Schema";
10import * as HttpClient from "effect/http/HttpClient";
11import * as HttpClientResponse from "effect/http/HttpClientResponse";
12
13const Instant = Schema.Finite.check(
14 Schema.isBetween({ minimum: -8640000000000000, maximum: 8640000000000000 }),
15);
16const Meter = Schema.Struct({
17 percent: Schema.Finite,
18 limited: Schema.Boolean,
19 resetsAt: Instant,
20});
21const BudgetModel = Schema.Struct({
22 providerID: Schema.String,
23 modelID: Schema.String,
24 name: Schema.String,
25 spent: Schema.optionalKey(Schema.Finite),
26});
27// Decode only fields needed by T3, independently of the pinned 2.0 client.
28const ResponseBody = Schema.Struct({
29 location: Schema.Struct({ directory: Schema.String }),
30 data: Schema.NullOr(
31 Schema.Struct({
32 workspace: Schema.Struct({
33 id: Schema.String,
34 name: Schema.optionalKey(Schema.String),
35 usageURL: Schema.String,
36 }),
37 go: Schema.optionalKey(Schema.Struct({ rolling: Meter, weekly: Meter, monthly: Meter })),
38 budgets: Schema.Array(
39 Schema.Struct({
40 id: Schema.String,
41 limit: Schema.Finite,
42 spent: Schema.Finite,
43 scope: Schema.Literals(["each", "combined"]),
44 per: Schema.Literals(["set", "model"]),
45 coverage: Schema.Literals(["all", "models"]),
46 models: Schema.Array(BudgetModel),
47 shared: Schema.Boolean,
48 resetsAt: Instant,
49 }),
50 ),
51 }),
52 ),
53});
54
55const Providers = Schema.Struct({
56 data: Schema.Array(
57 Schema.Struct({
58 id: Schema.String,
59 integrationID: Schema.optionalKey(Schema.String),
60 }),
61 ),
62});
63
64const resetTime = (ms: number) => DateTime.formatIso(DateTime.makeUnsafe(ms));
65const budgetPercent = (spent: number, limit: number) =>
66 limit <= 0 ? 100 : clampPercent((spent / limit) * 100);
67
68/** Undefined alone permits the legacy credential reader; null data is authoritative. */
69export const readServerUsageLimits = Effect.fn("OpenCode2Client.usageLimits")(function* (
70 directory: string,
71) {
72 const checkedAt = DateTime.formatIso(yield* DateTime.now);
73 return yield* Effect.gen(function* () {
74 const response = yield* HttpClient.get("/api/experimental/usage/limits", {
75 urlParams: { "location[directory]": directory },
76 });
77 if (
78 response.status === 404 ||
79 response.status === 405 ||
80 (response.status === 200 &&
81 response.headers["content-type"]?.split(";")[0]?.trim() === "text/html")
82 )
83 return undefined;
84 const body = yield* HttpClientResponse.filterStatusOk(response).pipe(
85 Effect.flatMap(HttpClientResponse.schemaBodyJson(ResponseBody)),
86 );
87 if (body.data === null)
88 return makeUnavailableUsageLimits({
89 checkedAt,
90 reason: "unsupported",
91 message: "No OpenCode Console workspace is signed in.",
92 });
93 const { go, budgets, workspace } = body.data;
94 const consoleProviders = budgets.some(
95 (budget) => budget.coverage === "all" && budget.per === "set",
96 )
97 ? yield* HttpClient.get("/api/provider", {
98 urlParams: { "location[directory]": directory },
99 }).pipe(
100 Effect.flatMap(HttpClientResponse.filterStatusOk),
101 Effect.flatMap(HttpClientResponse.schemaBodyJson(Providers)),
102 Effect.map((page) =>
103 page.data
104 .filter(
105 (provider) =>
106 provider.integrationID === "opencode" && provider.id !== "opencode-go",
107 )
108 .map((provider) => provider.id),
109 ),
110 Effect.orElseSucceed(() => []),
111 )
112 : [];
113 const windows: ServerProviderUsageWindow[] = [];
114 if (go) {
115 for (const [key, kind, label, duration] of [
116 ["rolling", "session", "Session", 300],
117 ["weekly", "weekly", "Weekly", 10080],
118 ["monthly", "monthly", "Monthly", undefined],
119 ] as const) {
120 const meter = go[key];
121 windows.push({
122 id: `go_${key}`,
123 kind,
124 label: `Go · ${label}`,
125 usedPercent: meter.limited ? 100 : clampPercent(meter.percent),
126 resetsAt: resetTime(meter.resetsAt),
127 ...(duration === undefined ? {} : { windowDurationMins: duration }),
128 });
129 }
130 }
131 for (const budget of budgets) {
132 const scope = budget.shared || budget.scope === "combined" ? "Shared" : "Individual";
133 const covered =
134 budget.coverage === "all"
135 ? "All spend"
136 : budget.models.map((model) => model.name).join(", ") || "Models";
137 if (
138 budget.per === "model" &&
139 budget.coverage === "models" &&
140 budget.models.length > 0 &&
141 budget.models.every((model) => model.spent !== undefined)
142 ) {
143 for (const model of budget.models) {
144 windows.push({
145 id: `budget_${budget.id}_${model.providerID}_${model.modelID}`,
146 kind: "other",
147 label: `Console · ${scope} · ${model.name}`,
148 modelIds: [`${model.providerID}/${model.modelID}`],
149 usedPercent: budgetPercent(model.spent ?? budget.spent, budget.limit),
150 resetsAt: resetTime(budget.resetsAt),
151 });
152 }
153 } else {
154 windows.push({
155 id: `budget_${budget.id}`,
156 kind: "other",
157 label: `Console · ${scope} · ${covered}${budget.per === "model" ? " · Most used model" : ""}`,
158 // Aggregate per-model spend cannot identify the exhausted model.
159 ...(budget.per === "model"
160 ? { modelIds: [] }
161 : budget.coverage === "models"
162 ? {
163 modelIds: budget.models.map((model) => `${model.providerID}/${model.modelID}`),
164 }
165 : { providerIds: consoleProviders }),
166 usedPercent: budgetPercent(budget.spent, budget.limit),
167 resetsAt: resetTime(budget.resetsAt),
168 });
169 }
170 }
171 const usageURL = URL.parse(workspace.usageURL);
172 return {
173 ...makeUsageLimits({ checkedAt, windows }),
174 ...(usageURL?.protocol === "http:" || usageURL?.protocol === "https:"
175 ? { externalUsage: { label: workspace.name?.trim() || workspace.id, url: usageURL.href } }
176 : {}),
177 };
178 }).pipe(
179 Effect.timeout("5 seconds"),
180 Effect.orElseSucceed(() =>
181 makeUnavailableUsageLimits({
182 checkedAt,
183 reason: "probeFailed",
184 message: "OpenCode could not read workspace usage.",
185 }),
186 ),
187 );
188});