Skip to main content

Plumbify

Give an open LLM a fast decision path, then serve it with vLLM.

CI PyPI License Python vLLM

Guide · Architecture · Recipes · Results · Changelog

Language models are slow and inconsistent at the small judgements that fill real conversations: which team gets this ticket, is this refund allowed, how urgent is it. Thinking helps, at the cost of seconds per decision.

Plumbify trains a small decision head, a plumb, onto an open model without changing its weights. With the plugin installed, vllm serve loads the result like any other model. The model generates as usual, and when a conversation reaches a decision, the plumb answers it in one forward pass, reading the KV cache the conversation already filled, with a calibrated probability for every option.

Faster, with thinking on.

A refund case: the normal model and the plumbed model side by side, thinking on

Qwen3.5-35B-A3B, one server, real time, thinking on in both panes. A support case asks for two decisions: approve the refund, and escalate to a manager. The normal model reasons both out in its thinking. The plumbed model hands them to its plumb, which reads the conversation from the KV cache (the escalation call takes 116 ms and reuses 1,056 cached tokens). Both plumb answers come back with two options in their conformal set, which marks them as unsure, so the model checks each one briefly before it replies. Both panes reach the same answer: approve, don't escalate. The plumbed model finishes in 10.5 s, the normal model in 32.6 s, 3.1× faster. Video (MP4)

More accurate, with thinking off.

An exam item: the normal model and the plumbed model side by side, thinking off

Qwen3.5-35B-A3B, thinking off in both panes. A grading item states a decision: what is the correct answer, given a student's working? The normal model agrees with the student's wrong answer, d ($278.40). The plumb picks b ($290.00), the correct answer, with probability 0.995 in 220 ms, and the model answers b. Both take under a second. Video (MP4)

A plumb is a Jev model built into the LLM. It answers the same kind of typed decision as a Jev-style decision model such as TypeSafe's Jev: pick one of these options, yes or no, or a score on a scale. The difference is that it is not a second model next to the LLM. It is a Jev decision head inside the model, sharing its weights and its KV cache, so plumbifying a model is jevifying it.

Quickstart

Plumbify needs Linux, an NVIDIA GPU and Python 3.10 or later. The vllm extra installs vLLM 0.30.0, the version the plugin is tested against.

pip install "plumbify[vllm]"

Train a plumb on a frozen base model, then serve the result:

plumbify train --base Qwen/Qwen3.5-9B \
    --rows train.jsonl --dev dev.jsonl --out plumbed-qwen3.5-9b
vllm serve plumbed-qwen3.5-9b

Ask it a decision through the standard OpenAI API:

curl -s localhost:8000/v1/chat/completions \
  -H 'content-type: application/json' \
  -d '{"model": "plumbed-qwen3.5-9b",
       "messages": [{"role": "user", "content":
         "Charged twice.\n\nWhich team?\n- billing\n- technical\n- sales"}]}' \
  | jq -c .plumb.answer

The response is a normal chat completion with an extra plumb field. Its answer says which option won, who decided (system1 is the plumb), and with what probability:

{"choice":"billing","by":"system1","p":0.973}

The guide covers the training data format, the request options and the response fields. python scripts/demo_chat.py opens a live chat that shows each decision as the model hands it off.

Results

The same model alone and with its plumb, on one vLLM server, over 447 held-out decisions (task families and templates no model trained on). Each prompt states a decision: context, question, bulleted options.

Model No thinking: alone → plumbed Thinking: alone → plumbed Thinking latency p50: alone → plumbed
Qwen3.5-27B 0.732 → 0.839 0.866 → 0.875 26.6 s → 17.1 s (1.6× faster)
Qwen3.5-35B-A3B (MoE) 0.720 → 0.810 0.852 → 0.875 6.8 s → 2.8 s (2.4× faster)
Gemma 4 12B 0.736 → 0.826 0.770 → 0.872 24.1 s → 5.0 s (4.8× faster)
Qwen3.5-9B 0.696 → 0.779 0.808 → 0.846 28.0 s → 5.7 s (4.9× faster)
Qwen3.5-4B 0.667 → 0.801 0.841 → 0.826 12.0 s → 3.8 s (3.1× faster)
Qwen3-1.7B 0.570 → 0.658 0.642 → 0.707 3.9 s → 3.1 s (1.2× faster)

Without thinking, the plumb adds 8 to 13 points on every model. With thinking, the plumbed model is at least as accurate on five of six models (Qwen3.5-4B is within noise) and answers 1.2 to 4.9 times faster, because it thinks 30 to 75% less: it hands the decision to the plumb instead of reasoning it all out. It also always answers, where the model alone runs out of tokens or gives no clear answer on 5 to 15% of decisions when thinking. With 447 decisions, one standard error is about 2 points.

With thinking on, the model alone is scored in its better setup. These models think much less when their prompt lists a tool (the plumbed model's prompt always lists plumb_decide), which is often but not always more accurate, so each model alone gets whichever prompt, with or without a tool, scores higher. Settings, training time and serving memory for each run are in recipes/; the benchmark is scripts/bench_s1_vs_s2.py.

How it works

Take a support chat that reaches a decision: which team should handle this ticket: billing, technical or sales?

  1. The question is added to the conversation. The question and the three options are appended, written like a normal message to the model.
  2. The model reads only the new part. It has already read the conversation, and that work is cached, so only the few words of the question are processed.
  3. The plumb scores the options. It looks at what the model computed while reading the question and turns that into a probability per option: billing 0.97, technical 0.02, sales 0.01.
  4. The model carries on. No text was generated for the decision. The model uses the answer and continues its reply.
conversation so far (cached)  +  "Which team? billing / technical / sales"  →  plumb  →  billing 0.97

Training adds two small pieces and nothing else: the plumb itself, and an adapter that switches on only while the model reads a decision question. The model's own weights never change, so everything else it does (chat, reasoning, tools) works exactly as before. Architecture explains each piece and where it lives in the code.

Documentation

Document Covers
Guide Install, training data, the plumbify commands, the serving API, limits
Architecture How training and serving fit together, the invariants, a map of the code
Recipes Tested settings and measured results for each model
Scripts Benchmark, vLLM parity check, live chat
Contributing Development setup, tests, conventions

Development

pip install -e ".[dev]" && pre-commit install
pytest -q tests
ruff check . && ruff format --check .

The tests run on CPU in seconds with a tiny random model and don't need vLLM. See CONTRIBUTING.md.

Citation

@misc{plumbify2026,
  title  = {Plumbify: a System 1 decision branch for open
            language models},
  author = {Kamalakannan, Naveenraj},
  year   = {2026},
  url    = {https://github.com/therealnaveenkamal/plumbify}
}

License

Apache-2.0.

Metadata

Release files for plumbify 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for plumbify 0.2.0
File Size Uploaded Provenance
plumbify-0.2.0.tar.gz 77.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for plumbify 0.2.0
File Interpreter ABI Platform
plumbify-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 153.7 kB

Release files / plumbify-0.2.0.tar.gz

Download URL plumbify-0.2.0.tar.gz
Size 77.2 kB
Tags Source
SHA-256 checksum
How to use checksums
7dbc1c493ca66500573d1fd1740f44386a11c8371c21c4c9a94bf40807be14f1
BLAKE2b-256 checksum
How to use checksums
dc34948f3beb27306f49112dbe15e46fd8126a494d0d6f683986c2f2577524ce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / plumbify-0.2.0-py3-none-any.whl

Download URL plumbify-0.2.0-py3-none-any.whl
Size 76.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0985055c6826269fe559aa2fc4f8c512c770a7ca8eae86676537f17a7c563378
BLAKE2b-256 checksum
How to use checksums
fdae12c2c3d9f1173f87ddb6a80dc8ef0c6a1d76b90cf66b29a4c235d7e0ea27
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for plumbify 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page