<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Robert Moore</title>
    <description>The latest articles on DEV Community by Robert Moore (@iamrobertmoore).</description>
    <link>https://dev.to/iamrobertmoore</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4076110%2F4e95763a-b90c-4749-8902-67794ea0f4e8.jpg</url>
      <title>DEV Community: Robert Moore</title>
      <link>https://dev.to/iamrobertmoore</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iamrobertmoore"/>
    <language>en</language>
    <item>
      <title>I Asked 11 AI Models to Predict My Race Splits. Once I Named the Race, 5 Clearly Beat My Model</title>
      <dc:creator>Robert Moore</dc:creator>
      <pubDate>Fri, 09 Oct 2026 16:38:30 +0000</pubDate>
      <link>https://dev.to/iamrobertmoore/i-asked-11-ai-models-to-predict-my-race-splits-told-which-race-8-beat-the-model-i-built-4o8f</link>
      <guid>https://dev.to/iamrobertmoore/i-asked-11-ai-models-to-predict-my-race-splits-told-which-race-8-beat-the-model-i-built-4o8f</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last weekend I built a model to predict my triathlon splits. It's called Race Card, it runs on TabPFN, and before trusting it I checked it against every race I've done since 2014. I was rather pleased with myself.&lt;/p&gt;

&lt;p&gt;Then, slightly later than would have been useful, I had a thought: could I have just pasted my training log into a chatbot and asked?&lt;/p&gt;

&lt;p&gt;So, naturally, I built something else to find out: a benchmark.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With just the numbers to go on, only one of 11 models clearly beat Race Card: GPT-6 Astra.&lt;/li&gt;
&lt;li&gt;Give them the race name as well and 10 of 11 improved, with 5 clearly beating Race Card. Once I gave TabPFN the same facts, 6 were still ahead, but none by a clear margin.&lt;/li&gt;
&lt;li&gt;The name helped for a much more basic reason than I'd expected: it told the models whether a 21 km run was a standalone half marathon or came straight after a 90 km bike. Without it, every model assumed "off the bike" and predicted my standalone halves too slow on average. My own model made exactly the same mistake. A follow-up with made-up race names backed this up: letting the models link repeat visits helped on races I'd done before, but only the real name fixed the half marathons.&lt;/li&gt;
&lt;li&gt;Most models' "80% ranges" are far too narrow. Blind, 7 of 11 caught fewer of my actual times than an honest range should. I'd put more faith in the middle estimate than the range around it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I Benchmarked
&lt;/h2&gt;

&lt;p&gt;I used &lt;strong&gt;79 real race legs&lt;/strong&gt; (swim, bike and run) from my racing between 2014 and 2026: half Ironmans, sea swims and half marathons. For each leg, the model gets my training history up to that race, with nothing from afterwards, and has to predict how fast I went. It gives a median speed and an 80% range: a lower and upper estimate that should contain the actual result eight times out of ten.&lt;/p&gt;

&lt;p&gt;For each leg, it sees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the sport, distance and how much the course climbs&lt;/li&gt;
&lt;li&gt;my training load on race day: hours in the last 7 and 42 days, and kilometres in that sport&lt;/li&gt;
&lt;li&gt;a table of previous sessions in the same sport: every earlier race, plus all sessions from the 42 days before race day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rows come from the same data Race Card's TabPFN was trained on, with the same cut-off. The idea was to give them the same information, ask the same question and see how they got on.&lt;/p&gt;

&lt;p&gt;I made two versions of the task, with one small change to the information they get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blind.&lt;/strong&gt; No names, no dates. Just numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Told which race.&lt;/strong&gt; The same numbers, plus the event's name and year for both the current race and earlier ones. Something like "Ironman 70.3 Weymouth (2019)."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second version came from something Race Card kept getting wrong. It predicted my Weymouth bike split too fast every time, by 6 to 11%. It knows how much the course climbs, but Weymouth's particular combination of hills and wind doesn't really come across in that number. I wondered whether a language model might have a better idea of what I was in for.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  What the model actually sees (one leg, trimmed)
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are predicting how fast an amateur triathlete will go in one leg of a race, using only their own training log.

The race is Saxon Shore Half Marathon, on 2022-05-22.      &amp;lt;- only in the "told which race" version
Sport: running. Race leg: 21.08 km with 85 m of climbing (4.0 m per km).
On race day the athlete had trained 5.7 hours in the previous 7 days and 43.3 hours in the previous 42 days, including 181.5 km of running.

Below are the athlete's earlier running sessions, oldest first: every race, and every session in the 42 days before race day ...

days_before,km,climb_m_per_km,load_7h,load_42h,sport_km_42,race,kmh,event
0,6.53,1.2,5.6,42.7,174.9,0,11.47,
1,8.57,9.8,5.2,42.1,166.4,0,13.05,
...
307,21.67,28.4,8.1,50.6,150.9,1,12.37,Ironman 70.3 Staffordshire (2021)
972,21.27,0.0,6.0,42.8,138.4,1,12.04,Ironman 70.3 Weymouth (2019)
1434,21.14,0.0,3.8,35.6,139.1,1,13.76,Saxon Shore Half Marathon (2018)
...

Predict the athlete's average moving speed in km/h for this race leg. Give your median estimate, and a low and a high value such that you would expect the true speed to be below low 10% of the time and above high 10% of the time. In basis, say in one short sentence what your estimate rests on.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;(&lt;code&gt;basis&lt;/code&gt; is the answer field for that one-line explanation.) The blind version is identical, apart from removing the race line and the &lt;code&gt;event&lt;/code&gt; column. Those three race rows are worth a look: all about 21 km, two at roughly 12 km/h and one at 13.8. Take away the names and there's nothing to tell you which one I started on fresh legs.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;I scored each model in two ways. &lt;strong&gt;Typical miss&lt;/strong&gt; measures how far its median prediction was from my actual time: the average percentage error across all 79 legs, regardless of whether it was too fast or too slow. &lt;strong&gt;Range&lt;/strong&gt; counts how many of my actual times fell inside its own 80% range. A well-calibrated range should catch about 63 of 79, with anything from 56 to 70 within the variation you'd expect by chance.&lt;/p&gt;

&lt;p&gt;The comparison includes Race Card itself (TabPFN with all its rows) and the baselines I'd checked it against: TabPFN with exactly the rows the language models see, and the method most of us probably use anyway: "same as my last race".&lt;/p&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;I tested 11 models from five labs, all through Kaggle's model proxy: GPT-6 Astra, GPT-5.6 Terra, GPT-5.4 mini, Claude Opus 5, Claude Sonnet 5, Claude Haiku 4.5, Gemini 3.1 Pro, Gemini 3.8 Flash, Gemini 3.7 Flash, Grok 4.20 Reasoning and GLM-5. For most labs, I included both a flagship and a smaller, faster model. I wanted to see whether the bigger models actually earned their keep here.&lt;/p&gt;

&lt;p&gt;A quick word on what I mean by &lt;strong&gt;clearly&lt;/strong&gt; better. There are only 79 legs here, and one bad race can shift an average quite a bit. I only call a win clear if the gap survives resampling those 79 legs 5,000 times: technically, a 95% bootstrap interval that doesn't cross zero. I also ran 16 model and task pairings at least twice. The biggest change in typical miss between runs was 0.7 percentage points, so I wouldn't get too excited about small gaps in the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Typical miss, blind&lt;/th&gt;
&lt;th&gt;Typical miss, told the race&lt;/th&gt;
&lt;th&gt;Inside its 80% range, blind&lt;/th&gt;
&lt;th&gt;Inside its 80% range, told the race&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;My model (Race Card: TabPFN, all rows)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;63 of 79&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN, same rows as the models&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6.0%&lt;/strong&gt; (given the race facts)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67 of 79&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67 of 79&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;6.0%&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;td&gt;65 of 79&lt;/td&gt;
&lt;td&gt;69 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;6.5%&lt;/td&gt;
&lt;td&gt;6.4%&lt;/td&gt;
&lt;td&gt;56 of 79&lt;/td&gt;
&lt;td&gt;55 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;td&gt;6.5%&lt;/td&gt;
&lt;td&gt;5.5%&lt;/td&gt;
&lt;td&gt;37 of 79&lt;/td&gt;
&lt;td&gt;42 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.7 Flash&lt;/td&gt;
&lt;td&gt;6.5%&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;td&gt;49 of 79&lt;/td&gt;
&lt;td&gt;56 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6 Terra&lt;/td&gt;
&lt;td&gt;6.6%&lt;/td&gt;
&lt;td&gt;5.6%&lt;/td&gt;
&lt;td&gt;52 of 79&lt;/td&gt;
&lt;td&gt;53 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.8 Flash&lt;/td&gt;
&lt;td&gt;6.6%&lt;/td&gt;
&lt;td&gt;5.4%&lt;/td&gt;
&lt;td&gt;48 of 79&lt;/td&gt;
&lt;td&gt;54 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok 4.20 Reasoning&lt;/td&gt;
&lt;td&gt;6.8%&lt;/td&gt;
&lt;td&gt;6.1%&lt;/td&gt;
&lt;td&gt;56 of 79&lt;/td&gt;
&lt;td&gt;55 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;6.9%&lt;/td&gt;
&lt;td&gt;5.8%&lt;/td&gt;
&lt;td&gt;59 of 79&lt;/td&gt;
&lt;td&gt;62 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5&lt;/td&gt;
&lt;td&gt;7.1%&lt;/td&gt;
&lt;td&gt;6.2%&lt;/td&gt;
&lt;td&gt;41 of 79&lt;/td&gt;
&lt;td&gt;42 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;7.7%&lt;/td&gt;
&lt;td&gt;7.5%&lt;/td&gt;
&lt;td&gt;42 of 79&lt;/td&gt;
&lt;td&gt;44 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4 mini&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;td&gt;38 of 79&lt;/td&gt;
&lt;td&gt;38 of 79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Same as my last race"&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Blind, all 11 beat "same as my last race", and one beat my model
&lt;/h3&gt;

&lt;p&gt;"Same as last time" missed by 8.3% across the 76 legs where I had a previous race to copy. Every model did better on those same legs, although GPT-5.4 mini only just scraped past at 8.1%.&lt;/p&gt;

&lt;p&gt;Race Card was harder to beat. Six models were ahead on average, with Grok also just edging it, but only GPT-6 Astra had a clear win: 6.0% against 6.8%, with both of its runs agreeing. Astra's range held up too, catching 65 of 79 actual times. The other gaps could be explained by chance, apart from GPT-5.4 mini, which was clearly worse.&lt;/p&gt;

&lt;p&gt;I hadn't expected TabPFN to get &lt;em&gt;better&lt;/em&gt; with fewer rows. Give it previous races and the last six weeks of training and it scored 6.3%, against 6.8% with everything. Apparently, most of my training sessions have very little to say about race day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Told which race, 5 clearly beat the model I built
&lt;/h3&gt;

&lt;p&gt;Adding the race name improved 10 of 11 models, with a clear improvement for 6. GPT-5.4 mini was the only one that stayed put. Five models now clearly beat Race Card: GPT-6 Astra (5.3%), Gemini 3.7 Flash (5.3%), Gemini 3.8 Flash (5.4%), Gemini 3.1 Pro (5.5%) and GPT-5.6 Terra (5.6%).&lt;/p&gt;

&lt;p&gt;At that point, though, the language models knew something my model didn't. To make it a fairer contest, I gave TabPFN the same facts the name gives away in two extra columns: "this run comes straight off the bike" and "this is the same event as today". Its typical miss dropped from 6.3% to 6.0%. Six models were still ahead on average, but none by a clear margin. Against TabPFN using the same rows without those extra facts, only Astra and Gemini 3.7 Flash were clearly better.&lt;/p&gt;

&lt;p&gt;That's where I have to be fair to my own model. Give the best language models the race name and they clearly beat Race Card as I shipped it. Give TabPFN the same facts and I can't claim a clear winner.&lt;/p&gt;

&lt;h3&gt;
  
  
  The off-the-bike assumption
&lt;/h3&gt;

&lt;p&gt;I'd expected the name to help because models know things about courses: Weymouth is windy, Staffordshire is hilly. Once I broke the results down by leg type, I realised I'd been looking past something rather more obvious.&lt;/p&gt;

&lt;p&gt;Run legs account for just over a third of the dataset, but two-thirds of the improvement. The biggest single effect was on my six standalone half marathons. Every model made what I've started calling the off-the-bike assumption:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faa1fnmypm6a3gpn3334e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faa1fnmypm6a3gpn3334e.png" alt="Chart: average error on my six standalone half marathons for each model. Blind, every model predicted them too slow, from 4% to 13%; told the race's name, 10 of 11 moved closer, with GPT-6 Astra going from 6.7% too slow to 0.3%. TabPFN on the same rows was 9.1% too slow." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 21 km run in my log is usually the final leg of a half Ironman, at about 12 km/h after a swim and 90 km on the bike. On fresh legs, I run a standalone half marathon 40 to 50 seconds a kilometre faster. Without the name, the model has no way of telling which sort of day it's looking at, so it guesses "off the bike". Every single model predicted my standalone halves too slow on average. Here's Claude Opus 5, blind, on my first one in 2014:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Half-marathon off the bike with low recent volume&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That prediction was 17.8% too slow. GPT-6 Astra hedged its bets on Saxon Shore in 2022, "allowing for the flatter course and triathlon fatigue" without the name, and came in 5 to 7% too slow across three runs. Give it the name and it went with "a faster half marathon on this relatively flat course", getting within 2%.&lt;/p&gt;

&lt;p&gt;It worked the other way round too. Without the name, the Gemini models and GLM-5 predicted my runs &lt;em&gt;after&lt;/em&gt; the bike 3.5 to 5% too fast. Once they knew the race, they were within a percentage point.&lt;/p&gt;

&lt;p&gt;I can't be too smug about spotting this: my model has exactly the same blind spot. TabPFN on the same rows was 9.1% too slow on those six half marathons, and Race Card itself was 8.7% too slow. Adding the "off the bike" column only brought that down to 7.5%; there simply aren't many standalone halves in my history for it to learn from. The best language models managed it with the name, a handful of examples and a sentence of common sense.&lt;/p&gt;

&lt;p&gt;Recognising a course I'd raced before mattered less than I'd expected. For most of the models that improved, the benefit was at least as big on races I was doing for the first time.&lt;/p&gt;

&lt;p&gt;Part of the explanation is that repeat races give everyone an easier job. On the 40 legs where I'd done the event before, simply copying my speed from that event last time missed by 5.0%. Using "same as my last race" missed by 8.1% on those legs. TabPFN on the same rows managed 4.7%, while the six models that clearly improved with the name managed 4.2 to 4.5%.&lt;/p&gt;

&lt;p&gt;The bigger gaps were on the 39 legs at events I hadn't done before: 7.9% for TabPFN on the same rows, compared with 6.3 to 6.6% for the best four language models given the race name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weymouth: the course knowledge is real, but smaller
&lt;/h3&gt;

&lt;p&gt;The Weymouth bike splits still show most clearly how a model can put the name to good use. Race Card was 10.8%, 6.2% and 8.3% too fast in 2018, 2019 and 2022. Here's how Claude Opus 5 did once it knew the race:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Weymouth bike&lt;/th&gt;
&lt;th&gt;Actual&lt;/th&gt;
&lt;th&gt;Opus 5, told the race&lt;/th&gt;
&lt;th&gt;Race Card&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;2:50:18&lt;/td&gt;
&lt;td&gt;2.1% too fast&lt;/td&gt;
&lt;td&gt;10.8% too fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;2:44:36&lt;/td&gt;
&lt;td&gt;0.3% too slow&lt;/td&gt;
&lt;td&gt;6.2% too fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;2:46:42&lt;/td&gt;
&lt;td&gt;1.6% too fast&lt;/td&gt;
&lt;td&gt;8.3% too fast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each answer includes a one-line reason. For 2019, Opus wrote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mainly his 2018 Weymouth ride (31.95 km/h) on the same course, adjusted slightly upward for his modestly better 2019 form [...] tempered by flat-but-windy coastal conditions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's pretty much how I'd work it out myself. Without the name, the same model was 5 to 7% too fast on all three, much like everything else.&lt;/p&gt;

&lt;p&gt;Some models made less of the clue. GPT-6 Astra mentioned my "slower past Weymouth results", then still predicted a time 7 to 8% too fast. It had spotted the evidence without doing much about it. Gemini 3.8 Flash improved a lot overall, but was still 5 to 9% too fast at Weymouth. Its explanations mention "similar 90 km race performances", without referring to the course.&lt;/p&gt;

&lt;h3&gt;
  
  
  Made-up names: knowing the course, or finding my last ride?
&lt;/h3&gt;

&lt;p&gt;The obvious objection to all this: maybe the name doesn't tell the model anything about the race at all. Maybe it just lets it find my last go at the same event. So on Sunday evening I ran one more version on three models, with every event name replaced by a label like "Race G". The same event always gets the same label, so a model can still link repeat visits, but the name itself means nothing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Typical miss&lt;/th&gt;
&lt;th&gt;Blind&lt;/th&gt;
&lt;th&gt;Made-up names&lt;/th&gt;
&lt;th&gt;Real names&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.7 Flash&lt;/td&gt;
&lt;td&gt;6.5%&lt;/td&gt;
&lt;td&gt;6.1%&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;6.0%&lt;/td&gt;
&lt;td&gt;5.7%&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;6.9%&lt;/td&gt;
&lt;td&gt;6.1%&lt;/td&gt;
&lt;td&gt;5.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Linking the repeat visits gets somewhere between a third and two-thirds of the way. It helps on races I'd done before: Opus went from 5.6% to 4.3% on those, as good as with the real names (4.5%). On races I was doing for the first time, the made-up labels barely moved anything.&lt;/p&gt;

&lt;p&gt;The off-the-bike assumption survived completely. With made-up names, all three were still 6 to 11% too slow on my standalone half marathons. "Race G" doesn't tell you it's a half marathon. "Saxon Shore Half Marathon" does.&lt;/p&gt;

&lt;p&gt;Weymouth sits in between. With made-up labels, Opus was 3.5 to 4.6% too fast at "Race K", against 5 to 7% blind. With the real name it got within about 2%. So the course knowledge is real; it's just the smaller part.&lt;/p&gt;

&lt;p&gt;This is one run per model, so I wouldn't lean on the smaller gaps. But all three moved the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Most of them are far too sure of themselves
&lt;/h3&gt;

&lt;p&gt;This is the bit I'd want to know before planning a race around any of these predictions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq2om1r1l9ro1tyn76js.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnq2om1r1l9ro1tyn76js.png" alt="Chart: how many of my 79 actual times landed inside each model's 80% range, blind. An honest range lands between 56 and 70. GPT-6 Astra caught 65, Claude Opus 5 59; seven models caught fewer than 56, down to 37 for Gemini 3.1 Pro. Most ranges would need to be 1.2 to 2.2 times wider to catch 8 in 10." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Race Card's 80% range caught 63 of my 79 actual times, pretty much as advertised. The language models caught between 37 and 65, with 7 of the 11 falling below what you'd reasonably expect by chance. GPT-5.4 mini's "80% range" managed 38 out of 79. That's closer to a coin flip, which isn't quite the reassurance the label suggests.&lt;/p&gt;

&lt;p&gt;Take Gemini 3.7 Flash on my 2022 Weymouth bike, without the race name. In one run, its 80% range was 2:26 to 2:39. I did 2:46:42. Even the slow end of its range was more than seven minutes too optimistic.&lt;/p&gt;

&lt;p&gt;The ranges need more room either side of the middle estimate. Gemini 3.1 Pro's were about 11% of the predicted time wide, compared with 24% for Race Card. To catch 8 in 10, most models would need ranges 1.2 to 2.2 times wider.&lt;/p&gt;

&lt;p&gt;GPT-6 Astra was the exception: 65 of 79 blind and 69 when told the race, with ranges nearly as wide as Race Card's. It had both the best middle estimate and the most reliable range in the blind task. It's the one I'd actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Got Wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why the name helps.&lt;/strong&gt; I built the second task with Weymouth's hills and wind in mind. The bigger issue was whether I'd already done a bike ride before starting a half marathon. I only spotted it when I broke the results down by leg type afterwards. Race Card, it turns out, had been making the same mistake all along.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking more data would help TabPFN.&lt;/strong&gt; It didn't. Cutting the history down to races and the last six weeks of training made it better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The headline.&lt;/strong&gt; The first version of this post said 8 models beat my model. What I actually had was 8 ahead of TabPFN on the same rows, based on averages. Once I checked which gaps survived resampling the legs, that became 2 clear wins against that version of TabPFN, 5 against Race Card as I shipped it, and none against TabPFN with the same facts. Rather less tidy as a headline, but more accurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd Tell Another Athlete
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tell it what you're racing.&lt;/strong&gt; The event name, or even just "standalone half" or "off the bike", is worth more than switching to a bigger model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put more faith in the middle estimate.&lt;/strong&gt; If you want a range, I'd widen whatever it gives you by about half.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at your last go at the same race.&lt;/strong&gt; If you've done the event before, your speed there last time is a much better starting point than your most recent race somewhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini Flash does a good job.&lt;/strong&gt; Both Flash models were within a whisker of the flagships blind (6.5% and 6.6%) and right up with the best when told the race (5.3% and 5.4%), for a fraction of the cost. The smaller models from the other labs, Haiku and GPT-5.4 mini, were the weakest in both versions. The name made little or no difference to them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Race Card, the fix is straightforward: an "off the bike" flag and a "same event" flag. Those are what took the typical miss from 6.3% to 6.0% above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Measure Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Someone else's legs.&lt;/strong&gt; So far, it's all been me. I'd like to know whether a model that knows Weymouth is as useful for someone who's never raced it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Made-up names on every model.&lt;/strong&gt; I ran that test on three models. The other eight would show how general it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asking for the range differently.&lt;/strong&gt; I'd try asking for the 10th and 90th percentile as times rather than speeds, or asking for five scenarios, to see whether the ranges become more honest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The dataset&lt;/strong&gt; comes from Race Card's own pipeline: the same 79 legs and the same rows TabPFN saw. I re-ran TabPFN and all the simple baselines on exactly the published rows so the comparisons use identical data. TabPFN's score is within a tenth of a percent of the one in my Race Card post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; It's my own training history, so I've stripped it right back: no GPS, coordinates, activity names, IDs, gear, weather or time of day. A check script blocks the upload if anything else slips through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The task&lt;/strong&gt; uses Kaggle's &lt;code&gt;kaggle-benchmarks&lt;/code&gt; library. Each leg gets one prompt and a structured answer: low, median and high speed, plus a one-line reason. Code checks the answer against my actual time, with no LLM judge involved. The data is built into the task, so anyone can re-run it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every number in this post&lt;/strong&gt; comes from facts files generated from the downloaded runs. A script checks the post against them before I publish. The whole week's testing used about $38 of Kaggle's free model allowance; the extra TabPFN run with the race facts didn't use any of that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is still one athlete and 79 legs. Each model had one to three runs, rather than dozens.&lt;/li&gt;
&lt;li&gt;Only six legs are standalone half marathons. The finding is consistent across 10 of 11 models, but I wouldn't want that to obscure the fact that we're talking about six races.&lt;/li&gt;
&lt;li&gt;With the race name, a model could in principle know something about the event from its training. It can't know my result: my name doesn't appear anywhere in the prompt.&lt;/li&gt;
&lt;li&gt;DeepSeek-R1 never completed the task properly. It wrote out its reasoning for every leg but didn't give an answer in the required format. Gemma 4 31B kept hitting "heavy load" errors and never finished a run either. Neither is in the table.&lt;/li&gt;
&lt;li&gt;A handful of legs failed because of problems on the provider's side, at most two per run. I've left those out of the average for that run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/iamrobertmoore/tri-race-splits" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Run it yourself on Kaggle: Triathlon race splits&lt;/a&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tasks: &lt;a href="https://www.kaggle.com/benchmarks/tasks/iamrobertmoore/tri-race-splits" rel="noopener noreferrer"&gt;tri-race-splits&lt;/a&gt; (blind) and &lt;a href="https://www.kaggle.com/benchmarks/tasks/iamrobertmoore/tri-race-splits-named" rel="noopener noreferrer"&gt;tri-race-splits-named&lt;/a&gt; (told the race), plus &lt;a href="https://www.kaggle.com/benchmarks/tasks/iamrobertmoore/tri-race-splits-anon" rel="noopener noreferrer"&gt;tri-race-splits-anon&lt;/a&gt; (made-up names). The leaderboard shows each model's latest run, scored by the share of legs within 5%. Here, I've averaged every complete run.&lt;/li&gt;
&lt;li&gt;Dataset: &lt;a href="https://www.kaggle.com/datasets/iamrobertmoore/race-splits" rel="noopener noreferrer"&gt;race-splits&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Race Card, which started all this: &lt;a href="https://github.com/iamrobertmoore/race-card" rel="noopener noreferrer"&gt;github.com/iamrobertmoore/race-card&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever asked a chatbot to predict your race time, I'd love to hear how it went. And whether you told it which race you were doing.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>My mate wants a sub-5 70.3. Before recommending one, I made an open model predict 79 of my old race legs.</title>
      <dc:creator>Robert Moore</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:00:39 +0000</pubDate>
      <link>https://dev.to/iamrobertmoore/my-mate-wants-a-sub-5-703-before-recommending-one-i-made-an-open-model-predict-79-of-my-old-race-4kcp</link>
      <guid>https://dev.to/iamrobertmoore/my-mate-wants-a-sub-5-703-before-recommending-one-i-made-an-open-model-predict-79-of-my-old-race-4kcp</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My mate Sam wants to go under five hours at an IRONMAN 70.3. He hasn't decided which one yet, which is quite an important detail.&lt;/p&gt;

&lt;p&gt;A 70.3 is a 1.9 km swim, a 90 km ride and a half marathon. The distances stay the same; the day out does not. Weymouth's bike course climbs about 1,043 metres. Westfriesland's climbs 55. You can see how picking the right one might help.&lt;/p&gt;

&lt;p&gt;I've raced Weymouth three times, so I wasn't short of opinions. Before passing those on as advice, though, I thought it might be worth getting some numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Race Card&lt;/strong&gt; uses your own training to work out which 70.3 gives you the best shot at your goal time.&lt;/p&gt;

&lt;p&gt;Give it a Strava or Garmin export and it predicts a range for each leg, then runs 6,000 simulated race days on eight real courses. You get a chance of going under your goal at each one, plus a look at which leg would make the biggest difference if you improved it.&lt;/p&gt;

&lt;p&gt;It all runs on your laptop. Prior Labs' &lt;strong&gt;TabPFN&lt;/strong&gt; handles the predictions, and Google's &lt;strong&gt;Gemma 4&lt;/strong&gt;, through Ollama, reads the log and writes the plan. Both are open models. Nothing you've logged leaves the machine.&lt;/p&gt;

&lt;p&gt;Sam's training is his, so I tested it on mine first. If I raced tomorrow, this is what it reckons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Erkner, La Quinta, Türkiye, Westfriesland&lt;/strong&gt; (all flat): about 4:48, with an 85 to 86% chance of sub 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weymouth:&lt;/strong&gt; 5:06, with a 31% chance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swansea&lt;/strong&gt;, the hilliest: 5:08, with a 24% chance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost all of that gap is on the bike. At Weymouth, it reckons sub 5 becomes a coin flip if my bike gets 3.4% faster, or my run gets 6.5% faster. That's a much more useful conversation to have with Sam than just telling him Weymouth is hard. He's since run it on his own training. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live: &lt;a href="https://iamrobertmoore.github.io/race-card/" rel="noopener noreferrer"&gt;iamrobertmoore.github.io/race-card&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the 80-second tour, with me talking you through it:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/IwEAzisXApE" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A few things to have a play with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick a course&lt;/strong&gt; from the dropdown, or click any row in "Every course, ranked". The ring and finish time animate to that course.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag the bike slider&lt;/strong&gt; on Weymouth. At 3% faster, the chance goes from 31% to 47% and the bike time drops by 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scroll to "Checked before trusted"&lt;/strong&gt; to see how TabPFN did against four simpler guesses on the same races.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open "See all 79 legs"&lt;/strong&gt; to look through every past race leg it predicted, including how far off it was.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live page uses my training. There's deliberately no upload box: if you want to use your own data, you run it on your own machine using the code below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/iamrobertmoore" rel="noopener noreferrer"&gt;
        iamrobertmoore
      &lt;/a&gt; / &lt;a href="https://github.com/iamrobertmoore/race-card" rel="noopener noreferrer"&gt;
        race-card
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Which IRONMAN 70.3 gives you the best shot at your goal time? TabPFN + Gemma 4 on your own laptop, from your Strava export.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Race Card&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/iamrobertmoore/race-card/docs/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fiamrobertmoore%2Frace-card%2FHEAD%2Fdocs%2Fdemo.gif" alt="Race Card: dragging the bike slider moves the chance of a sub-5 at Weymouth from 31% to 47%, then the flat Erkner course shows 86%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which IRONMAN 70.3 gives you the best shot at your goal time?&lt;/strong&gt; Race Card reads your own training
export, predicts every leg of race day as a range, and ranks eight real 70.3 courses by your chance of
going under the goal. Then it shows which leg would move that chance most.&lt;/p&gt;
&lt;p&gt;It runs on your laptop with two open models, and nothing you've logged is uploaded anywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo, built from my own Strava export:&lt;/strong&gt; &lt;a href="https://iamrobertmoore.github.io/race-card/" rel="nofollow noopener noreferrer"&gt;https://iamrobertmoore.github.io/race-card/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I built it for my friend Sam, who wants to go under five hours at a 70.3 and hasn't picked which one
His training is his, so I tested it on mine first. He has since run it on his own export, on his own
laptop, and is working on his bike.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it found on my data&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;If I raced tomorrow: about 4:47 on the flat courses (around 86% chance…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/iamrobertmoore/race-card" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licence, and you can run the tests without downloading a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TabPFN does the predicting
&lt;/h3&gt;

&lt;p&gt;TabPFN is a tabular foundation model. You give it a table and ask it to predict a column, without setting up a training loop yourself. For each discipline, I give it one row per session: distance, climbing per km, how many days ago it was, my training load over the preceding 7 and 42 days, and whether it was a race. The thing it's trying to predict is speed.&lt;/p&gt;

&lt;p&gt;What makes it useful here is that it gives me a spread rather than a single number. I take the 5th to 95th percentile of likely speed for each leg on each course, and the page draws 6,000 race days from those spreads. The proportion that finish under five hours becomes the chance in the ring. Drag a slider and your browser runs that simulation again.&lt;/p&gt;

&lt;p&gt;It's quick, too. A full card from 4,484 sessions takes about 20 seconds on a CPU, with no GPU needed. The backtest takes a few minutes more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking it before trusting it
&lt;/h3&gt;

&lt;p&gt;Before handing Sam a prediction, I wanted to know whether it was any good. So I made it predict my past races, using only the training I'd logged before each race day. No peeking at what happened afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;62 of 79 race legs landed inside its 80% range.&lt;/strong&gt; That's 78%, which is about what you'd hope an honest 80% range would catch. The typical miss was 6.9%, with no consistent tendency to predict me too fast or too slow.&lt;/p&gt;

&lt;p&gt;Then I got round to the question I should probably have asked first: was TabPFN actually better than something simple? I tried four other ways of predicting the same 79 legs, all with the same cut-off:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Same 79 legs&lt;/th&gt;
&lt;th&gt;Typical miss&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradient-boosted trees, trained on exactly the same rows&lt;/td&gt;
&lt;td&gt;7.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same as my last race at that distance&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A straight-line fit&lt;/td&gt;
&lt;td&gt;11.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My recent training pace&lt;/td&gt;
&lt;td&gt;12.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A win, but not a thrashing. TabPFN was closer than the trees on 48 of 79 legs. The other useful difference is that the trees here give me one number, whereas TabPFN gives me the range the whole page depends on.&lt;/p&gt;

&lt;p&gt;There was one mistake it made rather reliably. I've raced Weymouth in 2018, 2019 and 2022, and it predicted my bike too fast every time, by 7 to 11%. It knows how much the course climbs, but not what Weymouth's hills and wind actually do to you. I've put that warning on the page: treat the bike predictions for hilly courses as optimistic. I certainly do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma reads the log
&lt;/h3&gt;

&lt;p&gt;For the model to learn what race effort looks like, it needs to know which sessions were races. Strava doesn't record that, and tagging 4,574 activities by hand wasn't something I wanted to make part of the job. Sam certainly wouldn't thank me for it.&lt;/p&gt;

&lt;p&gt;That's Gemma 4's job. It reads the activity titles through Ollama on the laptop and picks out the races. I'd already been through my own list by hand for my card, so I had something to mark its homework against. It took three goes.&lt;/p&gt;

&lt;p&gt;The first run took 47 minutes and got about two-thirds right. Gemma 4 thinks before answering by default. Useful for a hard question; less useful when it spends 45 seconds deliberating over "Morning Run". It also called old uploads with nothing but a date for a title races, while missing whole actual race days.&lt;/p&gt;

&lt;p&gt;To be fair, it found races I'd missed as well: five Canterbury 10s, a swimrun, a DNF, an Oysterman swim and a Hampstead Heath aquathlon I won in 2015. Those went into the answer key, along with another DNF I spotted while checking. So the human wasn't getting full marks either.&lt;/p&gt;

&lt;p&gt;By the third run, I'd turned thinking off, skipped titles written by the app rather than me, and given it the other sessions from the same day as context. I also spelt out the things it kept getting wrong, including Sufferfest sessions, bricks and "Open Water Swim".&lt;/p&gt;

&lt;p&gt;That version read 1,005 titles in under three minutes and found &lt;strong&gt;166 of my 181 races, with 6 false alarms&lt;/strong&gt;. I tuned the prompt on the same list I scored it against, so that's a best-case result, not an independent test. It still missed all three legs of Challenge Roth, which is quite a race to overlook.&lt;/p&gt;

&lt;p&gt;Gemma also writes the note on the card. It only gets the numbers Race Card has already calculated. If it puts a number in the note that wasn't in those facts, the note gets binned and it has another go. This is what it wrote in 13 seconds on my laptop:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hey Robert, remember the 70.3 Weymouth? Your chance of hitting sub 5 is 31%, which is lower than the 86% at the 70.3 Erkner. If you can shave 3% off your bike leg, your chance jumps to 47%. Let's nail that bike leg!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The courses are real
&lt;/h3&gt;

&lt;p&gt;The climbing figures come from IRONMAN's own course routes or finishers.com. I checked them on Friday. If a run's climbing isn't published, the page says so and treats it as flat rather than making something up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Things I got wrong first
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strava's export has two columns both called "Distance".&lt;/strong&gt; The first is in kilometres, except for swims, where it's metres with a comma in. My first parser turned a 2,192 metre swim into 2,192 km. Quite a morning in the water.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One race swim came out at 6.4 km/h.&lt;/strong&gt; That's faster than the 1,500 metre world record pace. Sadly, it was a GPS glitch. Anything outside a sensible speed range now gets dropped before the model sees it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The newest TabPFN wants an account and an accepted licence before it'll download.&lt;/strong&gt; Fine for me, but not what I wanted to mean by "just run it, Sam". TabPFN v2's weights download from Hugging Face without an account, so that's the version it uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma's first note said "Hey Sam" and then gave him my numbers.&lt;/strong&gt; Exactly the thing I'd said I wouldn't do. It's now explicitly told whose training the numbers come from, and the note on the page is addressed to me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The note said 48% while the slider said 47%.&lt;/strong&gt; Two simulations, different random draws, and a result hovering around 47.5. They now use the same draws and agree to the percent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My first design was beige and green and looked like a pension statement.&lt;/strong&gt; It's dark now, with rings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Handing it over
&lt;/h2&gt;

&lt;p&gt;I sent Sam the link on Sunday morning. He had a look through my numbers, then ran it on his own export, on his own laptop. That's the version I actually built it for. His numbers stayed on his machine, but he told me what it had said and that he'd found it "very insightful".&lt;/p&gt;

&lt;p&gt;Weymouth looks even less promising for him: an 11% chance of sub 5, against my 31%. He's not changing races yet, though. He wants to put the work into his bike and see whether he can close the gap. If it doesn't come together, he'll aim for a course where his odds are better.&lt;/p&gt;

&lt;p&gt;That's the decision I wanted him to be able to make. I wasn't trying to talk him out of Weymouth. I wanted him to know what choosing it costs him, and where the work needs to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Because this is someone's training log, not just a handy table of numbers.&lt;/p&gt;

&lt;p&gt;A Strava export contains heart rate, GPS tracks starting at your front door, and fifteen years of your mornings. Asking Sam to send all of that to a service just to help him pick a race didn't sit right with me. With open models, I don't have to. TabPFN and Gemma both run on the laptop, so the data stays there. I never saw Sam's file. I only know the number he chose to tell me.&lt;/p&gt;

&lt;p&gt;He doesn't need an account, an API key or a subscription, either. TabPFN v2 downloads without signing up, and Gemma runs offline through Ollama. It costs nothing to run, and there's nobody who can put the price up or switch it off before his race.&lt;/p&gt;

&lt;p&gt;Being able to see inside it also meant I could test it properly. I backtested it on my own races, found the places it got things wrong, and put those on the page. That's much harder with a closed API.&lt;/p&gt;

&lt;p&gt;There is a trade-off. A frontier model would probably have found more of my races, faster, than Gemma 4 running on a laptop. Reading the log is where a closed model would likely have won. But I'd have had to send fifteen years of activity titles to somebody else's server, and Gemma found 166 of 181 without any of them leaving the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with Claude in a cloud sandbox linked to my laptop, so there's no DevRelay transcript to embed. The &lt;a href="https://github.com/iamrobertmoore/race-card/commits/main" rel="noopener noreferrer"&gt;commit history&lt;/a&gt; is the record, starting with an empty repo on Friday morning.&lt;/p&gt;

&lt;p&gt;Claude wrote the code, ran the backtests and drafted the page. I made the calls, including a few worth mentioning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whose data it shows.&lt;/strong&gt; I asked whether I could just use my training and call it Sam's. Claude pushed back. It was right, and the first screen now says whose training you're looking at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma ran on my Mac, not in the sandbox.&lt;/strong&gt; Whenever the race-finding prompt changed, I ran it locally and pasted the score back. Three runs: 47 minutes, then 4, then under 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The baselines.&lt;/strong&gt; On Sunday morning, I went through the other TabPFN entries with Claude. The good ones all compared their model against something simple. Mine didn't. That's how the table above came about.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of TabPFN.&lt;/strong&gt; TabPFN does the predicting, rather than being an extra bolted on for the challenge. It takes my own sessions and predicts a full spread of speeds for each leg. Those spreads drive the 6,000 simulated race days behind every chance on the page. On the backtest of 79 of my race legs, 62 landed inside its 80% range. Its typical miss was smaller than the gradient-boosted trees, a straight-line fit, my recent pace or "same as last time".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 4 runs locally through Ollama and has two jobs. First, it finds the races in the activity log so the prediction model can learn what race effort looks like: 166 of my 181 races from 1,005 titles in under three minutes, with 6 false alarms. Then it writes the note on the card. Any number it invents gets rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;A few limits worth being clear about before anyone plans a season around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's based on one person's training. It isn't coaching or medical advice.&lt;/li&gt;
&lt;li&gt;It knows distance and climbing, but nothing about heat, wind, sea state or whether the swim is non-wetsuit.&lt;/li&gt;
&lt;li&gt;It simulates each leg separately, which makes the range a little narrower than real life. If you're having a bad day, it tends to follow you through all three.&lt;/li&gt;
&lt;li&gt;Transitions are a flat 7 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I picked Weymouth three times. Nobody showed me this first.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
    <item>
      <title>Five things I found in Google's Agent Registry and Agent Gateway that aren't in the docs</title>
      <dc:creator>Robert Moore</dc:creator>
      <pubDate>Thu, 13 Aug 2026 10:33:34 +0000</pubDate>
      <link>https://dev.to/iamrobertmoore/five-things-i-found-in-googles-agent-registry-and-agent-gateway-that-arent-in-the-docs-236f</link>
      <guid>https://dev.to/iamrobertmoore/five-things-i-found-in-googles-agent-registry-and-agent-gateway-that-arent-in-the-docs-236f</guid>
      <description>&lt;p&gt;&lt;em&gt;I created this piece of content and the build for the purposes of entering the All Things Agentic Hackathon. The project is &lt;a href="https://github.com/iamrobertmoore/agent-attest" rel="noopener noreferrer"&gt;agent-attest&lt;/a&gt;, if you want the context.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I spent a couple of weeks building on Google's new Gemini Enterprise Agent Platform, and hit five things that cost me real time because none of them are documented. Writing them down so the next person can search for the error message and find something.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Agent Registry silently requires interface URLs to be globally unique
&lt;/h2&gt;

&lt;p&gt;This one cost me the most.&lt;/p&gt;

&lt;p&gt;I had four agents sharing one container image, which meant they all defaulted to the same port. Registering the first one worked. The other three vanished.&lt;/p&gt;

&lt;p&gt;Not errored. Vanished. &lt;code&gt;services.create&lt;/code&gt; returned HTTP 200 with an operation name, my code treated that as success, and moved on. The Firestore records I wrote alongside said those agents were registered. The registry said otherwise.&lt;/p&gt;

&lt;p&gt;The reason only turned up when I went looking at the operations list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The request was invalid: generic::invalid_argument:
            Interface URL 'http://localhost:8080' is already in use by another service."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things make this expensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;services.create&lt;/code&gt; returns a &lt;strong&gt;long-running operation&lt;/strong&gt;, and the uniqueness check happens asynchronously inside it. If you take the 200 as success you never see the failure.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;first&lt;/strong&gt; registration always works. So it looks like a race condition, not a constraint on a field nobody thinks of as a key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reference for &lt;code&gt;Service.interfaces[]&lt;/code&gt; says nothing about uniqueness. If you're registering more than one agent, give each a distinct URL and poll the operation to &lt;code&gt;done&lt;/code&gt; rather than trusting the acknowledgement.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Agent Gateway doesn't support A2A
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;protocols&lt;/code&gt; field on an AgentGateway accepts exactly two values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;protocols&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MCP&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PROTOCOL_UNSPECIFIED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no A2A.&lt;/p&gt;

&lt;p&gt;That matters if you're building the kind of thing the docs encourage. ADK's inter-agent transport is A2A: &lt;code&gt;RemoteA2aAgent&lt;/code&gt; on the calling side, &lt;code&gt;to_a2a()&lt;/code&gt; on the serving side, agent cards at &lt;code&gt;/.well-known/&lt;/code&gt;. So if you build a fleet where agents delegate to each other over the network, Agent Gateway can't sit in front of those calls. It handles MCP tool and server traffic only.&lt;/p&gt;

&lt;p&gt;I only found this because the import validator prints its entire JSON schema when you hand it a document it doesn't like, which turned out to be the single most useful error message on the whole platform. Worth remembering as a technique.&lt;/p&gt;

&lt;p&gt;While I was in there, three more things from that same schema dump that aren't in the prose docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;top-level &lt;code&gt;network&lt;/code&gt; and &lt;code&gt;subnetwork&lt;/code&gt; are rejected. &lt;code&gt;additionalProperties&lt;/code&gt; is false, and networking goes under &lt;code&gt;networkConfig&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;you have to pick &lt;code&gt;googleManaged&lt;/code&gt; or &lt;code&gt;selfManaged&lt;/code&gt;: a Google-run proxy in a tenant project, or attaching to an existing load balancer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;registries&lt;/code&gt; uses an unusual double-slash form, &lt;code&gt;//agentregistry.googleapis.com/projects/{p}/locations/{l}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Google's own Agent Gateway samples set failOpen: true
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;failOpen&lt;/code&gt; on an authorization extension defaults to &lt;code&gt;false&lt;/code&gt;, and the reference is clear about what that means: if the extension times out or fails, request processing stops and the client gets a 500.&lt;/p&gt;

&lt;p&gt;That's the right default for an authorization control. But every authz sample in the Agent Gateway docs sets it to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Copy one of those and your authorization check quietly becomes advisory. Anything that makes the callout unreachable also makes it irrelevant, and an attacker can arrange that. Set it to &lt;code&gt;false&lt;/code&gt; explicitly so nobody later assumes the sample was fine.&lt;/p&gt;

&lt;p&gt;Related, and worse: &lt;strong&gt;&lt;code&gt;CONTENT_AUTHZ&lt;/code&gt; can't be made fail-closed at all.&lt;/strong&gt; Under &lt;code&gt;FULL_DUPLEX_STREAMED&lt;/code&gt; the docs say the proxy fail-opens up to the first chunk of body data, and that connection and header failures fail open &lt;em&gt;regardless&lt;/em&gt; of what you set &lt;code&gt;failOpen&lt;/code&gt; to. If you need a genuine fail-closed guarantee, &lt;code&gt;REQUEST_AUTHZ&lt;/code&gt; is your only option.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A googleManaged gateway is describable long before it's usable
&lt;/h2&gt;

&lt;p&gt;I created an Agent Gateway. It came back with a name, an etag and timestamps. &lt;code&gt;describe&lt;/code&gt; found it straight away. Everything looked fine.&lt;/p&gt;

&lt;p&gt;Then binding an authorization policy to it failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to update tenant configuration for AgentGateway:
generic::cancelled: agent gateway "agent-attest-gw" is not yet provisioned in the tenant project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;googleManaged&lt;/code&gt; gateway gets provisioned asynchronously in a Google-run tenant project. The resource exists in your project well before that finishes. There's no state field to poll, so the only signal you get is the error on whatever operation needs it.&lt;/p&gt;

&lt;p&gt;I waited. Four hours later it was still the same message.&lt;/p&gt;

&lt;p&gt;If you're scripting this, retry on that specific string and fail fast on anything else. Don't blanket-retry, or you'll turn an unrelated error into a timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A private Cloud Run service 404s, it doesn't 403
&lt;/h2&gt;

&lt;p&gt;Not Agent Platform specific, but it ate an afternoon so it's going in.&lt;/p&gt;

&lt;p&gt;Deploy a Cloud Run service with &lt;code&gt;--no-allow-unauthenticated&lt;/code&gt; and an unauthorised caller gets &lt;strong&gt;404 from the Google frontend&lt;/strong&gt;, not 403. That's deliberate, so private services don't leak their own existence. It also means "wrong URL" and "no access" are indistinguishable from outside.&lt;/p&gt;

&lt;p&gt;Now stack two more things on top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gcloud run services describe --format='value(status.url)'&lt;/code&gt; can hand you a legacy hostname that doesn't route, while &lt;code&gt;gcloud run services list&lt;/code&gt; shows the one that does&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gcloud auth print-identity-token&lt;/code&gt; with no &lt;code&gt;--audiences&lt;/code&gt; mints a token audienced to gcloud's own OAuth client, which Cloud Run rejects. And &lt;code&gt;--audiences&lt;/code&gt; &lt;strong&gt;requires a service account&lt;/strong&gt;, so as a human you can't mint one it will accept at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: a service reporting &lt;code&gt;Ready: True&lt;/code&gt; and &lt;code&gt;RoutesReady: True&lt;/code&gt;, 404ing on every URL you try, that is completely healthy. I spent a long time debugging routing when the problem was authentication.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcloud run services proxy&lt;/code&gt; handles the auth for you. But note it reads &lt;code&gt;status.url&lt;/code&gt; too, so if that field is stale the proxy fails as well, and a proxy failure tells you nothing about your container.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Four of these five share a shape: &lt;strong&gt;something failed, and the thing reporting back said success or said nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An operation that returns 200 and fails later. A launcher that logs &lt;code&gt;Workload completed&lt;/code&gt; after refusing to run your workload (that's a Confidential Space one I left out). A gateway that reports created when it means queued. A 404 that means 403.&lt;/p&gt;

&lt;p&gt;The habit I ended up with was to stop trusting acknowledgements and check the thing itself. Poll the operation. Read the resource. And when a system gives you a genuinely detailed error, like that schema dump, treat it as the documentation, because sometimes it is.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written for the All Things Agentic Hackathon. Code is at &lt;a href="https://github.com/iamrobertmoore/agent-attest" rel="noopener noreferrer"&gt;github.com/iamrobertmoore/agent-attest&lt;/a&gt; if you want to see what these were getting in the way of.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
