Skip to content

Commit 533c2ce

Browse files
authored
Merge pull request #50 from simonsmh/feature/kiro-rate-limiter-config
feat(kiro): rate limiter config, system prompt control, Claude format fixes
2 parents a1cafe6 + 33cfa22 commit 533c2ce

15 files changed

Lines changed: 580 additions & 94 deletions

File tree

cmd/server/main.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,32 +557,44 @@ func main() {
557557
// and don't share config with StartService (which is in the else branch)
558558
setKiroIncognitoMode(cfg, useIncognito, noIncognito)
559559
kiro.InitFingerprintConfig(cfg)
560+
kiro.InitRateLimiterConfig(cfg)
561+
kiro.InitSystemPromptInjectConfig(cfg)
560562
cmd.DoKiroLogin(cfg, options)
561563
} else if kiroGoogleLogin {
562564
// For Kiro auth, default to incognito mode for multi-account support
563565
// Users can explicitly override with --no-incognito
564566
// Note: This config mutation is safe - auth commands exit after completion
565567
setKiroIncognitoMode(cfg, useIncognito, noIncognito)
566568
kiro.InitFingerprintConfig(cfg)
569+
kiro.InitRateLimiterConfig(cfg)
570+
kiro.InitSystemPromptInjectConfig(cfg)
567571
cmd.DoKiroGoogleLogin(cfg, options)
568572
} else if kiroAWSLogin {
569573
// For Kiro auth, default to incognito mode for multi-account support
570574
// Users can explicitly override with --no-incognito
571575
setKiroIncognitoMode(cfg, useIncognito, noIncognito)
572576
kiro.InitFingerprintConfig(cfg)
577+
kiro.InitRateLimiterConfig(cfg)
578+
kiro.InitSystemPromptInjectConfig(cfg)
573579
cmd.DoKiroAWSLogin(cfg, options)
574580
} else if kiroAWSAuthCode {
575581
// For Kiro auth with authorization code flow (better UX)
576582
setKiroIncognitoMode(cfg, useIncognito, noIncognito)
577583
kiro.InitFingerprintConfig(cfg)
584+
kiro.InitRateLimiterConfig(cfg)
585+
kiro.InitSystemPromptInjectConfig(cfg)
578586
cmd.DoKiroAWSAuthCodeLogin(cfg, options)
579587
} else if kiroImport {
580588
kiro.InitFingerprintConfig(cfg)
589+
kiro.InitRateLimiterConfig(cfg)
590+
kiro.InitSystemPromptInjectConfig(cfg)
581591
cmd.DoKiroImport(cfg, options)
582592
} else if kiroIDCLogin {
583593
// For Kiro IDC auth, default to incognito mode for multi-account support
584594
setKiroIncognitoMode(cfg, useIncognito, noIncognito)
585595
kiro.InitFingerprintConfig(cfg)
596+
kiro.InitRateLimiterConfig(cfg)
597+
kiro.InitSystemPromptInjectConfig(cfg)
586598
cmd.DoKiroIDCLogin(cfg, options, kiroIDCStartURL, kiroIDCRegion, kiroIDCFlow)
587599
} else {
588600
// In cloud deploy mode without config file, just wait for shutdown signals
@@ -680,6 +692,8 @@ func main() {
680692
}
681693

682694
if cfg.AuthDir != "" {
695+
kiro.InitRateLimiterConfig(cfg)
696+
kiro.InitSystemPromptInjectConfig(cfg)
683697
kiro.InitializeAndStart(cfg.AuthDir, cfg)
684698
defer kiro.StopGlobalRefreshManager()
685699
}

config.example.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,27 @@ nonstream-keepalive-interval: 0
249249
# profile-arn: "arn:aws:codewhisperer:us-east-1:..."
250250
# proxy-url: "socks5://proxy.example.com:1080" # optional: proxy override
251251

252+
# Kiro rate limiter configuration
253+
# Controls request throttling and backoff behavior for Kiro tokens.
254+
# All fields are optional; omitted fields fall back to defaults shown below.
255+
#kiro-rate-limit:
256+
# enabled: true # set to true to enable rate limiting (default: false)
257+
# min-token-interval: "1s" # minimum interval between requests per token (default: 1s)
258+
# max-token-interval: "2s" # maximum interval between requests per token (default: 2s)
259+
# daily-max-requests: 500 # maximum requests per token per day (default: 500)
260+
# jitter-percent: 0.3 # random jitter ratio applied to intervals (default: 0.3)
261+
# backoff-base: "30s" # base duration for exponential backoff on failure (default: 30s)
262+
# backoff-max: "5m" # maximum backoff duration cap (default: 5m)
263+
# backoff-multiplier: 1.5 # exponential backoff multiplier (default: 1.5)
264+
# suspend-cooldown: "1h" # cooldown after suspension keywords detected (default: 1h)
265+
266+
# Controls whether system prompts are wrapped with --- SYSTEM PROMPT --- markers
267+
# and injected into Kiro user messages. Kiro/Amazon Q API has no native system
268+
# field, so when enabled the proxy prepends the wrapped system prompt to the
269+
# user message. By default the proxy drops system prompts entirely.
270+
# Default: false.
271+
#kiro-system-prompt-inject-enable: true
272+
252273
# Kilocode (OAuth-based code assistant)
253274
# Note: Kilocode uses OAuth device flow authentication.
254275
# Use the CLI command: ./server --kilo-login

internal/auth/kiro/rate_limiter.go

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"strings"
77
"sync"
88
"time"
9+
10+
log "github.com/sirupsen/logrus"
911
)
1012

1113
const (
@@ -19,7 +21,7 @@ const (
1921
DefaultSuspendCooldown = 1 * time.Hour
2022
)
2123

22-
// TokenState Token 状态
24+
// TokenState tracks per-token request and cooldown state.
2325
type TokenState struct {
2426
LastRequest time.Time
2527
RequestCount int
@@ -32,10 +34,11 @@ type TokenState struct {
3234
SuspendReason string
3335
}
3436

35-
// RateLimiter 频率限制器
37+
// RateLimiter throttles Kiro requests and tracks token health.
3638
type RateLimiter struct {
3739
mu sync.RWMutex
3840
states map[string]*TokenState
41+
enabled bool
3942
minTokenInterval time.Duration
4043
maxTokenInterval time.Duration
4144
dailyMaxRequests int
@@ -47,24 +50,19 @@ type RateLimiter struct {
4750
rng *rand.Rand
4851
}
4952

50-
// NewRateLimiter 创建默认配置的频率限制器
53+
// NewRateLimiter creates a rate limiter with default settings.
5154
func NewRateLimiter() *RateLimiter {
52-
return &RateLimiter{
53-
states: make(map[string]*TokenState),
54-
minTokenInterval: DefaultMinTokenInterval,
55-
maxTokenInterval: DefaultMaxTokenInterval,
56-
dailyMaxRequests: DefaultDailyMaxRequests,
57-
jitterPercent: DefaultJitterPercent,
58-
backoffBase: DefaultBackoffBase,
59-
backoffMax: DefaultBackoffMax,
60-
backoffMultiplier: DefaultBackoffMultiplier,
61-
suspendCooldown: DefaultSuspendCooldown,
62-
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
55+
rl := &RateLimiter{
56+
states: make(map[string]*TokenState),
57+
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
6358
}
59+
rl.resetConfigLocked()
60+
return rl
6461
}
6562

66-
// RateLimiterConfig 频率限制器配置
63+
// RateLimiterConfig defines rate limiter settings.
6764
type RateLimiterConfig struct {
65+
Enabled *bool
6866
MinTokenInterval time.Duration
6967
MaxTokenInterval time.Duration
7068
DailyMaxRequests int
@@ -75,9 +73,37 @@ type RateLimiterConfig struct {
7573
SuspendCooldown time.Duration
7674
}
7775

78-
// NewRateLimiterWithConfig 使用自定义配置创建频率限制器
76+
// NewRateLimiterWithConfig creates a rate limiter with custom settings.
7977
func NewRateLimiterWithConfig(cfg RateLimiterConfig) *RateLimiter {
8078
rl := NewRateLimiter()
79+
rl.applyConfigLocked(cfg)
80+
return rl
81+
}
82+
83+
// ApplyConfig reapplies config defaults and then applies overrides.
84+
func (rl *RateLimiter) ApplyConfig(cfg RateLimiterConfig) {
85+
rl.mu.Lock()
86+
defer rl.mu.Unlock()
87+
rl.applyConfigLocked(cfg)
88+
}
89+
90+
func (rl *RateLimiter) resetConfigLocked() {
91+
rl.enabled = false
92+
rl.minTokenInterval = DefaultMinTokenInterval
93+
rl.maxTokenInterval = DefaultMaxTokenInterval
94+
rl.dailyMaxRequests = DefaultDailyMaxRequests
95+
rl.jitterPercent = DefaultJitterPercent
96+
rl.backoffBase = DefaultBackoffBase
97+
rl.backoffMax = DefaultBackoffMax
98+
rl.backoffMultiplier = DefaultBackoffMultiplier
99+
rl.suspendCooldown = DefaultSuspendCooldown
100+
}
101+
102+
func (rl *RateLimiter) applyConfigLocked(cfg RateLimiterConfig) {
103+
rl.resetConfigLocked()
104+
if cfg.Enabled != nil {
105+
rl.enabled = *cfg.Enabled
106+
}
81107
if cfg.MinTokenInterval > 0 {
82108
rl.minTokenInterval = cfg.MinTokenInterval
83109
}
@@ -102,10 +128,16 @@ func NewRateLimiterWithConfig(cfg RateLimiterConfig) *RateLimiter {
102128
if cfg.SuspendCooldown > 0 {
103129
rl.suspendCooldown = cfg.SuspendCooldown
104130
}
105-
return rl
131+
132+
// Validate interval bounds: max must be > min to avoid rand.Int63n panic.
133+
if rl.maxTokenInterval <= rl.minTokenInterval {
134+
log.Warnf("kiro: rate limiter max-token-interval (%v) <= min-token-interval (%v), clamping max to min+1s",
135+
rl.maxTokenInterval, rl.minTokenInterval)
136+
rl.maxTokenInterval = rl.minTokenInterval + time.Second
137+
}
106138
}
107139

108-
// getOrCreateState 获取或创建 Token 状态
140+
// getOrCreateState returns the existing token state or creates one.
109141
func (rl *RateLimiter) getOrCreateState(tokenKey string) *TokenState {
110142
state, exists := rl.states[tokenKey]
111143
if !exists {
@@ -117,7 +149,7 @@ func (rl *RateLimiter) getOrCreateState(tokenKey string) *TokenState {
117149
return state
118150
}
119151

120-
// resetDailyIfNeeded 如果需要则重置每日计数
152+
// resetDailyIfNeeded resets the daily counter when the window rolls over.
121153
func (rl *RateLimiter) resetDailyIfNeeded(state *TokenState) {
122154
now := time.Now()
123155
if now.After(state.DailyResetTime) {
@@ -126,22 +158,26 @@ func (rl *RateLimiter) resetDailyIfNeeded(state *TokenState) {
126158
}
127159
}
128160

129-
// calculateInterval 计算带抖动的随机间隔
161+
// calculateInterval returns a randomized interval with jitter applied.
130162
func (rl *RateLimiter) calculateInterval() time.Duration {
131163
baseInterval := rl.minTokenInterval + time.Duration(rl.rng.Int63n(int64(rl.maxTokenInterval-rl.minTokenInterval)))
132164
jitter := time.Duration(float64(baseInterval) * rl.jitterPercent * (rl.rng.Float64()*2 - 1))
133165
return baseInterval + jitter
134166
}
135167

136-
// WaitForToken 等待 Token 可用(带抖动的随机间隔)
168+
// WaitForToken blocks until the token is allowed to send another request.
137169
func (rl *RateLimiter) WaitForToken(tokenKey string) {
170+
if !rl.enabled {
171+
return
172+
}
173+
138174
rl.mu.Lock()
139175
state := rl.getOrCreateState(tokenKey)
140176
rl.resetDailyIfNeeded(state)
141177

142178
now := time.Now()
143179

144-
// 检查是否在冷却期
180+
// Wait until the cooldown window ends.
145181
if now.Before(state.CooldownEnd) {
146182
waitTime := state.CooldownEnd.Sub(now)
147183
rl.mu.Unlock()
@@ -151,7 +187,7 @@ func (rl *RateLimiter) WaitForToken(tokenKey string) {
151187
now = time.Now()
152188
}
153189

154-
// 计算距离上次请求的间隔
190+
// Respect the randomized spacing since the last request.
155191
interval := rl.calculateInterval()
156192
nextAllowedTime := state.LastRequest.Add(interval)
157193

@@ -169,8 +205,12 @@ func (rl *RateLimiter) WaitForToken(tokenKey string) {
169205
rl.mu.Unlock()
170206
}
171207

172-
// MarkTokenFailed 标记 Token 失败
208+
// MarkTokenFailed records a failed request for a token.
173209
func (rl *RateLimiter) MarkTokenFailed(tokenKey string) {
210+
if !rl.enabled {
211+
return
212+
}
213+
174214
rl.mu.Lock()
175215
defer rl.mu.Unlock()
176216

@@ -179,8 +219,12 @@ func (rl *RateLimiter) MarkTokenFailed(tokenKey string) {
179219
state.CooldownEnd = time.Now().Add(rl.calculateBackoff(state.FailCount))
180220
}
181221

182-
// MarkTokenSuccess 标记 Token 成功
222+
// MarkTokenSuccess clears any failure state after a successful request.
183223
func (rl *RateLimiter) MarkTokenSuccess(tokenKey string) {
224+
if !rl.enabled {
225+
return
226+
}
227+
184228
rl.mu.Lock()
185229
defer rl.mu.Unlock()
186230

@@ -189,8 +233,12 @@ func (rl *RateLimiter) MarkTokenSuccess(tokenKey string) {
189233
state.CooldownEnd = time.Time{}
190234
}
191235

192-
// CheckAndMarkSuspended 检测暂停错误并标记
236+
// CheckAndMarkSuspended detects suspension-like errors and records them.
193237
func (rl *RateLimiter) CheckAndMarkSuspended(tokenKey string, errorMsg string) bool {
238+
if !rl.enabled {
239+
return false
240+
}
241+
194242
suspendKeywords := []string{
195243
"suspended",
196244
"banned",
@@ -219,8 +267,12 @@ func (rl *RateLimiter) CheckAndMarkSuspended(tokenKey string, errorMsg string) b
219267
return false
220268
}
221269

222-
// IsTokenAvailable 检查 Token 是否可用
270+
// IsTokenAvailable reports whether the token can currently be used.
223271
func (rl *RateLimiter) IsTokenAvailable(tokenKey string) bool {
272+
if !rl.enabled {
273+
return true
274+
}
275+
224276
rl.mu.RLock()
225277
defer rl.mu.RUnlock()
226278

@@ -231,20 +283,20 @@ func (rl *RateLimiter) IsTokenAvailable(tokenKey string) bool {
231283

232284
now := time.Now()
233285

234-
// 检查是否被暂停
286+
// Suspended tokens stay unavailable until their suspension window expires.
235287
if state.IsSuspended {
236288
if now.After(state.SuspendedAt.Add(rl.suspendCooldown)) {
237289
return true
238290
}
239291
return false
240292
}
241293

242-
// 检查是否在冷却期
294+
// Cooldown blocks reuse after failures.
243295
if now.Before(state.CooldownEnd) {
244296
return false
245297
}
246298

247-
// 检查每日请求限制
299+
// Enforce the per-day cap after refreshing the rolling window.
248300
rl.mu.RUnlock()
249301
rl.mu.Lock()
250302
rl.resetDailyIfNeeded(state)
@@ -260,15 +312,15 @@ func (rl *RateLimiter) IsTokenAvailable(tokenKey string) bool {
260312
return true
261313
}
262314

263-
// calculateBackoff 计算指数退避时间
315+
// calculateBackoff returns the exponential backoff for a failure count.
264316
func (rl *RateLimiter) calculateBackoff(failCount int) time.Duration {
265317
if failCount <= 0 {
266318
return 0
267319
}
268320

269321
backoff := float64(rl.backoffBase) * math.Pow(rl.backoffMultiplier, float64(failCount-1))
270322

271-
// 添加抖动
323+
// Add jitter so clients do not retry in lockstep.
272324
jitter := backoff * rl.jitterPercent * (rl.rng.Float64()*2 - 1)
273325
backoff += jitter
274326

@@ -278,7 +330,7 @@ func (rl *RateLimiter) calculateBackoff(failCount int) time.Duration {
278330
return time.Duration(backoff)
279331
}
280332

281-
// GetTokenState 获取 Token 状态(只读)
333+
// GetTokenState returns a defensive copy of the token state.
282334
func (rl *RateLimiter) GetTokenState(tokenKey string) *TokenState {
283335
rl.mu.RLock()
284336
defer rl.mu.RUnlock()
@@ -288,19 +340,19 @@ func (rl *RateLimiter) GetTokenState(tokenKey string) *TokenState {
288340
return nil
289341
}
290342

291-
// 返回副本以防止外部修改
343+
// Return a copy so callers cannot mutate internal state.
292344
stateCopy := *state
293345
return &stateCopy
294346
}
295347

296-
// ClearTokenState 清除 Token 状态
348+
// ClearTokenState removes all tracked state for a token.
297349
func (rl *RateLimiter) ClearTokenState(tokenKey string) {
298350
rl.mu.Lock()
299351
defer rl.mu.Unlock()
300352
delete(rl.states, tokenKey)
301353
}
302354

303-
// ResetSuspension 重置暂停状态
355+
// ResetSuspension clears a token's suspension and cooldown markers.
304356
func (rl *RateLimiter) ResetSuspension(tokenKey string) {
305357
rl.mu.Lock()
306358
defer rl.mu.Unlock()

0 commit comments

Comments
 (0)