{"slug": "how-i-made-a-small-decision-model-s-belief-visible-and-what-that-taught-me-about", "title": "How I made a small decision model's belief visible, and what that taught me about building honest AI demos.", "summary": "A developer built AI Grand Prix, a browser-based demo that races five small open-weight decision models on a closed circuit, where each model receives a JSON state 20 times per second and returns probabilities over five steering options via a single POST /v1/systemone call. The project visualizes each model's belief as a translucent \"belief cone\" fan sized by the returned probabilities, and samples from the distribution rather than taking argmax so uncertain decisions stay visible. The author reports a 50x latency spread across the five models (17 ms median for the fastest versus 861 ms on an M1 Pro for the slowest), which motivated decoupling decision rate from render rate.", "body_md": "Every AI demo you have watched in the last two years looks the same. A prompt box, a streaming answer, a slow reveal. It is a good demo, and it has taught everyone the same lesson: AI means a model that writes.\n\nBut most of the models shipping onto devices this year are not writing anything. They are picking an option. A phone decides whether to surface a notification. A camera decides whether a frame is worth keeping. A car decides whether the object ahead is a shadow or a hazard. These are **decision models**: you hand them a state and a list of options, and they return a probability for each option in a single forward pass.\n\nNobody has seen one work. That is the gap this project fills.\n\nAI Grand Prix puts five small decision models on the same track, in the same car, and lets you watch them drive.\n\nFive open-weight models race on a closed circuit. Twenty times a second, each one is handed a JSON state describing its car and the road ahead, and returns a probability for each of five steering options. The car then drives the option drawn from that distribution.\n\nNothing is generated. There is no text, no prompt, no output parsing. The whole inference call is one HTTP POST to `POST /v1/systemone` and the answer is a set of probabilities.\n\nThe part worth watching is not the racing. It is the **belief cone**: a translucent fan drawn out of every car, one wedge per steering option, sized by the probability the model just returned. When a car runs wide on a corner, you watched it choose the less likely move.\n\nFour decisions shaped this architecture, and each one is a lesson that generalises past this project.\n\nFive racer panels could have been five canvases. On a laptop that means five WebGL contexts competing for the same GPU, and it degrades badly. PixiJS gives us one renderer and one WebGL context, with each panel drawn as a masked viewport inside it.\n\nI picked PixiJS rather than a game engine on purpose. PixiJS is a renderer, not a physics engine, and the interesting drawing here is custom: a probability fan, a steering gauge, a track ribbon. A game engine would have handed us a scene graph and then fought us on the parts I actually cared about.\n\nThe gateway is Python's `http.server`. That is not a lack of ambition. The single most important property of a demo like this is that a fresh clone runs. No `pip install`, no virtualenv, no version conflict, no network. If a reader needs to install something before they see a car move, you have already lost most of them.\n\nThis is the one that bites everybody building agent demos.\n\nOur fastest racer answers in about 17 ms at the median. Our slowest measured decision on an M1 Pro was **861 ms**. That is a 50x spread, and it comes from the same five models on the same machine.\n\nIf you tie a decision to a frame, the whole board stutters every time the slow model thinks. So we split the rates:\n\nThe panel shows `p50` and the live decision rate, so the latency is visible instead of hidden behind a smooth animation.\n\nThis is the most interesting decision in the project, and it was a judgement call rather than a technical one.\n\nThe obvious implementation is `argmax`: take the highest probability and drive it. That produces a field of perfectly obedient cars that never make mistakes, and it hides the entire point of a decision model.\n\nI sampled from the distribution instead:\n\n``` python\ndef draw_action(probabilities, policy, seed, tick):\n    \"\"\"Pick one option. `sample` draws from the distribution; `argmax` takes the top.\"\"\"\n    if policy == \"argmax\":\n        return max(probabilities.items(), key=lambda item: item[1])[0]\n    ...\n```\n\nThe sampling is seeded per racer and per tick, so a race is reproducible, and both the distribution and the drawn action are recorded in telemetry. The consequence on screen is that a model saying 45 percent hard left and 40 percent left will sometimes take the 40, clip the apex, and run wide.\n\nThat is a real uncertain decision, and it is the moment viewers understand what they are looking at. `policy` is a config value, so you can flip to `argmax` and watch the personality drain out of the field.\n\nThere is no hidden context and no prompt history. This is the entire input, built in `sim.py`:\n\n```\nreturn {\n    \"task\": \"steer\",\n    \"track\": {\n        \"width_m\": round(self.track.half_width * 2, 1),\n        \"bends_ahead\": bends,\n    },\n    \"car\": {\n        \"lateral_m\": round(car.lateral, 2),\n        \"side\": \"right of centre\" if car.lateral > 0 else \"left of centre\",\n        \"heading_error_deg\": round(math.degrees(car.heading_error), 1),\n        \"speed_mps\": round(car.speed, 1),\n        \"lap\": car.lap,\n        \"laps_total\": self.laps,\n        \"off_track_ticks\": car.off_ticks,\n        \"spins\": car.spins,\n        \"position\": place,\n        \"cars_total\": len(self.cars),\n        \"gap_to_leader_m\": round(max(0.0, leader.distance_m - car.distance_m), 1),\n    },\n    \"goal\": (...),\n}\n```\n\nAnd the options are a fixed list, each with a description that is sent to the model as the option's criterion:\n\n```\nSTEER_BUCKETS: tuple[tuple[str, float, str], ...] = (\n    (\"hard_left\", -0.036, \"Steer hard left: a firm left arc, about a 28 m radius.\"),\n    (\"left\", -0.018, \"Steer left: a gentle left arc, about a 55 m radius.\"),\n    (\"straight\", 0.0, \"Hold the wheel straight: no steering input.\"),\n    (\"right\", 0.018, \"Steer right: a gentle right arc, about a 55 m radius.\"),\n    (\"hard_right\", 0.036, \"Steer hard right: a firm right arc, about a 28 m radius.\"),\n)\n```\n\nThe request body, in full:\n\n```\nbody = {\n    \"state\": state,\n    \"questions\": {\n        question_id: {\n            \"type\": \"choice\",\n            \"instructions\": instructions,\n            \"criteria\": {option.label: option.description for option in options},\n        }\n    },\n}\n```\n\nOne POST in, one set of probabilities out. No streaming, no tokens, no parsing.\n\nMost of the work in this project was not the model integration. It was making the simulation honest. Each of these produced a demo that looked correct and was lying.\n\n**The lap counter handed out free laps.** Cars were placed just behind the start line, so the first physics tick crossed it and credited a lap after 13.5 m. A car \"won\" a 700 m circuit in half a second. The fix has two parts: the grid starts *past* the line, and a lap only counts after most of a circuit of real travel.\n\n```\ncrossed_line = previous_s > 0.75 * self.track.length and self.track.s_at(index) < 0.25 * self.track.length\nif crossed_line and car.since_lap_m >= LAP_MIN_TRAVEL * self.track.length:\n    car.lap += 1\n    car.since_lap_m = 0.0\n```\n\n**Distance was taken modulo the circuit.** A car that stepped backwards across the start line had its travel wrap to nearly a full lap. Three cars showed identical 1020 m on a 700 m track while all still on lap zero. Travel is now unwrapped, and a regression test drives a car backwards to prove it earns nothing.\n\n**The maps were undrivable and it looked like the models were broken.** Generated circuits produced 1 m corners while the sharpest steering option turns on a 27.8 m radius. The cars spiralled, spun fifteen times a lap, and it read as \"these models are bad.\" The models were fine; the tracks were impossible. There is now a tool that fails the build if any corner is tighter than the sharpest option:\n\n```\npython3 tools/diagnose_track.py\n```\n\n**The canvas was never attached.** The user reported a blank board while the race ran. The DOM was perfect, there were zero page errors, and PixiJS had created its canvas. It simply had not been appended to the document. PixiJS v8 does not attach it for you. The fix is one line, and the lesson is that \"the DOM looks right\" is not evidence that anything rendered.\n\nThat last one is worth repeating: a screenshot from a vision model told us the gauges were missing when they were present, and the DOM said the canvas existed when it did not. The only reliable check was reading the live scene graph out of the running page.\n\n`tools/probe_engines.py` prints the distribution each engine actually returned, and the panel shows `p50` from the live race.\n| Racer | Params | Latency measured on an M1 Pro | \n|---|---|---|\n| Julia-1 144M | 144M | 18 to 32 ms | \n| Laya 421M | 421M | 81 to 91 ms | \n| Kev 0.8B | 812 MB Q8 | 157 to 184 ms | \n| Tev1 0.8B | 752 MB | 10 to 861 ms | \n| Clef-flash 9B | 11 GB | first call is a model load | \n\nThe spread is the story. A 144M model answering in 18 ms and a 9B model answering in a second are both \"a decision model\", and picking between them for an on-device feature is exactly the trade this board makes visible.\n\nThe steering options are just a list of choices, and the simulator is just the consequence. Swapping both gives you a different game with the same belief cone, the same telemetry and the same tests: a drone landing pad, a trading bot, a traffic simulator.\n\nCloser in, the three things I would build next are a lap time sparkline under each panel (all the data is already in telemetry), a per-racer top speed so latency becomes a visible handicap, and a decision log view that shows state in, distribution out, option taken, latency, for the last twenty decisions. That last one is the teaching view, and it is the reason the telemetry records distributions rather than just labels.\n\n```\npython3 tests/run_all.py     # 35 tests, no models, no network\n./run.sh --replay            # two synthetic racers, watch the board\n./tools/engines_up.sh        # then bring up the real models\n./run.sh                     # pick your racers and map, press Start\n```\n\nThe code is MIT. The models keep their own licenses.\n\nCode & more: [https://www.dailybuild.xyz/project/284-grand-prix](https://www.dailybuild.xyz/project/284-grand-prix)", "url": "https://wpnews.pro/news/how-i-made-a-small-decision-model-s-belief-visible-and-what-that-taught-me-about", "canonical_source": "https://dev.to/harishkotra/how-i-made-a-small-decision-models-belief-visible-and-what-that-taught-me-about-building-honest-4gc1", "published_at": "2026-10-15 17:38:35+00:00", "updated_at": "2026-10-15 17:54:51.651657+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "machine-learning", "artificial-intelligence"], "entities": ["AI Grand Prix", "PixiJS", "M1 Pro"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-i-made-a-small-decision-model-s-belief-visible-and-what-that-taught-me-about", "markdown": "https://wpnews.pro/news/how-i-made-a-small-decision-model-s-belief-visible-and-what-that-taught-me-about.md", "text": "https://wpnews.pro/news/how-i-made-a-small-decision-model-s-belief-visible-and-what-that-taught-me-about.txt", "jsonld": "https://wpnews.pro/news/how-i-made-a-small-decision-model-s-belief-visible-and-what-that-taught-me-about.jsonld"}}