Inference API
Three doors, one service: a button, HTTP, and an SDK
A CPU baseline answers today — not the catalogued weights.
This deployment has no GPU and no inference cluster. Everything around the model is real: authorisation, the pinned revision, the result cache, the credit debit, the rate limits. The forward pass is not. In its place the hub runs a small, deterministic baseline — a hashing k-mer embedding, a softmax over the endpoint's declared labels, a lexical span pass, a composition-derived number.
Every response says so: runner names who computed it and notice
carries the caveat in one line. A baseline is weak but it is real — similar inputs do land
near each other, and the mass of caffeine is the mass of caffeine. It is not the model's
published performance, and a number taken from it must never be reported as one. When the
cluster arrives, runner reads orchestrator:<version> and this
page needs no edit.
The three doors
Test this model
The button on any servable model page. Free, no account needed, no credits spent — bounded instead by a rolling window and a stricter input ceiling than the API's. It exists to sell the model to somebody deciding whether to care about it.
HTTP
POST /{namespace}/{slug}/infer. One sequence or a batch of sixteen, a token, and
curl. Costs credits — and /quote says how many before you spend
them, for free.
SDKs
A Python client with no dependencies, and an R sketch. Same contract, same credits, fewer keystrokes.
Authentication
Every call through the HTTP door carries a personal access token as a bearer credential:
Authorization: Bearer atlp_...
Create one on Settings / API tokens with the
infer:run scope. That scope, and only that scope, may spend credits:
repo:write does not satisfy it. Publishing artifacts and draining
a balance are different blast radii, so a CI token that pushes nightly builds cannot empty your
account — it gets 403 forbidden_scope, which is the feature working. A browser
session cookie is also accepted, which is how the model page's own panel calls the same
service.
Pass the token through the environment, never as a literal in a notebook cell. Notebooks persist their own source: a token pasted into a cell survives every restart and is readable by everyone who can open the file.
What a model may be asked
A servable model declares its endpoint in advance — task, declared labels, input ceiling, the unit it bills in with its rate and its floor, and the pinned commit answers are computed against. Read it before submitting a batch; a model with no declaration is simply not servable, and learning that by failing wastes a round trip.
GET /{namespace}/{slug}/infer
Authorization: Bearer atlp_...
200
{
"task": "EMBEDDING",
"runtime": "transformers",
"runner": "baseline-cpu",
"baseline": true,
"notice": "Answered by the in-process CPU baseline, not the catalogued weights.",
"revision": "a1b2c3d4e5f6",
"endpointVersion": 1,
"billingUnit": "residue",
"creditsPer1kUnits": 20,
"minimumCredits": 1,
"maxInputChars": 4000,
"labels": [],
"inputExample": "MKTFFVAGLLLA",
"outputUnit": null
}
404 the model does not exist, is not visible to you,
or declares no endpoint -- it is not servable.Running a model
POST /{namespace}/{slug}/infer
Authorization: Bearer atlp_...
Content-Type: application/json
{"input": "MKTFFVAGLLLA"}
or, for a batch of at most 16:
{"inputs": ["MKTFFVAGLLLA", "MKVLFA"]}200
{
"model": {
"namespace": "atlas", "slug": "prot-embed-demo",
"revision": "a1b2c3d4e5f6", "endpointVersion": 1,
"task": "EMBEDDING"
},
"runner": "baseline-cpu",
"notice": "Answered by the in-process CPU baseline, not the catalogued weights.",
"billingUnit": "residue",
"units": 53,
"creditsCharged": 2,
"results": [
{
"requestId": "8f1c...", "cached": false, "durationMs": 3,
"unitsConsumed": 53, "creditsCharged": 2,
"vector": [0.0125, -0.0413, ...], "vectorDimension": 128
}
]
} revision and endpointVersion come back with every answer, so a result
can be quoted against the exact thing that produced it. The cache is keyed on both: bump either
and every old entry becomes unreachable rather than wrong. Nothing is ever cached against a
tag.
Output shapes
Four tasks, a closed set. Which fields a result carries follows from the endpoint's task; the others are absent.
Errors
Every failure is the same envelope, and the code is stable:
{"error": {"code": "insufficient_credits",
"message": "balance is 0, this call costs 2"}}What a call costs
Pricing is per consumed unit, and the unit is declared per endpoint. There is no single unit for this catalogue: language models share a tokeniser, so a token measures every one of them, and nothing of the kind is shared by a protein encoder, a single-cell model and a docking engine. So a protein endpoint counts residues, a genomic one base pairs, a biomedical-text one tokens, a chemistry one molecules — and the hub multiplies.
credits(one input) = max(minimumCredits, ceil(units x creditsPer1kUnits / 1000)) credits(a batch) = the sum of that, input by input
Three fields on the declaration say all of it: billingUnit,
creditsPer1kUnits — the rate is per thousand, because a residue is too
small a thing to price in whole credits — and minimumCredits. Every answer carries
the count back: unitsConsumed per result, units for the call. The
quantity is reported even when nothing was charged, on a cache hit and through the free browser
door, because what would this have cost is a question worth answering.
The unit, family by family
This inventory is seed data, not code: a family is added by a manifest entry and a calibration, reviewed as a diff, not by a release. Served today means the hub can count that unit and will accept an endpoint declaring it — not that one exists in this catalogue; the demo endpoints here bill in residues and tokens. The rest are refused at seed time: a hub with no accelerator behind it cannot count a cell or a megapixel honestly, and a unit nobody can count is a bill nobody can check.
The two doors spell a unit differently, and it is a convention rather than a bug: REST carries
the stored value (residue) and GraphQL exposes the enum member
(RESIDUE), exactly as they already differ on task and every other
enum in this API. Both SDKs pass the value through untouched.
How the hub counts
The count is the hub's published rule, not the model's tokeniser. That is deliberate, and it is the less obvious of the two options. A real tokeniser would be more faithful to the work done; it would also mean that the price of the same paragraph changes the day a model is swapped behind an endpoint, and that you could not reproduce your own bill without shipping that model's vocabulary. A rule you can implement in ten lines is worth more than fidelity to an implementation detail you cannot see. What follows is therefore a contract — reimplement it and predict your invoice.
The molecule rule is the one that would cost you money if it were wrong the other way:
a dot in SMILES separates the components of one molecule — a salt, a
co-crystal — so ...C(=O)N2C.Cl is one hydrochloride and not two molecules. Billing
it as two would be a silent overcharge, invisible in a total and wrong on every chemistry batch
anyone sends.
The same character can mean different things to different units — the dot is an alignment gap in a sequence and a bond between the parts of a salt in SMILES. The unit decides, which is why it is declared and not inferred. End to end, on one sequence:
>sp|P0DTC2|SPIKE_SARS2 surface glycoprotein MK-T.AY the header line is dropped whole; the gap and the dot are not residues -> 5 residues 5 x 20 / 1000 = 0.1 -> rounded up: 1 1 is not below the endpoint's floor of 1 -> 1 credit, and "units": 5 comes back with the answer
And in full, because "ten lines" is a claim you should be able to check rather than take on faith. This is the whole of it, and it reproduces every case the hub's own contract tests use:
import re
def count_units(unit, text):
if unit in ("residue", "base"):
return sum(c.isascii() and c.isalpha()
for line in text.split("\n")
if not line.strip().startswith(">")
for c in line)
if unit == "token":
return len(re.findall(r"[A-Za-z0-9]+|[^ \t\r\nA-Za-z0-9]", text))
if unit == "molecule": # never split on "."
return sum(1 for line in text.split("\n") if line.strip())
raise ValueError(f"{unit} arrives with the inference cluster")
def credits(units, per_1k, minimum): # per input, rounded up
return max(minimum, -(-units * per_1k // 1000))
The count is taken after whitespace normalisation — the same normalisation the cache key uses,
so a sequence you wrapped differently is the same sequence at the same price. The consequence
to accept, stated rather than hidden: this count is an approximation of the model's own
tokenisation, and changing a rule is a pricing change that bumps
endpointVersion rather than shipping as a patch.
Ask before you spend
The counting rules are reproducible, but you should not have to reproduce them to know what you
are about to pay. POST /{namespace}/{slug}/infer/quote answers exactly that, and it is
free, unmetered and needs no infer:run scope: nothing is charged,
nothing lands in your usage ledger. Charging for the question what will this cost is
the beginning of a pricing page nobody trusts.
POST /{namespace}/{slug}/infer/quote
Content-Type: application/json
{"input": "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGDGTQDNLSGAEK"}
200
{
"billingUnit": "residue",
"units": 53,
"credits": 2,
"perInput": [{"units": 53, "credits": 2}]
}
It runs the same counter the debit runs, so the number it returns is the price and not an
estimate of it — a quote and a bill cannot disagree, by construction rather than by discipline.
perInput is the breakdown in submission order, which is where the one oversized
sequence in an otherwise cheap batch shows up; the total alone hides it.
The floor, the rounding, and the cache
The floor is not a detail. Every call pays the same fixed overhead — authorisation, cache lookup, scheduling — whatever its size, so an endpoint billing per residue would otherwise charge nothing for a dipeptide while doing most of the work of a full request. The floor applies per input: a batch of sixteen short sequences pays sixteen floors. Rounding is up rather than to nearest, for the same reason — a call that did work is never free.
The balance moves before the runner is called, so a second request sees the money gone; a run that fails is refunded by a compensating entry, and both the charge and the refund stay visible in your ledger.
A cache hit costs nothing and says so — cached: true,
creditsCharged: 0, and unitsConsumed still reported. The hub did no
work, so billing for it would be charging you for its own storage layout. It is also the one
pricing rule on this page you can verify yourself in two calls: send the same input twice.
Where a unit cannot be counted at all
Two families have no countable unit that is honest. Structure prediction is superlinear: folding a 1 000-residue protein is not ten times folding a 100-residue one, and the cost also moves with recycles, sampled seeds and MSA depth. Imaging varies by two orders of magnitude per item: "per image" would price a thumbnail like a whole-slide scan.
Those endpoints will declare accelerator-seconds, and the contract changes
shape with the unit: a caller cannot count seconds in advance, so there is nothing to quote.
The request will carry a maxCredits ceiling instead, and the hub will
refuse rather than overspend. A bill nobody can predict is a bill nobody will
authorise. None of it is served today — it arrives with the inference cluster, alongside the
weights themselves.
What this deployment charges
Five models are servable on this deployment — three billing in tokens, two in residues — and every one of them is priced at 20 credits per 1 000 units with a floor of 1 (2 on the biomedical NER model). Those numbers are not calibrated and are not claimed to be: the real cost of a forward pass cannot be measured before there is a cluster to measure it on, so they are a placeholder that exercises the mechanism honestly rather than an estimate dressed as one.
Top-ups on this deployment are labelled demo because there is no payment processor behind them and no card is charged. That is the one thing here that would be a lie rather than a simplification, so it is named instead of dressed up.
SDKs
Python
sdk/python in the repository. Standard library only — no requests, no
wheels to resolve — so it installs inside a cluster job or a locked-down lab image.
export ATLAS_API_TOKEN=atlp_...
from atlas_inference import Client
atlas = Client() # token and URL from the environment
quote = atlas.quote("atlas/prot-embed-demo", "MKTFFVAGLLLA")
quote.units # 12 residues
quote.credits # 1 -- what the call below will charge, not an estimate
result = atlas.infer("atlas/prot-embed-demo", "MKTFFVAGLLLA")
result.runner # "baseline-cpu"
result.baseline # True -- not the catalogued weights
result.units_consumed # 12, in result.billing_unit -- what you were billed for
result.vector # (0.0125, -0.0413, ...)
result.cached # False; a cache hit would be True and cost 0
Every declared error code is a named exception (InsufficientCredits,
RateLimited with .retry_after, NotServable,
InputTooLong, Unauthorized). Nothing retries by itself: a client that
sleeps on a 429 turns one rate limit into a queue of blocked processes.
R
sdk/r/atlas.R — a hundred-odd lines on httr and
jsonlite, same three functions: declare, quote, run.
It is a sketch that has never been run: R is not installed on the machine that serves this
hub, so the file has not been executed, sourced or linted even once.
It is published as a starting point, not as a tested client. The Python client is the tested
one.
atlas <- atlas_client() # ATLAS_API_TOKEN from the environment atlas_quote(atlas, "atlas/prot-embed-demo", "MKTFFVAGLLLA")$credits result <- atlas_infer(atlas, "atlas/prot-embed-demo", "MKTFFVAGLLLA")[[1]] length(result$vector)
curl
# What will it cost? Free, nothing metered, and no infer:run scope --
# a token only if the model is private enough to need one to read it.
curl -X POST https://demoatlas.online/atlas/prot-embed-demo/infer/quote \
-H "Content-Type: application/json" \
-d '{"input": "MKTFFVAGLLLA"}'
# Run it. Same body, one path shorter, and this one spends credits.
curl -X POST https://demoatlas.online/atlas/prot-embed-demo/infer \
-H "Authorization: Bearer $ATLAS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"input": "MKTFFVAGLLLA"}'