Getting started

Install it, generate a config, point your app at it. Ten minutes, most of which is waiting for your first API key to save.

Install

Pick whichever fits your setup. All four produce the same single binary.

curl -fsSL https://raw.githubusercontent.com/Oluiy/ai-cost-guard/main/install.sh | sh
npm install -g fitguard
go install github.com/Oluiy/ai-cost-guard/cmd/fitguard@latest
git clone https://github.com/Oluiy/ai-cost-guard.git
cd ai-cost-guard
docker build -t fitguard .

The install script detects your OS and architecture, downloads the matching binary from the releases page, verifies its checksum, and puts it in /usr/local/bin. Set INSTALL_DIR to install somewhere else, or VERSION to pin a specific release.

Installing with Go instead puts fitguard in $(go env GOPATH)/bin, which needs to be on your PATH. Prebuilt binaries for Linux, macOS, and Windows are attached to every release if you'd rather download one directly.

On piping a script into a shell. The install script is fetched over HTTPS and verifies each download's SHA-256 against the checksums published with the release before it extracts anything, so a corrupted or swapped archive is rejected rather than executed. What that does not cover is the script itself: you are trusting this repository and GitHub's TLS at the moment you run it. If you'd rather look first, that's two commands instead of one:
curl -fsSLO https://raw.githubusercontent.com/Oluiy/ai-cost-guard/main/install.sh
less install.sh        # read it
sh install.sh
Or skip the script entirely and use go install, or download a binary and its checksum from the releases page by hand.

Set it up

Run this from wherever you want config.yaml and the request log to live, commonly the root of the project you're protecting:

fitguard init

It asks which providers you're using, your real API key for each, and how much daily budget to give each caller. For every budgeted user it hands back a virtual key that looks like this:

Authorization: Bearer sk-guard-f81e09e76c5533dcb8e47e6eeeca09c354f2fbc8c24b2ab5
Copy it now. It's written into config.yaml but never shown again by the CLI itself, and that file also holds your real provider keys. Both are already in .gitignore if you're working inside a git repo, don't check either into source control.

Last step, it'll ask you to set up a login for the dashboard — a username and password, the same way Grafana or Coolify would. Say yes unless you're the only person who can reach this machine at all.

Run it

fitguard run

Starts listening on :8787 and prints the port, the providers it loaded, and a link to the dashboard. Leave it running, this is now the thing your app talks to instead of the provider directly.

Point your app at it

Same request shape you already send, different base_url and a virtual key instead of your real one:

curl http://localhost:8787/v1/chat/completions \
  -H "Authorization: Bearer sk-guard-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role": "user", "content": "hello"}]
  }'
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8787/v1", api_key="sk-guard-...")
client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "hello"}],
)
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "http://localhost:8787/v1",
  apiKey: "sk-guard-...",
});

await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "hello" }],
});
using System.Net.Http.Json;

var client = new HttpClient { BaseAddress = new Uri("http://localhost:8787/v1/") };
client.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "sk-guard-...");

var payload = new {
    model = "gpt-4o",
    messages = new[] { new { role = "user", content = "hello" } }
};
await client.PostAsJsonAsync("chat/completions", payload);
HttpClient client = HttpClient.newHttpClient();
String json = """
    {"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("http://localhost:8787/v1/chat/completions"))
    .header("Authorization", "Bearer sk-guard-...")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();
client.send(request, HttpResponse.BodyHandlers.ofString());
body, _ := json.Marshal(map[string]any{
    "model":    "gpt-4o",
    "messages": []map[string]string{{"role": "user", "content": "hello"}},
})
req, _ := http.NewRequest("POST", "http://localhost:8787/v1/chat/completions", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer sk-guard-...")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
let client = reqwest::blocking::Client::new();
let resp = client.post("http://localhost:8787/v1/chat/completions")
    .bearer_auth("sk-guard-...")
    .json(&serde_json::json!({
        "model": "gpt-4o",
        "messages": [{"role": "user", "content": "hello"}]
    }))
    .send()?;

Anthropic and Groq models route automatically from the model field, no other change needed. Your real provider key never leaves config.yaml.

Already using Anthropic's own SDK?

This is the part worth reading carefully if your app currently calls Claude with anthropic.Anthropic(...) and .messages.create(...). FitGuard's client-facing endpoint is OpenAI-shaped, not Anthropic-shaped. There is no /v1/messages route to point the native SDK's base URL at, and the auth header is different (Authorization: Bearer, not x-api-key). Pointing the Anthropic SDK straight at FitGuard will not work.

The integration is a small, mechanical swap: use an OpenAI-compatible client, keep the same Claude model name.

Before — direct to Anthropic:

from anthropic import Anthropic

client = Anthropic(api_key="sk-ant-...")
resp = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    system="You are a helpful assistant.",
    messages=[{"role": "user", "content": "hello"}],
)
print(resp.content[0].text)

After — the OpenAI Python package, not Anthropic's, still returning a Claude answer:

from openai import OpenAI  # yes, the OpenAI package — FitGuard speaks its request format

client = OpenAI(base_url="http://localhost:8787/v1",
    api_key="sk-guard-...")
resp = client.chat.completions.create(
    model="claude-sonnet-5",  # unchanged: the same Claude model
    max_tokens=1024,
    messages=[
        {
            "role": "system", 
            "content": "You are a helpful assistant."
        },
        {"role": "user", "content": "hello"},
    ],
)
print(resp.choices[0].message.content)

The system prompt moves from a top-level field into a normal role: "system" message; FitGuard translates it back to Anthropic's shape on the way out. Tool calls and image content in messages are translated the same way, in both directions.

Full example: migrating an existing Anthropic integration

A complete, concrete walkthrough of everything above — install, set up, swap the code, run in dev, deploy to production — for a real project that already calls Anthropic directly. Available in Python, TypeScript, .NET, Java, Go, and Rust.

Read the full example

Streaming

Set stream: true like you would against the provider directly. Two things behave a little differently because of what streaming actually is:

Budgets and virtual keys

A budget only means something if it's tied to an identity the caller can't choose for itself. That's why authentication isn't an X-User-Id header or a user field you set on the request, either of which a bug (or a bored intern) can just omit. It's the Authorization: Bearer key fitguard init generated for you, mapped server-side to a user_id and a daily_limit_usd in config.yaml.

Go over budget and the request never reaches the provider:

HTTP/1.1 429 Too Many Requests

{"error":{"message":"daily budget of $5.00 exceeded or would be exceeded by this request (spent $4.87)","type":"budget_exceeded"}}

See the configuration reference for adding more users, and the Redis backend if you're running more than one FitGuard instance.

The dashboard

Open http://localhost:8787/dashboard while fitguard run is up. It's served by the same binary, no separate setup: spend today, cache hit rate, spend by hour, spend by user, and the requests that cost the most, updating live. Pick a different time range or a single user from the controls at the top to narrow what you're looking at.

If you set up a login during fitguard init, you'll see a login form first — that account is separate from the virtual API keys above, it's not something your app ever sends, just you in a browser. Forgot the password? Run this on the machine fitguard is on, server doesn't need to be running:

fitguard reset-dashboard-password

It also works if you skipped the login during init and want to add one now.

Esc