Skip to main content

A deterministic prompt-complexity router: score a prompt's structure and recommend a local or cloud model — offline, reproducible, no model call.

Project description

Wayfinder — choose your path to your answers. Deterministic. Calibrated. No RAG, no guessing.

A deterministic prompt-complexity router. Hand it a prompt, get back a reproducible structural complexity score and a recommendation:

route this prompt to your local model, or to the cloud model?

It is a standalone tool. It calls no model, needs no API key, makes no network request, and has zero dependency on RAC — it is pure text scanning plus a threshold. The recommendation is a fact you act on; Wayfinder stops there, and the caller runs inference.

Quickstart (gateway)

Put Wayfinder in front of your models — your app keeps using the OpenAI API, you just change base_url. Pilot-facing one-pager: EXPLAINER.md.

  1. Describe your two models in wayfinder-router.toml:

    [routing]
    threshold = 0.5            # below -> local, at/above -> cloud
    
    [gateway.models.local]
    base_url = "http://localhost:11434/v1"
    model = "llama3.2"
    
    [gateway.models.cloud]
    base_url = "https://api.openai.com/v1"
    model = "gpt-4o"
    api_key_env = "OPENAI_API_KEY"   # key read from this env var, never stored
    
  2. Run the gateway:

    pip install -e ".[gateway]"
    export OPENAI_API_KEY=sk-...
    wayfinder-router serve --port 8088
    
  3. Point your existing client at it — no code change:

    client = openai.OpenAI(base_url="http://localhost:8088/v1", api_key="unused")
    client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "..."}])
    

Easy prompts go to local, hard ones to cloud; each response carries x-wayfinder-router-model and x-wayfinder-router-score so you can see the routing.

Check it's working (the headers show where each request went):

curl -s localhost:8088/healthz                         # {"status":"ok","models":["cloud","local"]}
curl -s -D - -o /dev/null http://localhost:8088/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"auto","messages":[{"role":"user","content":"hi"}]}' \
  | grep -i x-wayfinder-router
# x-wayfinder-router-model: local
# x-wayfinder-router-score: 0.00

Why deterministic

The obvious way to route by complexity is to ask a model how complex the prompt is — an LLM-as-judge router. That is non-deterministic, costs a model call to decide whether to make a model call, and cannot be reproduced or tested. Wayfinder takes the opposite stance: it scores structure — length, headings, instruction steps, links, code blocks, tables — combines the signals into a bounded 0.0–1.0 score, and compares that to a threshold you control. Same prompt and same threshold always give the same answer.

The score is a structural proxy, not a verdict on difficulty: whether it tracks "this prompt needs the cloud model" is your calibration, which is exactly why the threshold is yours to set.

Run it (offline, no install)

cd wayfinder-router
echo "Summarise this paragraph in one sentence." | python -m wayfinder_router.cli route -
make route PROMPT=path/to/prompt.md
Recommended Model: local
Complexity Score: 0.00  (mode: tiered)

Tiers:
  >= 0.00  local <-
  >= 0.50  cloud

Contributing Features:
  Word Count: 6
  ...

JSON for machine consumers (an agent reads this and routes to its own model):

wayfinder-router route prompt.md --json
{
  "schema_version": "2",
  "score": 0.66,
  "recommendation": "cloud",
  "mode": "tiered",
  "features": { "word_count": 545, "heading_count": 12, "...": 0 },
  "tiers": [{ "min_score": 0.0, "model": "local" }, { "min_score": 0.5, "model": "cloud" }]
}

Install

pip install -e .              # the `wayfinder-router` command on PATH (zero dependencies)
pip install -e ".[gateway]"   # plus the OpenAI-compatible routing gateway
pip install -e ".[ui]"        # plus the local calibration/explain/configure UI
pip install -e ".[dev]"       # plus the test runner

Configure routing

Wayfinder reads its own config — never RAC's .rac/. Drop a wayfinder-router.toml anywhere at or above where you run it. Three modes, in precedence order (classifier > tiers > threshold); weights (the scalar-score weights) apply to any of them.

Binary (the default) — one cut:

[routing]
threshold = 0.6
weights = { word_count = 4.0, list_item_count = 2.5 }

--threshold N overrides it for one run; WAYFINDER_ROUTER_THRESHOLD overrides via the environment.

Tiered (WF-ADR-0002) — ordered score bands route to any number of models:

[[routing.tiers]]
min_score = 0.0
model = "llama-3b"
[[routing.tiers]]
min_score = 0.3
model = "llama-70b"
[[routing.tiers]]
min_score = 0.6
model = "claude-cloud"

Classifier (WF-ADR-0003) — a fitted multinomial-logistic model; argmax over per-model linear scores. Usually produced by calibrate, not hand-written.

Calibrate from data

The cut is a proxy; calibrate it against your traffic. wayfinder-router calibrate reads a labeled JSONL dataset ({"text": ..., "label": ...}) and emits a config fragment — offline, deterministic, and it never calls a model (labels come from your own oracle):

wayfinder-router calibrate data.jsonl --mode threshold              # sweep the binary cut
wayfinder-router calibrate data.jsonl --mode tiers                  # ordinal multi-model
wayfinder-router calibrate data.jsonl --mode classifier --out wayfinder-router.toml

The emitted fragment drops straight into wayfinder-router.toml; the summary (accuracy, chosen breakpoints) is printed to stderr. The classifier is fit by deterministic L2-regularized Newton/IRLS — pure Python, converging in a handful of iterations.

Route with your own key (gateway)

To actually route — score the prompt, then call the chosen model with your own key — run the OpenAI-compatible gateway (WF-ADR-0004). Your existing client points its base_url at Wayfinder; no application code changes.

# wayfinder-router.toml — map each routed model name to an upstream + a key env var.
[routing]
threshold = 0.6

[gateway.models.local]
base_url = "http://localhost:11434/v1"
model = "llama3.2"

[gateway.models.cloud]
base_url = "https://api.example.com/v1"
model = "big-model"
api_key_env = "EXAMPLE_API_KEY"   # the *name* of the env var; the secret is never in this file
pip install -e ".[gateway]"
export EXAMPLE_API_KEY=...     # read at request time, only inside the gateway
wayfinder-router serve --port 8088
import openai
client = openai.OpenAI(base_url="http://localhost:8088/v1", api_key="unused")
client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "..."}])
# Wayfinder scores the prompt, forwards to local or cloud, and returns the response.
# Response headers carry x-wayfinder-router-model and x-wayfinder-router-score.

The gateway is the only part that touches keys or the network; the scorer, config, and calibrator stay pure, offline, and deterministic. Keys are read from the environment at request time and never enter wayfinder-router.toml or the scored path.

Learn from feedback (onboarding)

Don't guess the cut — learn it from your own judgment of local vs hosted output (WF-ADR-0006). The loop is: collect judgments → calibrate → route automatically.

Bootstrap with A/B onboarding. For each sample prompt, wayfinder-router onboard runs both arms and asks which was good enough; the answer is a label:

wayfinder-router onboard prompts.jsonl --arms local,cloud --calibrate > wayfinder-router.toml

The A/B comparison and the prompt go to stderr; --calibrate prints the resulting config to stdout. Each judgment appends a {"text", "label"} line to a feedback log — which is the calibrate dataset, so the log turns straight into a config.

Keep it honest with steady-state feedback. Once routing automatically, record which model was actually good enough; the label feeds the next recalibration:

curl localhost:8088/v1/feedback -d '{"text": "...", "label": "cloud"}'

Recalibrate on a schedule (WF-ADR-0007). Re-fit the routing config from the log — run it from cron / a k8s CronJob, or click "Recalibrate & save" in the UI's Onboard tab. It rewrites only the [routing] section and preserves your [gateway] endpoints; a running gateway hot-reloads the new config with no restart:

wayfinder-router recalibrate                  # log → calibrate → write wayfinder-router.toml
wayfinder-router recalibrate --min-labels 50  # no-op until you have enough signal

The judging runs models, so it lives in the gateway/invocation layer (BYO key); the deterministic core is untouched and the label log carries no secrets.

Deploy & integrate (WF-ADR-0008)

Wayfinder doesn't only work from the CLI — the CLI, onboarding, and UI are the operator/bootstrap surfaces. In production, prompts flow through the gateway (transparent) or the library (in-process); routing happens where prompts already are, not by re-typing them.

Run the gateway as a service (sidecar or standalone):

docker build -t wayfinder-router . && docker run -p 8088:8088 -v "$PWD/data:/data" wayfinder-router
# or: docker compose up gateway   (see docker-compose.example.yml)

Point your existing client at it — no app code change. Anything that speaks the OpenAI API takes a base_url:

client = openai.OpenAI(base_url="http://localhost:8088/v1", api_key="unused")

The same base_url works for agent frameworks (LangChain/LlamaIndex), IDE assistants that allow a custom endpoint (Cursor, Continue), or a gateway like LiteLLM. Wayfinder scores each incoming prompt and forwards to the chosen model with your key.

Wire feedback from the host surface. Your app/IDE/chat decides how to show a 👍/👎 and posts the judgment; Wayfinder records it and the next recalibration learns from it:

fetch("http://localhost:8088/v1/feedback", {
  method: "POST",
  body: JSON.stringify({ text: prompt, label: wasGoodEnough ? "local" : "cloud" }),
});

Schedule recalibration with cron / a k8s CronJob (or docker compose run --rm recalibrate); the gateway hot-reloads the result. Keys always come from the environment (each model's api_key_env) — never the image or the config file.

Explain & tune

To see why a prompt routed where, ask for the per-feature breakdown — each feature's value, its normalized level, its weight, and its share of the score:

wayfinder-router route prompt.md --explain

For interactive tuning there's a local web UI (WF-ADR-0005) with three tabs:

  • Explain — paste a prompt; see the score, tier ladder, and contribution bars, and drag a threshold slider to watch routing change live.
  • Calibrate — paste a labeled JSONL dataset; run a mode; see accuracy, the threshold-sweep curve, and the resulting config fragment, then send it to Configure.
  • Configure — edit wayfinder-router.toml with live validation (the real loaders) and save.
  • Onboard — A/B a local vs hosted model on sample prompts in the browser, judge each, record labels, then calibrate from the log (needs [gateway] too, for the model calls).
pip install -e ".[ui]"
wayfinder-router ui --port 8099    # then open http://localhost:8099

The UI is a thin consumer of the same pure functions; it never calls a model, and no secret ever appears in it (a gateway model names an api_key_env; the key lives in the environment).

Python API

from wayfinder_router import score_complexity, RoutingConfig, explain_score

result = score_complexity(prompt_text, config=RoutingConfig.binary(threshold=0.7))
print(result.recommendation, result.score, result.features)
for fc in explain_score(result.features, RoutingConfig().weights):
    print(fc.name, fc.contribution)

Heritage

Wayfinder began as the rac route exploration inside requirements-as-code, and its scoring shape is inspired by RAC's deterministic classification.py (points / ceiling). It was split out because routing is a runtime inference concern, divergent from RAC/Lore's recorded-knowledge product line — a prompt router should not require installing a requirements-as-code engine. The shipped tool shares no runtime code with RAC; see decisions/WF-ADR-0001.

Repository layout

wayfinder-router/
  wayfinder_router/     the package: complexity scorer, tiers + classifier, own config
                 loader + writer, offline calibration (Newton/IRLS), explain, the
                 feedback log + onboarding harness, recalibration, CLI, and the
                 optional OpenAI-compatible gateway and local UI (impure layers,
                 behind their extras)
  tests/         scorer, config, calibration, explain, feedback, onboard,
                 recalibrate, CLI, gateway, and UI coverage
  decisions/     ADRs grounding the tool's own choices (dogfooded)
  Dockerfile, docker-compose.example.yml   deploy the gateway as a service

Test

pip install -e .[dev]   # or: pip install pytest
make test

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wayfinder_router-0.1.0.tar.gz (55.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wayfinder_router-0.1.0-py3-none-any.whl (45.4 kB view details)

Uploaded Python 3

File details

Details for the file wayfinder_router-0.1.0.tar.gz.

File metadata

  • Download URL: wayfinder_router-0.1.0.tar.gz
  • Upload date:
  • Size: 55.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wayfinder_router-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a9ae1fdb057cff34d4c7fb7abcd706f093a3ad1ad532ee3fe5599d998aa1dfef
MD5 179a950d84ef059802703f022b30da64
BLAKE2b-256 80404900e7358b1f4f8950300c7f040ed00d32f5124382a5149d1a356034c38d

See more details on using hashes here.

Provenance

The following attestation bundles were made for wayfinder_router-0.1.0.tar.gz:

Publisher: release.yml on itsthelore/wayfinder-router

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wayfinder_router-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for wayfinder_router-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6c42f7064691cbb7b3aaff314fb556fd4de141f9de4f9897b1b67b7a21797ade
MD5 57aeb481aa9f93d3f99d689628c914e5
BLAKE2b-256 73ec25546fe365d48c1e4d53e6df8cb41146287b36cdcba989c0229209a04dcf

See more details on using hashes here.

Provenance

The following attestation bundles were made for wayfinder_router-0.1.0-py3-none-any.whl:

Publisher: release.yml on itsthelore/wayfinder-router

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page