build/apps/ninfer-serve loads one registered artifact and exposes OpenAI- and
Anthropic-compatible HTTP endpoints over one resident NInfer Engine.
./build/apps/ninfer-serve models/qwen3_6_27b.ninfer \
--host 127.0.0.1 \
--port 8080 \
--max-context 16384 \
--kv-capacity 32768 \
--max-concurrency 2 \
--spec mtp --draft-tokens 3 \
--lm-head-draftFor the 35B-A3B artifact, select its artifact path; the public model ID follows the container identity automatically:
./build/apps/ninfer-serve models/qwen3_6_35b_a3b.ninfer \
--max-context 16384 \
--spec mtp --draft-tokens 3 \
--lm-head-draftWhen --model-id is omitted, the server advertises and accepts the loaded container's exact
identity.model_id. An explicit --model-id remains a public HTTP alias override and does not
select or alter the artifact.
Vision is disabled by default: its weights, Vision scratch phase, and frozen request-transient
buffer are not allocated, and media
requests and token-count requests fail with HTTP 400 vision_disabled. Add --vision when the
server must accept image or video input. Speculative residency is likewise frozen by
--spec mtp|dflash and --draft-tokens; omitting --spec loads neither backend.
--lm-head-draft additionally loads the optimized proposal head. DFlash is 35B-A3B text-only and
cannot be combined with --vision. A later request cannot enable a capability omitted at startup.
--chat-template-file PATH replaces the artifact's embedded prompt renderer for this server
process.
| Method and path | Behavior |
|---|---|
GET /health |
process health |
GET /v1/models |
configured OpenAI model alias |
GET /v1/models/{id} |
lookup of the configured alias |
POST /v1/chat/completions |
OpenAI-style chat generation |
POST /v1/responses |
OpenAI Responses Core generation, state, typed Items, and SSE |
POST /v1/responses/input_tokens |
Responses prompt-token count without generation |
GET /v1/responses/{id} |
retrieve a locally stored terminal Response |
DELETE /v1/responses/{id} |
delete a locally stored Response |
GET /v1/responses/{id}/input_items |
list that Response's normalized input Items |
POST /v1/messages |
Anthropic-style message generation |
POST /v1/messages/count_tokens |
checkpoint-native expanded input-token count |
GET /v1/models returns a list object holding a single data entry, and
GET /v1/models/{id} returns that same model object directly. Both describe the
one registered artifact's public model ID: the artifact identity.model_id by
default, or the explicit --model-id override. A GET /v1/models/{id} request
for any other ID returns HTTP 404 with code model_not_found.
Each model object is:
{
"id": "qwen3.6-27b",
"object": "model",
"created": 1786813490,
"owned_by": "ninfer",
"max_model_len": 16384
}id— the public model ID, identical to themodelvalue the other OpenAI endpoints require.object— always"model".created— the server's current unix time in seconds at request time.owned_by— always"ninfer".max_model_len— the process's configured context window in tokens, exactly the--max-contextvalue used to size every sequence. It is a per-server property, not a per-model one: thelistand single-object forms report the same value, and it does not change with the request. Clients using these endpoints for context discovery should treatmax_model_lenas the hard per-sequence ceiling;--kv-capacityis a separate limit that sizes the shared KV pool and is not advertised here.
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3.6-27b",
"messages": [
{"role": "system", "content": "Answer concisely."},
{"role": "user", "content": "What is speculative decoding?"}
],
"max_tokens": 128
}'The endpoint supports:
system,developer,user,assistant, andtoolhistory;- string content and ordered text,
image_url, andvideo_urlparts; - tool messages with string content or an array of
textcontent parts (the OpenAI contract allows no other part type for tool messages); max_completion_tokensand the legacymax_tokensspelling;temperature,top_p,top_k, presence/frequency penalties, and a nonnegativeseed;- one stop string or an array of stop strings;
- non-streaming responses and server-sent event streams;
stream_options.include_usage;- function tools, tool choices, assistant tool-call history, and tool-result messages;
- the top-level
reasoning_effortfield; - the
enable_thinkingextension; chat_template_kwargs.preserve_thinkingand the top-levelpreserve_thinkingalias;- the top-level
response_formatfield with typestext,json_object, andjson_schema.
The request model must equal the public model ID: the artifact identity.model_id by default, or
the explicit --model-id override. Reasoning is returned separately as reasoning_content; answer
text remains in content.
Message roles retain their input order through schema translation. The Qwen family frontend maps
both system and developer to system-class ChatML blocks at their original positions; it does not
move later instructions to the beginning of the conversation. A leading instruction keeps the
artifact template's existing tool/reasoning-instruction composition.
At startup, NInfer resolves prompt capabilities from the exact frontend/chat_template.jinja
resource embedded in the loaded artifact. It does not infer them from the request's model field,
the artifact identity, or a target profile. A recognized effort-capable template exposes low,
medium, and xhigh; omitting effort uses that template's declared default. An explicit effort
not exposed by the loaded template returns HTTP 400 with code
reasoning_effort_not_supported before prompt preparation.
For Chat Completions, reasoning_effort: "none" disables thinking. low, medium, and xhigh
select the corresponding template effort when available. The other OpenAI protocol values
minimal, high, and max are parsed but rejected when the loaded template does not expose them.
enable_thinking controls the same new-turn thinking switch; a contradictory combination with
reasoning_effort returns conflicting_template_option.
preserve_thinking controls whether reasoning from closed assistant turns remains in later
prompts. It defaults to the server setting, which is off unless --preserve-thinking is used. If
both OpenAI spellings are present they must carry the same boolean value. Unknown non-null
chat_template_kwargs are rejected.
response_format accepts {"type":"text"}, {"type":"json_object"}, and
{"type":"json_schema","json_schema":{"name":...,"strict":...,"schema":{...}}}. The engine has
no token-level constraint, so the JSON types are enforced by injecting an instruction into the
prompt: json_object requests a single JSON object and nothing else; json_schema additionally
embeds the client schema object. The instruction is appended to a leading system turn when one
is present, otherwise a system turn is prepended. The name and strict fields of json_schema
are carried for wire compatibility only. Any other response_format type, a non-object
response_format, or a json_schema entry without an object schema returns HTTP 400 with code
response_format_not_supported.
Streaming begins with an assistant-role chunk, sends separate reasoning and content deltas, then a
finish-reason chunk and [DONE]. When stream_options.include_usage is true, a final empty
choices chunk contains completed usage.
Start the server with --vision before sending media:
curl http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3.6-27b",
"messages": [{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "https://example.com/image.png"}},
{"type": "text", "text": "Describe this image."}
]
}],
"max_tokens": 128
}'OpenAI image and video sources may be HTTP(S) URLs or base64 data URLs.
Text and media requests use one complete-prompt context contract. After chat-template rendering and
media-token expansion, the result must fit Engine --max-context. The current Vision runtime also
has a 32,768 merged-token envelope (131,072 raw patches); the effective Vision limit is therefore
min(--max-context, 32768). There is no fixed image/video item-count limit: item count is admitted
through aggregate source-byte, decoded-pixel, raw-patch, Vision-token, and live-memory budgets.
Media cache misses run as independent decode → resize → BF16-pack tasks on a bounded host worker
pool. Prepared payloads are keyed by SHA-256 of the acquired bytes plus modality, so repeated media
in later requests reuses the exact immutable BF16 patch input; concurrent identical misses use one
single-flight build. --media-cache-mib bounds LRU-retained payloads, while
--media-live-mib bounds every cache-, request-, or runtime-referenced payload. Cache eviction does
not invalidate a request reference, and live bytes are returned only when the final reference is
released. A request-level preparation gate derived from the live limit prevents concurrent partial
builds from deadlocking the memory account.
preprocessor_config.json ships the model's capability ceiling: size.longest_edge there is
large enough that a full-resolution screen capture is never resized, so a single image can occupy
thousands of prompt tokens. An agent client resends every screenshot it has read on every turn,
so that number decides how many turns fit the context at all. --image-token-budget N applies a
serving policy ceiling on top of the capability one, counted in Vision tokens, one Vision token
being a 32x32 pixel square: an image above the budget is scaled to fit rather than rejected, and
0 keeps the artifact's own number. It is a per-image ceiling and deliberately does not depend on
how many images a request carries, because waterfilling a shared budget across items would move
the prompt prefix as a conversation grows and invalidate prefix reuse on every turn. Videos are
unaffected.
On the Qwen3.8-27B NVFP4 artifact served with --vision --max-context 32768, a one-image chat
request reports these usage.prompt_tokens; the same request without an image reports 53:
| image | default | --image-token-budget 1280 |
--image-token-budget 256 |
|---|---|---|---|
| 2880x1800 | 5,095 | 1,315 | 295 |
| 1600x1200 | 1,955 | 1,285 | 289 |
An expanded prompt beyond --max-context returns HTTP 400 context_length_exceeded, including
the prepared token count and configured context ceiling. A media preprocessing resource rejection
returns HTTP 400 media_budget_exceeded. HTTP 413 request_too_large is reserved for a raw request
body that exceeds --max-request-mib before JSON parsing; it is not used for model-context or media
resource errors.
NInfer implements the typed-Item and semantic-event core of the OpenAI Responses API. All registered artifact identities use this same adapter and Engine route. It is intentionally not advertised as full parity with OpenAI-hosted tools, durable cloud storage, background jobs, Conversations, or compaction.
curl http://127.0.0.1:8080/v1/responses \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3.6-27b",
"instructions": "Answer concisely.",
"input": "What is speculative decoding?",
"max_output_tokens": 128,
"store": true
}'The same endpoint works with OpenAI SDKs by replacing their base URL:
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="local-secret")
response = client.responses.create(
model="qwen3.6-27b",
instructions="Answer concisely.",
input="What is speculative decoding?",
max_output_tokens=128,
)
print(response.output_text) # SDK helper derived from response.outputoutput_text is an SDK convenience property. It is not emitted as a top-level wire field; the
wire response contains typed output Items.
| Field | NInfer Responses Core contract |
|---|---|
model |
required non-empty string; must equal the artifact-derived public model ID or explicit --model-id override |
input |
required string or non-empty typed Item array |
instructions |
optional string, inserted before the reconstructed conversation for this request only |
previous_response_id |
optional ID of a retained local Response |
max_output_tokens |
integer at least 16; default is --default-max-tokens |
stream |
boolean; true selects Responses SSE rather than a JSON body |
store |
boolean, default true; controls local retrieval and continuation state |
temperature |
finite number in [0,2] |
top_p |
finite number in [0,1] |
metadata |
at most 16 string pairs; keys at most 64 characters and values at most 512 |
reasoning.effort |
none disables thinking; low, medium, or xhigh selects an effort exposed by the loaded chat template; minimal, high, and max return reasoning_effort_not_supported for the registered templates |
chat_template_kwargs.preserve_thinking |
optional boolean controlling whether closed-turn reasoning remains in reconstructed prompts |
preserve_thinking |
top-level alias for the same option; conflicting values are rejected |
text.format |
omitted or {"type":"text"} only |
tools |
flat Responses function definitions; see below |
tool_choice |
auto or none |
parallel_tool_calls |
omitted or true |
truncation |
omitted or disabled; overlong input fails instead of silently dropping Items |
top_logprobs |
omitted or 0 |
service_tier |
omitted, auto, or default; the response reports default |
background |
omitted or false |
include |
omitted or an empty array |
stream_options |
omitted or {"include_obfuscation":false} |
Unknown top-level fields fail with unknown_parameter. Recognized but unsupported features fail
with a field-specific 400 error instead of being silently ignored.
String input is normalized to one user message with an input_text part. Array input accepts:
| Item | Supported form |
|---|---|
message |
roles user, assistant, system, and developer; string content or typed content array |
input_text |
message content part containing string text |
output_text |
assistant-message replay part containing string text |
input_image |
user-message part with HTTP(S) or data-URI image_url; detail omitted or auto; requires server --vision |
input_video |
NInfer extension with HTTP(S) or data-URI video_url; requires server --vision |
reasoning |
raw replay Item with an empty summary and reasoning_text content parts |
function_call |
completed assistant call with optional id, and required call_id, name, and JSON-object string arguments |
function_call_output |
completed tool result with required call_id and string output |
Adjacent function-call Items are grouped into one assistant history turn. A reasoning Item attaches to the following assistant message or function call. Input Item IDs are preserved when supplied and generated otherwise; duplicate IDs fail.
System and developer message Items retain their positions in the input array. Top-level
instructions is represented as a leading developer turn for the current request; target-specific
role lowering occurs only in the Qwen family frontend.
input_file, input_audio, image file_id, non-auto image detail, reasoning summaries or
encrypted reasoning, message phase, and other Item/content types are not supported. HTTP media
URLs stored in a response chain are fetched again when that chain is continued; use data URIs when
the historical media bytes must be immutable.
Responses function definitions are flat rather than Chat Completions' nested function object:
{
"type": "function",
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
},
"strict": false
}NInfer renders these definitions in the Qwen prompt and parses model output into separate
function_call output Items. Each output has a protocol Item id (fc_...) and a distinct
call_id (call_...). The client executes the function and sends a function_call_output Item in
a later request. NInfer does not execute functions or enforce JSON Schema through constrained
decoding, so strict:true, tool_choice:required, named tool choice, hosted tools, MCP tools, and
custom free-form tools are rejected.
A terminal wire response has object: "response", one of completed, incomplete, or
cancelled in status, and a typed output array. NInfer may emit:
- a
reasoningItem containing rawreasoning_textand an empty summary; - an assistant
messagecontaining anoutput_textpart; - one or more
function_callItems.
Ordinary model/string stops produce completed. Output-token or context-capacity exhaustion
produces incomplete with incomplete_details.reason: "max_output_tokens". Errors accepted after
an SSE response has started produce response.failed; validation and preparation errors remain
normal HTTP error responses.
Usage is checkpoint-native:
{
"input_tokens": 42,
"input_tokens_details": {"cached_tokens": 17},
"output_tokens": 12,
"output_tokens_details": {"reasoning_tokens": 5},
"total_tokens": 54
}input_tokens includes the chat template and expanded media tokens. cached_tokens is the exact
resident prompt prefix reused by Engine. output_tokens is the count of accepted generated token
IDs, including a withheld stop token when applicable. reasoning_tokens is counted in the Qwen
output decoder while accepted tokens are still in the reasoning channel; it is not estimated by
re-tokenizing decoded text.
Set stream:true for semantic Server-Sent Events. Every frame uses both the SSE event name and a
matching JSON type, and every JSON event has a monotonically increasing sequence_number:
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":7,...}
The normal lifecycle is:
response.created, thenresponse.in_progress;response.output_item.addedandresponse.content_part.added;- zero or more
response.reasoning_text.deltaorresponse.output_text.deltaevents; - matching
*.done,response.content_part.done, andresponse.output_item.doneevents; - exactly one
response.completed,response.incomplete, orresponse.failedterminal event.
Function arguments use response.function_call_arguments.delta and .done. IDs, output indices,
and content indices remain stable, and concatenated deltas equal the terminal Item. Responses SSE
does not emit the Chat Completions [DONE] sentinel. With tools enabled, ordinary answer text still
streams immediately; only an ambiguous <tool_call> suffix or the structured tool region is held.
Malformed tool markup is flushed back as ordinary text without losing bytes.
store defaults to true. Stored Responses live only in this server process and are bounded by an
LRU store. They are lost on restart and are not OpenAI's durable cloud retention service.
previous_response_id reconstructs the complete stored input/output Item history before the new
input. The current instructions value is placed first but is not saved into the continuation
context, matching the Responses rule that previous top-level instructions do not carry forward.
Function definitions are request configuration rather than conversation Items and must be sent
again on tool-result turns. The reconstructed prompt follows the ordinary Engine path, so resident
prefix reuse applies naturally.
A stored Response also retains its resolved preserve_thinking value. A child which omits the
field inherits the parent value. An explicit different value creates a new semantic branch; prompt
rendering and identity still determine reuse. Changing the boolean alone never invalidates an exact
current frontier or a complete matching rewrite checkpoint.
Resource behavior:
| Endpoint | Contract |
|---|---|
GET /v1/responses/{id} |
returns the stored terminal object, or 404 response_not_found |
DELETE /v1/responses/{id} |
removes public retrieval and returns response.deleted; descendant contexts already retained by other Responses remain usable |
GET /v1/responses/{id}/input_items |
returns normalized Items supplied to that request; supports after, limit 1..100 (default 20), and order `asc |
POST /v1/responses/{id}/cancel |
explicitly fails because background execution is unsupported |
POST /v1/responses/compact |
explicitly fails with compaction_not_supported |
store:false Responses cannot be retrieved or used as previous_response_id. LRU eviction and
explicit deletion also make an ID unavailable. A single Response larger than the configured store
capacity fails with response_store_capacity_exceeded rather than silently pretending it was
stored.
POST /v1/responses/input_tokens accepts exactly model and input, performs the same typed Item,
template, and media expansion, and does not run generation:
curl http://127.0.0.1:8080/v1/responses/input_tokens \
-H 'Content-Type: application/json' \
-d '{"model":"qwen3.6-27b","input":"Count this prompt."}'{"object":"response.input_tokens","input_tokens":11}Unsupported Create fields include Conversations, prompt templates, context management, hosted
moderation, prompt-cache controls, safety/user identifiers, Structured Outputs/JSON mode,
non-empty include, background execution, compaction, files/audio, and OpenAI-hosted/MCP/custom
tools. These are compatibility boundaries, not silently accepted placeholders.
curl http://127.0.0.1:8080/v1/messages \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3.6-27b",
"max_tokens": 128,
"messages": [
{"role": "user", "content": "Explain prefix reuse in one sentence."}
]
}'The endpoint supports top-level system text, ordered mid-conversation system messages,
user/assistant history, text and image blocks, thinking blocks, tool-use history, tool results,
client-defined tools, non-streaming responses, and Anthropic SSE events.
tool_result blocks remain in request order, including nested text and image blocks; their order
does not need to match the preceding assistant tool_use blocks. Literal Qwen Vision control-token
spellings in text, reasoning, tool definitions, or tool arguments remain text and do not create
media placeholders.
Mid-conversation system messages remain at their messages array position and are not merged into
the top-level system instruction. A system section must follow a user/tool-result message and be
final or immediately precede an assistant message; it cannot interrupt a tool-use/tool-result pair.
Consecutive system messages remain separate ordered turns.
thinking.type: "disabled" disables thinking; other supported values enable it.
The independent top-level preserve_thinking boolean controls closed-turn history and otherwise
uses the server default.
Anthropic output_config.effort accepts the protocol values low, medium, high, xhigh, and
max. The value is then checked against the loaded chat template in the same way as the OpenAI
endpoints; the registered effort-capable template exposes low, medium, and xhigh. Combining
an effort with thinking.type: "disabled" is rejected as contradictory.
Anthropic's model field is treated as a response label and does not select the loaded artifact.
POST /v1/messages/count_tokens uses the artifact's tokenizer, chat template, and media expansion
without running GPU generation:
curl http://127.0.0.1:8080/v1/messages/count_tokens \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3.6-27b",
"messages": [{"role": "user", "content": "Count this prompt."}]
}'Pass --api-key VALUE to require the same value as an OpenAI bearer token or Anthropic
x-api-key header. GET /health and CORS preflight requests remain unauthenticated.
curl http://127.0.0.1:8080/v1/models \
-H 'Authorization: Bearer local-secret'--cors adds permissive browser CORS headers. It is disabled by default.
| Option | Meaning | Default |
|---|---|---|
--host H |
listen address | 127.0.0.1 |
--port N |
listen port | 8080 |
--api-key KEY |
required bearer or x-api-key value |
unset |
--model-id ID |
override the public OpenAI model alias | artifact identity.model_id |
--max-context N |
logical context ceiling of each sequence | 8192 |
--kv-capacity N|auto |
explicit shared Main Text KV capacity, or maximize it from remaining GPU memory; omitted means --max-context |
8192 |
--max-concurrency N |
maximum admitted requests; valid range 1..8 |
1 |
--max-pending-requests N |
additional requests allowed to wait for admission | 16 |
--pending-timeout-ms N |
maximum preparation-plus-admission wait | 30000 |
--prefill-chunk N |
text-prefill chunk | 1024 |
--log-stats-interval-ms N |
aggregate throughput report interval; 0 disables it |
5000 |
--device N |
CUDA device index | 0 |
--max-request-mib N |
body-size limit before JSON parsing | 384 |
--media-cache-mib N |
LRU-retained prepared BF16 media payloads; 0 disables retention |
1024 |
--media-live-mib N |
all live prepared BF16 media payloads | 2048 |
--media-preprocess-threads N |
bounded media preprocessing workers; 0 selects at most 16 from host concurrency |
0 |
--image-token-budget N |
per-image serving ceiling in Vision tokens; 0 keeps the artifact ceiling |
0 |
--request-log-jsonl FILE |
append full-precision server/request records | disabled |
--response-store-max-records N |
maximum locally retained Responses objects | 1024 |
--response-store-max-mib N |
total local Response envelope/Item/context budget | 256 |
--kv-dtype bf16|int8 |
KV-cache storage | bf16 |
--host-kv-cache-mib N |
park evicted sequences in a pinned host-RAM budget of N MiB instead of discarding them; each parked sequence takes only its real size, and a session larger than the budget falls back to re-prefill; not supported with --spec dflash or --no-prefix-reuse (startup error) |
0 (off) |
--spec mtp|dflash |
speculative backend | off |
--draft-tokens N |
MTP 1..5; DFlash 1..15 |
unset |
--lm-head-draft |
optimized proposal head | off |
--default-max-tokens N |
output limit when omitted by a request | 8192 |
--vision |
enable media input and load Vision GPU allocations | off |
--no-cuda-graph |
disable CUDA Graph decode | graphs on |
--kv-host-cache-mib N |
pinned-host content cache for computed context: previously computed prefixes restore through PCIe instead of re-prefilling, and branches sharing a prefix deduplicate against the same stored pages; conflicts with --no-prefix-reuse and --spec dflash; the budget shares process pinned host RAM with the vision pinned block and overlay mirrors, carved pinned chunks are retained until shutdown (RSS follows the high-water mark), and a failed pinned allocation degrades to skipping the save, never to an error |
0 (off) |
--no-prefix-reuse |
disable compatible-prefix caching | prefix reuse on |
--no-thinking |
disable thinking by default | thinking on |
--preserve-thinking |
preserve closed-turn assistant reasoning by default | off |
--cors |
permissive browser CORS headers | off |
--temperature F |
process-level temperature override | unset |
--top-p F |
process-level top-p override | unset |
--top-k N |
process-level top-k override | unset |
--min-p F |
process-level min-p override | unset |
--presence-penalty F |
process-level presence-penalty override | unset |
--frequency-penalty F |
process-level frequency-penalty override | unset |
--seed N |
fixed seed when a request omits one | fresh random seed per request |
--greedy |
force exact argmax for all requests | off |
Engine selects sampling defaults from the loaded model and the request's resolved thinking mode.
Qwen3.6-27B and Qwen3.8-27B use 1.0/0.95/20/0/0 for
temperature/top-p/top-k/min-p/presence penalty in thinking mode and 0.7/0.80/20/0/1.5 in
non-thinking mode. Qwen3.6-35B-A3B differs only in its thinking presence penalty, which is 1.5.
Frequency penalty is 0 for all registered presets. Process flags override registered values,
request fields override process flags, and --greedy finally forces temperature 0.
Run ./build/apps/ninfer-serve --help for the exact option contract.
--request-log-jsonl FILE enables the machine-readable measurement log. The server opens FILE
in append mode and flushes every event, so successive model or MTP blocks may share one campaign
file. The parent directory must already exist. Failure to open the file aborts startup; the log path
is also rejected if it resolves to the model artifact.
./build/apps/ninfer-serve models/qwen3_6_27b.ninfer \
--request-log-jsonl profiles/bench/run/server.requests.jsonlEvery line is one ninfer_serve_request_log schema-v10 JSON object. All events carry
timestamp_unix_ms and a process-unique server_instance_id; request IDs are monotonic only within
that server instance. Successful request-start records include request-scoped acquisition,
media-preprocessing wall/work, tokenizer, cache hit/miss/single-flight, and payload-size fields;
they do not infer request behavior from process-global counter deltas.
| Event | Contents |
|---|---|
server_start |
target/weights identity and artifact, resolved Engine, registered thinking/non-thinking sampler defaults plus process overrides, thinking-history defaults, weights/sequence/workspace/request-transient arenas, KV sizing ledger, CUDA Graph observed/allowance bytes, CUDA/GPU environment, and redacted argv |
request_start |
protocol, resolved sampler and seed, thinking modes, Responses semantic-change flag, output budget, stream/message/tool shape |
request_rejected |
parsed request shape, media-item count, phase: "prepare", and the exact HTTP status/type/code/parameter/message for a synchronous preparation rejection |
request_done |
finish reason, prompt/completion/cache/computed-prefill tokens, prefix reuse path, unrounded phase seconds, and complete speculative-decoding counters |
request_error |
the resolved request configuration and generation error message |
throughput |
interval token deltas and rates, scheduler occupancy, and decode-round batch statistics |
request_done.timings_seconds contains prepare, ttft, vision, prefill, decode, and total
as full-precision JSON numbers. Its speculative object contains backend, draft_window, rounds,
drafted_tokens, accepted_tokens, fallback_steps, and accepted_per_position. Rates can be
derived downstream from raw token counts and seconds instead of rounded stderr strings.
The JSONL file contains no generated response text and never records an API-key value; argv
replaces that value with <redacted>. The existing stderr summaries remain available for operators
but are rounded and are not the aggregation source. Console lines use local
[YYYY-MM-DD HH:MM:SS.mmm] [level] timestamps. OpenAI Responses, OpenAI Chat, and Anthropic
generation requests receive a request ID when they enter synchronous preparation. Successful
preparation produces request_start; a preparation failure produces request_rejected without a
matching start. Later generation failures produce request_error. Schema/model validation
rejections before preparation and token-count-only calls are not measurement requests and do not
receive request IDs.
By default the server also reports aggregate activity every five seconds. prefill counts prompt
suffix tokens actually computed during the interval, excluding prefix-cache hits; decode counts
tokens finally committed by decode rounds, excluding the first token produced by prefill. For MTP
and DFlash this is the accepted committed output, not draft or rejected tokens.
avg_decode_batch is decode row-rounds divided by decode rounds during the same interval. The
running, prefilling, decode_ready, and waiting fields are the Engine scheduler snapshot at
the end of the interval. Fully idle zero intervals are omitted. The JSONL throughput event keeps
the raw token and round deltas as well as derived rates; downstream measurement should prefer those
raw values.
The server owns one resident Engine with a startup-fixed capacity of 1..8 active generation
requests. At each decode boundary, every decode-ready request is compacted into one batch and
processed by one model traversal and, when graphs are enabled, one exact-batch CUDA Graph replay. A
request joins that batch only after its single-request prefill finishes; when it completes or is
cancelled, the next boundary rebuilds the batch without an empty row.
--max-pending-requests bounds the requests waiting behind the active set. The total generation
request lifetime capacity is max_concurrency + max_pending_requests, including requests still in
CPU/media preparation and completed model results whose response has not yet been released. A full
capacity returns HTTP 429 with code server_overloaded. The absolute
--pending-timeout-ms deadline starts before preparation, covers media acquisition and Engine FIFO
waiting, and returns HTTP 503 with code request_queue_timeout if admission does not occur in time.
There is no admission ETA or unbounded overflow queue.
Input memory is bounded by the outstanding-request count and the per-request
--max-request-mib limit. Media requests additionally share one preparation permit, so a waiting
media request retains the same cancellation and timeout deadline. Model output is bounded by the
same finite request count and each request's effective output-token limit; output callbacks and
network serialization run outside the GPU executor and do not delay formation of the next batch.
--max-context and the resolved --kv-capacity are independent limits. The former is each
sequence's logical ceiling; the latter sizes the shared Main Text KV pool used by all active
requests and retained prefixes. Both are represented with 64-token pages internally, while a
sequence can never cross the exact --max-context frontier. --kv-capacity N requests an explicit
capacity; --kv-capacity auto chooses the largest legal capacity that fits the memory remaining
after weights are loaded while keeping 1 GiB of sizing headroom. When omitted it follows
--max-context, preserving one full-length request's capacity. The shared pool is fixed at startup
and is not divided evenly among request lanes.
Automatic sizing evaluates the complete target runtime layout for the chosen concurrency, KV dtype, speculative backend, draft window, Vision setting, workspace, and CUDA Graph allowance. It uses a direct page-capacity calculation rather than allocation probing. Startup reports the policy, resolved capacity, runtime reservation, free memory after weights, automatic headroom, planned slack, actual free memory after complete startup, and observed Graph memory. An explicit capacity is never silently reduced, and neither policy permits request-time pool growth.
Admission reserves the full prompt-plus-effective-output page entitlement, so an admitted request can finish within its declared bound. A later request waits in FIFO order when the remaining shared pages cannot satisfy its complete entitlement; the Engine never admits it and later truncates an older request to recover capacity. Startup rejects a KV pool smaller than one sequence, too small to provide one page per configured lane, or larger than all configured lanes could use.
Compatible resident prefixes are reused for both text and multimodal histories unless the server is
started with --no-prefix-reuse. A multimodal hit requires matching token types, three-axis MRoPE
positions, encoded-media digest, grid, and consumer spans; changing an earlier image or video
therefore resets the prefix instead of reusing placeholder-token KV. Media wholly inside a matched
prefix skips Vision execution, while new suffix media is encoded normally. The completion log
reports the reused token count as cache=.
The shared family runtime distinguishes full_reset, append_frontier,
restore_turn_checkpoint, and restore_response_checkpoint. Both checkpoint kinds include the
recurrent, hidden, and selected speculative-backend continuation state required to recompute a
rewritten suffix; matching KV tokens alone never authorize a partial hit. With stable
preserve_thinking=true, the auxiliary checkpoint rolls to the current response's deterministic
generation prologue. For thinking generation this is the <think> opener, excluding the trailing
newline: that newline is not a BPE-stable prefix of a later closed empty think block
(<think>\n\n</think>), which is how a tool-call turn is reconstructed when the client omits
reasoning_content. For non-thinking generation the checkpoint is the complete empty thinking
block at the prompt frontier. A normalized response which no longer matches the raw generated
tokens then replays only that response and its suffix. Stable false keeps the first assistant
opener in the open turn so a newly closed turn can be recomputed without its reasoning.
preserve_thinking selects where the next checkpoint should live; it is not a cache-compatibility
bit. An exact current frontier or matching complete checkpoint remains reusable across a mode
change. If the newly desired boundary is already behind the selected reuse frontier and no snapshot
exists there, the Engine keeps the valid hit and defers installing that new checkpoint rather than
forcing an eager full reset. A later request that diverges before every retained checkpoint then
resets normally. The JSONL completion record exposes the checkpoint actually restored as
prefix_reuse_path. Changing reasoning effort changes rendered tokens and therefore does not reuse
a prefix whose effort instruction differs.
An appended mid-conversation system message is an ordinary prompt suffix, so an unchanged prior
history remains eligible for append_frontier. If the client modifies, removes, or moves a
historical system message, the token prefix genuinely differs and a miss/reset is correct.
Greedy decoding is reproducible for an identical execution schedule: the same prompt served on an
otherwise idle server produces the same tokens on every run. It is not bitwise-reproducible across
different batch compositions. Decode-phase projection kernels are selected by the number of tokens
in the step (the sum over active lanes of one plus the accepted speculative drafts), and the
variants differ in reduction order; the resulting sub-ulp logit differences flip the argmax only on
near-ties, but over a long greedy continuation such flips accumulate into divergent text. A request
therefore yields distribution-equivalent, not byte-equivalent, output when other requests share its
decode steps, with the cache on or off. Short answers are stable in practice; treat long greedy
continuations under concurrent load as equivalent rather than identical, and compare them against a
solo run of the same prompt when bitwise reproduction matters (tools/smoke/determinism_fingerprint.py).
An appended mid-conversation system message is an ordinary prompt suffix, so an unchanged prior
history remains eligible for append_frontier. If the client modifies, removes, or moves a
historical system message, the token prefix genuinely differs and a miss/reset is correct.
Speculative decoding is an engine option and does not change protocol output shapes, stop behavior,
or usage accounting. If a stop truncates a multi-token MTP or DFlash round, the Engine commits the
exact accepted target prefix so a following compatible turn can still reuse it. Output-limit and
context-capacity finishes map to length/ max_tokens; ordinary model or string stops map to
stop/ end_turn.
Function tools are rendered into the model prompt and generated calls are parsed into protocol
responses. NInfer does not execute tools and does not validate tool arguments against the full
client JSON Schema through constrained decoding; that remains the client's responsibility. When
parsing a generated call, NInfer does consult the top-level parameter "type" declared in each
tool's schema to decide whether a parameter value that is valid JSON may be deserialized into the
corresponding JSON type (number, boolean, array, object, null): only parameters whose declared
type(s) are all valid non-string JSON Schema types are deserialized. Parameters typed as "string"
(or declared via a type array that includes "string"), parameters with an unknown or misspelled
"type", and parameters absent from the schema preserve the model's raw text so the string
contract reaches the client intact. Full JSON Schema validation (constraints, required sets,
formats, nested keywords) is not performed server-side and remains the client's job.
Duplicate tool names within a single request are rejected with a 400 on all three
protocol surfaces (OpenAI Chat Completions, OpenAI Responses, Anthropic Messages),
keeping the per-tool parameter type map unambiguous.
Prompt-token usage includes chat-template and expanded media tokens. Generated-token usage comes from accepted output token IDs, including a stop token whose decoded text may be withheld.