Skip to content

Commit fda014d

Browse files
committed
feat(opencode): read usage limits from the connected server
1 parent 5f2bb93 commit fda014d

10 files changed

Lines changed: 489 additions & 11 deletions

File tree

‎packages/contracts/src/providerUsageLimits.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export const ServerProviderUsageWindow = Schema.Struct({
1414
id: TrimmedNonEmptyString,
1515
kind: Schema.Literals(["session", "weekly", "monthly", "other"]),
1616
label: TrimmedNonEmptyString,
17+
/** When present, this limit applies only to these provider model IDs. */
18+
modelIds: Schema.optional(Schema.Array(TrimmedNonEmptyString)),
1719
usedPercent: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 100 })),
1820
resetsAt: Schema.optional(IsoDateTime),
1921
windowDurationMins: Schema.optional(NonNegativeInt),

‎packages/provider-core/src/server/usageLimits.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ describe("applyUsageLimitsUpdate", () => {
3333
},
3434
});
3535
expect(next).toBe(published);
36+
const scoped = { checkedAt, windows: [{ ...weekly, modelIds: ["opencode/a"] }] };
37+
expect(
38+
applyUsageLimitsUpdate({
39+
previous: scoped,
40+
checkedAt,
41+
update: { windows: [{ ...weekly, modelIds: ["opencode/a"] }] },
42+
}),
43+
).toBe(scoped);
44+
expect(
45+
applyUsageLimitsUpdate({
46+
previous: scoped,
47+
checkedAt,
48+
update: { windows: [{ ...weekly, modelIds: ["opencode/b"] }] },
49+
})?.windows[0]?.modelIds,
50+
).toEqual(["opencode/b"]);
3651
});
3752

3853
it("upserts by id and keeps the reset a percent-only update omits", () => {

‎packages/provider-core/src/server/usageLimits.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ function usageWindowEquals(a: ServerProviderUsageWindow, b: ServerProviderUsageW
9494
a.kind === b.kind &&
9595
a.label === b.label &&
9696
a.usedPercent === b.usedPercent &&
97+
(a.modelIds === b.modelIds ||
98+
(a.modelIds !== undefined &&
99+
b.modelIds !== undefined &&
100+
a.modelIds.length === b.modelIds.length &&
101+
a.modelIds.every((id, index) => id === b.modelIds?.[index]))) &&
97102
a.resetsAt === b.resetsAt &&
98103
a.windowDurationMins === b.windowDurationMins
99104
);

‎packages/provider-opencode/src/server/driver.test.ts‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,80 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => {
511511
}).pipe(Effect.scoped, Effect.provide(TestClock.layer())),
512512
);
513513

514+
it.effect.each([
515+
{ status: 200, data: null, reason: "unsupported" },
516+
{ status: 404, data: null, reason: "unsupported" },
517+
{ status: 503, data: null, reason: "probeFailed" },
518+
{
519+
status: 200,
520+
data: {
521+
workspace: {
522+
id: "remote-workspace",
523+
name: "Remote workspace",
524+
usageURL: "https://opencode.ai/console/remote-workspace",
525+
},
526+
budgets: [
527+
{
528+
id: "all",
529+
limit: 20,
530+
spent: 15,
531+
scope: "combined",
532+
per: "set",
533+
coverage: "all",
534+
models: [],
535+
shared: true,
536+
resetsAt: 1793491200000,
537+
},
538+
],
539+
},
540+
reason: undefined,
541+
},
542+
])(
543+
"reads external workspace usage without local credentials, HTTP $status, $reason",
544+
(scenario) =>
545+
Effect.gen(function* () {
546+
const requested: string[] = [];
547+
const replay = replayOpenCodeServer(OPENCODE_2_RESPONSES, "secret", requested);
548+
const server = HttpClient.make((request) => {
549+
assert.notInclude(request.url, "opencode.ai/zen");
550+
if (request.url.endsWith("/api/experimental/usage/limits")) {
551+
assert.strictEqual(request.headers.authorization, "Basic b3BlbmNvZGU6c2VjcmV0");
552+
return Effect.succeed(
553+
HttpClientResponse.fromWeb(
554+
request,
555+
Response.json(
556+
{
557+
location: { directory: "/work" },
558+
data: scenario.data,
559+
},
560+
{ status: scenario.status },
561+
),
562+
),
563+
);
564+
}
565+
return replay.execute(request);
566+
});
567+
const instance = yield* create(
568+
{ serverUrl: "https://remote.example", serverPassword: "secret" },
569+
server,
570+
);
571+
const [snapshot] = Array.from(
572+
yield* instance.snapshot.streamChanges.pipe(
573+
Stream.filter((snapshot) => snapshot.usageLimits !== undefined),
574+
Stream.take(1),
575+
Stream.runCollect,
576+
),
577+
);
578+
assert.strictEqual(snapshot?.usageLimits?.unavailable?.reason, scenario.reason);
579+
if (scenario.reason === undefined) {
580+
assert.strictEqual(snapshot?.usageLimits?.windows[0]?.usedPercent, 75);
581+
assert.strictEqual(snapshot?.usageLimits?.externalUsage?.label, "Remote workspace");
582+
} else {
583+
assert.deepStrictEqual(snapshot?.usageLimits?.windows, []);
584+
}
585+
}).pipe(Effect.scoped),
586+
);
587+
514588
it.effect.each(
515589
[
516590
{ name: "recovers" },
@@ -550,6 +624,10 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => {
550624
);
551625
}
552626
const path = new URL(request.url).pathname;
627+
if (path === "/api/experimental/usage/limits")
628+
return Effect.succeed(
629+
HttpClientResponse.fromWeb(request, new Response(null, { status: 404 })),
630+
);
553631
if (path === "/api/model") modelReads += 1;
554632
const body =
555633
path === "/api/info"

‎packages/provider-opencode/src/server/driver.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as Digest from "effect/crypto/Digest";
44
import * as SecureRandom from "effect/crypto/SecureRandom";
55
import * as Deferred from "effect/Deferred";
66
import * as Effect from "effect/Effect";
7+
import * as DateTime from "effect/DateTime";
78
import * as Option from "effect/Option";
89
import * as FileSystem from "effect/FileSystem";
910
import * as Path from "effect/Path";
@@ -385,7 +386,7 @@ export const OpenCodeDriver: ProviderDriver<
385386
),
386387
);
387388

388-
const checkUsageLimits = readOpenCodeGoUsageLimits({
389+
const legacyUsageLimits = readOpenCodeGoUsageLimits({
389390
enabled: effectiveConfig.enabled,
390391
serverUrl: effectiveConfig.serverUrl,
391392
environment: processEnv,
@@ -395,6 +396,30 @@ export const OpenCodeDriver: ProviderDriver<
395396
Effect.provideService(HttpClient.HttpClient, httpClient),
396397
Effect.provideService(Digest.Digest, digest),
397398
);
399+
let unsupportedUsageVersion: string | undefined;
400+
const checkUsageLimits = Effect.gen(function* () {
401+
if (!effectiveConfig.enabled) return yield* legacyUsageLimits;
402+
const runtime = yield* runtimeProbe.get;
403+
if (runtime.generation === "v1" || runtime.version === unsupportedUsageVersion)
404+
return yield* legacyUsageLimits;
405+
const limits = yield* openCode2Server.withConnection((connection) =>
406+
connection.usageLimits(host.paths.cwd),
407+
);
408+
if (limits === undefined) unsupportedUsageVersion = runtime.version;
409+
return limits ?? (yield* legacyUsageLimits);
410+
}).pipe(
411+
Effect.timeout("10 seconds"),
412+
Effect.catch(() =>
413+
Effect.map(DateTime.now, (now) => ({
414+
checkedAt: DateTime.formatIso(now),
415+
windows: [],
416+
unavailable: {
417+
reason: "probeFailed" as const,
418+
message: "OpenCode could not read workspace usage.",
419+
},
420+
})),
421+
),
422+
);
398423
const checkProvider = Effect.all(
399424
{
400425
provider: checkOpenCodeProviderStatus(

‎packages/provider-opencode/src/server/v2/OpenCode2Client.test.ts‎

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,176 @@ describe("OpenCode2Client message history", () => {
167167
}),
168168
);
169169
});
170+
171+
describe("OpenCode2Client usage limits", () => {
172+
it.effect("reads the connected workspace and maps Go and Console budgets", () =>
173+
Effect.gen(function* () {
174+
let workspace = "wrk_remote";
175+
const transport = HttpClient.make((request) => {
176+
assert.strictEqual(
177+
request.url,
178+
"https://remote.example/prefix/api/experimental/usage/limits",
179+
);
180+
assert.strictEqual(request.headers.authorization, utf8Basic("remote-password"));
181+
assert.deepStrictEqual(Array.from(request.urlParams), [
182+
["location[directory]", "/remote/project"],
183+
]);
184+
return Effect.succeed(
185+
HttpClientResponse.fromWeb(
186+
request,
187+
Response.json({
188+
location: { directory: "/remote/project" },
189+
data: {
190+
workspace: {
191+
id: workspace,
192+
name: workspace,
193+
usageURL: "https://opencode.ai/console/" + workspace,
194+
},
195+
go: {
196+
rolling: { percent: 23, limited: false, resetsAt: 1793491200000 },
197+
weekly: { percent: 99, limited: true, resetsAt: 1793491200000 },
198+
monthly: { percent: 12, limited: false, resetsAt: 1793491200000 },
199+
},
200+
budgets: [
201+
{
202+
id: "all",
203+
limit: 20,
204+
spent: 15,
205+
scope: "combined",
206+
per: "set",
207+
coverage: "all",
208+
models: [],
209+
vendors: [],
210+
picked: false,
211+
shared: true,
212+
resetsAt: 1793491200000,
213+
},
214+
{
215+
id: "models",
216+
limit: 10,
217+
spent: 12,
218+
scope: "each",
219+
per: "model",
220+
coverage: "models",
221+
models: [
222+
{ providerID: "opencode", modelID: "a", name: "Model A", spent: 12 },
223+
{ providerID: "opencode", modelID: "b", name: "Model B", spent: 2 },
224+
],
225+
vendors: [],
226+
picked: true,
227+
shared: false,
228+
resetsAt: 1793491200000,
229+
},
230+
{
231+
id: "zz_missing",
232+
limit: 10,
233+
spent: 10,
234+
scope: "each",
235+
per: "model",
236+
coverage: "models",
237+
models: [{ providerID: "opencode", modelID: "unknown", name: "Unknown spend" }],
238+
shared: false,
239+
resetsAt: 1793491200000,
240+
},
241+
{
242+
id: "zero",
243+
limit: 0,
244+
spent: 0,
245+
scope: "each",
246+
per: "set",
247+
coverage: "all",
248+
models: [],
249+
vendors: [],
250+
picked: false,
251+
shared: false,
252+
resetsAt: 1793491200000,
253+
},
254+
],
255+
},
256+
}),
257+
),
258+
);
259+
});
260+
const opencode = yield* OpenCode2Client.make.pipe(
261+
Effect.provideService(HttpClient.HttpClient, transport),
262+
);
263+
const api = yield* opencode.connect({
264+
baseUrl: "https://remote.example/prefix",
265+
password: "remote-password",
266+
});
267+
const first = yield* api.usageLimits("/remote/project");
268+
assert.isDefined(first);
269+
assert.deepStrictEqual(
270+
first?.windows.map((window) => window.usedPercent),
271+
[23, 100, 12, 75, 100, 20, 100, 100],
272+
);
273+
assert.isTrue(
274+
first?.windows.every((window) => window.resetsAt === "2026-11-01T00:00:00.000Z"),
275+
);
276+
assert.include(first?.windows[3]?.label ?? "", "Shared");
277+
assert.include(first?.windows[4]?.label ?? "", "Model A");
278+
assert.deepStrictEqual(first?.windows[4]?.modelIds, ["opencode/a"]);
279+
assert.strictEqual(first?.windows[3]?.modelIds, undefined);
280+
assert.deepStrictEqual(first?.windows[7]?.modelIds, []);
281+
assert.include(first?.windows[7]?.label ?? "", "Most used model");
282+
assert.strictEqual(first?.credentialFingerprint, undefined);
283+
workspace = "wrk_switched";
284+
const next = yield* api.usageLimits("/remote/project");
285+
assert.strictEqual(next?.externalUsage?.label, "wrk_switched");
286+
assert.strictEqual(next?.externalUsage?.url, "https://opencode.ai/console/wrk_switched");
287+
}),
288+
);
289+
290+
it.effect("recognizes an older server's SPA fallback", () =>
291+
Effect.gen(function* () {
292+
const opencode = yield* OpenCode2Client.make.pipe(
293+
Effect.provideService(
294+
HttpClient.HttpClient,
295+
HttpClient.make((request) =>
296+
Effect.succeed(
297+
HttpClientResponse.fromWeb(
298+
request,
299+
new Response("<!doctype html><title>OpenCode</title>", {
300+
headers: { "content-type": "text/html; charset=utf-8" },
301+
}),
302+
),
303+
),
304+
),
305+
),
306+
);
307+
const api = yield* opencode.connect({ baseUrl: "https://old.example", password: "secret" });
308+
assert.strictEqual(yield* api.usageLimits("/remote"), undefined);
309+
}),
310+
);
311+
312+
it.effect("distinguishes missing endpoints, signed out, and failed remote probes", () =>
313+
Effect.gen(function* () {
314+
for (const [status, body, reason] of [
315+
[404, {}, undefined],
316+
[405, {}, undefined],
317+
[200, { location: { directory: "/remote" }, data: null }, "unsupported"],
318+
[503, {}, "probeFailed"],
319+
[401, {}, "probeFailed"],
320+
[200, { data: { workspace: {} } }, "probeFailed"],
321+
] as const) {
322+
const opencode = yield* OpenCode2Client.make.pipe(
323+
Effect.provideService(
324+
HttpClient.HttpClient,
325+
HttpClient.make((request) =>
326+
Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(body, { status }))),
327+
),
328+
),
329+
);
330+
const api = yield* opencode.connect({
331+
baseUrl: "https://remote.example",
332+
password: "secret",
333+
});
334+
const result = yield* api.usageLimits("/remote");
335+
if (reason === undefined) assert.strictEqual(result, undefined);
336+
else assert.strictEqual(result?.unavailable?.reason, reason);
337+
if (status === 200 && reason === "unsupported")
338+
assert.include(result?.unavailable?.message ?? "", "signed in");
339+
}
340+
}),
341+
);
342+
});

‎packages/provider-opencode/src/server/v2/OpenCode2Client.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import * as HttpClientError from "effect/http/HttpClientError";
1414
import * as HttpClientRequest from "effect/http/HttpClientRequest";
1515
import * as HttpClientResponse from "effect/http/HttpClientResponse";
1616

17+
import { readServerUsageLimits } from "./usageLimits.ts";
18+
1719
/** OpenCode 2 accepts only HTTP Basic auth, always with this user name. */
1820
const OPENCODE_USERNAME = "opencode";
1921

@@ -53,6 +55,9 @@ export type OpenCode2StreamEvent =
5355
/** events resolves on subscription acceptance; subscribe before work because the stream is volatile. */
5456
export interface OpenCode2Api {
5557
readonly client: OpenCodeClient;
58+
readonly usageLimits: (
59+
directory: string,
60+
) => Effect.Effect<import("@t3tools/contracts").ServerProviderUsageLimits | undefined>;
5661
readonly events: Effect.Effect<
5762
Stream.Stream<
5863
OpenCode2StreamEvent,
@@ -212,6 +217,13 @@ export const make = Effect.gen(function* () {
212217
Effect.provideService(HttpClient.HttpClient, fillCompactionRecents(authenticated)),
213218
Effect.map((client) => ({
214219
client,
220+
usageLimits: (directory) =>
221+
readServerUsageLimits(directory).pipe(
222+
Effect.provideService(
223+
HttpClient.HttpClient,
224+
HttpClient.mapRequest(authenticated, HttpClientRequest.prependUrl(baseUrl)),
225+
),
226+
),
215227
events: readEvents.pipe(
216228
Effect.provideService(
217229
HttpClient.HttpClient,

0 commit comments

Comments
 (0)