Skip to content

Timcuan/freebuff2api-v2

Repository files navigation

freebuff2api v2

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.


When to use this

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.

Installation

# 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 server

After 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>"
    }
  ]
}

Quick start

Run the server

freebuff2api-app
# or
freebuff2api  # if no CLI verb → starts uvicorn

Default bind: 127.0.0.1:8080. Override with FREEBUFF_HOST and FREEBUFF_PORT.

Hit the OpenAI surface

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).

Operator CLI

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 call

Architecture

freebuff2api/
├── 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/)

Request flow

  1. Client → POST /v1/chat/completions.
  2. core.auth.require_local_bearer(request) validates Bearer or rejects.
  3. api/openai_router.chat_completions calls 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.
  4. 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.


Adding a new Upstream Provider

# 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.


Adding a new Selection Strategy

# 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

Security & secrets

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.


API surface

All endpoints live under 127.0.0.1:8080 by default. Authentication: Bearer required if FREEBUFF_API_KEY is set; otherwise open.

OpenAI-compatible

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

Admin / status

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

Errors

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.


Migration from v0.1.0

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.


Development

Run the test suite:

.venv/bin/python -m pytest -q

Coverage 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.pycreate_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.

Lint & typecheck

.venv/bin/python -m pip install ruff pyright mypy
.venv/bin/ruff check .
.venv/bin/pyright freebuff2api/

Operational recipes

Free-up upstreams on quota exhaustion

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

Add a second upstream (e.g. june.so)

  1. Write freebuff2api/providers/june.py with @register_provider("june").
  2. Add routing logic in api/openai_router.py to pick provider based on req.model prefix ("june:..." → JuneProvider).
  3. Add JUNE_* env to core/settings.py.
  4. Done — clients hit model: "june:claude-..." and it just works.

Disaster recovery (account banned upstream)

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.)


License

Apache-2.0 (same as upstream freebuff2api).

About

Multi-account, multi-upstream OpenAI-compatible HTTP adapter. Modular refactor of freebuff2api with pluggable providers + strategies + CLI.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages