Decision 3.0 (d3) External Policy
Metrum AI Router can use vLLM Semantic Router
Decision 3.0 ("d3") models as an advisory routing signal. A d3 model does not
generate text. It reads a state (here, the redacted request text) and a set of
typed questions, and returns a probability for every option. The Metrum AI
reference adapter, examples/external-routing-policy/d3_policy.py, turns those
answers into a target choice for a strategy: external model group.
The router keeps ownership of caller authorization, eligibility filtering,
fallback, usage records and decision telemetry. The adapter can only pick among
the eligible targets the router sends, as for any
External Routing Policy Service.
When to use it
- You want a learned difficulty or task-type signal without training your own router, and you can run a small GPU model inside your trust boundary.
- You want to measure d3 recommendations in
shadowagainst your current routing before changing what is served.
For outcome-trained routing on your own traffic, see Learned Routing Policy. For the full comparison, see the Routing Strategy Decision Tree.
How the adapter decides
For each request, the adapter:
- Takes the router's redacted
textfield. It needsexternal_policy.include_request: true. The text is left-truncated totext.max_chars(default 32,768) so the most recent turns are kept. - Asks d3 the configured questions in one
POST /v1/systemonecall. The defaults are:difficulty(score: trivial, moderate, hard, expert)reasoning(noul: does this need multi-step reasoning?)kind(choice: code, math, chat, agentic)
- Applies the first matching rule. By default,
hardorexpertdifficulty, or a reasoning probability of 0.6 or more, preferstier: strong. Everything else preferstier: cheap. The first eligible target that matches becomestargetIndex, and the other eligible targets follow infallbackIndexes. If no eligible target matches a rule (for example the group has nostrongtier), the next matching rule applies. - Returns a
classLabelsuch asd3:hard:code:r0.71. Labels are built only from operator-configured option keys and a probability, never from request content, and are at most 64 characters.
Without request text (include_request: false, or an image-only request), the
adapter does not call d3. It applies no_content_rules to the scalar
context (by default estimatedTokens >= 8000 prefers strong) and returns
classLabel: d3:no-request-content.
If d3 is unreachable, times out, returns non-JSON or malformed answers, rejects
the input (HTTP 422, for example over the model's context limit), or rejects
the bearer key, the adapter returns HTTP 502. The router then applies
on_error: fail_closed returns routing-policy-error without calling an
upstream, and fallback serves the normal target order. The adapter never
guesses a target.
Score answers use the most likely level for in conditions, with ties going to
the higher level, and the expected level index for numeric conditions. noul
answers compare the probability of "yes".
Configure the adapter
The built-in defaults equal
examples/external-routing-policy/d3_policy.example.yaml (and the stdlib-only
d3_policy.example.json). Edit a copy to change questions or rules:
d3:
url: http://127.0.0.1:8000
model: vllm-sr/d3-lite
revision: a8d25a91ef75552803f7009d25b7bd435991abd9
timeout_ms: 250 # includes waiting for a concurrency slot
api_key_env: D3_API_KEY # env var holding DECISION_API_KEY's value
max_concurrency: 1 # d3_server.py serves one request at a time
text:
max_chars: 32768
label: [difficulty, kind, reasoning]
rules:
- name: strong-hard-or-reasoning
when:
any:
- {question: difficulty, in: [hard, expert]}
- {question: reasoning, gte: 0.6}
prefer: [{tier: strong}]
- name: cheap-default
prefer: [{tier: cheap}]
no_content_rules:
- name: strong-large-context
when: {context: estimatedTokens, gte: 8000}
prefer: [{tier: strong}]
- name: cheap-default
prefer: [{tier: cheap}]
Conditions can test a question (in, not_in, eq, gte, lte, gt,
lt) or a scalar policy-request context field, and can be combined with
any, all and not. Selectors in prefer match target tier, provider,
model or modelRef. The adapter validates the config at startup and exits if
a rule names an unknown question or option.
Run it next to the router:
D3_API_KEY=... python3 examples/external-routing-policy/d3_policy.py \
--port 18094 --url http://127.0.0.1:8000 \
--model vllm-sr/d3-lite --revision a8d25a91ef75552803f7009d25b7bd435991abd9
# YAML config: uv run --with pyyaml python examples/external-routing-policy/d3_policy.py --config my-d3.yaml
Command-line flags (--url, --model, --revision, --timeout-ms,
--api-key-env, --max-concurrency, --max-chars) override the config file.
GET /health reports the model, revision and counters. The adapter logs one
scalar JSON line per decision (outcome, rule, target index, label, d3
milliseconds, text length). It never logs request text.
Configure the router group
The reference config includes a d3-policy-demo group; see
External Routing Policy Service
for the canonical snippet. A typical group:
models:
d3-auto:
strategy: external
external_policy:
url: http://127.0.0.1:18094/route
allow_hosts: [127.0.0.1]
include_request: true
timeout_ms: 300
mode: shadow
on_error: fail_closed
targets:
- { provider: fireworks, model_ref: deepseek-v4p1-flash, tier: cheap }
- { provider: openai, model_ref: gpt-6-luna, tier: strong }
Tier names describe capability, not price. A cost saving needs a strong tier
that really costs more per token than the cheap one; in the pairing above,
gpt-6-luna has the lower list price. For a local instance,
scripts/local_pi_instance.py --d3-policy-url ... --d3-strong-model gpt-6.1-sol
selects a pricier strong tier and writes its list prices into the provider
catalog, so usage rows carry real costs. --d3-cheap-model does the same for
the cheap tier, and --d3-tier-groups adds single-target d3-auto-cheap and
d3-auto-strong groups for always-cheap and always-strong cost baselines
(issue #287).
Keep the router's timeout_ms above the adapter's d3 timeout_ms, and
validation caps it at 5,000 ms. Aim for 300 ms or less, because every policy
call, including in shadow, runs before the upstream call.
Serve d3
Each d3 model package ships d3_server.py. On an NVIDIA GPU host:
python3.12 -m venv d3env && . d3env/bin/activate
pip install -r requirements.txt fastapi uvicorn # from the pinned model revision
DECISION_API_KEY="$(openssl rand -hex 24)" \
python d3_server.py --model vllm-sr/d3-lite \
--revision a8d25a91ef75552803f7009d25b7bd435991abd9 \
--device cuda:0 --host 127.0.0.1 --port 8000
Do not expose d3 publicly. Reach it from the router host over loopback or an
SSH tunnel, for example
ssh -N -L 127.0.0.1:18300:127.0.0.1:8000 <gpu-host>, and point the adapter
at http://127.0.0.1:18300. The router's allow_hosts then stays
127.0.0.1.
d3_server.py serves one request at a time. Keep max_concurrency: 1 with it.
For concurrent traffic, use the batching vllm-sr serve ... --engine runtime
(same /v1/systemone API) or several workers, and raise max_concurrency to
match. Pin the model revision and check the package's MODEL_MANIFEST.json:
the family was first published on 2026-10-11.
Privacy and trust boundary
include_request: truesends request text to the adapter and on to d3. When the group haspii_filter, the text is redacted first. Without it, raw prompt text leaves the router.- Run the adapter and d3 inside the same trust boundary as the router. Never send this traffic to a third party.
- The adapter's responses, labels and logs carry only scalar values. The
router's policy telemetry stores the outcome, duration and
classLabel, not the policy request.
Tests
Credential-free checks, part of make test:
make d3-policy-test # adapter unit tests against a scripted fake d3
make d3-policy-e2e # router binary + adapter + fake d3 + mock upstreams
The e2e covers enforce (easy prompt to cheap, hard prompt to strong, also over
streaming), shadow (target 0 served, the d3 pick recorded as
shadow_recommended_candidate), d3 down with fail_closed (502
routing-policy-error, no upstream call) and with fallback (target 0), a d3
422, include_request: false (d3:no-request-content, d3 not called), and a
scan that finds no prompt text or credentials in the usage database or logs.
The fake d3 (examples/external-routing-policy/fake_d3.py) is a keyword
heuristic that matches the d3 wire format. It is wiring evidence, not model
quality evidence.
Live GPU measurements (d3 latency, shadow agreement, enforce tier split and
cost, coding-agent continuation and a failure drill) are recorded under
docs/evidence/d3/ when they are run.
Copyright 2026 Metrum AI. Decision 3.0 and vLLM Semantic Router are projects of their respective authors; Metrum AI Router integrates them through the public external-policy contract.