Add provider rate limit snapshot state

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 19:30:11 -04:00
1 parent c6be8a4ee4
commit 0388e1003b
7 files changed
+102 -34

No files matched your search

@@ -4,6 +4,7 @@ import { BrowserWindow, session } from "electron";
import type { CreateProviderResponseInput, SaveProviderCredentialInput, UpdateProviderPolicyInput } from "../../shared/commands";
import { FEATURE_FLAG_KEY, PROVIDER_AUTH_TYPE, PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, SLO_SIGNAL_KEY, type ProviderAuthType, type ProviderExecutionMode } from "../../shared/domain";
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
import { currentProviderUsageWindow } from "../../shared/provider-runtime-domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { AuthBrokerService, type ProviderSecretVault } from "./auth-broker-service";
@@ -271,7 +272,7 @@ export class ProviderService {
if (executionMode === PROVIDER_EXECUTION_MODE.BACKGROUND && provider.background_enabled !== 1) {
throw new Error("Background execution is disabled for this provider policy.");
}
const window = currentMonthlyWindow();
const window = currentProviderUsageWindow();
const usage = this.db.prepare(`
select request_count
from provider_usage_windows
@@ -341,7 +342,7 @@ export class ProviderService {
}
private recordUsage(orgId: string, providerId: string, inputCharCount: number, outputCharCount: number): void {
const window = currentMonthlyWindow();
const window = currentProviderUsageWindow();
const now = new Date().toISOString();
this.db.prepare(`
insert into provider_usage_windows(
@@ -374,10 +375,3 @@ interface ProviderRow {
function hashPrompt(prompt: string): string {
return createHash("sha256").update(prompt).digest("hex");
}
function currentMonthlyWindow(): { start: string; end: string } {
const now = new Date();
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
return { start: start.toISOString(), end: end.toISOString() };
}
@@ -1,8 +1,13 @@
import type { DatabaseSync, StatementSync } from "node:sqlite";
import type { AppSnapshot } from "../../shared/domain";
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
import { currentProviderUsageWindow } from "../../shared/provider-runtime-domain";
type Row = Record<string, unknown>;
type ProviderUsageWindowRow = Pick<
AppSnapshot["providerRateLimits"][number],
"orgId" | "providerId" | "windowStart" | "windowEnd" | "requestCount" | "inputCharCount" | "outputCharCount"
>;
function all<T>(statement: StatementSync): T[] {
return statement.all() as T[];
@@ -45,6 +50,8 @@ export class SnapshotService {
.map((provider) => ({ ...provider, enabled: toBool(provider.enabled), backgroundEnabled: toBool(provider.backgroundEnabled) }));
const providerAuthProfiles = list<AppSnapshot["providerAuthProfiles"][number]>(this.db, "provider_auth_profiles", "created_at desc")
.map((profile) => ({ ...profile, active: toBool(profile.active) }));
const providerUsageWindows = list<ProviderUsageWindowRow>(this.db, "provider_usage_windows", "window_start desc");
const providerRateLimits = buildProviderRateLimits(providers, providerUsageWindows);
const skillBindings = list<AppSnapshot["skillBindings"][number]>(this.db, "skill_bindings", "priority asc")
.map((binding) => ({ ...binding, enabled: toBool(binding.enabled) }));
const files = list<AppSnapshot["files"][number]>(this.db, "file_objects", "created_at desc")
@@ -95,6 +102,7 @@ export class SnapshotService {
providerAuthProfiles,
providerCatalog: PROVIDER_CATALOG,
providerRequests: list(this.db, "provider_requests", "created_at desc"),
providerRateLimits,
skillDefinitions: list(this.db, "skill_definitions", "created_at asc"),
skillRevisions: list(this.db, "skill_revisions", "published_at desc"),
skillBindings,
@@ -140,3 +148,30 @@ export class SnapshotService {
return known[0] ?? null;
}
}
function buildProviderRateLimits(
providers: AppSnapshot["providers"],
providerUsageWindows: ProviderUsageWindowRow[],
): AppSnapshot["providerRateLimits"] {
const window = currentProviderUsageWindow();
return providers.map((provider) => {
const usage = providerUsageWindows.find((item) => item.providerId === provider.id && item.windowStart === window.start);
const requestCount = usage?.requestCount ?? 0;
const inputCharCount = usage?.inputCharCount ?? 0;
const outputCharCount = usage?.outputCharCount ?? 0;
const remainingRequestCount = Math.max(0, provider.monthlyRequestLimit - requestCount);
return {
orgId: provider.orgId,
providerId: provider.id,
providerKey: provider.providerKey,
windowStart: usage?.windowStart ?? window.start,
windowEnd: usage?.windowEnd ?? window.end,
monthlyRequestLimit: provider.monthlyRequestLimit,
requestCount,
inputCharCount,
outputCharCount,
remainingRequestCount,
exhausted: remainingRequestCount === 0,
};
});
}
@@ -65,6 +65,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
const employees = useMemo(() => snapshot.employees.filter((item) => item.orgId === orgId), [snapshot.employees, orgId]);
const agents = useMemo(() => snapshot.agents.filter((item) => item.orgId === orgId), [snapshot.agents, orgId]);
const providers = useMemo(() => snapshot.providers.filter((item) => item.orgId === orgId), [snapshot.providers, orgId]);
const providerRateLimits = useMemo(() => snapshot.providerRateLimits.filter((item) => item.orgId === orgId), [snapshot.providerRateLimits, orgId]);
const skills = useMemo(() => snapshot.skillDefinitions.filter((item) => item.orgId === orgId), [snapshot.skillDefinitions, orgId]);
const skillRevisions = useMemo(() => {
const skillIds = new Set(skills.map((skill) => skill.id));
@@ -133,7 +134,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
providers={providers}
catalog={snapshot.providerCatalog}
agents={agents}
providerRequests={providerRequests}
providerRateLimits={providerRateLimits}
actions={actions}
/>
)}
@@ -4,8 +4,8 @@ import { Dialog } from "../../shared/Dialog";
import type {
AgentRecord,
ProviderCatalogEntry,
ProviderRateLimitStateRecord,
ProviderRecord,
ProviderRequestRecord,
} from "../../../../shared/domain";
import { PROVIDER_DATA_POLICY } from "../../../../shared/domain";
import { ProviderLogo } from "./ProviderLogo";
@@ -16,7 +16,7 @@ interface ProvidersPanelProps {
providers: ProviderRecord[];
catalog: ProviderCatalogEntry[];
agents: AgentRecord[];
providerRequests: ProviderRequestRecord[];
providerRateLimits: ProviderRateLimitStateRecord[];
actions: SnapshotActions;
}
@@ -29,7 +29,7 @@ interface CloudFields {
const CLOUD_DEFAULTS: CloudFields = { baseUrl: "", region: "", projectId: "", deployment: "" };
export function ProvidersPanel({ orgId, providers, catalog, agents, providerRequests, actions }: ProvidersPanelProps) {
export function ProvidersPanel({ orgId, providers, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) {
const [active, setActive] = useState<ProviderRecord | null>(null);
const [signInLaunched, setSignInLaunched] = useState(false);
const connected = providers.filter((provider) => provider.enabled);
@@ -82,7 +82,7 @@ export function ProvidersPanel({ orgId, providers, catalog, agents, providerRequ
<ProviderPolicyTable
orgId={orgId}
providers={connected}
providerRequests={providerRequests}
providerRateLimits={providerRateLimits}
actions={actions}
/>
</>
@@ -122,12 +122,12 @@ export function ProvidersPanel({ orgId, providers, catalog, agents, providerRequ
function ProviderPolicyTable({
orgId,
providers,
providerRequests,
providerRateLimits,
actions,
}: {
orgId: string;
providers: ProviderRecord[];
providerRequests: ProviderRequestRecord[];
providerRateLimits: ProviderRateLimitStateRecord[];
actions: SnapshotActions;
}) {
return (
@@ -140,16 +140,19 @@ function ProviderPolicyTable({
>
<div>Provider</div><div>Data policy</div><div>Background</div><div>Monthly limit</div><div>Usage</div>
</div>
{providers.map((provider, index) => (
<ProviderPolicyRow
key={`${provider.id}:${provider.dataPolicy}:${provider.backgroundEnabled}:${provider.monthlyRequestLimit}`}
orgId={orgId}
provider={provider}
requests={providerRequests.filter((request) => request.providerId === provider.id && inCurrentUtcMonth(request.createdAt))}
last={index === providers.length - 1}
actions={actions}
/>
))}
{providers.map((provider, index) => {
const rateLimit = requireProviderRateLimit(provider, providerRateLimits);
return (
<ProviderPolicyRow
key={`${provider.id}:${provider.dataPolicy}:${provider.backgroundEnabled}:${provider.monthlyRequestLimit}`}
orgId={orgId}
provider={provider}
rateLimit={rateLimit}
last={index === providers.length - 1}
actions={actions}
/>
);
})}
</div>
</>
);
@@ -158,13 +161,13 @@ function ProviderPolicyTable({
function ProviderPolicyRow({
orgId,
provider,
requests,
rateLimit,
last,
actions,
}: {
orgId: string;
provider: ProviderRecord;
requests: ProviderRequestRecord[];
rateLimit: ProviderRateLimitStateRecord;
last: boolean;
actions: SnapshotActions;
}) {
@@ -228,7 +231,7 @@ function ProviderPolicyRow({
onChange={(event) => setMonthlyRequestLimit(event.target.value)}
/>
<div>
<div className="num mono">{requests.length} / {provider.monthlyRequestLimit}</div>
<div className="num mono">{rateLimit.requestCount} / {rateLimit.monthlyRequestLimit}</div>
<button type="button" className="button small" aria-label={`Save ${provider.label} policy`} onClick={savePolicy}>
Save
</button>
@@ -439,8 +442,11 @@ function readableAuth(authType: ProviderRecord["authType"]): string {
return authType;
}
function inCurrentUtcMonth(iso: string): boolean {
const value = new Date(iso);
const now = new Date();
return value.getUTCFullYear() === now.getUTCFullYear() && value.getUTCMonth() === now.getUTCMonth();
function requireProviderRateLimit(
provider: ProviderRecord,
rateLimits: ProviderRateLimitStateRecord[],
): ProviderRateLimitStateRecord {
const rateLimit = rateLimits.find((item) => item.providerId === provider.id);
if (!rateLimit) throw new Error(`Provider rate limit state missing for ${provider.id}.`);
return rateLimit;
}
+3
View File
@@ -6,6 +6,7 @@ import type { ApprovalAssignmentRecord, ApprovalRoutingRuleRecord, HiringPolicyR
import type { ChaosDrillRecord, ChaosDrillRunRecord, FeatureFlagRecord, OperationalEventRecord, SloTargetRecord } from "./operations-domain";
import type { ProviderAuthProfileRecord } from "./provider-auth-domain";
import type { ProviderDataPolicy, ProviderExecutionMode } from "./provider-policy-domain";
import type { ProviderRateLimitStateRecord } from "./provider-runtime-domain";
import type { RollbackSnapshotRecord } from "./rollback-domain";
import type { EvalCaseRecord, EvalRunRecord, EvalSuiteRecord, PrBotGateRecord, SdlcReviewAgentRecord, SdlcReviewRunRecord } from "./sdlc-domain";
export { BOARD_INTERVENTION_ACTION, BOARD_INTERVENTION_MODE, CHAT_THREAD_BOARD_STATE, type BoardInterventionAction, type BoardInterventionMode, type BoardInterventionRecord, type ChatThreadBoardState } from "./board-domain";
@@ -16,6 +17,7 @@ export { ROUTE_TO_TYPE, type ApprovalAssignmentRecord, type ApprovalRoutingRuleR
export { CHAOS_DRILL_SCENARIO, CHAOS_DRILL_STATE, FEATURE_FLAG_KEY, SLO_SIGNAL_KEY, type ChaosDrillRecord, type ChaosDrillRunRecord, type ChaosDrillScenario, type ChaosDrillState, type FeatureFlagKey, type FeatureFlagRecord, type OperationalEventRecord, type SloSignalKey, type SloTargetRecord } from "./operations-domain";
export type { ProviderAuthProfileRecord } from "./provider-auth-domain";
export { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, type ProviderDataPolicy, type ProviderExecutionMode } from "./provider-policy-domain";
export type { ProviderRateLimitStateRecord } from "./provider-runtime-domain";
export type { RollbackSnapshotRecord } from "./rollback-domain";
export { EVAL_SUITE_KEY, PR_BOT_GATE_KEY, SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE, SDLC_RUN_STATE, type EvalCaseRecord, type EvalRunRecord, type EvalSuiteKey, type EvalSuiteRecord, type PrBotGateKey, type PrBotGateRecord, type SdlcReviewAgentKey, type SdlcReviewAgentRecord, type SdlcReviewRunRecord, type SdlcReviewTargetType, type SdlcRunState } from "./sdlc-domain";
export { SEARCH_ENTITY_TYPES, type SearchEntityType, type SearchResultRecord } from "./search-domain";
@@ -470,6 +472,7 @@ export interface AppSnapshot {
providerAuthProfiles: ProviderAuthProfileRecord[];
providerCatalog: ProviderCatalogEntry[];
providerRequests: ProviderRequestRecord[];
providerRateLimits: ProviderRateLimitStateRecord[];
skillDefinitions: SkillDefinitionRecord[];
skillRevisions: SkillRevisionRecord[];
skillBindings: SkillBindingRecord[];
@@ -0,0 +1,19 @@
export interface ProviderRateLimitStateRecord {
orgId: string;
providerId: string;
providerKey: string;
windowStart: string;
windowEnd: string;
monthlyRequestLimit: number;
requestCount: number;
inputCharCount: number;
outputCharCount: number;
remainingRequestCount: number;
exhausted: boolean;
}
export function currentProviderUsageWindow(now = new Date()): { start: string; end: string } {
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
return { start: start.toISOString(), end: end.toISOString() };
}
@@ -212,13 +212,23 @@ describe("provider service", () => {
where org_id = ? and provider_id = ?
`).get(orgId, openAiProviderId) as { request_count: number; input_char_count: number; output_char_count: number };
expect(usage).toEqual({ request_count: 1, input_char_count: 16, output_char_count: 25 });
expect(snapshot.load().providerRequests[0]).toMatchObject({
const state = snapshot.load();
expect(state.providerRequests[0]).toMatchObject({
providerId: openAiProviderId,
inputCharCount: 16,
outputCharCount: 25,
executionMode: PROVIDER_EXECUTION_MODE.FOREGROUND,
policyDecision: "allowed:standard",
});
expect(state.providerRateLimits.find((item) => item.providerId === openAiProviderId)).toMatchObject({
providerId: openAiProviderId,
monthlyRequestLimit: 1,
requestCount: 1,
inputCharCount: 16,
outputCharCount: 25,
remainingRequestCount: 0,
exhausted: true,
});
connection.close();
});