Last updated 2026-05-17
Contributing
Thanks for considering a contribution. llmrouter is a
small, deliberately narrow library with a high bar on stability,
test coverage, and dependency footprint. The rules below exist so
contributions land cleanly and stay landed.
Before you start work on anything beyond a typo or a one-file fix, please open an issue first so we can agree on the shape. Most PR rework happens because the design conversation didn’t happen up front.
License
llmrouter is licensed under the
Apache License 2.0.
All contributions are accepted under the same licence; submitting
a pull request constitutes agreement to license your contribution
under Apache-2.0.
The library does not currently require a separate CLA. The Apache-2.0 license includes an implicit patent grant from contributors, which covers the project’s needs.
Code of conduct
We follow a simple, standard rule: be respectful, assume good faith, and keep discussion focused on the technical question at hand. Inclusive language is required in code, comments, commit messages, issues, and PR reviews.
If a CODE_OF_CONDUCT.md file exists at the repo
root, it is the authoritative version and supersedes this
paragraph. If not, the
Contributor Covenant v2.1
is the de facto policy. Report concerns privately to
security@elloloop.com (same address as security
reports; we triage them separately).
Development setup
Clone the repository and run the test suite — that’s the whole bootstrap:
git clone https://github.com/elloloop/llmroutercd llmroutergo test ./...Requirements:
- Go 1.23+ — pinned in
go.mod. We use generics and a few newer stdlib APIs. gofmt— bundled with the Go toolchain. Run it before every commit (most editors do it on save).- Nothing else. No
make, nojust, no docker, no protobuf compiler, no pre-commit hooks.go test,go vet,gofmt, and your editor are the whole tool chain.
Optional but recommended:
-
go test -race ./...to catch concurrency issues in the streaming producer. CI runs with-raceon every PR. -
go test -count=1 ./...to bypass the test cache when you want to be sure a flaky test is actually fixed. -
go vet ./...for the standard set of lints.
Project layout
The repository is intentionally flat:
llmrouter/├── llmrouter.go // core types: Provider, ChatRequest, Message, Chunk├── options.go // option pattern (WithAPIKey, WithBaseURL, ...)├── stream.go // Stream type, channel + goroutine lifecycle├── errors.go // ErrUpstream + helpers├── *_test.go // table-driven tests, mirroring each source file├── providers/│ ├── openai/ // OpenAI provider (passthrough)│ │ ├── openai.go│ │ └── openai_test.go│ └── anthropic/ // Anthropic provider (translating)│ ├── anthropic.go│ └── anthropic_test.go├── docs-site/ // Astro source for the docs site (this site)├── docs/ // GENERATED output of docs-site — do not edit by hand├── README.md├── LICENSE└── go.modTwo layout rules that matter:
-
Anything user-facing lives in the root package or under
providers/<name>/. There is nointernal/directory yet; if private helpers grow enough to need one, we’ll add it. - The
docs/directory is generated output. It is the built static site that GitHub Pages serves. Edits to files underdocs/will be wiped by the next build. Edit the Astro source underdocs-site/instead.
Code style
Standard Go. We follow Google’s Go Style Guide where it overlaps with the toolchain’s defaults, and the toolchain’s defaults everywhere else. Specifically:
gofmtfor formatting. No exceptions.- Doc comments on every exported identifier, in the standard Godoc style (“Provider names ...”, not “This function returns the name of the provider.”).
- Error strings start lowercase, do not end with punctuation, do not capitalise the first word.
- Wrap errors with
fmt.Errorf("context: %w", err)to keep the chain intact. -
Channels are typed and bounded. Document the buffer size and
the producer/consumer contract in a comment above the
makecall. -
Use
context.Contextas the first parameter of any function that does I/O. Never store a context in a struct.
The dependency bar
llmrouter has a strict and unusual dependency
policy. The bar is:
- The Go standard library, or
- An official vendor SDK for a provider we ship — i.e. published by Google, AWS, Anthropic, or OpenAI directly.
That’s it. We do not depend on community helper libraries,
structured-logging frameworks, error packages, or any
third-party utility. The current dependency tree is the
standard library plus
google/uuid
(because reimplementing RFC 4122 in the library would be silly,
and Google’s package is the de facto standard).
PRs that add a new third-party dependency outside this bar will be asked to remove it. If the functionality is small, re-implement it inside the library. If it is large enough that re-implementation is unreasonable, file an issue first — most such PRs are better handled by not implementing the feature in the library at all and pushing the dependency up into the calling code.
Tests
Every exported function gets a table-driven test. This is not a style preference — it is enforced by code review and by coverage gates in CI.
-
Use the standard library’s
testingpackage. Notestify, nogomock, no BDD frameworks. -
Table-driven tests with named sub-tests:
t.Run(tc.name, func(t *testing.T) { ... }). Sub-test names should describe the scenario in English (“rejects empty api key”, not “tc1”). -
Mock external HTTP via
httptest.NewServer— never reach out to real endpoints in unit tests. Real-endpoint smoke tests live in a separate, opt-inintegrationbuild tag. - Edge cases are required: empty inputs, nil pointers, context cancellation mid-stream, non-2xx upstreams, malformed SSE, premature EOF.
-
Race-clean:
go test -race -count=1 ./...must pass on every PR. CI runs this on every push.
Coverage targets:
- Root package: 100% statement coverage.
- Each provider: ≥90% statement coverage.
New code that drops coverage below these thresholds will be asked for more tests before it lands.
Adding a provider
Adding a new provider is the most common substantial contribution. The mechanics are documented in detail in Adding a new provider. The short version:
-
File an issue first describing the provider, its auth model,
its request body shape, and its streaming format. We’ll
check it against the existing surface and agree on whether it
lives under
providers/<name>/or is better served by aWithBaseURLoverride on an existing provider. -
Create
providers/<name>/<name>.goandproviders/<name>/<name>_test.go. Mirror the package layout of the existing providers exactly. -
Implement the
llmrouter.Providerinterface. Use the OpenAI provider as a reference for passthrough providers, the Anthropic provider as a reference for translating providers. -
Use
httptest.NewServerto mock the upstream in the provider test. Cover at minimum: success path, non-2xx upstream, malformed SSE, mid-stream context cancellation, and empty deltas. -
Add a docs page under
docs-site/src/pages/docs/providers/<name>.astroand wire it intodocs-site/src/data/nav.ts.
Pull request process
- Open an issue first for anything beyond a typo or a one-file bug fix. The issue is where the design conversation happens; the PR is where the code review happens. Sending a 400-line PR with no issue attached usually means one of us asks you to reshape it from scratch.
- One logical change per PR. “Add Azure provider” is one PR. “Add Azure provider, refactor stream.go, and rename ErrUpstream” is three PRs. Small PRs land fast; big PRs sit.
- Conventional commits are encouraged.
Prefixes:
feat:(new functionality),fix:(bug fix),refactor:(no behaviour change),docs:(docs only),test:(tests only),chore:(deps, tooling, CI). The first line of the PR description is what ends up in the squash-merge commit, so treat it like the commit message. - Tests are required for any source change. Doc-only and test-only PRs are exempt.
- CI must be green. CI runs
go vet ./...,gofmtcheck,go test -race -count=1 ./..., and a coverage gate. Red CI = no merge. - Squash-merge. All PRs land as a single
commit on
main. The PR title becomes the commit title; the PR description becomes the commit body.
Release process
Releases are cut by maintainers. Contributors don’t need to bump versions or touch the changelog file — that’s handled in the release commit. Here is the process for transparency:
- The changelog page summarises the user-visible delta for the upcoming release; we keep it accurate as PRs land.
-
A maintainer cuts a tag
vX.Y.Zonmain. The tag is what GitHub Releases keys off. - CI builds the docs site and publishes a GitHub Release with auto-generated notes plus the curated changelog entry.
-
pkg.go.devpicks up the new tag within a few minutes.
Version bumps follow the rules on the roadmap page:
- Patch (
v0.1.0 → v0.1.1): bug fixes, tests, docs — no API changes. - Minor (
v0.1.x → v0.2.0): new providers, new features. May break the API while pre-1.0. - Major (
v0.x → v1.0): reserved for the v1.0 API freeze. After v1.0, only breaking changes bump major.
The release pattern is loosely modelled on
release-please
— conventional-commit messages on main determine
the next bump.
Filing issues
Good bug reports save everyone time. Include all of the following — if any are missing, the first reply is usually going to ask for them:
- Go version:
go version. - OS and architecture:
go env GOOS GOARCH. llmrouterversion: the tag in yourgo.mod.- Which provider you were calling, and its base URL if non-default.
- A minimal reproduction: the smallest piece of Go that triggers the bug. If the bug only reproduces against a live provider, include the upstream response or the SSE transcript (redact API keys).
- Expected behaviour and actual behaviour.
- The full error message and stack trace, if there is one.
Feature requests should include a use case — see How to influence the roadmap.
Security
Please do not file security vulnerabilities as
public GitHub issues. We take the boring approach of private
disclosure: send a report to
security@elloloop.com with as much detail as you
can give us, and we’ll acknowledge within a couple of
business days.
What counts as a security issue:
- API keys, secrets, or auth tokens leaking into logs, error messages, or panics.
- Memory-safety or goroutine-leak bugs reachable from untrusted input.
-
SSRF or path-traversal reachable through a configuration
option (e.g. an unsanitised
WithBaseURL). - Any way the library can be coerced into making an outbound HTTP request to an endpoint the caller did not configure.
What does not count: feature requests phrased as
security concerns (“the library should have rate
limiting”), upstream-provider issues, or general
hardening suggestions. Those should be regular issues with the
type:feature or type:hardening label.
Thanks
The library exists because the Kite AI Router project needed it and the resulting code was clean enough to extract. Thanks to the contributors and reviewers on that project for proving the design in production, and to anyone sending a PR or filing an issue here for keeping the library honest.