A multi-account, multi-upstream, OpenAI-compatible HTTP adapter. Modular successor to freebuff2api v0.1.0, restructured so a new project can plug in additional upstreams (Claude, june.so, codebuff-pro, Pollinations, your own auth-aware proxy) or switch the account-selection strategy without forking the service.
This package ships:
- An OpenAI-compatible HTTP surface (
/v1/models,/v1/chat/completions) backed by a pluggable Upstream Provider. - Multi-account pools with selectable strategies (least-loaded, round-robin, model-affinity) — add your own strategy in 4 lines.
- A CLI for operator workflows (
doctor,pool ls,rotate,dryrun). - A clean provider abstraction — adding a new upstream is one file +
@register_provider("name"), no router changes required.
The legacy v0.1.0 architecture is preserved at the wire level: every URL and response shape is identical to v0.1.0, so existing callers migrate without client changes.
You need:
- An OpenAI-compatible endpoint that hides multiple free/cheap upstreams behind a single auth token (Bearer) and round-robins / picks-best across them.
- Different upstreams per environment / per project — codebuff today, Claude Clone tomorrow.
- A way to onboard new upstreams in under one hour without touching routers, error handlers, or settings.
You do not need this if:
- You only need a single upstream. Use that upstream's SDK directly.
- You do not run a multi-account pool — skip
pool/strategies.py.
# Inside the package, install as editable so console scripts wire up.
cd freebuff2api-v2
python -m pip install -e .
# Then you have two console entries:
freebuff2api # CLI (doctor / pool / rotate / dryrun)
freebuff2api-app # uvicorn serverAfter install, prepare the credentials file:
cp .env.example .env
chmod 600 .env # atomic single-user only
# Edit .env to set:
# FREEBUFF_TOKEN=<upstream Bearer>
# FREEBUFF_API_KEY=<local Bearer — clients send this in Authorization header>
# accounts.json lives alongside .env, also chmod 600.accounts.json has the shape used by the legacy pool:
{
"accounts": [
{
"userId": "<uuid>",
"email": "you@example.com",
"name": "primary",
"label": "primary",
"enabled": true,
"addedAt": "2026-06-09T04:01:26.211Z",
"token": "<upstream token>"
}
]
}freebuff2api-app
# or
freebuff2api # if no CLI verb → starts uvicornDefault bind: 127.0.0.1:8080. Override with FREEBUFF_HOST and FREEBUFF_PORT.
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: NOTSETFORREPRO" \
-H "Content-Type: application/json" \
-d '{"model":"minimax/minimax-m3","messages":[{"role":"user","content":"hi"}]}'Authorization: Bearer is required when FREEBUFF_API_KEY is set in .env.
Without FREEBUFF_API_KEY, the API is open (do not run this in production).
freebuff2api doctor # settings + accounts/health snapshot, no upstream call
freebuff2api pool ls # list accounts and rotated tokens (last-4 only)
freebuff2api rotate # force upstream session refresh
freebuff2api dryrun # verify imports + module surface, no upstream callfreebuff2api/
├── core/
│ ├── settings.py # Typed Settings singleton, parsed from .env + env vars
│ ├── auth.py # require_local_bearer + require_cli_token (machine-id secret)
│ ├── exceptions.py # FreebuffError hierarchy + handler-friendly to_response()
│ └── logging.py # JSON formatter, optional PII redaction
├── providers/
│ ├── base.py # ChatRequest, ChatResponse, AccountQuota, UpstreamProvider Protocol
│ ├── freebuff.py # FreebuffProvider — adapter wrapping legacy freebuff2api.codebuff
│ ├── codebuff.py # CodebuffProvider — alias (same upstream, separate name)
│ └── registry.py # PROVIDER_REGISTRY + @register_provider("name") decorator
├── pool/
│ └── strategies.py # SelectionStrategy: LeastLoaded / RoundRobin / ModelAffinity
├── api/
│ ├── health_router.py # /healthz + /healthz/full (settings diagnostics, no secrets)
│ ├── openai_router.py # /v1/models + /v1/chat/completions (delegates to legacy handlers)
│ └── admin_router.py # /v1/freebuff/* (account/state/refresh/reload)
├── app_factory.py # create_app(): lifespan + handler wiring
├── cli.py # operator CLI (doctor / pool / rotate / dryrun)
├── __main__.py # `python -m freebuff2api doc|pool|rotate|dryrun|server`
└── pool/, codebuff.py # LEGACY modules retained (delegated to by api/)
- Client →
POST /v1/chat/completions. core.auth.require_local_bearer(request)validates Bearer or rejects.api/openai_router.chat_completionscalls into the legacy chat handler (freebuff2api.app.chat_completions) which:- Selects account via
pool.strategies.LeastLoadedStrategy. (Plug in another class to change selection.) - Acquires upstream session (cached).
- Streams OpenAI-compatible SSE.
- Selects account via
- Response wires back through FreebuffError handler if applicable.
The provider abstraction (providers/base.py) lets you swap the chat
implementation entirely without touching the router.
# freebuff2api/providers/myupstream.py
from freebuff2api.providers.base import (
AccountQuota, ChatRequest, ChatResponse, ProviderError, UpstreamProvider,
)
from freebuff2api.providers.registry import register_provider
@register_provider("myupstream")
class MyUpstreamProvider:
name = "myupstream"
@classmethod
def from_settings(cls, settings):
return cls(settings)
async def list_models(self) -> list[str]:
...
async def chat(self, req: ChatRequest) -> ChatResponse:
...
async def list_account_quotas(self) -> list[AccountQuota]:
...
async def refresh_session(self) -> dict:
...Then from freebuff2api.providers import registry and PROVIDER_REGISTRY.names()
will include "myupstream". The router requires no changes.
If the new upstream surfaces a new setting (e.g. MYUPSTREAM_API_KEY), add
a field to core/settings.py:Settings and parse it in Settings.load() —
single place for env wiring.
# freebuff2api/pool/strategies.py
from dataclasses import dataclass
from freebuff2api.pool.strategies import AccountSnapshot, SelectionStrategy
class TierAwareStrategy:
"""Prefer 'enterprise' tier accounts when quota comparable."""
name = "tier_aware"
def pick(self, requested: str | None, snapshots):
eligible = [s for s in snapshots if not s.error and s.strong_remaining_total > 0]
if not eligible:
return None
# prefer admin/pro
eligible.sort(key=lambda s: (0 if s.access_tier in ("admin", "pro") else 1,
-s.strong_remaining_total))
return eligible[0]
STRATEGIES["tier_aware"] = TierAwareStrategy()Pick it at runtime:
FREEBUFF_POOL_STRATEGY=tier_aware freebuff2api-app| Path | Permission | Why |
|---|---|---|
.env |
chmod 600 |
FREEBUFF_TOKEN, FREEBUFF_API_KEY |
accounts.json |
chmod 600 |
Per-account upstream tokens |
data/ |
chmod 700 root |
Cached quota / session state |
/var/log/freebuff2api.log |
chmod 640 root |
Optional rotated logs |
The CLI doctor and the new /healthz/full endpoint return settings
snapshots with secrets stripped. No token or upstream Bearer is ever
exposed by diagnostics endpoints.
To rotate the upstream session: freebuff2api rotate. Daily-quota pools
do not reset on rotation — only the upstream OAuth session.
All endpoints live under 127.0.0.1:8080 by default. Authentication:
Bearer required if FREEBUFF_API_KEY is set; otherwise open.
| Method | Path | Auth | Body | Notes |
|---|---|---|---|---|
GET |
/v1/models |
bearer | — | Returns data: [{id, owned_by}] |
POST |
/v1/chat/completions |
bearer | OpenAI schema | Streams or returns final response |
| Method | Path | Auth | Notes |
|---|---|---|---|
GET |
/healthz |
public | {"status":"ok"} |
GET |
/healthz/full |
public | settings summary, secrets stripped |
GET |
/v1/freebuff/status |
bearer | account quota + warmup config |
GET |
/v1/freebuff/limits |
bearer | quota summary only |
GET |
/v1/freebuff/accounts |
bearer | per-account snapshot (email + tokenTail) |
POST |
/v1/freebuff/reload-accounts |
bearer | reload accounts.json, reset pool |
POST |
/v1/freebuff/refresh |
bearer | force upstream session refresh |
Imports raise FreebuffError subclasses from core.exceptions — handler
maps them to consistent 4xx/5xx JSON envelopes:
{"error": {"code": "all_accounts_exhausted", "message": "...", "data": {}}}
Add new codes only if the upstream has a new failure mode you want to expose; otherwise generic 500 with the message is fine for transient errors.
| Aspect | v0.1.0 | v2 |
|---|---|---|
| Settings | freebuff2api.config.load_settings() |
freebuff2api.core.settings.get_settings() |
| /v1/chat/completions | freebuff2api.app.chat_completions |
Same handler, mounted via api/openai_router |
| Account pool | hard-coded CodebuffAccountPool |
Same; app.state.accounts |
| Settings strategy | acquire_best heuristic |
pool.strategies.*Strategy class — FREEBUFF_POOL_STRATEGY env |
| Auth check | _check_local_auth |
core.auth.require_local_bearer |
| CLI | none | `freebuff2api doctor |
| New upstream | n/a — fork the repo | one file + @register_provider("name") |
The wire-level contract (status codes, response JSON keys, header semantics) is preserved. Clients do not need to change.
Run the test suite:
.venv/bin/python -m pytest -qCoverage today (75 tests):
tests/test_settings.py— env-var parsing, immutability, reload cache.tests/test_pool_strategies.py— pure-function strategy unit tests.tests/test_smoke_api.py—create_app()route inventory + CLI subcommands.tests/test_token_web.py(legacy) — token-web OAuth flow smoke.
Add a test when adding a new provider or strategy. Pure-function strategies are easiest to test — no upstream mock required.
.venv/bin/python -m pip install ruff pyright mypy
.venv/bin/ruff check .
.venv/bin/pyright freebuff2api/The default least_loaded strategy prefers accounts with the highest
remaining quota. If quota drops uniformly, switch to round_robin to
spread load:
systemctl edit freebuff2api.service
# add: Environment="FREEBUFF_POOL_STRATEGY=round_robin"
systemctl restart freebuff2api.service- Write
freebuff2api/providers/june.pywith@register_provider("june"). - Add routing logic in
api/openai_router.pyto pick provider based onreq.modelprefix ("june:..."→ JuneProvider). - Add
JUNE_*env tocore/settings.py. - Done — clients hit
model: "june:claude-..."and it just works.
If 99% accounts banned situation:
freebuff2api doctor # see current state
freebuff2api rotate # cycle session
# Spin up a pool of new accounts in cloud (PVA farm): tool/farm_pva.py (legacy)
# Then:
freebuff2api-cli pool add --label backup --from-stdin # add account interactively
freebuff2api-cli pool ls # verify counts
# Pool rebuilds at next chat-completions call via app.state.accounts.(The freebuff2api-cli script reference is for the legacy CLI tooling at
tool/; pool add is a TODO, see CONTRIBUTING.md.)
Apache-2.0 (same as upstream freebuff2api).