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
2 changes: 2 additions & 0 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { SuspensionService } from "../services/suspension/service";
import { TaskLinkService } from "../services/task-link/service";
import { UIService } from "../services/ui/service";
import { UpdatesService } from "../services/updates/service";
import { UsageMonitorService } from "../services/usage-monitor/service";
import { WatcherRegistryService } from "../services/watcher-registry/service";
import { WorkspaceService } from "../services/workspace/service";
import { MAIN_TOKENS } from "./tokens";
Expand Down Expand Up @@ -147,6 +148,7 @@ container.bind(MAIN_TOKENS.ShellService).to(ShellService);
container.bind(MAIN_TOKENS.SlackIntegrationService).to(SlackIntegrationService);
container.bind(MAIN_TOKENS.UIService).to(UIService);
container.bind(MAIN_TOKENS.UpdatesService).to(UpdatesService);
container.bind(MAIN_TOKENS.UsageMonitorService).to(UsageMonitorService);
container.bind(MAIN_TOKENS.TaskLinkService).to(TaskLinkService);
container.bind(MAIN_TOKENS.InboxLinkService).to(InboxLinkService);
container.bind(MAIN_TOKENS.NewTaskLinkService).to(NewTaskLinkService);
Expand Down
1 change: 1 addition & 0 deletions apps/code/src/main/di/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,5 @@ export const MAIN_TOKENS = Object.freeze({
ProvisioningService: Symbol.for("Main.ProvisioningService"),
WorkspaceService: Symbol.for("Main.WorkspaceService"),
EnrichmentService: Symbol.for("Main.EnrichmentService"),
UsageMonitorService: Symbol.for("Main.UsageMonitorService"),
});
2 changes: 2 additions & 0 deletions apps/code/src/main/services/agent/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export const AgentServiceEvent = {
SessionsIdle: "sessions-idle",
SessionIdleKilled: "session-idle-killed",
AgentFileActivity: "agent-file-activity",
LlmActivity: "llm-activity",
} as const;

export interface AgentSessionEventPayload {
Expand Down Expand Up @@ -234,6 +235,7 @@ export interface AgentServiceEvents {
[AgentServiceEvent.SessionsIdle]: undefined;
[AgentServiceEvent.SessionIdleKilled]: SessionIdleKilledPayload;
[AgentServiceEvent.AgentFileActivity]: AgentFileActivityPayload;
[AgentServiceEvent.LlmActivity]: undefined;
}

// Permission response input for tRPC
Expand Down
4 changes: 4 additions & 0 deletions apps/code/src/main/services/agent/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,10 @@ For git operations while detached:
}
}

if (isNotification(method, POSTHOG_NOTIFICATIONS.USAGE_UPDATE)) {
this.emit(AgentServiceEvent.LlmActivity, undefined);
}

// Extension notifications already flow through the tapped stream
// (same pattern as sessionUpdate). No need to re-emit here.
},
Expand Down
4 changes: 3 additions & 1 deletion apps/code/src/main/services/llm-gateway/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface AnthropicErrorResponse {

export const usageBucketSchema = z.object({
used_percent: z.number(),
resets_in_seconds: z.number(),
reset_at: z.string().datetime(),
exceeded: z.boolean(),
});

Expand All @@ -69,6 +69,8 @@ export const usageOutput = z.object({
sustained: usageBucketSchema,
burst: usageBucketSchema,
is_rate_limited: z.boolean(),
is_pro: z.boolean(),
billing_period_end: z.string().datetime().nullable().optional(),
});

export type UsageBucket = z.infer<typeof usageBucketSchema>;
Expand Down
11 changes: 10 additions & 1 deletion apps/code/src/main/services/llm-gateway/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,18 @@ export class LlmGatewayService {

log.debug("Fetching usage from gateway", { url: usageUrl });

const response = await this.authService.authenticatedFetch(fetch, usageUrl);
let response: Response;
try {
response = await this.authService.authenticatedFetch(fetch, usageUrl);
} catch (err) {
log.warn("Usage fetch network error", {
error: err instanceof Error ? err.message : String(err),
});
throw err;
}

if (!response.ok) {
log.warn("Usage fetch failed", { status: response.status });
throw new LlmGatewayError(
`Failed to fetch usage: HTTP ${response.status}`,
"usage_error",
Expand Down
35 changes: 35 additions & 0 deletions apps/code/src/main/services/usage-monitor/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { UsageOutput } from "@main/services/llm-gateway/schemas";
import { usageOutput } from "@main/services/llm-gateway/schemas";
import { z } from "zod";

export const USAGE_THRESHOLDS = [50, 75, 90, 100] as const;
export type UsageThreshold = (typeof USAGE_THRESHOLDS)[number];

export const thresholdCrossedEvent = z.object({
bucket: z.enum(["burst", "sustained"]),
threshold: z.union([
z.literal(50),
z.literal(75),
z.literal(90),
z.literal(100),
]),
usedPercent: z.number(),
resetAt: z.string().datetime(),
isPro: z.boolean(),
userIsActive: z.boolean(),
});

export type ThresholdCrossedEvent = z.infer<typeof thresholdCrossedEvent>;

export const usageSnapshotOutput = usageOutput.nullable();
export type UsageSnapshot = UsageOutput | null;

export const UsageMonitorEvent = {
ThresholdCrossed: "threshold-crossed",
UsageUpdated: "usage-updated",
} as const;

export interface UsageMonitorEvents {
[UsageMonitorEvent.ThresholdCrossed]: ThresholdCrossedEvent;
[UsageMonitorEvent.UsageUpdated]: UsageOutput;
}
Loading
Loading