faucet
Request access

Guide

Everything you need to go from nothing to a metered request. It takes about five minutes, and you will not install a Faucet SDK at any point in it.

Faucet is in closed alpha. There is no self-serve sign-up yet — organizations are created by hand. If you want an account, email keunwoopark.524@gmail.com with a line about what you are building. Step 1 below is what happens once you have one.

What Faucet is

One HTTPS endpoint that speaks the OpenAI wire format, sitting in front of Anthropic, OpenAI, Google and Fireworks. You send an OpenAI-shaped request with a Faucet key, name a model as provider/model, and Faucet routes it, streams the response back, and records what it cost.

Two things follow from that:

  • Any OpenAI-compatible client already works. The Vercel AI SDK, the official OpenAI SDKs, LangChain, curl. Change the base URL and the key, nothing else.
  • Switching model or provider is a string change. No second integration, no second key, no second invoice.
Base URL   https://api.intfaucet.com/v1
Auth       Authorization: Bearer fct_live_…
Models     anthropic/claude-opus-5, openai/gpt-5.2, google/gemini-3.5-flash, …

1. Get an organization

During the alpha we create it for you and send you a sign-in link to the console. The organization — not your user account — is what owns keys, credit and usage, so tell us what to name it after: your company or your product, rather than yourself.

You can invite teammates from Members. Roles decide who may mint and revoke keys, so an engineer who only needs to read usage does not need the ability to issue credentials.

Credit is prepaid. An organization with a zero balance is refused with a 402 rather than allowed to run up a bill, which is also what caps your exposure if a key ever leaks. Alpha organizations are funded with starting credit when we create them; the current balance and the last 30 days of usage are on the organization's overview. When it runs low, ask for more.

2. Create an API key

API keys → Create key. Give it a name that says where it runs (production-web, staging-worker) and pick an environment:

EnvironmentPrefixUse
Livefct_live_…Production traffic
Testfct_test_…Local development and CI

The key is shown once. Only its SHA-256 is stored, so a lost key is reissued, never recovered — copy it straight into your secret manager and never into a repository. Revoking a key from the console takes effect across the whole gateway fleet within seconds.

Set it in your environment:

export FAUCET_API_KEY="fct_live_…"

3. Make your first request

The fastest check that everything is wired up — key, credit, model:

curl https://api.intfaucet.com/v1/chat/completions \
  -H "Authorization: Bearer $FAUCET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-opus-5",
    "messages": [{ "role": "user", "content": "Say hello in five words." }]
  }'

Vercel AI SDK

The recommended path in TypeScript. includeUsage is worth setting: it is what puts token counts on the streamed response you receive.

import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { streamText } from 'ai';

const faucet = createOpenAICompatible({
  name: 'faucet',
  baseURL: 'https://api.intfaucet.com/v1',
  apiKey: process.env.FAUCET_API_KEY,
  includeUsage: true,
});

const result = streamText({
  model: faucet('anthropic/claude-opus-5'),
  prompt: 'Explain a leaky bucket rate limiter in two sentences.',
});

for await (const chunk of result.textStream) process.stdout.write(chunk);
console.log(await result.usage);

OpenAI SDK

Point the official client at Faucet. Nothing else in your code changes.

import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.intfaucet.com/v1",
    api_key=os.environ["FAUCET_API_KEY"],
)

completion = client.chat.completions.create(
    model="google/gemini-3.5-flash",
    messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(completion.choices[0].message.content)

Choosing a model

Model ids are namespaced provider/model, and that slug is a contract: it is never repointed at a different model, only at a newer snapshot of the same one. Pinning anthropic/claude-opus-5 in your code is safe.

Aliases let a bare name resolve too — claude-opus-5 reaches the same entry — so a codebase migrating off a single provider can change only its base URL and keep working.

Ask the gateway what your key can reach, which is narrower than the public catalog because it also applies that key's allowlist and skips providers with no usable credential:

curl https://api.intfaucet.com/v1/models \
  -H "Authorization: Bearer $FAUCET_API_KEY"

The full catalog lists every slug with its provider, capabilities and context window. GET /v1/models stays the authoritative answer for a given key, because it is the only one narrowed to what that key may reach; prices are in the console, where the figure shown is the figure you are charged.

Streaming and usage

Set "stream": true and you get standard OpenAI SSE chunks, forwarded as they arrive rather than buffered — first token out as soon as the provider emits it.

Usage is reported on every request, streamed ones included, whether or not you asked for it. That matters more than it sounds: OpenAI omits token counts from a stream unless stream_options.include_usage is set, most clients never set it, and a gateway that simply forwards the omission meters the majority of its traffic as zero. Faucet forces the flag upstream and reads the counts back out of the bytes on their way through.

Cached and reasoning tokens are counted separately where the provider prices them separately, and a cancelled stream is still metered for what it generated before you hung up.

Multi-turn chat

There is no session, thread or conversation id, and no store flag. Every request stands alone: you send the whole messages array, and what comes back is a completion rather than a handle to resume from. Your client owns the conversation — keep the array, append each turn to it, and send it again.

import { generateText, type ModelMessage } from 'ai';

const model = faucet('anthropic/claude-opus-5');
const messages: ModelMessage[] = [
  { role: 'user', content: 'Explain a leaky bucket rate limiter in two sentences.' },
];

const first = await generateText({ model, messages });
messages.push(...first.responseMessages); // the assistant's turn

messages.push({ role: 'user', content: 'Now contrast it with a fixed window.' });
const second = await generateText({ model, messages });

Against the wire format directly it is the same move in OpenAI's own shape: push the choices[0].message you were given, then the next user message after it.

Keep the assistant turns that carry tool_calls. A tool result identifies itself only by tool_call_id, and the tool's name — which Anthropic and Google both require — exists nowhere but the assistant turn that announced the call. Faucet recovers it by walking the conversation you sent, so pruning that turn to save tokens turns the next request into a 400 rather than a cheaper call.

This is a design decision rather than a missing feature. A conversation you can resume by id is a conversation somebody is storing, and Faucet records what a request cost, never what it said — there is no payload table here that could hold your history. Keeping the state on your side is what lets that stay true.

Two things to plan for as a thread grows:

  • You pay for the history on every turn unless it is cached. The next section is how to make it cached.
  • The context window is the ceiling, and it is per request, not per conversation. Overrun it and the provider's own 400 reaches you with its message intact. Summarize or drop old turns before that point; the window for each model is in the catalog.

Caching a long prompt

A growing conversation, a long system prompt, a document you ask ten questions about — all the same shape: a large stable prefix, re-sent every turn. Mark where that prefix ends and the provider stores it, so later requests read it back instead of paying for it again.

The marker is cache_control, on a message or on a single content part:

{
  "model": "anthropic/claude-opus-5",
  "messages": [
    {
      "role": "system",
      "content": "<long, unchanging instructions>",
      "cache_control": { "type": "ephemeral" }   // cache everything to here
    },
    { "role": "user", "content": "the question, which changes every turn" }
  ]
}

Put it on the last message that will not change. Everything before and including it is cached; everything after is priced normally. In a chat loop that usually means moving the marker forward as the thread grows — onto the last assistant turn before the new question.

ttl is optional and takes "5m" or "1h". The longer window costs more to write and is worth it for a prefix that outlives a single burst of activity.

Reading a cached prefix is cheaper than sending it fresh; writing it costs more than sending it once (Anthropic charges 1.25×). So caching pays off from the second request onward, and a prefix used once is worse than no caching at all. Both numbers come back in the response, and both are metered and billed as their own line rather than as ordinary input:

"usage": {
  "prompt_tokens": 4210,
  "prompt_tokens_details": {
    "cached_tokens": 4096,           // read back — the cheap ones
    "cache_creation_tokens": 0       // written this request — the 1.25× ones
  }
}

cache_creation_tokens is a Faucet addition, since OpenAI's format has no field for it. Clients that do not know it ignore it.

Three things to know before relying on it:

  • Anthropic honours the marker; Google ignores it. Google caches automatically instead, so there is nothing to mark — the field is accepted and does nothing rather than failing your request, which keeps a model swap a one-string change. On the providers Faucet passes through untouched (OpenAI, Fireworks), whatever that provider does with the field is what happens; OpenAI caches automatically too.
  • Four breakpoints, maximum. Anthropic allows four; markers past the fourth are ignored. One at the end of your stable prefix is the usual answer.
  • The prefix must match exactly, byte for byte, from the start of the request. One edited character in your system prompt invalidates everything after it, which is why the marker belongs after the parts that never change — not after a timestamp you inject each turn.

Tools, JSON and images

Tool calls (including parallel ones), tool results, JSON mode and JSON schema, image inputs and reasoning text all survive a round trip on every provider. Reasoning is emitted as reasoning_content.

A few OpenAI parameters only work on providers Faucet can pass through byte-for-byte, and not on the two whose wire format has to be translated (Anthropic and Google):

ParameterNote
n > 1Rejected with a 400 on translated providers, rather than silently returning one choice
logprobsNo equivalent on translated providers
Audio inputPassthrough providers only
Unknown / brand-new parametersForwarded on passthrough; use provider_options elsewhere

provider_options is the escape hatch for provider-native features the OpenAI format cannot express. It is keyed by provider slug, so options meant for one upstream can never leak into a request to another:

{
  "model": "anthropic/claude-opus-5",
  "messages": [{ "role": "user", "content": "…" }],
  "provider_options": {
    "anthropic": { "thinking": { "type": "adaptive" } }
  }
}

Embeddings

POST /v1/embeddings works exactly like the chat endpoint — same key, same namespaced slugs, same limits and the same billing — and the AI SDK reaches it through embeddingModel:

import { embed, embedMany } from 'ai';

const { embedding } = await embed({
  model: faucet.embeddingModel('voyage/voyage-4'),
  value: 'sunny day at the beach',
});

const { embeddings, usage } = await embedMany({
  model: faucet.embeddingModel('fireworks/qwen3-embedding-8b'),
  values: ['chunk one', 'chunk two', 'chunk three'],
});

A model belongs to one endpoint. Sending an embedding model to /v1/chat/completions, or a chat model here, is a 400 that names the endpoint you wanted — not a 404, because the model does exist and the slug is not what needs fixing.

Shorter vectors, where the model offers them. dimensions picks a width, and the catalog lists what each model will truncate to. Voyage's voyage-4 family and the Nomic models are trained so a shorter vector is still a good vector, which makes the width a storage decision rather than a quality one:

{ "model": "voyage/voyage-4", "input": ["a", "b"], "dimensions": 512 }

From the AI SDK, that width is a provider option rather than a top-level argument:

await embed({
  model: faucet.embeddingModel('voyage/voyage-4'),
  value: 'sunny day at the beach',
  providerOptions: { faucet: { dimensions: 512 } },
});

Retrieval quality: tell Voyage what the text is for. Voyage models embed a search query and a stored document differently, and saying which you have measurably improves retrieval. OpenAI's format has no field for it, so it rides on provider_options — embed your corpus as document and your queries as query:

{
  "model": "voyage/voyage-4",
  "input": ["chunk one", "chunk two"],
  "provider_options": { "voyage": { "input_type": "document" } }
}

Billing is input tokens only — a vector is not tokens, and nothing charges for producing one. Token counts come back on every request, including on Voyage, which reports only a total of its own accord.

Errors

Errors always use OpenAI's { "error": { "message", "type", "param", "code" } } envelope, so the AI SDK parses them as errors rather than as an opaque "unexpected response".

StatusWhat happenedWhat to do
401The key is missing, malformed, unknown, revoked or expiredCheck the key. The response deliberately does not say which of those it was
403Valid key, but the model is outside its allowlistWiden the allowlist, or use a model the key may reach
404Faucet does not carry that modelCheck the slug against GET /v1/models
400Real model, wrong endpoint — an embedding model on /v1/chat/completions, or the reverseThe message names the endpoint to use; the slug is fine
429Past the key's requests- or tokens-per-minute allowanceBack off — retry-after tells you how long
402Out of prepaid credit, or past a monthly budgetTop the organization up
503Faucet carries the model but holds no credential that can serve itNot your fault; try another model or wait

A 401 from Faucet always means your Faucet key is bad. An upstream provider's 401 is never forwarded — it becomes a 502, so you are never sent rotating a credential you do not own.

Limits and credit

Rate limits are leaky buckets that refill continuously rather than resetting on a minute boundary, so you cannot spend a whole allowance at 59.9s and another at 60.1s. Requests-per-minute is charged when the request is admitted; tokens-per-minute cannot be, because the count does not exist until the response does, so an overshoot is repaid out of the next window instead of forgiven.

Credit is prepaid and spent live. Between hourly settlements the balance you are admitted against includes unsettled spend, so a burst cannot overrun the balance and be discovered an hour later.

Turn on the low-credit warning under Notifications. Set a threshold and the organization's owners are emailed once when the live balance crosses it — including on the request that is about to be refused, because that is the moment someone most needs to know.

Where to go next

  • The console — keys, members, usage and prices. Your sign-in link comes with your organization.
  • Model catalog — every slug we carry, with capabilities and context windows
  • GET /v1/models — what your key can actually reach, which is the authoritative answer

Something not behaving? The organization overview shows the last 30 days of usage broken down by model, which is usually where a surprising bill or a mysterious 402 explains itself.

Anything else — a model you want carried, a limit raised, a bug — goes to keunwoopark.524@gmail.com. During the alpha that address is the support channel.