Turn a Factory fk- API key into standard OpenAI / Anthropic / Responses APIs.
nicedroid is a single-binary Go gateway that sits in front of Factory's LLM gateway — the same one the Droid CLI talks to. Point your existing OpenAI or Anthropic SDK at it and you get Droid's models, including the cheap open-weight Droid Core pool, in the wire format your client already speaks. It ships with an embedded admin console for quota tracking, key pooling, provider routing and per-request cost.
Important
For learning and reference only. This is an unofficial, reverse-engineered client for internal, undocumented endpoints. It is published to document how the gateway behaves, not for production use, resale, or as a replacement for the official product. Not affiliated with or endorsed by Factory.
- Four wire formats, freely mixed. OpenAI Chat Completions, Anthropic Messages, OpenAI Responses and Google generate. Any entry point can request any model: ask for GLM from an Anthropic client and the proxy translates the request and the streamed response for you.
- Droid Core access. The open-weight pool (
glm-5.3-flash,kimi-k3,deepseek-v4.1-flash, …) is reachable through/v1/responsesas well as the other formats. - Multi-provider routing. One model, several backends (fireworks, baseten, databricks, mistral). Pin the fastest one by hand, or let the built-in streaming benchmark measure them and pick the winner. On 402 / 429 / 5xx the proxy fails over to the next provider transparently.
- Quota awareness. Polls Factory's billing limits and shows the standard and core pools across their 5-hour, weekly and monthly windows.
- Honest accounting. Per-request latency, time to first token, cache read / write tokens, tokens per second, and Factory Standard Credits estimated with the same formula the CLI uses.
- Single binary. The React admin UI is embedded with
go:embed. State lives in one SQLite file. No external services.
| Method | Path | Format |
|---|---|---|
POST |
/v1/chat/completions |
OpenAI Chat Completions |
POST |
/v1/messages |
Anthropic Messages |
POST |
/v1/messages/count_tokens |
Anthropic token counting |
POST |
/v1/responses |
OpenAI Responses |
POST |
/v1/generate |
Google generate (passthrough) |
POST |
/v1beta/models/<m>:generateContent |
Google native URL shape |
POST |
/v1beta/models/<m>:streamGenerateContent |
Google native URL shape |
GET |
/v1/models |
Model list |
GET |
/healthz |
Health check |
Requires Go 1.27+ and Node 20+ (Node is only needed to rebuild the UI; a
prebuilt web/dist is committed, so go build works on a clean clone).
git clone https://github.com/Sibuxiangx/nicedroid
cd nicedroid
./build.shThen start it:
NICEDROID_ADMIN_PASSWORD=change-me ./nicedroidOpen http://localhost:3000, sign in, and add your Factory API key (fk-…) on
the Keys page. That key is what the gateway presents upstream.
Prefer to build it yourself:
cd web && npm install && npm run build && cd ..
go build -o nicedroid ./cmd/serverOnce an upstream key is configured, downstream clients authenticate with a
client key created in the admin console (sk-nicedroid-…). Authentication is
default-deny: these endpoints spend your upstream quota, so the proxy refuses
every request until at least one client key exists rather than serving them
openly. The admin console has its own credentials and stays reachable, so that
is where you create the first key.
OpenAI-compatible:
curl http://localhost:3000/v1/chat/completions \
-H 'content-type: application/json' \
-H 'authorization: Bearer sk-nicedroid-…' \
-d '{
"model": "glm-5.3-flash",
"messages": [{"role": "user", "content": "Hello"}],
"stream": true
}'Anthropic-compatible — point ANTHROPIC_BASE_URL at the gateway and Claude
Code or any Anthropic SDK works unchanged:
curl http://localhost:3000/v1/messages \
-H 'content-type: application/json' \
-d '{
"model": "claude-sonnet-5-5",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Hello"}]
}'OpenAI Responses:
curl http://localhost:3000/v1/responses \
-H 'content-type: application/json' \
-d '{"model": "deepseek-v4.1-flash", "input": "Hello"}'The catalog is not maintained here by hand. manifest.json is
the source of truth — 93 models at the time of writing — extracted from the Droid
CLI's own model registry, and the Go binary embeds that file at build time.
Each entry carries everything needed to route and price a request:
{
"id": "glm-5.3-flash",
"name": "GLM-5.3-Flash",
"provider": "factory",
"providers": ["fireworks", "baseten", "databricks"],
"quota_pool": "core",
"vision": true,
"reasoning": { "efforts": ["low", "high", "max"], "default": "high" },
"cost": {
"token_multiplier": 0.06,
"output_multiplier": 3.34,
"cache_read_multiplier": 0.2
}
}provider names the platform that serves the model; providers lists the
backends the gateway can dispatch to. They differ for open-weight models, which
belong to Factory but are served by fireworks, baseten, databricks or mistral.
Cloud marketplace transports (azure_openai, bedrock_anthropic, …) are not
routable with x-api-provider and are filtered out during extraction.
Two quota pools exist: standard (Claude, GPT, Gemini, Grok) and core (the
cheap open-weight models, several of which are served by more than one backend).
Gemini models are passthrough-only: reachable through /v1/generate and the
Google-native /v1beta/models/<model>:generateContent routes, and never
translated.
A small OpenCode plugin fills the provider's model list from GET /v1/models,
so the catalog needs no hand editing: drop the plugin in
~/.config/opencode/plugins/, add a nicedroid provider block with a
baseURL and no models, and every current model shows up in /models —
@ai-sdk/openai for the GPT family, @ai-sdk/google for Gemini,
@ai-sdk/openai-compatible for the rest.
The admin console groups models by platform and collapses the ones the CLI has
retired — 59 current models and 34 legacy ones at the time of writing. A model is
legacy when the registry gives it a retirement date or hides it from the CLI's
own picker; the console shows both the date and the suggested replacement.
GET /v1/models lists retired models too, flagged with deprecated,
retired_on and replacement, so an existing client can still see what it is
calling and filter accordingly.
The catalog ships inside the Droid CLI, so that is what manifest.json tracks —
cli.version is the field that says which build it came from, and the proxy
identifies itself upstream with that same version. The
refresh workflow compares it against the
latest @factory/cli on npm once a day. When they differ it downloads that exact
build, verifies its SHA-256, checks the build reports the version it was
published under, re-extracts the registry and commits the result.
Extraction is deterministic: the same binary always yields byte-identical JSON, with no timestamps or other run-dependent fields. That is what makes comparing versions sufficient — an unchanged version cannot produce a diff, so the daily run is a no-op rather than an empty commit.
To do the same by hand:
go run ./tools/extract-manifest ~/.local/bin/droid \
--version "$(droid --version)" \
--out manifest.jsonThe extractor validates its own work: it refuses to write a manifest that is
missing models the CLI lists in its own droid exec --help, so a change in the
bundle layout fails the job instead of quietly shrinking the catalog.
Factory's gateway picks the serving backend from the x-api-provider header, and
backends differ a lot: measured on glm-5.3-flash, databricks finished roughly
20% faster end-to-end than fireworks and baseten. nicedroid therefore resolves a
provider per request:
- Pin it manually from the provider dropdown on the Models page. The choice is stored in SQLite; clearing it restores the catalog default.
- Benchmark it with the Speed test button. The server runs the same streaming generation through every provider of that model, round-robin over 1–3 rounds, streams each result to the browser, and ranks providers by the median across rounds. Tick auto-pin the fastest to apply the winner.
- Fail over automatically at runtime: the pinned provider is tried first, then the rest of the catalog order, moving on at 402 / 429 / 5xx.
The benchmark reports end-to-end throughput (output tokens ÷ total request time), not tokens ÷ generation window. Providers that buffer a stream and flush it as one burst make the generation window collapse to a few milliseconds and inflate that ratio into the hundreds of thousands; total elapsed time cannot be faked that way. Time to first token is reported separately.
Served from the same binary at /admin:
- Dashboard — quota water levels for both pools, request volume, cache hit rate, latency and TPS trends.
- Keys — upstream key pool (weighted round-robin, skip-exhausted, one-click verification) and downstream client keys.
- Models — full catalog, reasoning-effort levels, provider pinning and the streaming speed test.
- Logs — every request with model, provider, pool, status, latency, TTFT, token counts, cache reads, TPS and estimated credits.
- Settings — system prompt override and a playground for smoke-testing a model.
The first boot generates an admin password and prints it to the log unless
NICEDROID_ADMIN_PASSWORD is set.
All configuration is environment-driven.
| Variable | Default | Purpose |
|---|---|---|
NICEDROID_LISTEN |
:3000 |
Listen address |
NICEDROID_DB |
nicedroid.db |
SQLite path |
NICEDROID_UPSTREAM |
https://api.factory.ai |
Upstream base URL |
NICEDROID_SYSTEM_PROMPT |
Droid gate prompt | Leading system message (see below) |
NICEDROID_ADMIN_USER |
admin |
Admin username |
NICEDROID_ADMIN_PASSWORD |
generated on first boot | Admin password |
NICEDROID_JWT_SECRET |
generated | Session signing secret |
NICEDROID_QUOTA_POLL_SECONDS |
60 |
Quota polling interval |
NICEDROID_REQUEST_TIMEOUT_SECONDS |
600 |
Upstream request timeout |
NICEDROID_LOG_RETENTION_DAYS |
30 |
Request log retention |
deploy/ has a Podman + systemd setup for running this on a
server: a 16.5 MB FROM scratch image (the binary is static, so there is no libc
or shell to ship), the systemd unit, and the reverse-proxy configuration.
Cross-compile, copy the binary, rebuild the image, restart the unit — the SQLite file in the mounted volume is the only state that has to survive.
Two constraints shape the whole proxy:
- The content gate. Factory's gateway rejects every LLM request that does not
begin with the system message
You are Droid, an AI software engineering agent built by Factory.with a403. nicedroid injects it automatically and appends your own system prompt after it. - Version identification. Requests must carry a
factory-cliuser agent and client version, so the proxy identifies itself the same way the CLI does.
Given both, the proxy translates freely between the four wire formats in each direction, including streaming (SSE) and tool calls.
- Not affiliated with Factory. This is an unofficial client that speaks to internal endpoints. Those endpoints are undocumented and can change without notice; a CLI update may break this project.
- Usage still counts against your own Factory quota and is billed by Factory. The credit figures shown in the logs are an estimate derived from the CLI's own formula, not an authoritative number.
- There is no model downgrade path by design: when a quota pool is exhausted, the upstream error is returned to the caller as-is.
- Gemini models are passthrough-only. They are reachable through
/v1/generateand never participate in format conversion. - Keep your database file private — it stores upstream API keys in plain text.
See AGENTS.md for the architecture, upstream requirements and the
reasoning behind the product policies. In short: ./build.sh builds everything,
there is no test suite, and changes are verified by running the server and
exercising the endpoints with curl.
No license is granted. The code is published for learning and reference only: it documents how the gateway's wire formats, model catalog and credit model work, and is meant as a study aid for anyone building similar tooling.
This is a reverse-engineering project against internal, undocumented endpoints. It is not affiliated with, endorsed by, or supported by Factory. Please do not resell it, run it as a service for others, or depend on it in production. All rights reserved.