Add a recorded demo, and the script that produces it

scripts/demo.sh is a real walkthrough: it starts the gateway, routes model:auto,
resolves four aliases to four different models, shows a cache hit costing zero
quota, recalls a name across a server-side session, 404s an unknown model with
suggestions, and prints where the tokens went. Nothing is staged — every line is
output the gateway actually produced against a live provider.

scripts/record-demo.html turns a captured transcript into video frames. ffmpeg
on macOS ships without freetype, so it can encode frames but cannot draw text;
the browser can do both, so frames are rasterised on a canvas and posted to a
local server for ffmpeg to assemble.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Opus 5 committed 2026-08-16 22:53:45 -05:00
1 parent b85542abc6
commit eeb5cad4ce
7 files changed
+295

No files matched your search

+3
View File
@@ -14,3 +14,6 @@ data/
*.log *.log
.DS_Store .DS_Store
# recorder scratch — the encoded demo is committed, the frames are not
demo/frames/
+6
View File
@@ -8,6 +8,11 @@ Your apps point at `http://localhost:8787/v1` and never see a provider key. When
one free tier runs out, the next one takes over — mid-conversation, even if it one free tier runs out, the next one takes over — mid-conversation, even if it
has a sixteenth of the context window. has a sixteenth of the context window.
![Freeway demo](demo/freeway-demo.gif)
*Every frame above is a real request against a live provider — recorded by
`./scripts/demo.sh`, which you can run yourself.*
``` ```
your app ──▶ Freeway ──┬──▶ Mistral 1B tokens/month your app ──▶ Freeway ──┬──▶ Mistral 1B tokens/month
├──▶ Cloudflare 10k neurons/day ├──▶ Cloudflare 10k neurons/day
@@ -399,6 +404,7 @@ gateway flatly refuses to bind one with no keys at all.
## Development ## Development
```bash ```bash
./scripts/demo.sh # scripted walkthrough against your real providers
npm run build # tsc --build, must be clean npm run build # tsc --build, must be clean
npm test # node:test across all workspaces npm test # node:test across all workspaces
npm run smoke # every route against mock upstreams — run this after adding a provider npm run smoke # every route against mock upstreams — run this after adding a provider
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# A scripted walkthrough of Freeway against real providers.
#
# ./scripts/demo.sh # narrate + run
# ./scripts/demo.sh --transcript # same, but machine-readable for recording
#
# Everything here is a real request. Nothing is faked, and the output you see is
# the output the gateway actually produced.
set -uo pipefail
cd "$(dirname "$0")/.."
PORT="${DEMO_PORT:-8790}"
B="http://127.0.0.1:$PORT"
TRANSCRIPT=""
[[ "${1:-}" == "--transcript" ]] && TRANSCRIPT=1
if [[ -n "$TRANSCRIPT" ]]; then
C_DIM=''; C_B=''; C_G=''; C_C=''; C_Y=''; C_X=''
else
C_DIM=$'\033[2m'; C_B=$'\033[1m'; C_G=$'\033[32m'; C_C=$'\033[36m'; C_Y=$'\033[33m'; C_X=$'\033[0m'
fi
say() { printf '\n%s# %s%s\n' "$C_DIM" "$1" "$C_X"; }
cmd() { printf '%s$ %s%s\n' "$C_C" "$1" "$C_X"; }
pause(){ [[ -n "$TRANSCRIPT" ]] || sleep "${1:-1}"; }
cleanup() { [[ -n "${GW_PID:-}" ]] && kill "$GW_PID" 2>/dev/null; }
trap cleanup EXIT
# ---------------------------------------------------------------------------
printf '%s┌─ Freeway ─ one endpoint in front of every free LLM tier%s\n' "$C_B" "$C_X"
say "Start the gateway. No keys? It still starts — every provider becomes a card telling you what to set."
cmd "npm start"
PORT=$PORT node packages/gateway/dist/bin.js > /tmp/freeway-demo.log 2>&1 &
GW_PID=$!
for _ in $(seq 1 30); do curl -fsS "$B/healthz" >/dev/null 2>&1 && break; sleep 0.4; done
grep -E "listening|✓|○" /tmp/freeway-demo.log | head -6
pause 2
# ---------------------------------------------------------------------------
say "model:\"auto\" — you don't name a vendor. Freeway picks whoever is healthy and under quota."
cmd "curl \$B/v1/chat/completions -d '{\"model\":\"auto\",\"messages\":[…]}' -D-"
curl -sS "$B/v1/chat/completions" -H 'content-type: application/json' \
-d '{"model":"auto","messages":[{"role":"user","content":"In one sentence: what is a rate limit?"}],"max_tokens":60}' \
-D /tmp/h -o /tmp/b
grep -iE "^HTTP|^x-freeway" /tmp/h | sed "s/^/ ${C_G}/;s/\$/${C_X}/"
printf ' %s→%s %s\n' "$C_G" "$C_X" "$(node -e "
const j=JSON.parse(require('fs').readFileSync('/tmp/b','utf8'));
console.log(j.choices[0].message.content.replace(/\n/g,' ').slice(0,150));")"
pause 2
# ---------------------------------------------------------------------------
say "Aliases route by capability, not vendor. Same call, four different models."
for m in fast cheap code reason; do
curl -sS -o /tmp/b -D /tmp/h "$B/v1/chat/completions" -H 'content-type: application/json' \
-d "{\"model\":\"$m\",\"messages\":[{\"role\":\"user\",\"content\":\"reply with the single word: ok\"}],\"max_tokens\":8}"
printf ' %-7s → %-24s %s\n' "$m" \
"$(grep -i '^x-freeway-model' /tmp/h | cut -d' ' -f2 | tr -d '\r')" \
"$(grep -i '^x-freeway-ms' /tmp/h | cut -d' ' -f2 | tr -d '\r')ms"
sleep 0.7
done
pause 2
# ---------------------------------------------------------------------------
say "Ask the same thing twice and the second is free — served from cache, zero quota."
Q='{"model":"fast","temperature":0,"messages":[{"role":"user","content":"what is 2+2?"}],"max_tokens":20}'
curl -sS -o /dev/null "$B/v1/chat/completions" -H 'content-type: application/json' -d "$Q"
curl -sS -o /dev/null -D /tmp/h "$B/v1/chat/completions" -H 'content-type: application/json' -d "$Q"
printf ' %scache: %s provider: %s %s0 tokens spent%s\n' "$C_G" \
"$(grep -i '^x-freeway-cache' /tmp/h | cut -d' ' -f2 | tr -d '\r')" \
"$(grep -i '^x-freeway-provider' /tmp/h | cut -d' ' -f2 | tr -d '\r')" "$C_B" "$C_X"
pause 2
# ---------------------------------------------------------------------------
say "Sessions live server-side. Send only the new message; the gateway rebuilds the thread."
cmd "-d '{\"session\":\"demo\",\"messages\":[{\"content\":\"my name is Ada\"}]}'"
curl -sS -o /dev/null "$B/v1/chat/completions" -H 'content-type: application/json' \
-d '{"model":"fast","session":"demo","messages":[{"role":"user","content":"My name is Ada. Remember it."}],"max_tokens":30}'
cmd "-d '{\"session\":\"demo\",\"messages\":[{\"content\":\"what is my name?\"}]}'"
curl -sS -o /tmp/b -D /tmp/h "$B/v1/chat/completions" -H 'content-type: application/json' \
-d '{"model":"fast","session":"demo","messages":[{"role":"user","content":"What is my name?"}],"max_tokens":30}'
printf ' %s→%s %s\n' "$C_G" "$C_X" "$(node -e "
const j=JSON.parse(require('fs').readFileSync('/tmp/b','utf8'));
console.log(j.choices[0].message.content.replace(/\n/g,' ').slice(0,120));")"
printf ' %s%s%s\n' "$C_DIM" "$(grep -i '^x-freeway-context' /tmp/h | tr -d '\r')" "$C_X"
pause 2
# ---------------------------------------------------------------------------
say "Ask for a model that doesn't exist. You get a 404 that tells you what does."
curl -sS "$B/v1/chat/completions" -H 'content-type: application/json' \
-d '{"model":"gpt-5-turbo-ultra","messages":[{"role":"user","content":"hi"}]}' \
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);
console.log(' '+j.error.message);
console.log(' did you mean: '+j.freeway.suggestions.slice(0,3).join(', '));});"
pause 2
# ---------------------------------------------------------------------------
say "Where did the tokens go?"
curl -sS "$B/api/usage" | node -e "
let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s),t=j.totals;
console.log(' '+t.tokensIn+' in / '+t.tokensOut+' out over '+t.requests+' requests, '+t.avgMs+'ms avg');
console.log(' '+t.cached+' served from cache · '+t.failed+' failed');
for (const m of j.byModel) console.log(' '+m.id.padEnd(26)+(m.tokensIn+m.tokensOut)+' tok ×'+m.requests);});"
pause 2
# ---------------------------------------------------------------------------
say "Adding a provider is one JSON file. Never a code change."
cmd "node scripts/verify-provider.mjs mistral"
node scripts/verify-provider.mjs mistral 2>&1 | sed -n '3,14p' | sed "s/^/ /"
printf '\n%s└─ dashboard at %s · 12 providers bundled · zero runtime dependencies%s\n\n' "$C_B" "$B" "$C_X"
+173
View File
@@ -0,0 +1,173 @@
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Freeway — demo recorder</title></head>
<body style="margin:0;background:#111;color:#ccc;font:13px system-ui">
<!--
Renders a captured demo transcript into video frames on a <canvas>.
ffmpeg on macOS ships without freetype, so it can encode frames but cannot
draw text. The browser can do both, so frames are rasterised here and handed
back as PNGs for ffmpeg to assemble.
Usage: open, paste the transcript into TRANSCRIPT, then call
window.renderAll() and window.getFrame(i).
-->
<div style="padding:10px">
<div id="status">idle</div>
<canvas id="c" width="960" height="600" style="border:1px solid #333;max-width:100%"></canvas>
</div>
<script>
'use strict';
const W = 960, H = 600, PAD = 22;
const LINE = 17, FONT = '13px Menlo, monospace';
const VISIBLE = Math.floor((H - PAD * 2 - 34) / LINE);
const COL = {
bg: '#0a0c0f', chrome: '#12151a', line: '#242a33',
fg: '#dbe2ea', dim: '#69737f', cyan: '#58a6ff',
green: '#3fb950', amber: '#d29922', purple: '#bc8cff', red: '#f85149',
};
let TRANSCRIPT = [];
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
/** Colour a line by what it is, matching the dashboard's palette. */
function styleOf(text) {
if (/^┌─|^└─/.test(text)) return { c: COL.fg, bold: true };
if (/^\s*#/.test(text)) return { c: COL.dim, bold: false };
if (/^\s*\$/.test(text)) return { c: COL.cyan, bold: false };
if (/^\s*(✓|→)/.test(text)) return { c: COL.green, bold: false };
if (/^\s*○/.test(text)) return { c: COL.amber, bold: false };
if (/x-freeway-(provider|model)/.test(text)) return { c: COL.green, bold: true };
if (/^\s*x-freeway|^\s*HTTP/.test(text)) return { c: COL.green, bold: false };
if (/cache: exact|0 tokens spent/.test(text)) return { c: COL.purple, bold: true };
if (/failed|unknown model/.test(text)) return { c: COL.amber, bold: false };
return { c: COL.fg, bold: false };
}
function drawChrome() {
ctx.fillStyle = COL.bg;
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = COL.chrome;
ctx.fillRect(0, 0, W, 34);
ctx.strokeStyle = COL.line;
ctx.beginPath(); ctx.moveTo(0, 34.5); ctx.lineTo(W, 34.5); ctx.stroke();
const dots = ['#f85149', '#d29922', '#3fb950'];
dots.forEach((c, i) => {
ctx.beginPath();
ctx.arc(20 + i * 17, 17, 5.5, 0, Math.PI * 2);
ctx.fillStyle = c;
ctx.fill();
});
ctx.font = '12px Menlo, monospace';
ctx.fillStyle = COL.dim;
ctx.textAlign = 'center';
ctx.fillText('freeway — demo', W / 2, 21);
ctx.textAlign = 'left';
}
/**
* One frame: every line up to `upto`, with the last one revealed `partial`
* characters in, so text appears to be typing rather than blinking into place.
*/
function drawFrame(upto, partial) {
drawChrome();
ctx.font = FONT;
ctx.textBaseline = 'top';
const start = Math.max(0, upto - VISIBLE + 1);
let y = 34 + PAD;
for (let i = start; i <= upto && i < TRANSCRIPT.length; i++) {
let text = TRANSCRIPT[i];
const last = i === upto;
if (last && partial !== undefined && partial < text.length) text = text.slice(0, partial);
const s = styleOf(TRANSCRIPT[i]);
ctx.font = (s.bold ? 'bold ' : '') + FONT;
ctx.fillStyle = s.c;
// Clip to the window rather than letting a long line run off the edge.
let shown = text.replace(/\t/g, ' ');
const maxW = W - PAD * 2 - 12;
if (ctx.measureText(shown).width > maxW) {
while (shown.length > 1 && ctx.measureText(shown + '…').width > maxW) shown = shown.slice(0, -1);
shown += '…';
}
ctx.fillText(shown, PAD, y);
if (last) {
// Block cursor, so a paused frame still reads as a live terminal.
const w = ctx.measureText(shown).width;
ctx.fillStyle = COL.green;
ctx.fillRect(PAD + w + 2, y + 1, 7, 14);
}
y += LINE;
}
}
/* Frames are precomputed so ffmpeg gets a deterministic sequence rather than
whatever the render loop happened to catch. */
let FRAMES = [];
window.buildFrames = function (transcript) {
TRANSCRIPT = transcript.split('\n').map((l) => l.replace(/\[[0-9;]*m/g, ''));
// Trim leading/trailing blank runs, keep interior spacing.
while (TRANSCRIPT.length && !TRANSCRIPT[0].trim()) TRANSCRIPT.shift();
while (TRANSCRIPT.length && !TRANSCRIPT[TRANSCRIPT.length - 1].trim()) TRANSCRIPT.pop();
FRAMES = [];
for (let i = 0; i < TRANSCRIPT.length; i++) {
const text = TRANSCRIPT[i];
// A comment line is narration — type it out. Everything else is output,
// which in a real terminal appears at once.
if (/^\s*#/.test(text) && text.length > 12) {
const steps = 3;
for (let s = 1; s <= steps; s++) FRAMES.push({ upto: i, partial: Math.ceil((text.length / steps) * s) });
} else {
FRAMES.push({ upto: i, partial: undefined });
}
// Hold on the lines worth reading.
if (/x-freeway-provider|cache: exact|→ Your name is|└─/.test(text)) {
for (let h = 0; h < 4; h++) FRAMES.push({ upto: i, partial: undefined });
}
}
for (let h = 0; h < 8; h++) FRAMES.push({ upto: TRANSCRIPT.length - 1, partial: undefined });
document.getElementById('status').textContent = `${TRANSCRIPT.length} lines → ${FRAMES.length} frames`;
return FRAMES.length;
};
window.frameCount = () => FRAMES.length;
/** Render frame `i` and return it as a bare base64 PNG. */
window.getFrame = function (i) {
const f = FRAMES[i];
drawFrame(f.upto, f.partial);
return canvas.toDataURL('image/png').split(',')[1];
};
/** Render a contiguous batch, so the whole sequence needs few round trips. */
window.getBatch = function (from, count) {
const out = [];
for (let i = from; i < Math.min(from + count, FRAMES.length); i++) out.push(window.getFrame(i));
return out;
};
window.preview = function () {
let i = 0;
const tick = () => {
if (i >= FRAMES.length) return;
const f = FRAMES[i++];
drawFrame(f.upto, f.partial);
setTimeout(tick, 120);
};
tick();
};
</script>
</body>
</html>