Can a stack of free-tier API keys carry a coding agent through a real task? I put FreeLLMAPI on a lab box and pointed OpenCode and Claude Code at it to find out. FreeLLMAPI is a self-hosted router that puts one OpenAI-compatible and Anthropic-compatible endpoint in front of your own keys for OpenRouter, Gemini, Groq and many other free tiers, with per-key rate tracking and failover. Plenty of pages describe it, but I couldn’t find one that showed it under Docker, wired into OpenCode, or timed.
These notes cover the installer, a Compose file pinned by digest, the dashboard, both agent configs, the latency, 429 and failover numbers the router logged, and a terms table read from each provider’s own pages. I ran all of it in November 2026 on an Ubuntu 24.04 VM in my Proxmox lab with Docker Engine, OpenCode and Claude Code installed and keys for OpenRouter, Hugging Face and OVH’s keyless tier, and every prompt and log line below comes from that box.
Knowing what FreeLLMAPI is (and what it isn’t)
FreeLLMAPI is a Node.js server with a React dashboard and a SQLite database. You give it your own provider keys, it stores them encrypted at rest, and it exposes /v1/chat/completions, /v1/responses and an Anthropic-style /v1/messages behind a single “unified” key. A request for auto lets the router pick a model, while a request for a named model goes to whichever provider serves it. When an upstream answers 429 or returns nothing, the router benches that model and key for a while. For auto, it also tries the next candidate.
What it isn’t is a way to get paid-model quality for free, or something to put in front of other people. The README itself calls it personal experimentation and says upstream terms still apply. My verdict after the run is that it’s a decent personal fallback for side projects and for keeping an agent alive when one free tier runs dry. A coding agent on it is slow, though, and the free models that answered quickly were the small ones.
The lab box had 4 vCPUs, 6 GB of RAM and a 20 GB disk, which was far more than the container needed. To follow along you need the same few things:
- A Linux host with Docker Engine and the Compose plugin
- SSH access to it from the machine with your browser
- At least two provider accounts, one key each, for failover to mean anything
- OpenCode or Claude Code on the same host, or able to reach it
One provider is enough to see the dashboard work, but you won’t see failover until the router has somewhere else to send a request.
Reading install.sh before running anything
The project’s quick start is a curl ... | bash one-liner. I don’t pipe a stranger’s script into a shell, so I downloaded it, hashed it and read it first. The script on GitHub Pages turned out to be a shim that fetches a second script from freellmapi.co and runs that, so the code you actually execute comes from a different domain than the repo:
mkdir -p ~/freellmapi && cd ~/freellmapi
curl -fsSL https://freellmapi.co/install.sh -o install.sh
sha256sum install.sh
grep -nE "IMAGE=|curl|compose (pull|up)|HOST_BIND=|ENCRYPTION_KEY=" install.sh
The script runs to 161 lines and does four things, in this order:
- Checks for Docker or Podman plus a Compose front end
- Writes a
.envwith a randomENCRYPTION_KEY - Writes a Compose file using the
:latestimage - Pulls, starts and waits for the health endpoint
Nothing in it touches system files or asks for sudo. The two lines that matter are the image tag and the bind address, which defaults to localhost.

So why not just run it? Rerunning the installer pulls whatever :latest is that day, and the project shipped a new release almost daily while I was testing. That’s why I wrote my own Compose file instead and deleted the script. If you’d rather use it, read it the same way, compare the hash with what you read, and then run bash install.sh.
Pinning the image in Docker Compose
Ubuntu’s own docker.io package works, but I used Docker’s apt repository so the Compose plugin matches the engine. Skip this block if docker compose version already answers on your host:
sudo apt-get update
sudo apt-get install -y ca-certificates curl jq
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER
Log out and back in so the docker group applies. Next, pull the release you want and ask Docker for its digest. Tags can be moved, but a digest can’t, so the digest is what goes into the Compose file:
# https://github.com/tashfeenahmed/freellmapi/releases
docker pull ghcr.io/tashfeenahmed/freellmapi:v0.16.16
docker image inspect ghcr.io/tashfeenahmed/freellmapi:v0.16.16 --format "{{index .RepoDigests 0}}"
Swap in the newest tag from the releases page when you do this, and copy the digest it prints. Then create the Compose file in the same directory:
vim ~/freellmapi/compose.yaml
This is the exact file my container ran from. The repo’s own Compose file adds an opt-in Watchtower updater that needs the Docker socket. I left it out, along with the host.docker.internal mapping that only matters if you route through a proxy or reach a local model server such as Ollama on the host, and published port 3001 on localhost only.
services:
freellmapi:
# pinned release: https://github.com/tashfeenahmed/freellmapi/releases
image: ghcr.io/tashfeenahmed/freellmapi:v0.16.16@sha256:3b744d6714737f2d0c8fe90cdd8517966f22acf615d44cc222c792d40ef9b0a9
env_file:
- .env
environment:
NODE_ENV: production
PORT: 3001
ports:
- "127.0.0.1:3001:3001"
volumes:
- freellmapi-data:/app/server/data
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/api/ping').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
volumes:
freellmapi-data:
The encryption key protects every provider key in the SQLite volume, so generate it once, keep the file private and back it up. Lose it and you re-enter every key, which is why the first line refuses to overwrite an existing .env:
cd ~/freellmapi
[ -f .env ] || (umask 077; printf "ENCRYPTION_KEY=%s\n" "$(openssl rand -hex 32)" > .env)
docker compose up -d
docker compose ps --format "table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
curl -s http://127.0.0.1:3001/api/ping
Give it about 15 seconds for the health check. You want (healthy), the full digest in the image column and 127.0.0.1:3001 in the ports column. If the ports column shows 0.0.0.0, the dashboard is open to your whole network:

The startup log also says it seeded a model catalog and that catalog-sync polls api.freellmapi.co every 12 hours. That’s how the model list stays current without a new image, and it’s also an outbound call to the project’s server you should know about if this host sits somewhere strict. Adding CATALOG_SYNC_DISABLED=1 to .env turns it off, at the cost of a catalog that only changes when you update the image. The Docker cheat sheet covers the logs, inspect and volume commands you’ll want for poking at it.
Creating the dashboard account
Since the port only listens on the server’s loopback, reach the dashboard through an SSH tunnel from your workstation, then open http://localhost:3001 in the browser:
ssh -N -L 3001:127.0.0.1:3001 [email protected]
My first attempt at the account form failed. The startup log says a browser on the same machine can finish setup without a code, and I was on localhost through the tunnel, so I expected it to just work. It didn’t, because Docker’s port mapping hands the app a connection from the bridge gateway (the analytics later logged my client IP as 172.18.0.1), not from loopback. The app treats that as a remote device and asks for the one-time code it printed at startup:
docker compose logs freellmapi | grep -i "setup code"
The code is a 10-character string, and I’ve blanked mine below. It changes on every container restart until an account exists, so grab it right before you use it:

Enter your email (1), a password of at least 8 characters, paste the code into the Setup code field (2), and click Create account (3). That email and password only protect this dashboard, and nothing is sent to the project.

- 1Dashboard login email
- 2Code from docker compose logs
- 3Create account
Once the account exists, the code is gone for good and the dashboard asks for the password instead.
Adding provider keys, one account each
On the Keys page, click Add key, pick the provider (1), paste the key from that provider’s console (2) and click Add key (3). The “Paste several” link exists so you can drop in a batch. Do not use it to stack keys from extra accounts at the same provider, because that’s exactly what the terms below forbid.

- 1Pick the provider
- 2One key per provider account
- 3Add key
I added three: an OpenRouter key, a Hugging Face token, and OVH AI Endpoints, which needs no key at all. After Check all, every one reported healthy.

- 1Keyless: 2 requests/min per IP per model
- 2Healthy, but serves no models here
Healthy doesn’t mean useful. The Hugging Face token validated, yet the router’s catalog had no Hugging Face models it would route to, most likely because free accounts get no monthly Inference Providers credit, which Hugging Face’s own pricing page confirms. So my working pool was OpenRouter’s :free models plus OVH’s anonymous tier, which allows 2 requests a minute per IP per model. Keep that OVH number in mind, because it shapes every agent result below.
Making the first call with the unified key
Open the Unified API key tab and click Copy (1). The same panel lists the endpoints, including /v1/messages (2), which is the one Claude Code will use:

- 1Copy the unified key
- 2What Claude Code calls
I kept the key in a file only my user can read, so it never lands in shell history. Create the empty file, paste the key into it, and export it for the session:
install -m 600 /dev/null ~/.freellmapi-key
vim ~/.freellmapi-key
export FREELLMAPI_API_KEY="$(cat ~/.freellmapi-key)"
Then count the models and send one small request, keeping only the status and routing headers:
curl -s http://127.0.0.1:3001/v1/models -H "Authorization: Bearer $FREELLMAPI_API_KEY" | jq ".data | length"
curl -s -D - -o /tmp/r.json http://127.0.0.1:3001/v1/chat/completions -H "Authorization: Bearer $FREELLMAPI_API_KEY" -H "Content-Type: application/json" -d '{"model":"mistral-small-3.2-24b","messages":[{"role":"user","content":"Reply with one word: ready"}]}' | grep -iE "^HTTP|x-routed-via|x-fallback"
jq -r ".choices[0].message.content, .usage" /tmp/r.json
X-Routed-Via names the provider and model that actually answered, which you’ll lean on constantly. The model count is the one that misled me. /v1/models returned 284 ids, but only about 20 of them had a configured provider behind them. The rest come from the shared catalog, and the router skips them with “no key configured for their platform” in its errors:

Pick agent models from what your keys serve, not from that list. The dashboard’s Models page shows which rows have a key behind them.
Pointing OpenCode at the /v1 endpoint
OpenCode talks to any OpenAI-compatible server through a custom provider in ~/.config/opencode/opencode.json. If OpenCode isn’t installed yet, the OpenCode setup guide covers it; I used the official script, downloaded and read first like the other one:
curl -fsSL https://opencode.ai/install -o opencode-install.sh
bash opencode-install.sh
export PATH="$HOME/.opencode/bin:$PATH"
mkdir -p ~/.config/opencode
vim ~/.config/opencode/opencode.json
This is the config I ran. The key stays out of the file through OpenCode’s {env:...} substitution, and the models map has to list every id you want, because OpenCode only resolves freellmapi/<id> against ids declared there:
{
"$schema": "https://opencode.ai/config.json",
"model": "freellmapi/gpt-oss-120b",
"provider": {
"freellmapi": {
"npm": "@ai-sdk/openai-compatible",
"name": "FreeLLMAPI",
"options": {
"baseURL": "http://127.0.0.1:3001/v1",
"apiKey": "{env:FREELLMAPI_API_KEY}"
},
"models": {
"auto": {
"name": "FreeLLMAPI auto router",
"limit": {
"context": 131072,
"output": 8192
}
},
"gpt-oss-120b": {
"name": "gpt-oss-120b",
"limit": {
"context": 131072,
"output": 8192
}
},
"north-mini-code": {
"name": "North Mini Code",
"limit": {
"context": 256000,
"output": 8192
}
},
"mistral-small-3.2-24b": {
"name": "Mistral Small 3.2 24B",
"limit": {
"context": 131072,
"output": 8192
}
}
}
}
}
}
Confirm OpenCode sees all four with opencode models freellmapi. For the test I made a scratch Git repository with a five-line access.log:
mkdir -p ~/logparse && cd ~/logparse && git init -q
printf '%s\n' "2026-11-01 10:00:01 GET /health 200" "2026-11-01 10:00:02 POST /v1/chat 429" "2026-11-01 10:00:03 POST /v1/chat 200" "2026-11-01 10:00:04 GET /metrics 500" "2026-11-01 10:00:05 POST /v1/chat 429" > access.log
Then I asked each model for the same small job: write a script that counts HTTP status codes, then run it. That’s enough to force several tool calls (read, write, shell) in one turn. The --auto flag approves every permission that isn’t explicitly denied, shell commands included, so only use it in a throwaway directory like this one:
opencode run --auto -m freellmapi/north-mini-code "Write a Python script count_status.py that reads access.log and prints a count per HTTP status code (last field), sorted by count descending. Then run it with python3 and show the output." < /dev/null
That trailing < /dev/null cost me five minutes. My first opencode run from a non-interactive SSH session sat silent until my 300-second timeout killed it, and the router log showed no request at all. It was waiting on stdin. With stdin closed, North Mini Code read the log, wrote the script and ran it in 13.9 seconds:

The router log showed two costs that OpenCode adds to every task:
- About 6,500 input tokens per agent step
- One extra request per session for the title
The token count comes from the system prompt and tool definitions riding along every time. The title call uses the same model, so on a model capped at 2 requests a minute it eats half your minute. OpenCode’s small_model setting can move that work to a different model, and the OpenCode CLI cheat sheet covers the other run flags I leaned on here.
Running Claude Code through /v1/messages
Claude Code speaks the Anthropic Messages API, and FreeLLMAPI answers it on /v1/messages. The base URL is the server root without /v1, because Claude Code appends the path itself. The Anthropic tab in the dashboard shows that (1) and maps Claude’s own model names to the router, with Haiku (2) also covering Claude Code’s background calls:

- 1Root URL, no /v1
- 2Haiku also runs background calls
I installed Claude Code with the native installer (the Claude Code install guide, written for Ubuntu 26.04, has the apt and npm routes too) and set the gateway variables in the shell. ANTHROPIC_AUTH_TOKEN goes out as a Bearer header, which FreeLLMAPI accepts. Pinning both the main and Haiku model keeps background calls off the slow routes:
export PATH="$HOME/.local/bin:$PATH"
export ANTHROPIC_BASE_URL=http://127.0.0.1:3001
export ANTHROPIC_AUTH_TOKEN="$(cat ~/.freellmapi-key)"
export ANTHROPIC_MODEL=north-mini-code ANTHROPIC_DEFAULT_HAIKU_MODEL=north-mini-code
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
The last line turns off Claude Code’s telemetry, auto-update checks and error reporting, which would otherwise still go to Anthropic. Then the same task in print mode from the ~/logparse directory, with the tools it may use listed after the prompt:
claude -p "Write a Python script count_status.py that reads access.log and prints a count per HTTP status code (last field), sorted by count descending. Then run it with python3 and show the output." --allowedTools "Read,Write,Glob,Bash(python3:*)" < /dev/null
Order matters there: when I put --allowedTools before the prompt, the flag appeared to take the prompt as part of its list and Claude Code exited with “Input must be provided either through stdin or as a prompt argument”. With the prompt first, the run finished in 13.0 seconds, and 11.3 on a rerun. The warning at the top is harmless for North Mini Code, whose window is 256k. Claude Code does not know the model name and assumes 200k, which is too big for the 131k models like gpt-oss-120b. For those, Claude Code’s model-config docs say to export CLAUDE_CODE_MAX_CONTEXT_TOKENS=131072 with the model’s real limit:

If you set only the base URL and token, Claude Code sends its built-in Claude model names and the dashboard mapping routes them to auto. A one-word test answered in 4.2 seconds that way. Claude Code’s prompts are heavier than OpenCode’s, though: 9,900 to 12,800 input tokens per request in my log. The Claude Code cheat sheet lists the other flags worth knowing for print mode. Anthropic’s gateway docs also say plainly that routing Claude Code to non-Claude models isn’t something they support, so treat odd behaviour here as yours to debug.
Measuring latency, 429s and failover
For raw numbers I wrote a small Python script that streams one prompt through /v1/chat/completions, records time to first token, total time and the routing headers, and prints one JSON line per call. Run 1 hit 11 model ids three times each, back to back, with a 400-token cap, between 00:15 and 00:23 UTC on a Monday. That’s a quiet hour, so treat every figure as a snapshot:

“Empty” means HTTP 200 with no visible answer. Most of those were reasoning models that spent the whole 400 tokens thinking, which is partly my fault, so run 2 gave the reasoning models 4,000 tokens:

Putting both runs together, these are the medians for the calls that produced an answer:
| Model id | Served by | Answers | Median TTFT | Median total | What failed |
|---|---|---|---|---|---|
| mistral-small-3.2-24b | OVH | 2/3 | 0.6 s | 2.4 s | 429 on call 3 |
| llama-3.3-70b | OVH | 2/3 | 0.9 s | 8.8 s | 429 on call 3 |
| gpt-oss-120b | OVH | 2/3 | 4.6 s | 6.7 s | 429 on call 3 |
| gpt-oss-20b | OVH | 3/5 | 3.5 s | 4.5 s | one 502, one empty |
| north-mini-code | OpenRouter | 3/3 | 4.7 s | 5.8 s | none |
| auto | both | 3/5 | 5.5 s | 6.6 s | two empty at 400 tokens |
| dots3-note-preview | OpenRouter | 1/5 | 7.2 s | 8.5 s | empty or 502 |
| qwen3.5-397b | OVH | 1/5 | 15.1 s | 125.4 s | empty or 502 |
| nemotron-3.5-lightning | OpenRouter | 0/5 | none | none | empty, 502, 429 |
| gemma-4-31b, lfm2.5-2.6b | OpenRouter | 0/3 each | none | none | upstream 429 on the first call |
Three patterns held across both runs:
- OVH’s small models were fastest, then 429 on call three
- Popular OpenRouter
:freemodels refused the first call - Long-thinking reasoning models never streamed before about 15 seconds
OVH’s 2-per-minute anonymous limit explains the first. The second means popularity, not my usage, decided whether a model worked. The third turned out to be the router rather than the providers. FreeLLMAPI holds reasoning-only output for up to 15 seconds so it can quietly fail over a dead attempt, and only then commits the stream to the client. Models that start visible output sooner, like the gpt-oss pair at 3 to 7 seconds, stream right away. Any call that thinks past 15 seconds hits the same floor: Qwen3.5-397B every time, the Nemotron reasoning model behind one auto call, and even one North Mini Code call.
The 429 behaviour is the part to understand before you pin a model in an agent config. A pinned model gets no cross-model failover; once its only route is on cooldown, the router answers 429 in a few milliseconds without calling anyone:

With auto, the same situation becomes a hop. The container log shows an OpenRouter 429 on Gemma 4 26B followed by next to North Mini Code inside the same request, and empty completions from the reasoning models being counted as failures rather than passed back to the client:
docker compose logs --no-log-prefix freellmapi | grep -E "\[Proxy\].* (fail|next) " | tail -8
Each fail line carries the latency and the provider’s error, and a0 and a1 are attempt numbers within one request.

To get those hop timings on the response itself, add FALLBACK_DETAIL_HEADER=1 to .env and run docker compose up -d; any request that fell over then carries an X-Fallback-Detail header. One of my early auto calls shows the whole chain: Dots3 returned nothing after 12.8 seconds, Gemma 4 31B answered 429 in 315 ms, and Nemotron 3 Nano Omni finally served it at 17 seconds total.
The agent runs tell the same story with more at stake. Here’s every coding-agent task I ran through the router, the same status-count job each time:
| Client | Model | Wall time | Result |
|---|---|---|---|
| OpenCode | north-mini-code | 13.9 s | Script written and run |
| OpenCode | auto | 47.8 s | First script crashed, agent fixed it and reran |
| OpenCode | mistral-small-3.2-24b | 84.1 s | No file: the model printed “write” as text |
| OpenCode | gpt-oss-120b | 184.0 s | Script written and run after 429 retries |
| Claude Code | north-mini-code | 13.0 s | Script written and run |
| Claude Code | auto | 21.7 s | Script written and run after one failover |
| Claude Code | gpt-oss-120b | 115.6 s | Script written and run after 429 retries |
The slow rows are rate limits, not slow models. gpt-oss-120b on OVH answers a single call in about 6 seconds, but a six-step agent turn needs six requests. At 2 per minute the router kept answering 429, while OpenCode backed off 2, 5, 8 and then 19 seconds before trying again. Want an agent that feels usable on free tiers? Point it at a model on a provider with a per-minute limit in the tens, and keep auto as the fallback rather than the default.
The dashboard’s Analytics page summed up the whole session. Of 75 requests, 73.3% succeeded, with a p95 latency of 30.4 seconds. Split by client, OpenCode’s requests succeeded 96% of the time and Claude Code’s 95%, against 40% for my benchmark script, because the agents retry and my script deliberately didn’t:

Ignore the “Est. savings” tile. It prices free tokens at paid rates, which says nothing about what you’d actually have spent.
Errors I hit along the way
The first four came up in the lab run, and the last is a wall I read about in OpenCode’s docs before wasting a key on it. Most are not bugs, just free tiers behaving like free tiers.
“All models exhausted: 1 route checked (1 rate-limited or on cooldown)”
You pinned a model whose only provider is on cooldown. The message often ends with “Soonest reset ~43s” or “~2m”, which is the honest answer: wait, request auto, or add a second provider that serves the same model. An ordinary per-minute 429 benches that route for about 90 seconds. Repeated 429s or a spent daily quota climb a longer ladder of 2 minutes, 10 minutes, an hour and a day (capped at 10 minutes for providers like OVH that publish no daily limit), and any successful request resets the count.
“empty completion … (stream produced no content and no tool calls)”
A reasoning model used its whole max_tokens budget on thinking, or the free endpoint dropped the stream. Raise max_tokens (4,000 rescued some of mine) or move off that model. With auto, the router treats it as a failure and tries the next model, which is what you want.
“OpenRouter API error 429: Provider returned error”
This one came from OpenRouter’s upstream, often on the first call of the day to a popular :free model. It isn’t your quota. OpenRouter’s own limits page gives :free models 20 requests a minute and 50 a day when you’ve bought less than $10 of credit, and 1,000 a day above that. A busy model can still refuse you well before either number.
A tool call printed as plain text
Mistral Small, reached through OpenCode, ran one glob and then answered with the bare word write instead of a structured tool call, so no file was created and the run still exited 0. Check that the agent actually changed something, and do not trust the exit code. In my runs North Mini Code and gpt-oss-120b never did this.
“Free-tier chat models require OpenCode”
OpenCode Zen’s free models only work from the OpenCode client itself, according to OpenCode’s Zen documentation, and FreeLLMAPI disables them in its catalog for that reason. Adding a Zen key to the router won’t give your other agents free Zen models.
Checking each provider’s terms
Is FreeLLMAPI against anyone’s terms of service? The router itself is not the problem, but how you feed it can be. I read each provider’s own terms or limits page on November 2, 2026 rather than the project’s summary of them. The pattern is consistent. Your own key, used by you, is fine, while extra accounts to dodge limits, sharing keys and reselling access are not.
| Provider | What its own terms say | Free-tier data and limits |
|---|---|---|
| OpenRouter | No multiple accounts “for purposes of bypassing or circumventing use limits”; no reselling API access | :free: 20 req/min, 50/day under $10 of purchased credit, 1,000/day above; some upstream models may train on inputs |
| OVHcloud AI Endpoints | Anonymous use is documented; I didn’t find wording on proxies or account limits | Anonymous: 2 req/min per IP per model; docs say data isn’t stored or shared |
| Hugging Face | Don’t disclose your password. Token sharing isn’t addressed | Free users get no Inference Providers credit; PRO gets $2 a month |
| Google Gemini API | 18+ only; only paid services when serving users in the EEA, Switzerland or the UK | Unpaid-tier content is used to improve Google products and may be read by human reviewers; limits are per project |
| Cerebras | No buying, selling or transferring API keys; don’t share credentials | Free trial on gpt-oss-120b: 5 req/min, 1M tokens a day |
| Mistral | No buying, selling or transferring keys or accounts; no third-party access without written consent | No training on customer data except listed cases, which include Labs and preview models; check the API privacy toggle for improvement data |
| Cohere | One account per person; business use, not personal or household use | Trial keys: 1,000 calls a month |
Read together, those pages give a short set of house rules for running this:
- One account per provider, used only by you
- No keys from friends, alt accounts or teammates
- No client code or secrets on unpaid tiers that train
- Dashboard on localhost, never shared as a service
Free tiers are often paid for with your data, which is why the third rule matters most for a coding agent that reads your whole repo. I couldn’t read NVIDIA’s trial terms (the PDF wouldn’t parse) or Groq’s cloud agreement, so check those yourself before adding them. The project’s own review rates Cohere “Avoid”, and its business-only clause is the reason I left it out.
What I still don’t know
My numbers come from one quiet hour with two working providers, so they say little about a busy afternoon or a pool of six providers. A few questions I couldn’t settle in the lab:
- How Gemini, Groq and Cerebras keys change the agent times
- Whether the 15-second reasoning hold should be tunable
- Why
/v1/messagescalls print no[Proxy]lines - How long-session context handoff behaves after a mid-task failover
The third one bothered me most. In this version, Claude Code’s requests showed up in Analytics but not in the container’s request log, apart from the fallback warnings. That means docker compose logs alone won’t tell you what Claude Code did. If you’re choosing between agents for a free stack, the OpenCode vs Claude Code comparison covers how they differ beyond the wire format, and if free tiers keep throttling you, running your own model with vLLM on a GPU box removes the rate limits from the equation entirely. I’d also like to rerun this once more providers are keyed; the pinned digest in the Compose file means I can compare against exactly what ran today.