Skip to content
Closed
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
6 changes: 5 additions & 1 deletion apps/mobile/src/features/threads/ComposerUsageLimits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { SymbolView } from "../../components/AppSymbol";
import { AppText as Text } from "../../components/AppText";
import { AccountLimits, ResetCredits } from "../usage/UsageLimitsSection";

const DRIVER_LABEL: Partial<Record<string, string>> = { codex: "Codex", claudeAgent: "Claude" };
const DRIVER_LABEL: Partial<Record<string, string>> = {
antigravity: "Antigravity",
claudeAgent: "Claude",
codex: "Codex",
};

/**
* The /usage-limits result, docked above the composer. It is the Usage → Limits
Expand Down
6 changes: 5 additions & 1 deletion apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import { environmentPresentations } from "../../state/presentation";
import { ResetCredits } from "./UsageLimitsSection";
import { useProviderColors } from "./usageProviders";

const DRIVER_LABEL: Partial<Record<string, string>> = { codex: "Codex", claudeAgent: "Claude" };
const DRIVER_LABEL: Partial<Record<string, string>> = {
antigravity: "Antigravity",
claudeAgent: "Claude",
codex: "Codex",
};
const PACE_LABEL = { ahead: "Ahead of pace", on: "On pace", under: "Under pace" } as const;

function accountName(account: LimitAccount) {
Expand Down
51 changes: 51 additions & 0 deletions apps/server/src/provider/Drivers/AntigravityDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
Expand All @@ -33,6 +34,7 @@ import {
ANTIGRAVITY_AUTH_STDOUT_PREFIX,
resolveAntigravityProfileDirectory,
resolveAntigravityRuntimeTempDirectory,
resolveAntigravityTokenPath,
} from "../antigravityAuthSupport.ts";
import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import * as ModelManifest from "../ModelManifest.ts";
Expand Down Expand Up @@ -258,6 +260,36 @@ const testLayer = ServerConfig.layerTest(process.cwd(), {
),
Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
Layer.provideMerge(ModelManifest.layerTest),
Layer.provideMerge(
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => {
if (!request.url.includes("retrieveUserQuotaSummary")) {
return Effect.die("Antigravity health check must not make this HTTP request");
}
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
Response.json({
groups: [
{
displayName: "Gemini Models",
buckets: [
{
bucketId: "gemini-5h",
window: "5h",
remainingFraction: 0.5,
resetTime: "2026-09-18T17:00:00.000Z",
},
],
},
],
}),
),
);
}),
),
),
);

it.layer(testLayer)("AntigravityDriver", (it) => {
Expand Down Expand Up @@ -529,8 +561,27 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
const snapshot = yield* h.instance.snapshot.refresh;
expect(snapshot.installed).toBe(true);
expect(snapshot.version).toBe(h.first.version);
expect(snapshot.usageLimits?.unavailable?.reason).toBe("unsupported");
expect(h.launches).toEqual([]);
expect(h.acquisitions).toEqual([]);
}).pipe(Effect.scoped),
);

it.effect(
"reads Cloud Code usage limits from the instance token without launching a process",
() =>
Effect.gen(function* () {
const h = yield* makeHarness({ enabled: true });
const tokenPath = resolveAntigravityTokenPath(h.profileDirectory);
yield* h.fs.makeDirectory(h.path.dirname(tokenPath), { recursive: true });
yield* h.fs.writeFileString(tokenPath, '{"token":{"access_token":"ya29.test"}}');
const snapshot = yield* h.instance.snapshot.refresh;
expect(snapshot.installed).toBe(true);
expect(h.launches).toEqual([]);
expect(snapshot.usageLimits?.windows.map((window) => window.id)).toEqual([
"gemini_five_hour",
]);
expect(snapshot.usageLimits?.windows[0]?.usedPercent).toBe(50);
}).pipe(Effect.scoped),
);
});
14 changes: 14 additions & 0 deletions apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import { HttpClient } from "effect/unstable/http";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import type { AcpError } from "effect-acp/errors";

Expand All @@ -35,6 +36,7 @@ import {
prepareAntigravityProfile,
resolveAntigravityProfileDirectory,
resolveAntigravityRuntimeTempDirectory,
resolveAntigravityTokenPath,
type AntigravityAuthConfig,
} from "../antigravityAuthSupport.ts";
import {
Expand All @@ -50,6 +52,7 @@ import {
import { ProviderDriverError } from "../Errors.ts";
import { makeAntigravityAdapter } from "../Layers/AntigravityAdapter.ts";
import { makeAntigravityProvider } from "../Layers/AntigravityProvider.ts";
import { readAntigravityUsageLimits } from "../Layers/antigravityUsageLimits.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import * as ModelManifest from "../ModelManifest.ts";
import {
Expand All @@ -71,6 +74,7 @@ export type AntigravityDriverEnv =
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| ModelManifest.ModelManifest
| Path.Path
| ProviderEventLoggers
Expand All @@ -91,6 +95,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const serverConfig = yield* ServerConfig;
const installation = yield* AntigravityInstallation;
const httpClient = yield* HttpClient.HttpClient;
const loggers = yield* ProviderEventLoggers;
const modelManifest = yield* ModelManifest.ModelManifest;
const settings = { ...config, enabled } satisfies AntigravitySettings;
Expand Down Expand Up @@ -362,6 +367,15 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
Effect.provideService(Path.Path, path),
Effect.orElseSucceed(() => false),
),
usageLimits: readAntigravityUsageLimits({
Comment thread
lukasbxy marked this conversation as resolved.
enabled: settings.enabled,
authMethod: auth.authMethod,
tokenPath: resolveAntigravityTokenPath(profileDirectory),
...(auth.gcpProject.trim() ? { gcpProject: auth.gcpProject } : {}),
}).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(HttpClient.HttpClient, httpClient),
),
}).pipe(
Effect.mapError(
(cause) =>
Expand Down
96 changes: 95 additions & 1 deletion apps/server/src/provider/Layers/AntigravityProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ProviderDriverKind,
ProviderInstanceId,
ProviderSetupError,
type ServerProviderUsageLimits,
} from "@t3tools/contracts";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
Expand All @@ -20,6 +21,7 @@ import type * as EffectAcpSchema from "effect-acp/schema";
import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import type { AcpSessionRuntimeStartResult } from "../acp/AcpSessionRuntime.ts";
import { makeUnavailableUsageLimits, makeUsageLimits } from "../providerUsageLimits.ts";
import {
buildAntigravityModelsFromSession,
makeAntigravityProvider,
Expand Down Expand Up @@ -111,7 +113,11 @@ const testLayer = Layer.merge(
type ProbeError = EffectAcpErrors.AcpError | ProviderSetupError;

const makeHarness = Effect.fn("makeAntigravityProviderHarness")(function* (
options: { readonly enabled?: boolean; readonly safe?: boolean } = {},
options: {
readonly enabled?: boolean;
readonly safe?: boolean;
readonly usageLimits?: Effect.Effect<ServerProviderUsageLimits>;
} = {},
) {
const initialProbe = yield* Deferred.make<EffectAcpSchema.InitializeResponse, ProbeError>();
const probeCalls = yield* Ref.make(0);
Expand All @@ -132,6 +138,7 @@ const makeHarness = Effect.fn("makeAntigravityProviderHarness")(function* (
Effect.andThen(Ref.get(safety)),
Effect.flatten,
),
...(options.usageLimits ? { usageLimits: options.usageLimits } : {}),
},
);
const initialUpdate = yield* Stream.toPull(
Expand Down Expand Up @@ -305,6 +312,93 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {
),
);

it.effect("publishes subscription limits from the health check and drops them on sign-out", () =>
Effect.scoped(
Effect.gen(function* () {
const limits = makeUsageLimits({
checkedAt: "2026-09-18T12:00:00.000Z",
windows: [
{
id: "gemini_five_hour",
kind: "session",
label: "Gemini · Session",
usedPercent: 12,
windowDurationMins: 5 * 60,
},
],
});
const usageCalls = yield* Ref.make(0);
const harness = yield* makeHarness({
usageLimits: Ref.update(usageCalls, (count) => count + 1).pipe(Effect.as(limits)),
});
expect(yield* Ref.get(usageCalls)).toBe(0);
yield* harness.initialize;
expect(yield* Ref.get(usageCalls)).toBe(1);
expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toEqual(limits);
yield* harness.provider.onSessionStarted(started);
expect(yield* Ref.get(usageCalls)).toBe(2);
expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toEqual(limits);
yield* harness.provider.onSignedOut;
expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toBeUndefined();
}),
),
);

it.effect("replaces an unsupported probe with live limits when a session starts", () =>
Effect.scoped(
Effect.gen(function* () {
const limits = makeUsageLimits({
checkedAt: "2026-09-18T12:00:00.000Z",
windows: [
{
id: "gemini_five_hour",
kind: "session",
label: "Gemini · Session",
usedPercent: 40,
windowDurationMins: 5 * 60,
},
],
});
const usageCalls = yield* Ref.make(0);
const harness = yield* makeHarness({
usageLimits: Ref.update(usageCalls, (count) => count + 1).pipe(
Effect.flatMap((count) =>
count === 1
? Effect.succeed(
makeUnavailableUsageLimits({
checkedAt: "2026-09-18T11:00:00.000Z",
reason: "unsupported",
}),
)
: Effect.succeed(limits),
),
),
});
yield* harness.initialize;
expect(yield* Ref.get(usageCalls)).toBe(1);
yield* harness.provider.onSessionStarted(started);
expect(yield* Ref.get(usageCalls)).toBe(2);
expect((yield* harness.provider.snapshot.getSnapshot).usageLimits).toEqual(limits);
}),
),
);

it.effect("does not read usage limits while disabled", () =>
Effect.scoped(
Effect.gen(function* () {
const usageCalls = yield* Ref.make(0);
const harness = yield* makeHarness({
enabled: false,
usageLimits: Ref.update(usageCalls, (count) => count + 1).pipe(
Effect.as(makeUsageLimits({ checkedAt: "2026-09-18T12:00:00.000Z", windows: [] })),
),
});
yield* harness.provider.snapshot.refresh;
expect(yield* Ref.get(usageCalls)).toBe(0);
}),
),
);

it.effect("publishes session metadata and native commands without another health probe", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
55 changes: 33 additions & 22 deletions apps/server/src/provider/Layers/AntigravityProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type ServerProvider,
type ServerProviderModel,
type ServerProviderSlashCommand,
type ServerProviderUsageLimits,
} from "@t3tools/contracts";
import { createModelCapabilities } from "@t3tools/shared/model";
import * as DateTime from "effect/DateTime";
Expand Down Expand Up @@ -126,6 +127,11 @@ interface AntigravityProviderOptions {
readonly maintenanceCapabilities?: ProviderMaintenanceCapabilities;
/** Auth type and label published once a session authenticates. */
readonly auth?: { readonly type: string; readonly label: string };
/**
* Subscription windows. Must not spawn the ACP agent; the health check
* already refuses to, because each launch unpacks about 1 GB.
*/
readonly usageLimits?: Effect.Effect<ServerProviderUsageLimits>;
}

/** Health uses initialize only. Session callbacks supply account-specific metadata. */
Expand Down Expand Up @@ -172,9 +178,12 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
const checkProvider = Effect.fn("checkAntigravityProvider")(function* () {
if (!settings.enabled) return yield* getSnapshot;
const before = yield* SubscriptionRef.get(metadata);
const result = yield* options.probe.pipe(
Effect.timeoutOption(HEALTH_CHECK_TIMEOUT),
Effect.result,
const [result, usageLimits] = yield* Effect.all(
[
options.probe.pipe(Effect.timeoutOption(HEALTH_CHECK_TIMEOUT), Effect.result),
options.usageLimits ?? Effect.succeed(undefined),
],
{ concurrency: "unbounded" },
);
const initialized =
Result.isSuccess(result) && Option.isSome(result.success) ? result.success.value : undefined;
Expand Down Expand Up @@ -228,6 +237,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
}
: {}),
...(message ? { message } : {}),
...(usageLimits ? { usageLimits } : {}),
},
} satisfies AntigravityProviderState;
});
Expand Down Expand Up @@ -261,6 +271,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
) {
const before = yield* SubscriptionRef.get(metadata);
const supportsTextGeneration = yield* options.supportsTextGeneration;
const usageLimits = yield* options.usageLimits ?? Effect.succeed(undefined);
const updatedAt = DateTime.formatIso(yield* DateTime.now);
yield* SubscriptionRef.update(metadata, (state) => {
if (
Expand Down Expand Up @@ -300,6 +311,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
].slice(-MAX_WORKSPACE_SNAPSHOTS),
}
: {}),
...(usageLimits ? { usageLimits } : {}),
},
} satisfies AntigravityProviderState;
});
Expand Down Expand Up @@ -351,25 +363,24 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func

const clearAccountMetadata = Effect.fn("AntigravityProvider.clearAccountMetadata")(function* () {
const updatedAt = DateTime.formatIso(yield* DateTime.now);
yield* SubscriptionRef.update(
metadata,
(state) =>
({
authRevision: state.authRevision + 1,
draft: {
...state.draft,
auth: { status: "unauthenticated" },
status: settings.enabled ? "warning" : "disabled",
message: SIGN_IN_MESSAGE,
checkedAt: updatedAt,
models: [],
slashCommands: [],
skills: [],
workspaceSnapshots: [],
supportsTextGeneration: false,
},
}) satisfies AntigravityProviderState,
);
yield* SubscriptionRef.update(metadata, (state) => {
const { usageLimits: _usageLimits, ...draft } = state.draft;
return {
authRevision: state.authRevision + 1,
draft: {
...draft,
auth: { status: "unauthenticated" },
status: settings.enabled ? "warning" : "disabled",
message: SIGN_IN_MESSAGE,
checkedAt: updatedAt,
models: [],
slashCommands: [],
skills: [],
workspaceSnapshots: [],
supportsTextGeneration: false,
},
} satisfies AntigravityProviderState;
});
discoveredSkills.clear();
});

Expand Down
Loading
Loading