Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/server/src/provider/Drivers/CursorDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ServerSettingsService } from "../../serverSettings.ts";
import { makeCursorTextGeneration } from "../../textGeneration/CursorTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeCursorAdapter } from "../Layers/CursorAdapter.ts";
import { readCursorUsageLimits } from "../Layers/cursorUsageLimits.ts";
import {
buildInitialCursorProviderSnapshot,
checkCursorProviderStatus,
Expand Down Expand Up @@ -139,7 +140,15 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
processEnv,
discoverModels,
).pipe(
Effect.flatMap((snapshot) =>
effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated"
? readCursorUsageLimits(effectiveConfig, processEnv).pipe(
Effect.map((usageLimits) => ({ ...snapshot, usageLimits })),
)
: Effect.succeed(snapshot),
),
Effect.map(stampIdentity),
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Expand Down
13 changes: 13 additions & 0 deletions apps/server/src/provider/Drivers/GrokDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
enrichGrokSnapshot,
} from "../Layers/GrokProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { readGrokUsageLimits } from "../Layers/grokUsageLimits.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
defaultProviderContinuationIdentity,
Expand Down Expand Up @@ -66,6 +67,8 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
const crypto = yield* Crypto.Crypto;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const httpClient = yield* HttpClient.HttpClient;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const serverSettings = yield* ServerSettingsService;
const { cwd } = yield* ServerConfig;
const eventLoggers = yield* ProviderEventLoggers;
Expand All @@ -90,7 +93,17 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe(
Effect.flatMap((snapshot) =>
Comment thread
maria-rcks marked this conversation as resolved.
effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated"
? readGrokUsageLimits(processEnv).pipe(
Effect.map((usageLimits) => ({ ...snapshot, usageLimits })),
)
: Effect.succeed(snapshot),
),
Effect.map(stampIdentity),
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);
Expand Down
19 changes: 15 additions & 4 deletions apps/server/src/provider/Drivers/OpenCodeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeOpenCodeAdapter } from "../Layers/OpenCodeAdapter.ts";
import { readOpenCodeGoUsageLimits } from "../Layers/openCodeUsageLimits.ts";
import {
checkOpenCodeProviderStatus,
makePendingOpenCodeProvider,
Expand Down Expand Up @@ -148,12 +149,22 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner),
);

const checkProvider = checkOpenCodeProviderStatus(
effectiveConfig,
serverConfig.cwd,
processEnv,
const checkProvider = Effect.all(
{
provider: checkOpenCodeProviderStatus(effectiveConfig, serverConfig.cwd, processEnv),
usageLimits: readOpenCodeGoUsageLimits({
enabled: effectiveConfig.enabled,
serverUrl: effectiveConfig.serverUrl,
environment: processEnv,
}),
},
{ concurrency: "unbounded" },
).pipe(
Effect.map(({ provider, usageLimits }) => ({ ...provider, usageLimits })),
Effect.map(stampIdentity),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, pathService),
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner),
Effect.provideService(OpenCodeRuntime, openCodeRuntime),
);
Expand Down
170 changes: 170 additions & 0 deletions apps/server/src/provider/Layers/CursorProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
} from "../Drivers/CursorSkills.ts";
import { execScriptSource, writeFakeCli } from "../../testUtils/fakeCli.ts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
import { cursorUsageResponseToLimits, readCursorUsageLimits } from "./cursorUsageLimits.ts";

const runNode = <A, E>(
effect: Effect.Effect<
Expand Down Expand Up @@ -952,3 +954,171 @@ describe("resolveCursorAcpConfigUpdates", () => {
]);
});
});

describe("Cursor usage limits", () => {
const checkedAt = "2026-09-16T00:00:00.000Z";

it("uses the advertised percentages and billing-cycle reset", () => {
const limits = cursorUsageResponseToLimits(
{
billingCycleEnd: "1789876386000",
planUsage: { totalPercentUsed: 72.4, autoPercentUsed: 69.5, apiPercentUsed: 100 },
},
checkedAt,
);
expect(limits.windows).toEqual(
expect.arrayContaining([
{
id: "totalPercentUsed",
kind: "monthly",
label: "Monthly",
usedPercent: 72.4,
resetsAt: "2026-09-20T03:53:06.000Z",
},
{
id: "autoPercentUsed",
kind: "monthly",
label: "Monthly · Auto",
usedPercent: 69.5,
resetsAt: "2026-09-20T03:53:06.000Z",
},
{
id: "apiPercentUsed",
kind: "monthly",
label: "Monthly · API",
usedPercent: 100,
resetsAt: "2026-09-20T03:53:06.000Z",
},
]),
);
});

it("does not invent unused allowance for absent buckets", () => {
expect(cursorUsageResponseToLimits({ planUsage: {} }, checkedAt).unavailable?.reason).toBe(
"unsupported",
);
expect(
cursorUsageResponseToLimits({ planUsage: { totalPercentUsed: 0 } }, checkedAt).windows,
).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Monthly", usedPercent: 0 }]);
expect(
cursorUsageResponseToLimits({ planUsage: { totalPercentUsed: 150 } }, checkedAt).windows,
).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Monthly", usedPercent: 100 }]);
});

it("reads the instance's credentials and endpoint even when usage enabled is false", async () => {
await runNode(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const directory = yield* fs.makeTempDirectoryScoped();
yield* fs.makeDirectory(path.join(directory, "cursor"));
yield* fs.writeFileString(
path.join(directory, "cursor", "auth.json"),
'{"accessToken":"instance-token"}',
);
const client = HttpClient.make((request) => {
expect(request.url).toBe(
"https://cursor.example/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
);
expect(request.method).toBe("POST");
expect(request.headers.authorization).toBe("Bearer instance-token");
expect(request.headers["connect-protocol-version"]).toBe("1");
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
Response.json({ enabled: false, planUsage: { totalPercentUsed: 42 } }),
),
);
});
yield* fs.makeDirectory(path.join(directory, ".cursor"));
yield* fs.writeFileString(
path.join(directory, ".cursor", "auth.json"),
'{"accessToken":"instance-token"}',
);
for (const platform of ["linux", "darwin"] as const) {
const limits = yield* readCursorUsageLimits(
{ apiEndpoint: "https://cursor.example/" },
{ XDG_CONFIG_HOME: directory, HOME: directory, AGENT_CLI_CREDENTIAL_STORE: "file" },
).pipe(
Effect.provideService(HostProcessPlatform, platform),
Effect.provideService(HttpClient.HttpClient, client),
);
expect(limits.windows[0]?.usedPercent).toBe(42);
}
}).pipe(Effect.scoped),
);
});

it("never reads stale files for keychain or memory logins, but accepts an explicit auth token", async () => {
for (const platform of ["linux", "darwin"] as const) {
for (const token of [undefined, "explicit-token"]) {
const limits = await runNode(
readCursorUsageLimits(
{ apiEndpoint: "" },
{
AGENT_CLI_CREDENTIAL_STORE: platform === "linux" ? "memory" : "default",
...(token ? { CURSOR_AUTH_TOKEN: token } : {}),
},
).pipe(
Effect.provideService(HostProcessPlatform, platform),
Effect.provideService(
FileSystem.FileSystem,
FileSystem.makeNoop({
readFileString: () => Effect.die("must not read an unrelated credential file"),
}),
),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) => {
expect(token).toBe("explicit-token");
expect(request.headers.authorization).toBe("Bearer explicit-token");
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
Response.json({ planUsage: { totalPercentUsed: 10 } }),
),
);
}),
),
),
);
if (token) expect(limits.windows[0]?.usedPercent).toBe(10);
else expect(limits.unavailable?.reason).toBe("unsupported");
}
}
});

it("reports failed requests without exposing credentials or response bodies", async () => {
const limits = await runNode(
readCursorUsageLimits({ apiEndpoint: "" }, { CURSOR_AUTH_TOKEN: "private-token" }).pipe(
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) =>
Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response("private response", { status: 401 }),
),
),
),
),
),
);
expect(limits.unavailable).toEqual({
reason: "probeFailed",
message: "Cursor could not read usage limits.",
});
});

it("does not use a stored login for an explicit API key", async () => {
const limits = await runNode(
readCursorUsageLimits({ apiEndpoint: "" }, { CURSOR_API_KEY: "different-account" }).pipe(
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make(() => Effect.die("must not request usage")),
),
),
);
expect(limits.unavailable?.reason).toBe("unsupported");
});
});
Loading
Loading