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/llmrouter
cd llmrouter
go 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, no just, 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 -race on 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.mod

Two layout rules that matter:

  • Anything user-facing lives in the root package or under providers/<name>/. There is no internal/ 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 under docs/ will be wiped by the next build. Edit the Astro source under docs-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:

  • gofmt for 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 make call.
  • Use context.Context as 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:

  1. The Go standard library, or
  2. 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 testing package. No testify, no gomock, 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-in integration build 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:

  1. 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 a WithBaseURL override on an existing provider.
  2. Create providers/<name>/<name>.go and providers/<name>/<name>_test.go. Mirror the package layout of the existing providers exactly.
  3. Implement the llmrouter.Provider interface. Use the OpenAI provider as a reference for passthrough providers, the Anthropic provider as a reference for translating providers.
  4. Use httptest.NewServer to mock the upstream in the provider test. Cover at minimum: success path, non-2xx upstream, malformed SSE, mid-stream context cancellation, and empty deltas.
  5. Add a docs page under docs-site/src/pages/docs/providers/<name>.astro and wire it into docs-site/src/data/nav.ts.

Pull request process

  1. 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.
  2. 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.
  3. 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.
  4. Tests are required for any source change. Doc-only and test-only PRs are exempt.
  5. CI must be green. CI runs go vet ./..., gofmt check, go test -race -count=1 ./..., and a coverage gate. Red CI = no merge.
  6. 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:

  1. The changelog page summarises the user-visible delta for the upcoming release; we keep it accurate as PRs land.
  2. A maintainer cuts a tag vX.Y.Z on main. The tag is what GitHub Releases keys off.
  3. CI builds the docs site and publishes a GitHub Release with auto-generated notes plus the curated changelog entry.
  4. pkg.go.dev picks 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.
  • llmrouter version: the tag in your go.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.