Conversation
The proxies now measure each inference response as it streams to the client and attach an additive `stats` object to the workload's terminal event: prompt/completion tokens, decode throughput and time to first token. Token counts come from the engine's own usage report (OpenAI `usage`, Ollama `eval_count`/`eval_duration`); a stream without one is counted from its chunks and flagged `estimated`. Overhead is a timestamp, a byte count and a copy into a fixed 4 KiB tail per body write; the body is never buffered or JSON-decoded. Only the trailing bytes are inspected, since every engine puts its usage report last. Error bodies and compressed bodies are never mined for tokens, and the stream content is discarded at the terminal event. - services/shared/inferstats: the tap and extraction, with unit tests - ollama-proxy, lmstudio-proxy: arm the tap at the commit point, attach stats in emitTerminal; proxy-level tests for SSE with and without usage, Ollama NDJSON, and error bodies - desktop: WorkloadStats type, Electron parsing, a stats row on the job card with a formatter and unit test - nvpair-tui: TOK/S column - READMEs and versions.json (ollama-proxy 0.27.0, lmstudio-proxy 0.17.0, nvpair-tui 0.8.0, product 0.92.0) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Can GULDOGAN <cguldogan@gmail.com>
Collaborator
|
Thanks @cguldogan! This is an awesome PR. I'd love to get something like this in PAIR to look at inference stats. @ckelseynv, check this one out. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The Jobs list says what ran, where and when, but nothing about how it performed. Anyone comparing two nodes, two engines or two quantizations of a model has to reach for a separate benchmark tool to learn the one number they care about: tokens per second.
This makes each finished job carry its own statistics, measured by the proxy that forwarded it, and shows them on the job card:
33.5 tok/s · 60 tokens · 1.8 s to first token); the terminal interface gains aTOK/Scolumn.usageobject (LM Studio, vLLM, SGLang, Ollama's OpenAI route) or Ollama's nativeeval_count, whoseeval_durationis also used as the decode time since it is more precise than a wall clock. A stream that carries no usage report (a client that did not ask forstream_options.include_usage) is counted from its chunks and shown with a~prefix, and the wire object saysestimated: true.The point of the design is that it costs nothing noticeable on the inference path. The proxies already wrap the client-side response writer to enforce the idle write deadline; the new tap adds, per body write, one timestamp, one byte count and a copy into a fixed 4 KiB rolling tail. The body is never buffered and never JSON-decoded. Every engine emits its usage report at the end of the response, so the tail is all that needs inspecting once the stream ends, and it is searched with a byte scan for the last occurrence of the relevant key. The tail is discarded at the terminal event; prompts and generated text are still never retained or logged.
Screenshot of the Jobs list after the change (the top card carries the new line; the cards below it finished before the change and have none):
Related: #42 asks for prompt tokens, completion tokens and tokens per second per request, alongside the node that served it. This delivers those figures on the job record itself (the node was already there as
scheduledOn); it does not add the prompt and response logging that issue also asks for, which the workload contract deliberately excludes.Scope
Included: a shared
nvpair-shared/inferstatspackage (the tap and the extraction) used by both proxies; an additive,omitemptystatsobject on the workload's terminalworkload:completed/workload:erroredevent; parsing and display in the desktop (WorkloadStatstype, Electron bridge, job card) and a column innvpair-tui; README updates for the workload shape and both proxies; tests at every layer.Excluded, deliberately: live throughput for a job that is still running (would need periodic upserts), any change to the request the client sent (the proxy does not inject
stream_options.include_usage, so a client's stream is byte-for-byte what the engine produced), per-node or per-model aggregates, and token counts for compressed bodies (a client that asked for gzip and an engine that obliged gets timing only). Embeddings show prompt tokens only, since nothing is generated.Validation
Environment: macOS 15 arm64, Go 1.27, Node 23; one DGX Spark (GB10) node running SGLang as the remote engine under test.
Results: vet clean and gofmt clean in every touched file (
shared/splitlisten/splitlisten_test.gois flagged by gofmt on the unmodified base too and is not touched here).shared,lmstudio-proxyandnvpair-tuifully green, including the newinferstatsunit tests (OpenAI non-streaming, SSE with and without usage, Ollama NDJSON, a body longer than the tail, generated text that mentions a usage key, compressed body, TTFT) and the proxy-level tests in both proxies (SSE with usage, SSE without usage, Ollama/api/chat, error body not measured). The one Go failure isollama-proxy TestAliasSelfTargetMatchesBoundLoopbackAddressNotPortAlone, which binds127.0.0.2and fails identically on the unmodified base on macOS; unrelated. Desktop: typecheck clean; lint 0 errors (one pre-existing prettier warning innode-info-poller.ts, untouched); 212 unit tests in 38 files pass, 4 of them new; service contracts and dead-code checks pass; the commit is signed off.End to end, from a macOS desktop node through its OpenAI-compatible proxy to a remote SGLang serving a Qwen3-8B NVFP4 model, one non-streaming chat completion:
"usage":{"prompt_tokens":71,"completion_tokens":60,...}.workloads:upsertwith"stats":{"promptTokens":71,"completionTokens":60,"tokensPerSecond":33.5,"ttftMs":1793}— counts identical to the engine's, noestimatedflag.33.5 tok/s · 60 tokens · 1.8 s to first token(screenshot above).workload:startedcarried nostats, and jobs that finished before the change render exactly as before.Risk
statsis one additive, optional field onworkloadInfo. Every hop already tolerates it: the broker stamps the origin through a generic JSON map, the workload-manager forwardsparamsas raw JSON, the broker store keeps the rawworkloadInfo, and the scheduler and TUI decode only the fields they use. An older peer simply drops it; a newer peer receiving from an older one sees nostatsand shows no line."completion_tokens":(or"eval_count":), so generated text that happens to contain the same key cannot shadow the engine's report, which always comes later; a test covers it. Keys are matched with their quotes and colon socompletion_tokens_detailsandprompt_eval_countnever match. Any residual mis-parse affects a display figure only.ollama-proxy0.27.0,lmstudio-proxy0.17.0 andnvpair-tui0.8.0 (MINOR, additive IPC), product 0.92.0. No packaging, migration or data changes.Checklist
git commit -s), certifying the Developer Certificate of Origin.services/versions.json, and described user-visible changes above so they reach the release notes.🤖 Generated with Claude Code