How to add a model to catalog/models.json: what each field is, where its value comes from, how to check a new row, and what fails when a figure is wrong. Tags: [DOC] a source read, [OBS] seen on a date, [UNKNOWN] not established. How the fit uses these figures is in the architecture; what the model servers do is in providers.
- What a catalog row is
- The fields
- Computing the three size figures
- Worked example: a safetensors row
- Traps: sliding-window and hybrid models
- GGUF rows versus safetensors rows
- trust_remote_code
- The parsers
- Checking a new row before trusting it
- What goes wrong when a figure is wrong
- What could not be established
catalog/models.json is one JSON array. Each element is a row: one model, in one file format, at one quantisation. GPUGate offers exactly the rows in this file in its model dropdown, and uses a row for three things: the fit column (does this machine hold the model, at this context and chat count), the serving command line sent to the rented machine, and the refusals made before any money is spent.
- When it is read. Once, at program start (
catalog.Loadincmd/gpugate/main.go, pathcatalog/models.jsonrelative to the working directory). Editing the file changes nothing in a running program: restart it.[OBS]read in the code; nothing re-reads the file later. - Load failures. A file that is not valid JSON, or has a value of the wrong type (a string where a number belongs), stops the program at start with "load the model catalog".
[OBS]internal/catalog/catalog.goLoadreturns thejson.Unmarshalerror andmain.goreturns it. - There is no other validation. Nothing checks that a name is unique, that a required field is present, that a figure is plausible, or that a key is spelled right: a column the Go code does not know is ignored without a word. A misspelled
kv_cache_bytes_per_tokenis therefore read as absent, and the row becomes "cannot be sized". A misspelledtrust_remote_codeis read as false. Re-read the row after typing it. - One row is one file. The same model in another quantisation is a second row naming the other file, never an edit to the first.
- The name is the row's identity inside GPUGate. Saved per-model settings are keyed by
name(tablemodel_setting), so renaming a row orphans what was saved for it. If two rows share a name, the later one silently wins in the program's lookup while the dropdown shows both. - What clients call it. The model server serves the model under its
hf_repostring, and a request must use that string. The catalognameis only what the dropdown shows.[OBS]docs/providers.md2026-10-05; llama.cpp is started with--alias <hf_repo>so both engines agree (internal/provider/runpod/runpod.go). - Write down where every figure came from. The shipped rows carry the basis of each figure in
architecture_basisornotes, so the next person can repeat the arithmetic. The code does not require it.
"Read by Go" means internal/catalog/catalog.go has a field for it. "Reference only" columns are kept so the next person can see where a figure came from; Go ignores them.
| Field | Required | Read by Go | What it is and where its value comes from |
|---|---|---|---|
name |
yes | yes | Dropdown label and key for saved settings. Free text; any unique string. |
hf_repo |
yes | yes | The Hugging Face repository the machine downloads, owner/name. Also the name the model is served and requested under. |
hf_file |
GGUF: yes. Otherwise null |
yes | The exact .gguf file inside hf_repo, from the repository's Files tab or its tree listing. A GGUF launch is refused ("has no hf_file in the catalog") if it is empty. |
format |
yes | yes | "gguf", "safetensors" or "fp8". Decides the engine and which provider may serve it (see GGUF rows). Any other value is refused by the provider's format check. |
size_gb |
yes | yes | Weight size in decimal gigabytes (bytes divided by 1,000,000,000). Used only when disk_bytes is null. |
disk_bytes |
strongly advised | yes | Exact byte total of the files the machine downloads. Wins over size_gb for the fit, and sizes the rented machine's disks. Null falls back to size_gb for the fit and to 30 GB container and 60 GB volume floors for disks. |
kv_cache_bytes_per_token |
yes in practice | yes | Memory one token of one conversation takes in the cache. Null makes the row "cannot be sized": it cannot be launched from the fit column. See computing. |
attention_layers, kv_heads, head_dim |
reference | no | The factors the cache figure was built from. |
state_layers |
0 if none |
yes | Number of layers that keep a recurrent state (hybrid models). Greater than zero marks the row "hybrid" in the dropdown. |
state_bytes_per_layer |
reference | no | Size of that state for one layer. |
weight_bits |
reference | no | Average bits per weight: file bytes times 8 over the parameter count. Sanity check for quantised files. |
quant_method |
reference | no | For example compressed-tensors; null when none. |
min_compute_capability |
optional | yes | The lowest CUDA compute capability the build runs on, as a two-digit integer (8.9 is 89). Null never excludes a card. [OBS] A former FP8 row used 89 because the serving image refused FP8 activations on an A100 (compute capability 8.0). |
context_length |
optional | yes | The model's published maximum context in raw tokens. A launch asking for more is refused before anything is rented. Also shown as the "model" value beside the context box. Null means no ceiling is enforced. |
context_size |
reference | no | A second context figure carried by some rows. What it records is [UNKNOWN]: no code or document reads it. |
reasoning_parser, tool_call_parser |
optional | yes | vLLM flag values; see the parsers. Ignored for GGUF rows. |
temperature, top_k, top_p, min_p, repeat_penalty |
optional | yes | The publisher's recommended sampling values. They fill the "Model settings" boxes as the fallback layer: a value the user saved wins, and an emptied box sends nothing. Null means the row has none. |
trust_remote_code |
optional, default false | yes | See trust_remote_code. |
architecture_basis, notes |
advised | no | Arrays of strings, each tagged [DOC], [OBS] or [UNKNOWN], giving the source of every figure above. |
Where the sampling values come from: the repository's generation_config.json (keys temperature, top_k, top_p, repetition_penalty) or, when it has none, the base model's. The shipped GGUF row says exactly that in its notes. [OBS] shipped rows.
All three come from files you can fetch without a login for a public repository. Use the repository the row names (or, for a GGUF repository that carries no config.json, the base model's).
- Safetensors: add up the byte sizes of every file the server downloads, which is the weight shards (
model-0000N-of-0000M.safetensors). Hugging Face's model API with?blobs=truelists each file with its size.[OBS]the shipped Ornith row citeshttps://huggingface.co/api/models/<repo>?blobs=true, 16 shards summing to 71,903,871,064 bytes, seen 2026-09-10. Itssize_gbis that number divided by 10^9, 71.9. - GGUF: the byte size of the one file named by
hf_file, from the repository's tree listing. The shipped Qwen row: 29,047,086,048 bytes,size_gb29.05. - Decimal GB, not GiB. GPUGate converts with a thousand million for
size_gband with 2^30 when it works in GiB internally. Typing a GiB figure intosize_gbunderstates the model by about 7%. disk_bytesalso sizes the rented machine's disks: container disk 30% of the weights (floor 30 GB), persistent volume 2.2 times the weights (floor 60 GB) (internal/provider/disksize.go). Wrong low and the download does not fit.
For a model where every cached layer is ordinary full attention:
kv_cache_bytes_per_token = 2 x full_attention_layers x num_key_value_heads x head_dim x bytes_per_element
- The 2 is one key and one value.
bytes_per_elementis 2 for the default 16-bit cache.[UNKNOWN]for an 8-bit cache: GPUGate never sends a cache-dtype flag (internal/provider/vllm.go), so 2 is what the server uses on--dtype auto.- Take the keys from the model's
config.json. Multimodal models nest the language model's keys undertext_config; read them there, not at the top level. full_attention_layersis the count of"full_attention"entries inlayer_typeswhen that list exists, otherwisenum_hidden_layers. Layers listed as"linear_attention"or"sliding_attention"are not in this count (see the traps below).- Some models name a different head count or head size for their full-attention layers (Gemma 4 uses
num_global_key_value_headsandglobal_head_dim). Read the vendor's modelling code for which pair a full-attention layer uses; reading the plainhead_dimwas the cause of one wrong shipped figure.[OBS]git history of this catalog, 2026-09-12.
What the fit does with it (internal/fit/fit.go): a conversation costs chats x context_tokens x kv_cache_bytes_per_token. Whole-machine need is weights + 1.3 x (2 GiB + 1 GiB per card + conversations), compared with 90% of the machine's total memory (every card summed). The 1.3, the 2, the 1 and the 0.9 are judgements, not measurements, named as such in the code.
The shipped row Ornith-1.5-35B-A3B (format safetensors, repository ornith-ai/Ornith-1.5-35B-A3B). [DOC] its config.json, fetched again 2026-10-19 from https://huggingface.co/ornith-ai/Ornith-1.5-35B-A3B/raw/main/config.json, and it matches what the row stored on 2026-09-10.
Keys under text_config: num_hidden_layers 40; layer_types with a full_attention at every fourth position and linear_attention at the rest (10 and 30); head_dim 256; num_key_value_heads 2; max_position_embeddings 262144.
- Cache per token: 2 x 10 x 2 x 256 x 2 = 20,480 bytes. This is the stored
kv_cache_bytes_per_token. Check: at 262,144 tokens one conversation takes 20,480 x 262,144 = 5.0 GiB. - The other 30 layers are linear attention, a recurrent state, not a per-token cache.
state_layers= 30. - State of one such layer: with
linear_key_head_dim128,linear_num_key_heads16,linear_value_head_dim128,linear_num_value_heads32,linear_conv_kernel_dim4,dtypebfloat16 (2 bytes) andmamba_ssm_dtypefloat32 (4 bytes):- convolution state: (128 x 16 x 2 + 128 x 32) x (4 - 1) x 2 = 49,152 bytes
- temporal state: 32 x 128 x 128 x 4 = 2,097,152 bytes
- together 2,146,304, the stored
state_bytes_per_layer.state_layersx that is 61.4 MiB per conversation. GPUGate does not count it (see below); it is small here.
disk_bytes: sum of the 16 shards, 71,903,871,064;size_gb71.9;weight_bits16.0 for a bfloat16 repository.context_length:max_position_embeddings= 262,144.- Sampling:
generation_config.json: temperature 1.0, top_p 0.95, top_k 20. - Parsers:
qwen3_coderandqwen3, the vLLM names for the Qwen3 family (see the parsers).
The second shipped row, Qwen3.8-27B-Q8_0, reproduces the same way: 2 x 16 x 4 x 256 x 2 = 65,536. Its repository is GGUF; the numbers come from the base model's config.json, which that repository keeps beside the files. Its state per layer is 61,440 + 3,145,728 = 3,207,168 bytes, 48 layers. [OBS] the row's own basis; the GGUF file itself was never opened to confirm.
Three kinds of layer appear in modern layer_types lists. Count them separately.
- Full attention. Costs the per-token figure above, for the whole context.
- Linear attention / state-space ("hybrid" models). Holds a fixed-size state per conversation, not per token. The row records
state_layersandstate_bytes_per_layerand leaves them out ofkv_cache_bytes_per_token. GPUGate's fit does not add that state, so its answer is slightly optimistic for such a row, roughly 0.4 to 1.5 GiB at ten chats (comment onStateLayersininternal/catalog/catalog.go). The dropdown labels such a row "hybrid state, sum is optimistic". Settingstate_layersabove zero is how a row says so; nothing else reads it. - Sliding-window attention. A single constant per token cannot describe it honestly, and counting only the full-attention layers is wrong.
[OBS]2026-09-12, Gemma-4-31B-it on one 96 GB card at a context of 131,072 tokens: the row stored 81,920 bytes per token (10 full layers only), vLLM needed 48.29 GiB of cache against 26.3 available and refused to start 26 times in a loop (vLLM v0.29.0, thevllm/vllm-openai:latestimage that day).[DOC]vLLM v0.29.0 sizes each sliding layer atmin(sliding_window - 1 + max_concurrent_batches x max_num_batched_tokens, max_model_len)tokens (vllm/v1/kv_cache_interface.pylines 695 to 735), withmax_concurrent_batches2 because async scheduling is on by default (vllm/config/vllm.py), and each sliding layer uses its own kv-head count and head size, which can differ from the full layers'. That reproduced the pod's 48.29 GiB exactly. The row for such a model should therefore be derived as: full layers at the full context, plus each sliding layer at the capped length, divided by the context, and its notes must state the context, chat count and batched-token figure it was derived at, because the answer moves with them.[UNKNOWN]the formula was checked against one pod log and one other model's arithmetic, not against other vLLM versions. - Shared cache layers. If the config says some layers reuse an earlier layer's cache, they are not counted (the shipped bases record "the last 0 sharing an earlier layer's").
- Vision towers and multi-token-prediction heads are not in any figure. Ornith's config lists
mtp_num_hidden_layers1; whether vLLM allocates anything for it at serving time is[UNKNOWN].
| safetensors / fp8 | gguf | |
|---|---|---|
| Engine | vLLM, official vllm/vllm-openai image |
llama.cpp server, official image pinned to one tag |
| Providers | RunPod and Verda | RunPod only. Verda's format check refuses GGUF by name. |
| Weights | whole repository downloaded by vLLM | -hf <hf_repo> -hff <hf_file>: only the one named file |
hf_file |
null |
required |
| Parsers, GPU-memory-utilization, batched-token flags | sent when set | never read; llama.cpp has no such flags |
trust_remote_code |
sent as --trust-remote-code |
never reaches the command line |
| Sampling | one --override-generation-config JSON |
bare --temp --top-k --top-p --min-p --repeat-penalty |
- GGUF support in vLLM is experimental and not installed in the official image, and GPUGate never rebuilds an image, so there is no vLLM path for a GGUF row.
[DOC]vLLM v0.28.0docs/features/quantization/gguf.md, quoted in the code comment onServesFormatininternal/provider/runpod/runpod.go. - llama.cpp loads the whole context pool shared by all chats: GPUGate sets
-cto context times chats.[DOC]llama.cpp server README at tag b10818 (the pinned tag at the time of that comment; re-read before relying on it). - llama.cpp does not read the model's
generation_config.json; its own defaults apply where a row carries no value (temperature 0.80, top-k 40, top-p 0.95, per the program's own settings text).[OBS]engineDefaultTextininternal/console/model_settings.go. - For a GGUF row the architecture figures come from the base model's
config.json, not from the file. The file's own header (gguf_dump) or the server's startup log is the proof, and the shipped row marks that as[UNKNOWN]. - A GGUF repository often publishes dozens of quantisations and vision-projector files (
mmproj-*.gguf). The row names one weights file; GPUGate downloads only that file, so a row serves text, not images. quant_methodandweight_bitsare reference columns;weight_bits= file bytes x 8 / parameter count (the shipped row: 8.51).format: "fp8"is accepted and goes to vLLM, but no FP8 row ships today. FP8 weights need compute capability 8.9 or higher; a build that also quantises activations refused to load on an 8.0 card even though the weights fitted.[OBS]removed row note, git history of this catalog. Setmin_compute_capabilityaccordingly.
"trust_remote_code": true makes GPUGate add --trust-remote-code to the vLLM command. That flag lets vLLM run Python code that lives in the model's own repository (custom model classes listed under auto_map in its config.json). The rented machine then runs code you have not reviewed unless you read it. What that exposes is bounded by what the machine holds: its own random serving key and, when configured, the Hugging Face token. It holds no cloud-account credential. [OBS] provider.VLLMServeEnv in internal/provider/vllm.go. The launch panel adds a sentence saying so when the chosen row sets it.
- Leave it out unless the model fails without it. Most rows do not need it: the model's class is already in vLLM's registry. How to tell: the repository has no
.pyfiles and noauto_mapinconfig.json(checked through the Hugging Face model API), and thearchitecturesentry is registered in vLLM'svllm/model_executor/models/registry.py. The shipped Ornith row cites registry line 593 for v0.28.0. - A model that needs it can still fail.
[OBS]2026-09-21 a model carrying the flag crashed inside its own repository code on vLLM 0.29.0 on every load and its row was dropped. - The flag never applies to GGUF rows.
tool_call_parser and reasoning_parser are values for vLLM's --tool-call-parser and --reasoning-parser. Both are optional; empty means the flag is not sent.
- A tool-call parser alone is not enough: GPUGate sends
--enable-auto-tool-choicewith it, because without that flag vLLM answers a coding tool'stool_choice: autorequest with 400.[OBS]2026-09. A row with no tool-call parser sends neither flag, so such a model will refuse a coding tool's tool calls. - Reasoning parser: splits the model's thinking from its answer into a
reasoningfield.[DOC]vLLM v0.28.0docs/features/reasoning_outputs.md. - Where to find the right value: the model card, or vLLM's docs on tool calling and reasoning outputs, which list the parser names and which model families each belongs to. The shipped values are
qwen3_coderwithqwen3(Qwen3 family) and, from a former row,gemma4for both. A parser copied from a sibling model of the same family is a reasonable first guess and is an assumption: the shipped Ornith row says so in its notes. - GPUGate always sends
--default-chat-template-kwargswith bothenable_thinkingandthinking; the model's chat template uses whichever it declares.[DOC]vLLMhf.py. - A parser name vLLM does not know: expected to stop vLLM at start-up. The exact message was not observed.
[UNKNOWN]
Nothing tests a new row automatically, and a first launch is the only complete proof, because it is the only step that loads the model on a card. A launch costs money. Everything below is free.
- Restart the program and watch the start: a parse error or a wrong type stops it, naming the file.
- Open the dropdown. The row appears as
name [format], with "hybrid state, sum is optimistic" ifstate_layersis above zero. A row missing from the list was not read. - Choose it and open the machine table. The fit column shows one verdict per machine: launchable (with the free GiB), too small (short by N GiB), cannot be sized (no
kv_cache_bytes_per_token), card cannot run it (belowmin_compute_capability), cannot serve this format (a GGUF row on Verda). If everything says "cannot be sized", the cache figure did not load. - Compare the verdict with your own arithmetic. For a chosen context and chat count, work out
weights + 1.3 x (2 + cards + chats x context x kv bytes / 2^30)GiB and compare with 0.9 times the machine's memory. The console's number should match yours. A large gap means a figure is mis-typed. - Check the context ceiling. A context above
context_lengthis refused with the model's published maximum named in the message. - Re-read the sources. The row's notes should let a stranger repeat the arithmetic; if you cannot reproduce
kv_cache_bytes_per_tokenfromconfig.json, the row is not ready. - Then the real launch. It is the only check that the repository downloads, the architecture is supported by the pinned or latest server, the parsers are valid, the cache really fits, and the model answers. Do it on the smallest machine the fit column calls launchable, at a short context, and read the machine log if it does not serve. GPUGate gives a launch one hour to serve and destroys the machine after that.
| Wrong figure | What you see |
|---|---|
kv_cache_bytes_per_token too low (a sliding-window model counted by its full layers only) |
The fit column says launchable. The machine rents, then vLLM refuses to start with "KV cache is needed, which is larger than the available KV cache memory" and restarts in a loop until the hour clock ends and GPUGate destroys it. The log names the cache size actually needed, which is the number to put back in the row: needed GiB x 2^30 / context. [OBS] 2026-09-12. |
kv_cache_bytes_per_token too high |
The fit column says too small for machines that would work. Nothing fails; capacity is wasted. |
kv_cache_bytes_per_token null or misspelled |
"cannot be sized" on every machine. |
size_gb or disk_bytes too low |
Optimistic fit; the machine's disks may be too small for the download. [UNKNOWN] the exact message. |
hf_file empty or wrong on a GGUF row |
Empty: refused at launch before renting, "has no hf_file in the catalog". A name that does not exist in the repository: not checked by GPUGate; llama.cpp fails on the machine. [UNKNOWN] the exact message. |
hf_repo wrong |
The download fails on the machine and it never serves; the one-hour clock destroys it. Also the name clients must use is wrong. |
format wrong |
A GGUF repository labelled safetensors goes to vLLM and cannot load. A safetensors repository labelled gguf is refused on Verda and sent to llama.cpp on RunPod. |
context_length too low |
Needlessly refuses launches. Too high or null: the launch passes the check and vLLM refuses at start-up. |
state_layers left at 0 on a hybrid model |
No warning label; memory slightly under-counted. |
min_compute_capability too high |
Cards that could run it are marked "card cannot run it". Too low or null: a card that cannot run it is let through and the load fails on the machine. |
| A parser value vLLM does not know | [UNKNOWN] exact failure; expected at vLLM start-up. A missing tool-call parser: tool calls refused with 400. |
| Spelling of a key | Silently ignored (see above). |
A failed launch is the expensive way to learn any of these; the notes in the shipped rows exist because one such launch had already taught it.
- What
context_sizerecords. No code or document says; it is262144on one shipped row andnullon the other. - Whether the cache dtype is always 16 bits for a model GPUGate serves (no cache-dtype flag is sent; vLLM's default behaviour under
--dtype autofor a quantised-KV model was not read). - The exact error text for a wrong
hf_file, a wrong parser name, or too-small disks. None was observed. - Whether the sliding-window formula holds for vLLM versions other than 0.29.0. The vLLM image is not pinned (
vllm/vllm-openai, unpinned), so a later release may change the numbers. - Whether vLLM allocates anything for a multi-token-prediction head or for the vision tower of a multimodal row. The fit ignores both.
- How a gated Hugging Face repository behaves: GPUGate passes the token when one is set but has no gated-model handling; whether a licence-gated model can be launched has not been tested. The shipped rows are public (
gated=false). - The vLLM and llama.cpp flag behaviour above rests on the vendor pages named beside each claim, read at the versions named; re-read them before relying on a newer version. Ornith's
config.jsonwas re-fetched on 2026-10-19 and matches its row.