go-ratelimit/CLAUDE.md
Virgil 1ec0ea4d28 fix(ratelimit): align module metadata and repo guidance
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-27 04:23:34 +00:00

4 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Overview

Provider-agnostic sliding window rate limiter for LLM API calls. Single Go package (no sub-packages) with two persistence backends: YAML (single-process, default) and SQLite (multi-process, WAL mode). Enforces RPM, TPM, and RPD quotas per model. Ships default profiles for Gemini, OpenAI, Anthropic, and Local providers.

Module: dappco.re/go/core/go-ratelimit — Go 1.26, no CGO required.

Commands

go test ./...                                              # run all tests
go test -race ./...                                        # race detector (required before commit)
go test -v -run TestCanSend ./...                          # single test
go test -v -run "TestCanSend/RPM_at_exact_limit" ./...     # single subtest
go test -bench=. -benchmem ./...                           # benchmarks
go vet ./...                                               # vet check
golangci-lint run ./...                                    # lint

Pre-commit gate: go test -race ./... and go vet ./... must both pass.

Standards

  • UK English everywhere: colour, organisation, serialise, initialise, behaviour
  • Conventional commits: type(scope): description — scopes: ratelimit, sqlite, persist, config
  • Co-Author line on every commit: Co-Authored-By: Virgil <virgil@lethean.io>
  • Coverage must not drop below 95%
  • Error format: core.E("ratelimit.FunctionName", "what", err) via dappco.re/go/core — lowercase, no trailing punctuation
  • No init() functions, no global mutable state
  • Mutex discipline: lock at the top of public methods, never inside helpers. Helpers that need the lock document "Caller must hold the lock". prune() mutates state, so even "read-only" methods that call it take the write lock. Never call a public method from another public method while holding the lock.

Architecture

All code lives in the root package. Key files:

  • ratelimit.go — core types (RateLimiter, ModelQuota, UsageStats, Config, Provider), sliding window logic (prune, CanSend, RecordUsage), YAML persistence, CountTokens (Gemini-specific), iterators (Models, Iter)
  • sqlite.gosqliteStore internal type, schema creation, load/save for quotas and state

Constructor matrix: New() / NewWithConfig() for YAML, NewWithSQLite() / NewWithSQLiteConfig() for SQLite. Always defer rl.Close() with SQLite.

Sliding window

1-minute window pruned on every CanSend/Stats/RecordUsage call. Daily counter is a rolling 24h window from first request, not a calendar boundary. Empty state entries are garbage-collected by prune() to prevent memory leaks.

Test Organisation

White-box tests (package ratelimit), all assertions via testify (require for fatal, assert for non-fatal). Do not use t.Error/t.Fatal directly.

File Scope
ratelimit_test.go Core logic, provider profiles, concurrency, benchmarks
sqlite_test.go SQLite backend, migration, concurrent persistence
error_test.go Error paths for SQLite and YAML
iter_test.go Iterators, CountTokens edge cases

SQLite tests use _Good/_Bad/_Ugly suffixes (happy path / expected errors / edge cases). Core tests use plain descriptive names with table-driven subtests. Use t.TempDir() for all file paths.

Dependencies

Four direct dependencies — do not add more without justification:

  • dappco.re/go/core — file I/O helpers, structured errors, JSON helpers, path/environment utilities
  • gopkg.in/yaml.v3 — YAML backend
  • modernc.org/sqlite — pure Go SQLite (no CGO)
  • github.com/stretchr/testify — test-only

Docs

  • docs/architecture.md — sliding window algorithm, provider quotas, YAML/SQLite backends, concurrency model
  • docs/development.md — prerequisites, test patterns, coding standards
  • docs/history.md — completed phases with commit hashes, known limitations