fix(ratelimit): align module metadata and repo guidance
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
75f27a4906
commit
1ec0ea4d28
16 changed files with 109 additions and 67 deletions
11
CLAUDE.md
11
CLAUDE.md
|
|
@ -1,3 +1,5 @@
|
||||||
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
|
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
@ -6,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
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.
|
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: `forge.lthn.ai/core/go-ratelimit` — Go 1.26, no CGO required.
|
Module: `dappco.re/go/core/go-ratelimit` — Go 1.26, no CGO required.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|
@ -28,7 +30,7 @@ Pre-commit gate: `go test -race ./...` and `go vet ./...` must both pass.
|
||||||
- **Conventional commits**: `type(scope): description` — scopes: `ratelimit`, `sqlite`, `persist`, `config`
|
- **Conventional commits**: `type(scope): description` — scopes: `ratelimit`, `sqlite`, `persist`, `config`
|
||||||
- **Co-Author line** on every commit: `Co-Authored-By: Virgil <virgil@lethean.io>`
|
- **Co-Author line** on every commit: `Co-Authored-By: Virgil <virgil@lethean.io>`
|
||||||
- **Coverage** must not drop below 95%
|
- **Coverage** must not drop below 95%
|
||||||
- **Error format**: `coreerr.E("ratelimit.FunctionName", "what", err)` via `go-log` — lowercase, no trailing punctuation
|
- **Error format**: `core.E("ratelimit.FunctionName", "what", err)` via `dappco.re/go/core` — lowercase, no trailing punctuation
|
||||||
- **No `init()` functions**, no global mutable state
|
- **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.
|
- **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.
|
||||||
|
|
||||||
|
|
@ -60,10 +62,9 @@ SQLite tests use `_Good`/`_Bad`/`_Ugly` suffixes (happy path / expected errors /
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Five direct dependencies — do not add more without justification:
|
Four direct dependencies — do not add more without justification:
|
||||||
|
|
||||||
- `forge.lthn.ai/core/go-io` — file I/O abstraction
|
- `dappco.re/go/core` — file I/O helpers, structured errors, JSON helpers, path/environment utilities
|
||||||
- `forge.lthn.ai/core/go-log` — structured error handling (`coreerr.E`)
|
|
||||||
- `gopkg.in/yaml.v3` — YAML backend
|
- `gopkg.in/yaml.v3` — YAML backend
|
||||||
- `modernc.org/sqlite` — pure Go SQLite (no CGO)
|
- `modernc.org/sqlite` — pure Go SQLite (no CGO)
|
||||||
- `github.com/stretchr/testify` — test-only
|
- `github.com/stretchr/testify` — test-only
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Thank you for your interest in contributing!
|
Thank you for your interest in contributing!
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- **Go Version**: 1.26 or higher is required.
|
- **Go Version**: 1.26 or higher is required.
|
||||||
- **Tools**: `golangci-lint` and `task` (Taskfile.dev) are recommended.
|
- **Tools**: `golangci-lint` is recommended.
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
1. **Testing**: Ensure all tests pass before submitting changes.
|
1. **Testing**: Ensure all tests pass before submitting changes.
|
||||||
```bash
|
```bash
|
||||||
|
go build ./...
|
||||||
go test ./...
|
go test ./...
|
||||||
|
go test -race ./...
|
||||||
|
go test -cover ./...
|
||||||
|
go mod tidy
|
||||||
```
|
```
|
||||||
2. **Code Style**: All code must follow standard Go formatting.
|
2. **Code Style**: All code must follow standard Go formatting.
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -22,14 +28,22 @@ Thank you for your interest in contributing!
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commit Message Format
|
## Commit Message Format
|
||||||
We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification using the repository format `type(scope): description`:
|
||||||
- `feat`: A new feature
|
- `feat`: A new feature
|
||||||
- `fix`: A bug fix
|
- `fix`: A bug fix
|
||||||
- `docs`: Documentation changes
|
- `docs`: Documentation changes
|
||||||
- `refactor`: A code change that neither fixes a bug nor adds a feature
|
- `refactor`: A code change that neither fixes a bug nor adds a feature
|
||||||
- `chore`: Changes to the build process or auxiliary tools and libraries
|
- `chore`: Changes to the build process or auxiliary tools and libraries
|
||||||
|
|
||||||
Example: `feat: add new endpoint for health check`
|
Common scopes: `ratelimit`, `sqlite`, `persist`, `config`
|
||||||
|
|
||||||
## License
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fix(ratelimit): align module metadata with dappco.re
|
||||||
|
|
||||||
|
Co-Authored-By: Virgil <virgil@lethean.io>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Licence
|
||||||
By contributing to this project, you agree that your contributions will be licensed under the **European Union Public Licence (EUPL-1.2)**.
|
By contributing to this project, you agree that your contributions will be licensed under the **European Union Public Licence (EUPL-1.2)**.
|
||||||
|
|
|
||||||
35
README.md
35
README.md
|
|
@ -1,30 +1,41 @@
|
||||||
[](https://pkg.go.dev/forge.lthn.ai/core/go-ratelimit)
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
[](LICENSE.md)
|
|
||||||
|
[](https://pkg.go.dev/dappco.re/go/core/go-ratelimit)
|
||||||
|

|
||||||
[](go.mod)
|
[](go.mod)
|
||||||
|
|
||||||
# go-ratelimit
|
# go-ratelimit
|
||||||
|
|
||||||
Provider-agnostic sliding window rate limiter for LLM API calls. Enforces requests per minute (RPM), tokens per minute (TPM), and requests per day (RPD) quotas per model using an in-memory sliding window. Ships with default quota profiles for Gemini, OpenAI, Anthropic, and a local inference provider. State persists across process restarts via YAML (single-process) or SQLite (multi-process, WAL mode). Includes a Gemini-specific token counting helper and a YAML-to-SQLite migration path.
|
Provider-agnostic sliding window rate limiter for LLM API calls. Enforces requests per minute (RPM), tokens per minute (TPM), and requests per day (RPD) quotas per model using an in-memory sliding window. Ships with default quota profiles for Gemini, OpenAI, Anthropic, and a local inference provider. State persists across process restarts via YAML (single-process) or SQLite (multi-process, WAL mode). Includes a Gemini-specific token counting helper and a YAML-to-SQLite migration path.
|
||||||
|
|
||||||
**Module**: `forge.lthn.ai/core/go-ratelimit`
|
**Module**: `dappco.re/go/core/go-ratelimit`
|
||||||
**Licence**: EUPL-1.2
|
**Licence**: EUPL-1.2
|
||||||
**Language**: Go 1.25
|
**Language**: Go 1.26
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "forge.lthn.ai/core/go-ratelimit"
|
import "dappco.re/go/core/go-ratelimit"
|
||||||
|
|
||||||
// YAML backend (default, single-process)
|
// YAML backend (default, single-process)
|
||||||
rl, err := ratelimit.New()
|
rl, err := ratelimit.New()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// SQLite backend (multi-process)
|
// SQLite backend (multi-process)
|
||||||
rl, err := ratelimit.NewWithSQLite("~/.core/ratelimits.db")
|
rl, err = ratelimit.NewWithSQLite("/tmp/ratelimits.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
defer rl.Close()
|
defer rl.Close()
|
||||||
|
|
||||||
ok, reason := rl.CanSend("gemini-2.0-flash", 1500)
|
if rl.CanSend("gemini-2.0-flash", 1500) {
|
||||||
if ok {
|
rl.RecordUsage("gemini-2.0-flash", 1000, 500)
|
||||||
rl.RecordUsage("gemini-2.0-flash", 1500)
|
}
|
||||||
|
|
||||||
|
if err := rl.Persist(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -37,12 +48,14 @@ if ok {
|
||||||
## Build & Test
|
## Build & Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
go build ./...
|
||||||
go test ./...
|
go test ./...
|
||||||
go test -race ./...
|
go test -race ./...
|
||||||
go vet ./...
|
go vet ./...
|
||||||
go build ./...
|
go test -cover ./...
|
||||||
|
go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
European Union Public Licence 1.2 — see [LICENCE](LICENCE) for details.
|
European Union Public Licence 1.2.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
|
|
||||||
# API Contract
|
# API Contract
|
||||||
|
|
||||||
Test coverage is marked `yes` when the symbol is exercised by the existing test suite in `ratelimit_test.go`, `sqlite_test.go`, `error_test.go`, or `iter_test.go`.
|
Test coverage is marked `yes` when the symbol is exercised by the existing test suite in `ratelimit_test.go`, `sqlite_test.go`, `error_test.go`, or `iter_test.go`.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
|
|
||||||
---
|
---
|
||||||
title: Architecture
|
title: Architecture
|
||||||
description: Internals of go-ratelimit -- sliding window algorithm, provider quota system, persistence backends, and concurrency model.
|
description: Internals of go-ratelimit -- sliding window algorithm, provider quota system, persistence backends, and concurrency model.
|
||||||
|
|
@ -10,7 +12,7 @@ three independent quota dimensions per model -- requests per minute (RPM), token
|
||||||
per minute (TPM), and requests per day (RPD) -- using an in-memory sliding window
|
per minute (TPM), and requests per day (RPD) -- using an in-memory sliding window
|
||||||
that can be persisted across process restarts via YAML or SQLite.
|
that can be persisted across process restarts via YAML or SQLite.
|
||||||
|
|
||||||
Module path: `forge.lthn.ai/core/go-ratelimit`
|
Module path: `dappco.re/go/core/go-ratelimit`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -252,7 +254,7 @@ state:
|
||||||
day_count: 42
|
day_count: 42
|
||||||
```
|
```
|
||||||
|
|
||||||
`Persist()` creates parent directories with `os.MkdirAll` before writing.
|
`Persist()` creates parent directories with the `core.Fs` helper before writing.
|
||||||
`Load()` treats a missing file as an empty state (no error). Corrupt or
|
`Load()` treats a missing file as an empty state (no error). Corrupt or
|
||||||
unreadable files return an error.
|
unreadable files return an error.
|
||||||
|
|
||||||
|
|
@ -317,8 +319,8 @@ precision and allows efficient range queries using the composite indices.
|
||||||
|
|
||||||
### Save Strategy
|
### Save Strategy
|
||||||
|
|
||||||
- **Quotas**: `INSERT ... ON CONFLICT(model) DO UPDATE` (upsert). Existing quota
|
- **Quotas**: full snapshot replace inside a single transaction. `saveQuotas()`
|
||||||
rows are updated in place without deleting unrelated models.
|
clears the table and reinserts the current quota map.
|
||||||
- **State**: Delete-then-insert inside a single transaction. All three state
|
- **State**: Delete-then-insert inside a single transaction. All three state
|
||||||
tables (`requests`, `tokens`, `daily`) are truncated and rewritten atomically.
|
tables (`requests`, `tokens`, `daily`) are truncated and rewritten atomically.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
|
|
||||||
---
|
---
|
||||||
title: Development Guide
|
title: Development Guide
|
||||||
description: How to build, test, and contribute to go-ratelimit -- prerequisites, test patterns, coding standards, and commit conventions.
|
description: How to build, test, and contribute to go-ratelimit -- prerequisites, test patterns, coding standards, and commit conventions.
|
||||||
|
|
@ -18,6 +20,9 @@ No C toolchain, no system SQLite library, no external build tools. A plain
|
||||||
## Build and Test
|
## Build and Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Compile all packages
|
||||||
|
go build ./...
|
||||||
|
|
||||||
# Run all tests
|
# Run all tests
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
|
|
@ -42,12 +47,16 @@ go vet ./...
|
||||||
# Lint (requires golangci-lint)
|
# Lint (requires golangci-lint)
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
# Coverage check
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
# Tidy dependencies
|
# Tidy dependencies
|
||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
All three commands (`go test -race ./...`, `go vet ./...`, and `go mod tidy`)
|
Before a commit is pushed, `go build ./...`, `go test -race ./...`,
|
||||||
must produce no errors or warnings before a commit is pushed.
|
`go vet ./...`, `go test -cover ./...`, and `go mod tidy` must all pass
|
||||||
|
without errors, and coverage must remain at or above 95%.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -147,17 +156,8 @@ The following benchmarks are included:
|
||||||
|
|
||||||
### Coverage
|
### Coverage
|
||||||
|
|
||||||
Current coverage: 95.1%. The remaining paths cannot be covered in unit tests
|
Maintain at least 95% statement coverage. Verify it with `go test -cover ./...`
|
||||||
without modifying production code:
|
and document any justified exception in the commit or PR that introduces it.
|
||||||
|
|
||||||
1. `CountTokens` success path -- the Google API URL is hardcoded; unit tests
|
|
||||||
cannot intercept the HTTP call without URL injection support.
|
|
||||||
2. `yaml.Marshal` error path in `Persist()` -- `yaml.Marshal` does not fail on
|
|
||||||
valid Go structs.
|
|
||||||
3. `os.UserHomeDir()` error path in `NewWithConfig()` -- triggered only when
|
|
||||||
`$HOME` is unset, which test infrastructure prevents.
|
|
||||||
|
|
||||||
Do not lower coverage below 95% without a documented reason.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -172,8 +172,8 @@ Do not use American spellings in identifiers, comments, or documentation.
|
||||||
|
|
||||||
- All exported types, functions, and fields must have doc comments.
|
- All exported types, functions, and fields must have doc comments.
|
||||||
- Error strings must be lowercase and not end with punctuation (Go convention).
|
- Error strings must be lowercase and not end with punctuation (Go convention).
|
||||||
- Contextual errors use `fmt.Errorf("ratelimit.Function: what: %w", err)` so
|
- Contextual errors use `core.E("ratelimit.Function", "what", err)` so errors
|
||||||
errors identify their origin clearly.
|
identify their origin clearly.
|
||||||
- No `init()` functions.
|
- No `init()` functions.
|
||||||
- No global mutable state. `DefaultProfiles()` returns a fresh map on each call.
|
- No global mutable state. `DefaultProfiles()` returns a fresh map on each call.
|
||||||
|
|
||||||
|
|
@ -196,6 +196,7 @@ Direct dependencies are intentionally minimal:
|
||||||
|
|
||||||
| Dependency | Purpose |
|
| Dependency | Purpose |
|
||||||
|------------|---------|
|
|------------|---------|
|
||||||
|
| `dappco.re/go/core` | File I/O helpers, structured errors, JSON helpers, path/environment utilities |
|
||||||
| `gopkg.in/yaml.v3` | YAML serialisation for legacy backend |
|
| `gopkg.in/yaml.v3` | YAML serialisation for legacy backend |
|
||||||
| `modernc.org/sqlite` | Pure Go SQLite for persistent backend |
|
| `modernc.org/sqlite` | Pure Go SQLite for persistent backend |
|
||||||
| `github.com/stretchr/testify` | Test assertions (test-only) |
|
| `github.com/stretchr/testify` | Test assertions (test-only) |
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
|
|
||||||
# Project History
|
# Project History
|
||||||
|
|
||||||
## Origin
|
## Origin
|
||||||
|
|
||||||
go-ratelimit was extracted from the `pkg/ratelimit` package inside
|
go-ratelimit was extracted from the `pkg/ratelimit` package inside
|
||||||
`forge.lthn.ai/core/go` on 19 February 2026. The extraction gave the package
|
`forge.lthn.ai/core/go` on 19 February 2026. The package now lives at
|
||||||
its own module path, repository, and independent development cadence.
|
`dappco.re/go/core/go-ratelimit`, with its own repository and independent
|
||||||
|
development cadence.
|
||||||
|
|
||||||
Initial commit: `fa1a6fc` — `feat: extract go-ratelimit from core/go pkg/ratelimit`
|
Initial commit: `fa1a6fc` — `feat: extract go-ratelimit from core/go pkg/ratelimit`
|
||||||
|
|
||||||
|
|
@ -25,7 +28,7 @@ Commit: `3c63b10` — `feat(ratelimit): generalise beyond Gemini with provider p
|
||||||
|
|
||||||
Supplementary commit: `db958f2` — `test: expand race coverage and benchmarks`
|
Supplementary commit: `db958f2` — `test: expand race coverage and benchmarks`
|
||||||
|
|
||||||
Coverage increased from 77.1% to 95.1%. The test suite was rewritten using
|
Coverage increased from 77.1% to above the 95% floor. The test suite was rewritten using
|
||||||
testify with table-driven subtests throughout.
|
testify with table-driven subtests throughout.
|
||||||
|
|
||||||
### Tests added
|
### Tests added
|
||||||
|
|
@ -58,18 +61,6 @@ testify with table-driven subtests throughout.
|
||||||
- `BenchmarkAllStats` — 5 models x 200 entries
|
- `BenchmarkAllStats` — 5 models x 200 entries
|
||||||
- `BenchmarkPersist` — YAML I/O
|
- `BenchmarkPersist` — YAML I/O
|
||||||
|
|
||||||
### Remaining uncovered paths (5%)
|
|
||||||
|
|
||||||
These three paths are structurally impossible to cover in unit tests without
|
|
||||||
modifying production code:
|
|
||||||
|
|
||||||
1. `CountTokens` success path — the Google API URL is hardcoded; unit tests
|
|
||||||
cannot intercept the HTTP call without URL injection support
|
|
||||||
2. `yaml.Marshal` error path in `Persist()` — `yaml.Marshal` does not fail on
|
|
||||||
valid Go structs; the error branch exists for correctness only
|
|
||||||
3. `os.UserHomeDir()` error path in `NewWithConfig()` — triggered only when
|
|
||||||
`$HOME` is unset, which test infrastructure prevents
|
|
||||||
|
|
||||||
`go test -race ./...` passed clean. `go vet ./...` produced no warnings.
|
`go test -race ./...` passed clean. `go vet ./...` produced no warnings.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -139,7 +130,7 @@ established elsewhere in the ecosystem.
|
||||||
|
|
||||||
- `TestNewSQLiteStore_Good / _Bad` — creation and invalid path handling
|
- `TestNewSQLiteStore_Good / _Bad` — creation and invalid path handling
|
||||||
- `TestSQLiteQuotasRoundTrip_Good` — save/load round-trip
|
- `TestSQLiteQuotasRoundTrip_Good` — save/load round-trip
|
||||||
- `TestSQLiteQuotasUpsert_Good` — upsert replaces existing rows
|
- `TestSQLite_QuotasOverwrite_Good` — the latest quota snapshot replaces previous rows
|
||||||
- `TestSQLiteStateRoundTrip_Good` — multi-model state with nanosecond precision
|
- `TestSQLiteStateRoundTrip_Good` — multi-model state with nanosecond precision
|
||||||
- `TestSQLiteStateOverwrite_Good` — delete-then-insert atomicity
|
- `TestSQLiteStateOverwrite_Good` — delete-then-insert atomicity
|
||||||
- `TestSQLiteEmptyState_Good` — fresh database returns empty maps
|
- `TestSQLiteEmptyState_Good` — fresh database returns empty maps
|
||||||
|
|
@ -168,11 +159,10 @@ Not yet implemented. Intended downstream integrations:
|
||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
**CountTokens URL is hardcoded.** The `CountTokens` helper calls
|
**CountTokens URL is hardcoded.** The exported `CountTokens` helper calls
|
||||||
`generativelanguage.googleapis.com` directly. There is no way to override the
|
`generativelanguage.googleapis.com` directly. Callers cannot redirect it to
|
||||||
base URL, which prevents testing the success path in unit tests and prevents
|
Gemini-compatible proxies or alternate endpoints without going through an
|
||||||
use with Gemini-compatible proxies. A future refactor would accept a base URL
|
internal helper or refactoring the API to accept a base URL or `http.Client`.
|
||||||
parameter or an `http.Client`.
|
|
||||||
|
|
||||||
**saveState is a full table replace.** On every `Persist()` call, the `requests`,
|
**saveState is a full table replace.** On every `Persist()` call, the `requests`,
|
||||||
`tokens`, and `daily` tables are truncated and rewritten. For a limiter tracking
|
`tokens`, and `daily` tables are truncated and rewritten. For a limiter tracking
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
|
|
||||||
---
|
---
|
||||||
title: go-ratelimit
|
title: go-ratelimit
|
||||||
description: Provider-agnostic sliding window rate limiter for LLM API calls, with YAML and SQLite persistence backends.
|
description: Provider-agnostic sliding window rate limiter for LLM API calls, with YAML and SQLite persistence backends.
|
||||||
|
|
@ -5,7 +7,7 @@ description: Provider-agnostic sliding window rate limiter for LLM API calls, wi
|
||||||
|
|
||||||
# go-ratelimit
|
# go-ratelimit
|
||||||
|
|
||||||
**Module**: `forge.lthn.ai/core/go-ratelimit`
|
**Module**: `dappco.re/go/core/go-ratelimit`
|
||||||
**Licence**: EUPL-1.2
|
**Licence**: EUPL-1.2
|
||||||
**Go version**: 1.26+
|
**Go version**: 1.26+
|
||||||
|
|
||||||
|
|
@ -19,7 +21,7 @@ migration helper is included.
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "forge.lthn.ai/core/go-ratelimit"
|
import "dappco.re/go/core/go-ratelimit"
|
||||||
|
|
||||||
// Create a limiter with Gemini defaults (YAML backend).
|
// Create a limiter with Gemini defaults (YAML backend).
|
||||||
rl, err := ratelimit.New()
|
rl, err := ratelimit.New()
|
||||||
|
|
@ -103,6 +105,7 @@ The module is a single package with no sub-packages.
|
||||||
|
|
||||||
| Dependency | Purpose | Category |
|
| Dependency | Purpose | Category |
|
||||||
|------------|---------|----------|
|
|------------|---------|----------|
|
||||||
|
| `dappco.re/go/core` | File I/O helpers, structured errors, JSON helpers, path/environment utilities | Direct |
|
||||||
| `gopkg.in/yaml.v3` | YAML serialisation for the legacy persistence backend | Direct |
|
| `gopkg.in/yaml.v3` | YAML serialisation for the legacy persistence backend | Direct |
|
||||||
| `modernc.org/sqlite` | Pure Go SQLite driver (no CGO required) | Direct |
|
| `modernc.org/sqlite` | Pure Go SQLite driver (no CGO required) | Direct |
|
||||||
| `github.com/stretchr/testify` | Test assertions (`assert`, `require`) | Test only |
|
| `github.com/stretchr/testify` | Test assertions (`assert`, `require`) | Test only |
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
||||||
|
|
||||||
# Security Attack Vector Mapping
|
# Security Attack Vector Mapping
|
||||||
|
|
||||||
Scope: external inputs that cross into this package from callers, persisted storage, or the network. This is a mapping only; it does not propose or apply fixes.
|
Scope: external inputs that cross into this package from callers, persisted storage, or the network. This is a mapping only; it does not propose or apply fixes.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
package ratelimit
|
package ratelimit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -647,7 +649,7 @@ func TestError_MigrateYAMLToSQLiteNilQuotasAndState_Good(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestError_NewWithConfigUserHomeDir_Bad(t *testing.T) {
|
func TestError_NewWithConfigHomeUnavailable_Bad(t *testing.T) {
|
||||||
// Clear all supported home env vars so defaultStatePath cannot resolve a home directory.
|
// Clear all supported home env vars so defaultStatePath cannot resolve a home directory.
|
||||||
t.Setenv("CORE_HOME", "")
|
t.Setenv("CORE_HOME", "")
|
||||||
t.Setenv("HOME", "")
|
t.Setenv("HOME", "")
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -1,4 +1,6 @@
|
||||||
module forge.lthn.ai/core/go-ratelimit
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
module dappco.re/go/core/go-ratelimit
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
package ratelimit
|
package ratelimit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
package ratelimit
|
package ratelimit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
package ratelimit
|
package ratelimit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
package ratelimit
|
package ratelimit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
package ratelimit
|
package ratelimit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -57,8 +59,8 @@ func TestSQLite_QuotasRoundTrip_Good(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSQLite_QuotasUpsert_Good(t *testing.T) {
|
func TestSQLite_QuotasOverwrite_Good(t *testing.T) {
|
||||||
dbPath := testPath(t.TempDir(), "upsert.db")
|
dbPath := testPath(t.TempDir(), "overwrite.db")
|
||||||
store, err := newSQLiteStore(dbPath)
|
store, err := newSQLiteStore(dbPath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer store.close()
|
defer store.close()
|
||||||
|
|
@ -68,7 +70,7 @@ func TestSQLite_QuotasUpsert_Good(t *testing.T) {
|
||||||
"model-a": {MaxRPM: 100, MaxTPM: 50000, MaxRPD: 1000},
|
"model-a": {MaxRPM: 100, MaxTPM: 50000, MaxRPD: 1000},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Upsert with updated values.
|
// Save a second snapshot with updated values.
|
||||||
require.NoError(t, store.saveQuotas(map[string]ModelQuota{
|
require.NoError(t, store.saveQuotas(map[string]ModelQuota{
|
||||||
"model-a": {MaxRPM: 999, MaxTPM: 888, MaxRPD: 777},
|
"model-a": {MaxRPM: 999, MaxTPM: 888, MaxRPD: 777},
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue