Skip to content

Commit 1da82b2

Browse files
hassoncsdaniel-lxs
andauthored
Add auto-approved cost limits (#6484)
Co-authored-by: Daniel Riccio <[email protected]>
1 parent 1a013b4 commit 1da82b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1404
-88
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const globalSettingsSchema = z.object({
6868
commandTimeoutAllowlist: z.array(z.string()).optional(),
6969
preventCompletionWithOpenTodos: z.boolean().optional(),
7070
allowedMaxRequests: z.number().nullish(),
71+
allowedMaxCost: z.number().nullish(),
7172
autoCondenseContext: z.boolean().optional(),
7273
autoCondenseContextPercent: z.number().optional(),
7374
maxConcurrentFileReads: z.number().optional(),
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { GlobalState, ClineMessage, ClineAsk } from "@roo-code/types"
2+
import { getApiMetrics } from "../../shared/getApiMetrics"
3+
import { ClineAskResponse } from "../../shared/WebviewMessage"
4+
5+
export interface AutoApprovalResult {
6+
shouldProceed: boolean
7+
requiresApproval: boolean
8+
approvalType?: "requests" | "cost"
9+
approvalCount?: number | string
10+
}
11+
12+
export class AutoApprovalHandler {
13+
private consecutiveAutoApprovedRequestsCount: number = 0
14+
private consecutiveAutoApprovedCost: number = 0
15+
16+
/**
17+
* Check if auto-approval limits have been reached and handle user approval if needed
18+
*/
19+
async checkAutoApprovalLimits(
20+
state: GlobalState | undefined,
21+
messages: ClineMessage[],
22+
askForApproval: (
23+
type: ClineAsk,
24+
data: string,
25+
) => Promise<{ response: ClineAskResponse; text?: string; images?: string[] }>,
26+
): Promise<AutoApprovalResult> {
27+
// Check request count limit
28+
const requestResult = await this.checkRequestLimit(state, askForApproval)
29+
if (!requestResult.shouldProceed || requestResult.requiresApproval) {
30+
return requestResult
31+
}
32+
33+
// Check cost limit
34+
const costResult = await this.checkCostLimit(state, messages, askForApproval)
35+
return costResult
36+
}
37+
38+
/**
39+
* Increment the request counter and check if limit is exceeded
40+
*/
41+
private async checkRequestLimit(
42+
state: GlobalState | undefined,
43+
askForApproval: (
44+
type: ClineAsk,
45+
data: string,
46+
) => Promise<{ response: ClineAskResponse; text?: string; images?: string[] }>,
47+
): Promise<AutoApprovalResult> {
48+
const maxRequests = state?.allowedMaxRequests || Infinity
49+
50+
// Increment the counter for each new API request
51+
this.consecutiveAutoApprovedRequestsCount++
52+
53+
if (this.consecutiveAutoApprovedRequestsCount > maxRequests) {
54+
const { response } = await askForApproval(
55+
"auto_approval_max_req_reached",
56+
JSON.stringify({ count: maxRequests, type: "requests" }),
57+
)
58+
59+
// If we get past the promise, it means the user approved and did not start a new task
60+
if (response === "yesButtonClicked") {
61+
this.consecutiveAutoApprovedRequestsCount = 0
62+
return {
63+
shouldProceed: true,
64+
requiresApproval: true,
65+
approvalType: "requests",
66+
approvalCount: maxRequests,
67+
}
68+
}
69+
70+
return {
71+
shouldProceed: false,
72+
requiresApproval: true,
73+
approvalType: "requests",
74+
approvalCount: maxRequests,
75+
}
76+
}
77+
78+
return { shouldProceed: true, requiresApproval: false }
79+
}
80+
81+
/**
82+
* Calculate current cost and check if limit is exceeded
83+
*/
84+
private async checkCostLimit(
85+
state: GlobalState | undefined,
86+
messages: ClineMessage[],
87+
askForApproval: (
88+
type: ClineAsk,
89+
data: string,
90+
) => Promise<{ response: ClineAskResponse; text?: string; images?: string[] }>,
91+
): Promise<AutoApprovalResult> {
92+
const maxCost = state?.allowedMaxCost || Infinity
93+
94+
// Calculate total cost from messages
95+
this.consecutiveAutoApprovedCost = getApiMetrics(messages).totalCost
96+
97+
// Use epsilon for floating-point comparison to avoid precision issues
98+
const EPSILON = 0.0001
99+
if (this.consecutiveAutoApprovedCost > maxCost + EPSILON) {
100+
const { response } = await askForApproval(
101+
"auto_approval_max_req_reached",
102+
JSON.stringify({ count: maxCost.toFixed(2), type: "cost" }),
103+
)
104+
105+
// If we get past the promise, it means the user approved and did not start a new task
106+
if (response === "yesButtonClicked") {
107+
// Note: We don't reset the cost to 0 here because the actual cost
108+
// is calculated from the messages. This is different from the request count.
109+
return {
110+
shouldProceed: true,
111+
requiresApproval: true,
112+
approvalType: "cost",
113+
approvalCount: maxCost.toFixed(2),
114+
}
115+
}
116+
117+
return {
118+
shouldProceed: false,
119+
requiresApproval: true,
120+
approvalType: "cost",
121+
approvalCount: maxCost.toFixed(2),
122+
}
123+
}
124+
125+
return { shouldProceed: true, requiresApproval: false }
126+
}
127+
128+
/**
129+
* Reset the request counter (typically called when starting a new task)
130+
*/
131+
resetRequestCount(): void {
132+
this.consecutiveAutoApprovedRequestsCount = 0
133+
}
134+
135+
/**
136+
* Get current approval state for debugging/testing
137+
*/
138+
getApprovalState(): { requestCount: number; currentCost: number } {
139+
return {
140+
requestCount: this.consecutiveAutoApprovedRequestsCount,
141+
currentCost: this.consecutiveAutoApprovedCost,
142+
}
143+
}
144+
}

src/core/task/Task.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import { ApiMessage } from "../task-persistence/apiMessages"
9292
import { getMessagesSinceLastSummary, summarizeConversation } from "../condense"
9393
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
9494
import { restoreTodoListForTask } from "../tools/updateTodoListTool"
95+
import { AutoApprovalHandler } from "./AutoApprovalHandler"
9596

9697
const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
9798

@@ -199,7 +200,7 @@ export class Task extends EventEmitter<TaskEvents> {
199200
readonly apiConfiguration: ProviderSettings
200201
api: ApiHandler
201202
private static lastGlobalApiRequestTime?: number
202-
private consecutiveAutoApprovedRequestsCount: number = 0
203+
private autoApprovalHandler: AutoApprovalHandler
203204

204205
/**
205206
* Reset the global API request timestamp. This should only be used for testing.
@@ -302,6 +303,7 @@ export class Task extends EventEmitter<TaskEvents> {
302303

303304
this.apiConfiguration = apiConfiguration
304305
this.api = buildApiHandler(apiConfiguration)
306+
this.autoApprovalHandler = new AutoApprovalHandler()
305307

306308
this.urlContentFetcher = new UrlContentFetcher(provider.context)
307309
this.browserSession = new BrowserSession(provider.context)
@@ -1968,18 +1970,16 @@ export class Task extends EventEmitter<TaskEvents> {
19681970
({ role, content }) => ({ role, content }),
19691971
)
19701972

1971-
// Check if we've reached the maximum number of auto-approved requests
1972-
const maxRequests = state?.allowedMaxRequests || Infinity
1973-
1974-
// Increment the counter for each new API request
1975-
this.consecutiveAutoApprovedRequestsCount++
1973+
// Check auto-approval limits
1974+
const approvalResult = await this.autoApprovalHandler.checkAutoApprovalLimits(
1975+
state,
1976+
this.combineMessages(this.clineMessages.slice(1)),
1977+
async (type, data) => this.ask(type, data),
1978+
)
19761979

1977-
if (this.consecutiveAutoApprovedRequestsCount > maxRequests) {
1978-
const { response } = await this.ask("auto_approval_max_req_reached", JSON.stringify({ count: maxRequests }))
1979-
// If we get past the promise, it means the user approved and did not start a new task
1980-
if (response === "yesButtonClicked") {
1981-
this.consecutiveAutoApprovedRequestsCount = 0
1982-
}
1980+
if (!approvalResult.shouldProceed) {
1981+
// User did not approve, task should be aborted
1982+
throw new Error("Auto-approval limit reached and user did not approve continuation")
19831983
}
19841984

19851985
const metadata: ApiHandlerCreateMessageMetadata = {

0 commit comments

Comments
 (0)