1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
//! Simple in‑process rate limiter for tool execution.
//!
//! This is a lightweight fallback used when the full `governor` crate is not
//! available in the build. It limits the number of tool calls per second
//! globally for the process. The limiter is deliberately cheap – a `Mutex`
//! protecting a counter and a timestamp – because the surrounding code already
//! performs async work, so contention is minimal.
//!
//! The implementation is intentionally minimalistic: it provides a `RateLimiter`
//! struct with a `try_acquire()` method that returns `Ok(())` when the call is
//! allowed or an `Err` when the limit would be exceeded. Callers can decide how
//! to handle the error (e.g., retry after a delay or surface a user‑friendly
//! message).
//!
//! Usage example:
//! ```rust
//! use vtcode_core::tools::rate_limiter::GLOBAL_RATE_LIMITER;
//!
//! fn execute_tool() -> anyhow::Result<()> {
//! GLOBAL_RATE_LIMITER.try_acquire()?;
//! // … actual tool logic …
//! Ok(())
//! }
//! ```
//!
//! The limiter is configured via environment variables to keep the core
//! library free of additional runtime configuration files:
//!
//! * `VTTOOL_RATE_LIMIT` – maximum calls per second (default = 20).
//! * `VTTOOL_BURST` – maximum burst size (default = 5).
//!
//! The values are read once at startup.
//!
//! This file is added as part of the optimization plan to provide a central
//! rate‑limiting mechanism for all external tool invocations (PTY, web fetch,
//! filesystem, etc.).
use anyhow::{Result, anyhow};
use std::sync::{Mutex, MutexGuard};
use std::time::Instant;
/// Configuration for the limiter.
#[derive(Debug, Clone, Copy)]
pub struct RateLimiterConfig {
/// Allowed calls per second.
pub per_sec: u32,
/// Maximum burst capacity.
pub burst: u32,
}
impl Default for RateLimiterConfig {
fn default() -> Self {
// Environment variables are optional; fall back to sensible defaults.
let per_sec = std::env::var("VTTOOL_RATE_LIMIT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(20);
let burst = std::env::var("VTTOOL_BURST")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5);
RateLimiterConfig { per_sec, burst }
}
}
/// Simple token‑bucket implementation.
pub struct RateLimiterInner {
config: RateLimiterConfig,
/// Current number of available tokens.
tokens: u32,
/// When the bucket was last refilled.
last_refill: Instant,
}
impl RateLimiterInner {
fn new() -> Self {
Self::new_with_config(RateLimiterConfig::default())
}
pub fn new_with_config(config: RateLimiterConfig) -> Self {
Self {
config,
tokens: config.burst,
last_refill: Instant::now(),
}
}
/// Refill tokens based on elapsed time.
/// Uses fractional refill based on milliseconds for smoother rate limiting.
/// `speed_multiplier` allows adaptive rate limiting (faster refill for high priority).
fn refill(&mut self, speed_multiplier: f64) {
let now = Instant::now();
let elapsed = now.duration_since(self.last_refill);
// Calculate refill based on milliseconds for finer granularity
let millis = elapsed.as_millis() as u64;
// Minimum refill interval of 50ms to avoid excessive overhead
if millis < 50 {
return;
}
// Fractional refill: (per_sec * multiplier) tokens per 1000ms
let effective_rate = (self.config.per_sec as f64 * speed_multiplier) as u64;
// Using integer math: tokens = (effective_rate * millis) / 1000
let added = (effective_rate.saturating_mul(millis) / 1000) as u32;
if added > 0 {
// Burst is also scaled by multiplier to allow larger spikes for high priority
let effective_burst = (self.config.burst as f64 * speed_multiplier.max(1.0)) as u32;
self.tokens = (self.tokens + added).min(effective_burst);
self.last_refill = now;
}
}
/// Attempt to acquire a single token with default speed (1.0).
pub fn try_acquire(&mut self) -> Result<()> {
self.try_acquire_scaled(1.0)
}
/// Attempt to acquire a single token with a speed multiplier.
pub fn try_acquire_scaled(&mut self, speed_multiplier: f64) -> Result<()> {
self.refill(speed_multiplier);
if self.tokens == 0 {
Err(anyhow!("tool rate limit exceeded"))
} else {
self.tokens -= 1;
Ok(())
}
}
}
/// Public alias for benchmark compatibility
pub type RateLimiter = PerToolRateLimiter;
use hashbrown::HashMap;
/// Global rate limiter instance used by all tools.
///
/// The `lazy_static` pattern is avoided to keep the dependency surface low;
/// instead we rely on `once_cell::sync::Lazy` which is already a transitive
/// dependency of the project.
use once_cell::sync::Lazy;
pub static GLOBAL_RATE_LIMITER: Lazy<Mutex<RateLimiterInner>> =
Lazy::new(|| Mutex::new(RateLimiterInner::new()));
/// Per-tool rate limiter for finer-grained control.
/// Each tool gets its own token bucket, allowing different rate limits per tool.
pub struct PerToolRateLimiter {
/// Per-tool token buckets. Key is tool name.
buckets: HashMap<String, RateLimiterInner>,
/// Default config for new tool buckets.
default_config: RateLimiterConfig,
}
impl Default for PerToolRateLimiter {
fn default() -> Self {
Self::new()
}
}
impl PerToolRateLimiter {
/// Create a new per-tool rate limiter with default configuration.
pub fn new() -> Self {
Self {
buckets: HashMap::new(),
default_config: RateLimiterConfig::default(),
}
}
pub fn new_with_config(config: RateLimiterConfig) -> Self {
Self {
buckets: HashMap::new(),
default_config: config,
}
}
/// Try to acquire a token for a specific tool.
/// Returns `Ok(())` if allowed, `Err` if rate limited.
pub fn try_acquire_for(&mut self, tool_name: &str) -> Result<()> {
self.try_acquire_for_scaled(tool_name, 1.0)
}
/// Try to acquire a token with a priority multiplier.
pub fn try_acquire_for_scaled(&mut self, tool_name: &str, multiplier: f64) -> Result<()> {
let bucket = self
.buckets
.entry(tool_name.to_owned())
.or_insert_with(|| RateLimiterInner::new_with_config(self.default_config));
bucket.try_acquire_scaled(multiplier)
}
/// Alias for try_acquire_for (used by benchmarks)
pub fn acquire(&mut self, tool_name: &str) -> Result<()> {
self.try_acquire_for(tool_name)
}
/// Check if a tool is currently rate limited without consuming a token.
pub fn is_limited(&mut self, tool_name: &str) -> bool {
if let Some(bucket) = self.buckets.get_mut(tool_name) {
bucket.refill(1.0); // Assume default refresher for check
bucket.tokens == 0
} else {
false
}
}
/// Reset the rate limiter for a specific tool.
pub fn reset_tool(&mut self, tool_name: &str) {
if let Some(bucket) = self.buckets.get_mut(tool_name) {
bucket.tokens = bucket.config.burst;
bucket.last_refill = Instant::now();
}
}
}
/// Global per-tool rate limiter instance.
pub static PER_TOOL_RATE_LIMITER: Lazy<Mutex<PerToolRateLimiter>> =
Lazy::new(|| Mutex::new(PerToolRateLimiter::new()));
/// Public API – try to acquire permission for a tool call.
///
/// Returns `Ok(())` when the call is allowed, otherwise an error.
pub fn try_acquire() -> Result<()> {
let mut guard: MutexGuard<'_, RateLimiterInner> = GLOBAL_RATE_LIMITER
.lock()
.map_err(|e| anyhow!("rate limiter poisoned: {}", e))?;
guard.try_acquire()
}
/// Try to acquire permission for a specific tool.
/// Uses per-tool rate limiting for finer-grained control.
pub fn try_acquire_for(tool_name: &str) -> Result<()> {
let mut guard = PER_TOOL_RATE_LIMITER
.lock()
.map_err(|e| anyhow!("per-tool rate limiter poisoned: {}", e))?;
guard.try_acquire_for(tool_name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_global_limiter_allows_burst() {
let mut limiter = RateLimiterInner::new();
// Should allow up to burst capacity
for _ in 0..limiter.config.burst {
assert!(limiter.try_acquire().is_ok());
}
// Next should fail
assert!(limiter.try_acquire().is_err());
}
#[test]
fn test_per_tool_limiter_isolates_tools() {
let mut limiter = PerToolRateLimiter::new();
// Exhaust tool_a
for _ in 0..5 {
let _ = limiter.try_acquire_for("tool_a");
}
// tool_b should still have tokens
assert!(limiter.try_acquire_for("tool_b").is_ok());
}
#[test]
fn test_reset_tool_restores_tokens() {
let mut limiter = PerToolRateLimiter::new();
// Exhaust tokens
for _ in 0..10 {
let _ = limiter.try_acquire_for("tool_x");
}
assert!(limiter.is_limited("tool_x"));
// Reset
limiter.reset_tool("tool_x");
assert!(!limiter.is_limited("tool_x"));
}
}