feat(llm): add configurable request/token rate limiting for LLM client
Introduce process-local and Redis-backed global rate limiting for the shared LLM client, plus a dedicated generation consumer prefetch knob. - Add request (RPM) and token (TPM) rate limits with burst controls, process or global scope, and fail-closed/process-degraded failover - Wire the LLM client through NewWithRedis so limits can be shared across replicas via a Redis token bucket - Add generation_consumer_prefetch to tune RabbitMQ generation consumers independently of the default prefetch - Bump generation worker_concurrency to 24 and promote golang.org/x/time to a direct dependency - Drop t.Parallel() from a few middleware/bootstrap tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,125 @@ compliance:
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAppliesLLMRateLimitConfig(t *testing.T) {
|
||||
configPath := writeTestConfig(t, `
|
||||
llm:
|
||||
rate_limit_scope: global
|
||||
rate_limit_key: ark:shared
|
||||
request_rate_limit_rpm: 30000
|
||||
request_rate_limit_burst: 100
|
||||
token_rate_limit_tpm: 5000000
|
||||
token_rate_limit_burst: 166000
|
||||
estimated_chars_per_token: 1.25
|
||||
rate_limit_failover_mode: process_degraded
|
||||
rate_limit_failover_replicas: 10
|
||||
`)
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.LLM.RateLimitScope != "global" ||
|
||||
cfg.LLM.RateLimitKey != "ark:shared" ||
|
||||
cfg.LLM.RequestRateLimitRPM != 30000 ||
|
||||
cfg.LLM.RequestRateLimitBurst != 100 ||
|
||||
cfg.LLM.TokenRateLimitTPM != 5000000 ||
|
||||
cfg.LLM.TokenRateLimitBurst != 166000 ||
|
||||
cfg.LLM.EstimatedCharsPerToken != 1.25 ||
|
||||
cfg.LLM.RateLimitFailoverMode != "process_degraded" ||
|
||||
cfg.LLM.RateLimitFailoverReplicas != 10 {
|
||||
t.Fatalf("unexpected llm rate limit config: %#v", cfg.LLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAppliesLLMRateLimitEnvOverrides(t *testing.T) {
|
||||
t.Setenv("LLM_RATE_LIMIT_SCOPE", "global")
|
||||
t.Setenv("LLM_RATE_LIMIT_KEY", "env-ark-shared")
|
||||
t.Setenv("LLM_REQUEST_RATE_LIMIT_RPM", "9000")
|
||||
t.Setenv("LLM_REQUEST_RATE_LIMIT_BURST", "90")
|
||||
t.Setenv("LLM_TOKEN_RATE_LIMIT_TPM", "1500000")
|
||||
t.Setenv("LLM_TOKEN_RATE_LIMIT_BURST", "150000")
|
||||
t.Setenv("LLM_ESTIMATED_CHARS_PER_TOKEN", "1.5")
|
||||
t.Setenv("LLM_RATE_LIMIT_FAILOVER_MODE", "local")
|
||||
t.Setenv("LLM_RATE_LIMIT_FAILOVER_REPLICAS", "12")
|
||||
|
||||
configPath := writeTestConfig(t, `
|
||||
llm:
|
||||
rate_limit_scope: process
|
||||
request_rate_limit_rpm: 10000
|
||||
token_rate_limit_tpm: 1660000
|
||||
`)
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.LLM.RateLimitScope != "global" ||
|
||||
cfg.LLM.RateLimitKey != "env-ark-shared" ||
|
||||
cfg.LLM.RequestRateLimitRPM != 9000 ||
|
||||
cfg.LLM.RequestRateLimitBurst != 90 ||
|
||||
cfg.LLM.TokenRateLimitTPM != 1500000 ||
|
||||
cfg.LLM.TokenRateLimitBurst != 150000 ||
|
||||
cfg.LLM.EstimatedCharsPerToken != 1.5 ||
|
||||
cfg.LLM.RateLimitFailoverMode != "process_degraded" ||
|
||||
cfg.LLM.RateLimitFailoverReplicas != 12 {
|
||||
t.Fatalf("unexpected llm env rate limit config: %#v", cfg.LLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNormalizesLLMRateLimitConfig(t *testing.T) {
|
||||
configPath := writeTestConfig(t, `
|
||||
llm:
|
||||
request_rate_limit_rpm: -1
|
||||
request_rate_limit_burst: -1
|
||||
token_rate_limit_tpm: -1
|
||||
token_rate_limit_burst: -1
|
||||
estimated_chars_per_token: 0
|
||||
rate_limit_scope: broken
|
||||
rate_limit_key: " shared-key "
|
||||
rate_limit_failover_mode: broken
|
||||
rate_limit_failover_replicas: -1
|
||||
`)
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.LLM.RequestRateLimitRPM != 0 ||
|
||||
cfg.LLM.RequestRateLimitBurst != 0 ||
|
||||
cfg.LLM.TokenRateLimitTPM != 0 ||
|
||||
cfg.LLM.TokenRateLimitBurst != 0 ||
|
||||
cfg.LLM.EstimatedCharsPerToken != 1 ||
|
||||
cfg.LLM.RateLimitScope != "process" ||
|
||||
cfg.LLM.RateLimitKey != "shared-key" ||
|
||||
cfg.LLM.RateLimitFailoverMode != "fail_closed" ||
|
||||
cfg.LLM.RateLimitFailoverReplicas != 0 {
|
||||
t.Fatalf("unexpected normalized llm rate limit config: %#v", cfg.LLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAppliesRabbitMQGenerationPrefetchEnvOverride(t *testing.T) {
|
||||
t.Setenv("RABBITMQ_GENERATION_CONSUMER_PREFETCH", "1")
|
||||
|
||||
configPath := writeTestConfig(t, `
|
||||
rabbitmq:
|
||||
consumer_prefetch: 10
|
||||
generation_consumer_prefetch: 5
|
||||
`)
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.RabbitMQ.ConsumerPrefetch != 10 || cfg.RabbitMQ.GenerationConsumerPrefetch != 1 {
|
||||
t.Fatalf("unexpected rabbitmq prefetch config: %#v", cfg.RabbitMQ)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseConfigDSNEscapesCredentials(t *testing.T) {
|
||||
cfg := DatabaseConfig{
|
||||
Host: "db.internal",
|
||||
|
||||
Reference in New Issue
Block a user