<?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: Tatiana Radchenko 🇩🇰</title>
    <description>The latest articles on DEV Community by Tatiana Radchenko 🇩🇰 (@taniacoder).</description>
    <link>https://dev.to/taniacoder</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%2F3999372%2F7af2116b-b834-4900-9ae3-ee2e2d294004.jpg</url>
      <title>DEV Community: Tatiana Radchenko 🇩🇰</title>
      <link>https://dev.to/taniacoder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/taniacoder"/>
    <language>en</language>
    <item>
      <title>I gave my drift monitor a denominator. The first thing it exposed was a hole in my own data collection</title>
      <dc:creator>Tatiana Radchenko 🇩🇰</dc:creator>
      <pubDate>Fri, 04 Sep 2026 18:23:38 +0000</pubDate>
      <link>https://dev.to/taniacoder/i-gave-my-drift-monitor-a-denominator-the-first-thing-it-exposed-was-a-hole-in-my-own-data-5508</link>
      <guid>https://dev.to/taniacoder/i-gave-my-drift-monitor-a-denominator-the-first-thing-it-exposed-was-a-hole-in-my-own-data-5508</guid>
      <description>&lt;p&gt;SEISMOGRAPH is an open-source early-warning network for silent drift in&lt;br&gt;
third-party LLM APIs -- the 2am question of whether the model changed&lt;br&gt;
underneath you or your prompt is simply worse today. It runs a fixed&lt;br&gt;
canary suite against provider endpoints on a schedule, ships only hashes&lt;br&gt;
and differentially-private aggregates, and publishes a public board.&lt;/p&gt;

&lt;p&gt;Two things about its current state, up front, because everything below&lt;br&gt;
depends on them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There is exactly one observer, and it is me.&lt;/strong&gt; This is a one-person
project, and that matters for reading everything below. A public drift alert
requires cross-observer agreement from three independent observers. At
one observer, no public alert can fire &lt;em&gt;by construction&lt;/em&gt;. That is a
property of the design, not a gap in it: a single organisation seeing
a wobble is not evidence that a provider changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The live board's baseline was re-established on 2026-08-04.&lt;/strong&gt; There
is no continuous history before that date. Separately, on historical
replay of a known incident, a seeded backtest flags it 38 days before
the postmortem. That is a synthetic replay, not a live catch, and I do
not describe it as one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last week I shipped a small read-side change: &lt;code&gt;/v1/weather&lt;/code&gt; now&lt;br&gt;
publishes, alongside each metric, the number of samples it rests on and&lt;br&gt;
the time bounds of the window those samples came from. Publishing a rate&lt;br&gt;
without its base is a defect; I had been doing it.&lt;/p&gt;

&lt;p&gt;This is what the denominator found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The board, read 2026-09-04
&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;google/gemini-3.5-flash-lite&lt;/th&gt;
&lt;th&gt;mistral/mistral-small-latest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;status&lt;/td&gt;
&lt;td&gt;STABLE&lt;/td&gt;
&lt;td&gt;STABLE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sample_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;json_sample_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;length_sample_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window_start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-08-28T01:34:25Z&lt;/td&gt;
&lt;td&gt;2026-08-28T17:27:23Z&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;window_end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-09-04T09:46:39Z&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2026-09-02T09:38:39Z&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;window span&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;176.2 h&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;112.2 h&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mean inter-sample interval&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.6 h&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12.5 h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The probe is scheduled twice a day. Ten samples should therefore span&lt;br&gt;
about &lt;strong&gt;108 hours&lt;/strong&gt;. The google leg's ten samples span 176.2 hours: it&lt;br&gt;
is emitting at 61% of its own cadence. And the mistral leg's window&lt;br&gt;
ends on 2026-09-02 -- as of the read at &lt;strong&gt;2026-09-04 18:00 UTC&lt;/strong&gt; that&lt;br&gt;
row is &lt;strong&gt;56 hours old&lt;/strong&gt;, four scheduled slots ago. Both figures are&lt;br&gt;
recomputable from the bounds in the table by anyone reading this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every count on that table reads 10.&lt;/strong&gt; Both legs. The whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the counts could never have found it
&lt;/h2&gt;

&lt;p&gt;The probe runs a 50-prompt canary suite and flushes one aggregated row&lt;br&gt;
per run. If any single prompt exhausts its retries, the runner discards&lt;br&gt;
the entire suite: flushing at a reduced &lt;em&gt;n&lt;/em&gt; would change the&lt;br&gt;
differential-privacy sensitivity of the stream (&lt;code&gt;MAX/n&lt;/code&gt;), so a partial&lt;br&gt;
batch is not a smaller sample, it is a differently-calibrated one.&lt;/p&gt;

&lt;p&gt;The consequence is that a lost run writes &lt;strong&gt;no row at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A class of loss that removes rows entirely is invisible to any count,&lt;br&gt;
however many counts you publish. &lt;code&gt;sample_count&lt;/code&gt; answers &lt;em&gt;how many rows&lt;br&gt;
survived into this metric&lt;/em&gt;. Only the window bounds answer &lt;em&gt;how long it&lt;br&gt;
took to collect them&lt;/em&gt; -- and it was the second question that found the&lt;br&gt;
defect. I did not anticipate this when I scoped the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it moved legs
&lt;/h2&gt;

&lt;p&gt;The interesting part is what happened between two reads 48 hours apart.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;leg&lt;/th&gt;
&lt;th&gt;2026-09-02&lt;/th&gt;
&lt;th&gt;2026-09-04&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;google window span&lt;/td&gt;
&lt;td&gt;196.2 h&lt;/td&gt;
&lt;td&gt;176.2 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;google &lt;code&gt;avg_output_length&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;131.060&lt;/td&gt;
&lt;td&gt;130.398&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral window span&lt;/td&gt;
&lt;td&gt;112.2 h&lt;/td&gt;
&lt;td&gt;112.2 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral &lt;code&gt;avg_output_length&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;89.42253&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89.42253&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;google partially recovered. mistral froze -- byte-identical values,&lt;br&gt;
byte-identical bounds, because not one new row arrived. The leg I was&lt;br&gt;
about to describe as the clean one is the leg that is currently dark.&lt;/p&gt;

&lt;p&gt;Both still publish &lt;code&gt;10 / 10 / 10&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the run logs actually say
&lt;/h2&gt;

&lt;p&gt;I reconciled the board against the scheduler for the window&lt;br&gt;
2026-08-24T05:58Z to 2026-09-01T10:11Z:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;16&lt;/strong&gt; scheduled runs fired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9&lt;/strong&gt; succeeded, &lt;strong&gt;7&lt;/strong&gt; failed, &lt;strong&gt;0&lt;/strong&gt; were cancelled at the run level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10&lt;/strong&gt; rows reached the board from the google leg.&lt;/li&gt;
&lt;li&gt;So &lt;strong&gt;6&lt;/strong&gt; runs produced no google row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ten rows from nine run-level successes is not a typo. A run is marked&lt;br&gt;
failed if &lt;strong&gt;any&lt;/strong&gt; provider leg fails, and a failed run can still carry a&lt;br&gt;
successful google leg -- which is the first hint that run-level status is&lt;br&gt;
a poor proxy for per-leg collection.&lt;/p&gt;

&lt;p&gt;That kills the comfortable explanations. The scheduler ran. Nothing hit&lt;br&gt;
a job timeout. The rows were lost &lt;em&gt;inside&lt;/em&gt; runs that happened.&lt;/p&gt;

&lt;p&gt;Per-run detail, from the emission logs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;run&lt;/th&gt;
&lt;th&gt;leg&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;#118&lt;/td&gt;
&lt;td&gt;google&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;40/50 prompts completed&lt;/code&gt;, 10 failed ids scattered mid-suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#122&lt;/td&gt;
&lt;td&gt;google&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;49/50 prompts completed&lt;/code&gt;, &lt;strong&gt;one&lt;/strong&gt; failed id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#126&lt;/td&gt;
&lt;td&gt;mistral&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0/50 prompts completed&lt;/code&gt;, all fifty failed, job dead in 75 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The emission policy is printed by the probe itself: &lt;code&gt;pacing: 4500 ms&lt;br&gt;
between prompts, &amp;lt;= 2 retries on 429/503&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the mechanism on the google leg is not a budget running out. It is&lt;br&gt;
&lt;strong&gt;one prompt out of fifty losing three attempts to a transient 429, and&lt;br&gt;
taking the other forty-nine with it.&lt;/strong&gt; In run #122 the suite was 98%&lt;br&gt;
complete and published nothing.&lt;/p&gt;

&lt;p&gt;The mistral leg is a different failure entirely: zero completions in 75&lt;br&gt;
seconds is not rate-limit turbulence, it is a hard condition at the&lt;br&gt;
endpoint. I do not yet know which, and that is its own finding -- the&lt;br&gt;
run log records the failed prompt ids and nothing else. No status code,&lt;br&gt;
no response body, no retry spend. &lt;strong&gt;I built a drift detector whose own&lt;br&gt;
collection failures are not diagnosable from its own logs.&lt;/strong&gt; That gets&lt;br&gt;
fixed before the collection logic does.&lt;/p&gt;

&lt;p&gt;One more thing worth stating because it invalidates arithmetic I had&lt;br&gt;
been doing: scheduled runs fire &lt;strong&gt;2.5 to 4.5 hours after&lt;/strong&gt; their cron&lt;br&gt;
slot. Any reasoning that treats missing samples as exact multiples of&lt;br&gt;
the 12-hour interval is unsound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that matters: the surviving samples are biased, not just sparse
&lt;/h2&gt;

&lt;p&gt;This is the finding I would most want a reader to take away, and it is&lt;br&gt;
uncomfortable.&lt;/p&gt;

&lt;p&gt;A run is discarded when prompts exhaust retries on 429. 429 correlates&lt;br&gt;
with provider load. And provider load under stress is precisely the&lt;br&gt;
condition under which a provider is most likely to shift behaviour --&lt;br&gt;
it is the mechanism this project exists to detect.&lt;/p&gt;

&lt;p&gt;So the samples that survive systematically exclude the periods most&lt;br&gt;
worth measuring. The google leg's drift readings are computed over a&lt;br&gt;
&lt;strong&gt;censored sample&lt;/strong&gt;, censored by a variable correlated with the thing&lt;br&gt;
being measured. This is not a footnote about data quality. It changes&lt;br&gt;
what the number means.&lt;/p&gt;

&lt;p&gt;For completeness: across the two reads neither leg shows a drift&lt;br&gt;
signal. google's normalised JSON validity moved +0.0016 in raw units&lt;br&gt;
(+0.12 sd of the per-batch DP noise) and its average output length&lt;br&gt;
-0.66 characters (-0.33 sd); mistral moved by exactly zero because it&lt;br&gt;
has no new data. &lt;strong&gt;Read those as "nothing to report from a sample I&lt;br&gt;
have just told you is compromised", not as "the providers are stable".&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, stated rather than buried
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One observer.&lt;/strong&gt; No public alert can fire. Everything above is
single-organisation fleet data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The censoring above.&lt;/strong&gt; The google leg's numbers are not a random
sample of provider behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlapping windows.&lt;/strong&gt; The last-10 window slides; two successive
reads share rows, so a difference between them is not a difference
between independent samples. The sd figures are indicative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The window is last-10-by-id, not time-bounded.&lt;/strong&gt; Publishing the
bounds makes its age visible; it does not change what it selects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counts do not say why a row was excluded.&lt;/strong&gt; Enough for a
denominator, not enough for a diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No staleness signal.&lt;/strong&gt; mistral's 56-hour-old row is visible on the
board but not flagged, because "stale after N hours" is a judgement
that needs its own defence. The timestamps are published; the reader
concludes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing volume is not authenticated.&lt;/strong&gt; Sample counts on the
public payload are not yet quorum-gated, so forged volume would
raise apparent support for an average. Open, tracked, undefended.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why publish this
&lt;/h2&gt;

&lt;p&gt;Because the honest version is more useful than the polished one, and&lt;br&gt;
because the bottleneck is structural rather than technical: at one&lt;br&gt;
observer this network is correct and silent. It becomes an early-warning&lt;br&gt;
system at three.&lt;/p&gt;

&lt;p&gt;If you run a canary suite against a provider API -- any provider, any&lt;br&gt;
suite -- and would consider contributing observations, or just want to&lt;br&gt;
tell me the collection design is wrong, the code and the board are open.&lt;/p&gt;

&lt;p&gt;Engine: &lt;strong&gt;SEISMOGRAPH&lt;/strong&gt; (Apache-2.0). Service: &lt;strong&gt;Drift Defense&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Board: &lt;a href="https://seismograph-weather.onrender.com/dashboard" rel="noopener noreferrer"&gt;https://seismograph-weather.onrender.com/dashboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/Tania-coder/SEISMOGRAPH" rel="noopener noreferrer"&gt;https://github.com/Tania-coder/SEISMOGRAPH&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Probe: &lt;code&gt;pip install seismograph-probe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;DOI: &lt;a href="https://doi.org/10.5281/zenodo.21045517" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.21045517&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All figures above are measured, dated, and reproducible from the public&lt;br&gt;
endpoint and the public Actions history at the time of writing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>observability</category>
      <category>opensource</category>
      <category>llm</category>
    </item>
    <item>
      <title>Did the model get worse, or is it just you? How to tell when an LLM API silently changes</title>
      <dc:creator>Tatiana Radchenko 🇩🇰</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:30:34 +0000</pubDate>
      <link>https://dev.to/taniacoder/did-the-model-get-worse-or-is-it-just-you-how-to-tell-when-an-llm-api-silently-changes-1lia</link>
      <guid>https://dev.to/taniacoder/did-the-model-get-worse-or-is-it-just-you-how-to-tell-when-an-llm-api-silently-changes-1lia</guid>
      <description>&lt;p&gt;You ship a feature on top of &lt;code&gt;gpt-x&lt;/code&gt; or &lt;code&gt;claude-y&lt;/code&gt;. It works. Three weeks later your users say the outputs feel worse — vaguer, sloppier, failing on the same prompts that used to pass. You didn't change your code. Did the model change, or are you imagining it?&lt;/p&gt;

&lt;p&gt;This question eats an astonishing number of engineering hours, and it almost always dissolves into a vibes argument: half the thread says "it's clearly worse", the other half says "you have no evidence, it's confirmation bias." Both sides are right, because nobody has instrumented the one thing that would settle it. Here is how to settle it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your gut is useless here (and so is a single comparison)
&lt;/h2&gt;

&lt;p&gt;Two things move at once when a hosted model "feels worse":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The provider's side&lt;/strong&gt; — a new checkpoint, a routing change, an inference optimization (batching, speculative decoding, quantization), or an infrastructure bug. You are not told when these happen. Often the provider doesn't announce it, and sometimes it isn't even a model change — it's an infra regression that only &lt;em&gt;looks&lt;/em&gt; like the model got dumber.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your side&lt;/strong&gt; — a prompt tweak, a temperature change, a new library version, a different context length, or just which prompts you happened to hit today.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From a single account, on a handful of anecdotal prompts, these two are &lt;strong&gt;indistinguishable&lt;/strong&gt;. That's the trap. You need two things to escape it: an &lt;em&gt;objective, dated signal&lt;/em&gt;, and a way to tell "it changed for me" from "it changed for everyone."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Fix a canary suite
&lt;/h2&gt;

&lt;p&gt;Pick a small, frozen set of prompts — 20 to 100 — that exercise the capabilities you actually depend on. Keep it small and cheap; this is a smoke detector, not a benchmark. Freeze it: same prompts, same parameters (&lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt;), same model string, forever. The moment you change the suite you lose your baseline.&lt;/p&gt;

&lt;p&gt;For each canary, define a &lt;strong&gt;cheap scalar&lt;/strong&gt; you can compute automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON/schema validity rate (did it return parseable, valid output?)&lt;/li&gt;
&lt;li&gt;Output length (a sudden shift often precedes a quality change)&lt;/li&gt;
&lt;li&gt;A pass/fail against a fixed assertion (does the function it wrote compile?)&lt;/li&gt;
&lt;li&gt;A rubric score from a cheap grader&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You are not trying to measure "quality" in the abstract. You are trying to detect &lt;em&gt;change&lt;/em&gt;. A boring, stable scalar is exactly what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Record a baseline while things are good
&lt;/h2&gt;

&lt;p&gt;Run the suite on a schedule — hourly, daily, whatever your budget allows — and store &lt;code&gt;(timestamp, prompt_id, metric)&lt;/code&gt;. Do this &lt;strong&gt;before&lt;/strong&gt; you suspect anything. The baseline is the whole game: without "what normal looked like last month" you have nothing to compare today against. Estimate the mean &lt;code&gt;mu0&lt;/code&gt; and standard deviation &lt;code&gt;sigma0&lt;/code&gt; of your metric per prompt from a stable window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Run a change-point test, not a threshold
&lt;/h2&gt;

&lt;p&gt;A fixed threshold ("alert if success rate &amp;lt; 90%") is noisy and late. What you want is a &lt;strong&gt;sequential change-point detector&lt;/strong&gt; that accumulates small, consistent shifts and fires with a timestamp. The classic is &lt;strong&gt;CUSUM&lt;/strong&gt; (Page, 1954). It's a few lines:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cusum_alarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sigma0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Two-sided CUSUM on standardized observations.
    k = slack (in std devs), h = alarm threshold.
    Returns the index where a sustained shift is detected, or None.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;s_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s_neg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mu0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;sigma0&lt;/span&gt;          &lt;span class="c1"&gt;# standardize against the baseline
&lt;/span&gt;        &lt;span class="n"&gt;s_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s_pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# catches upward shifts
&lt;/span&gt;        &lt;span class="n"&gt;s_neg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s_neg&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# catches downward shifts
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s_pos&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;s_neg&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;                     &lt;span class="c1"&gt;# dated change-point
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feed it your per-prompt metric over time. Instead of "feels worse", you now get &lt;em&gt;"json_success_rate on prompt #14 stepped down starting Tuesday"&lt;/em&gt; — a claim with a date on it that you can put in a ticket. (&lt;code&gt;h=5.0&lt;/code&gt;, &lt;code&gt;k=0.5&lt;/code&gt; are reasonable starting points for standardized data; tune on your own history. Bayesian Online Change-Point Detection is a fancier alternative if you want posterior probabilities.)&lt;/p&gt;

&lt;p&gt;This alone puts you ahead of ~95% of "the model got worse" threads, which never get past anecdotes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — The part one account can never answer: "is it just me?"
&lt;/h2&gt;

&lt;p&gt;Here's the ceiling you hit: even with a perfect change-point signal, &lt;strong&gt;you still can't tell a provider-side change from your own drift&lt;/strong&gt;, because from one seat they look identical. Your CUSUM fired — but was it the model, or the LlamaIndex bump you did last week, or a flaky region?&lt;/p&gt;

&lt;p&gt;The only way to separate them is &lt;strong&gt;correlation across independent observers&lt;/strong&gt;. If your canary fires on Tuesday, and several &lt;em&gt;other&lt;/em&gt; organizations watching the &lt;em&gt;same&lt;/em&gt; model on the &lt;em&gt;same&lt;/em&gt; metric also fire around Tuesday, that agreement is the signal that it's provider-side — not your harness. One observer has N=1 forever; that's why these threads never converge. This is a genuinely different kind of detection: not "monitor my app" (every observability tool does that), but "did this change for &lt;em&gt;everyone&lt;/em&gt;, privately confirmed across the fleet."&lt;/p&gt;

&lt;p&gt;That cross-observer angle is what &lt;a href="https://github.com/Tania-coder/SEISMOGRAPH" rel="noopener noreferrer"&gt;SEISMOGRAPH&lt;/a&gt; is built around: a lightweight probe runs your canary suite, a change-point detector flags shifts locally, and only privacy-preserving distributional features — never your prompts or outputs — are shared, so a public "model weather" alert only fires when &lt;em&gt;multiple independent observers agree&lt;/em&gt;. A single org's signal stays private.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does this actually work? A worked example
&lt;/h2&gt;

&lt;p&gt;The method isn't hypothetical. Take the September 2025 Anthropic incident: for a stretch, &lt;code&gt;claude-sonnet-4&lt;/code&gt; degraded because of infrastructure bugs — notably a context-window routing error — &lt;strong&gt;not&lt;/strong&gt; a model update (Anthropic said so explicitly in &lt;a href="https://www.anthropic.com/engineering/a-postmortem-of-three-recent-issues" rel="noopener noreferrer"&gt;their postmortem&lt;/a&gt;). Exactly the "is it the model or the infra?" fog this whole article is about.&lt;/p&gt;

&lt;p&gt;Replaying a canary suite over that period with the CUSUM method above: &lt;strong&gt;a seeded backtest flags it 38 days before the postmortem.&lt;/strong&gt; Not a live catch — a reproducible backtest, seeded and open — but it shows the signal was sitting in cheap distributional metrics well before the official acknowledgement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Probe (open source, Apache-2.0):&lt;/strong&gt; &lt;code&gt;pip install seismograph-probe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live "model weather" dashboard:&lt;/strong&gt; &lt;a href="https://driftdefense.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=evergreen_howto" rel="noopener noreferrer"&gt;https://driftdefense.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=evergreen_howto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code + method:&lt;/strong&gt; &lt;a href="https://github.com/Tania-coder/SEISMOGRAPH" rel="noopener noreferrer"&gt;https://github.com/Tania-coder/SEISMOGRAPH&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need any of that to start — the four steps above work with a cron job, a CSV, and the ten lines of CUSUM. But if you want the "is it just me?" half answered, that needs more than one observer, and that's the part worth federating.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Did GPT / Claude / Gemini actually get worse recently?&lt;/strong&gt;&lt;br&gt;
Sometimes yes, sometimes it's your setup, and often it's a provider-side infra change that isn't a "worse model" at all. The point of this article is that you shouldn't answer from vibes — instrument it and get a dated signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can I prove a hosted model changed?&lt;/strong&gt;&lt;br&gt;
You can't &lt;em&gt;prove&lt;/em&gt; it from one account, but you can get strong objective evidence: a frozen canary suite + a baseline + a change-point test gives you a dated shift. Provider-side vs. local drift is only separable with agreement across multiple independent observers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isn't this just LLM observability / evals?&lt;/strong&gt;&lt;br&gt;
Related but different. Evals score quality at a point in time; observability watches &lt;em&gt;your&lt;/em&gt; traffic. This is about detecting &lt;em&gt;change over time&lt;/em&gt; in a hosted dependency you don't control — and, uniquely, correlating that change across organizations to rule out "it's just me."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What metrics should I track?&lt;/strong&gt;&lt;br&gt;
Start with the cheapest ones that move when quality moves: schema/JSON validity, output length, and a single fixed pass/fail per canary. Add a rubric score later if you want resolution.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your LLM didn't get worse. It changed — and nobody told you.</title>
      <dc:creator>Tatiana Radchenko 🇩🇰</dc:creator>
      <pubDate>Thu, 02 Jul 2026 11:22:37 +0000</pubDate>
      <link>https://dev.to/taniacoder/your-llm-didnt-get-worse-it-changed-and-nobody-told-you-4ecl</link>
      <guid>https://dev.to/taniacoder/your-llm-didnt-get-worse-it-changed-and-nobody-told-you-4ecl</guid>
      <description>&lt;p&gt;It's 2am. Your JSON parse errors are up 12%. Latency: normal. Uptime: 100%. Your prompt didn't change. Your code didn't change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it you, or did the model silently change underneath you?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every team building on third-party LLM APIs eventually hits this moment. And the frustrating part: your monitoring stack can't answer the question. Latency, error rates, uptime — all green. The thing that changed is &lt;em&gt;behavior&lt;/em&gt;, and behavior emits no infrastructure signal.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/Tania-coder/SEISMOGRAPH" rel="noopener noreferrer"&gt;SEISMOGRAPH&lt;/a&gt; — an open-source, privacy-preserving early-warning network for exactly this failure mode. This post covers what silent drift is, how to detect it without leaking a single prompt, and a reproducible backtest where the detector flags a real, publicly documented provider incident &lt;strong&gt;38 days before the official postmortem&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "silent drift" actually is
&lt;/h2&gt;

&lt;p&gt;Provider APIs do not broadcast behavioral changes. An endpoint that returns 200 can still start producing subtly different outputs: degraded JSON fidelity, shifted response-length distributions, changed reasoning patterns.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. In September 2025, Anthropic published a detailed postmortem describing &lt;strong&gt;three infrastructure bugs&lt;/strong&gt; that silently degraded Claude output quality across August–September 2025. The most long-lived one: a context-window routing error, introduced on August 5, that misrouted a fraction of Claude Sonnet 4 requests to servers configured for the 1M-token context window. It started at ~0.8% of traffic. On August 29 a load-balancer change escalated it to ~16%. The public postmortem landed on September 17.&lt;/p&gt;

&lt;p&gt;Note what this incident was &lt;em&gt;not&lt;/em&gt;: it was not a model update, and not intentional degradation — Anthropic was explicit about that. It was infrastructure. Which is precisely the point: &lt;strong&gt;the provider's own uptime and latency dashboards stayed green the whole time.&lt;/strong&gt; The signal only existed at the semantic layer.&lt;/p&gt;

&lt;p&gt;So the question I wanted to answer in Phase 0 of the project: would a lightweight behavioral canary have caught it, and how early?&lt;/p&gt;

&lt;h2&gt;
  
  
  The detection idea: canaries + change-point statistics
&lt;/h2&gt;

&lt;p&gt;The approach is deliberately boring:&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;canary suite&lt;/strong&gt; — a fixed, content-addressed set of prompts (≤200, temperature 0) — runs on a schedule against the API you depend on. Deterministic prompts against a deterministic-ish endpoint give you a behavioral baseline. Every suite version is immutably hash-addressed, so a baseline can never silently mutate either.&lt;/p&gt;

&lt;p&gt;Each response is reduced to &lt;strong&gt;distributional features&lt;/strong&gt;: JSON parse success rate, output length, result counts. Not the text — the shape of the behavior.&lt;/p&gt;

&lt;p&gt;Those features feed a &lt;strong&gt;Page-CUSUM change-point detector&lt;/strong&gt; per &lt;code&gt;(model_tuple, metric_name)&lt;/code&gt; stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S+(n) = max(0, S+(n-1) + z(n) - k)    # upward shifts
S-(n) = max(0, S-(n-1) - z(n) - k)    # downward shifts
Alert when S+ or S- &amp;gt; h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CUSUM accumulates small standardized deviations from a learned baseline (mu0, sigma0 from the first 30 observations). That's what makes it good at this job: a 0.8% fault doesn't produce a visible spike on any single day, but it produces a &lt;em&gt;persistent small negative bias&lt;/em&gt; — and CUSUM integrates exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy boundary (the part that makes federation possible)
&lt;/h2&gt;

&lt;p&gt;One org's canary is a smoke detector. A &lt;em&gt;network&lt;/em&gt; of canaries across independent orgs is an early-warning system — if orgs are willing to participate. They're only willing if participation leaks nothing.&lt;/p&gt;

&lt;p&gt;So the probe enforces a hard perimeter. &lt;strong&gt;Raw prompts and outputs never leave your infrastructure.&lt;/strong&gt; What gets transmitted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SHA-256 hash of each response (not the response)&lt;/li&gt;
&lt;li&gt;DP-noised aggregates: &lt;code&gt;json_success_rate&lt;/code&gt;, &lt;code&gt;avg_output_length&lt;/code&gt; (Laplace mechanism, ε = 2.0 per flush)&lt;/li&gt;
&lt;li&gt;the content-addressed canary suite version hash&lt;/li&gt;
&lt;li&gt;a pseudonymous Ed25519 public key — batches are signed, org identity is not disclosed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the alerting side has a matching rule: &lt;strong&gt;a single organization's signal is never promoted to a public alert.&lt;/strong&gt; A quorum scorer requires ≥ 2 independent orgs to agree on the same &lt;code&gt;(model, metric)&lt;/code&gt; drift before anything goes public. That one gate filters probe bugs, network hiccups, and Sybil injection in a single move — and it means your private fleet data stays private fleet data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof: a reproducible backtest
&lt;/h2&gt;

&lt;p&gt;Here's the Phase 0 experiment (seeded, fully reproducible — &lt;code&gt;SEED=42&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;Reconstruct the Aug–Sep 2025 incident timeline from the public postmortem as synthetic daily probe traffic:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Dates&lt;/th&gt;
&lt;th&gt;json_success_rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;Jul 1 – Aug 4&lt;/td&gt;
&lt;td&gt;0.990&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phase 1 (0.8% misrouting)&lt;/td&gt;
&lt;td&gt;Aug 5 – Aug 28&lt;/td&gt;
&lt;td&gt;0.982&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phase 2 (16% misrouting)&lt;/td&gt;
&lt;td&gt;Aug 29 – Sep 17&lt;/td&gt;
&lt;td&gt;0.840&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Feed it through the exact CUSUM detector from the codebase (&lt;code&gt;h=5.0, k=0.5&lt;/code&gt;). Watch when it fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;Date&lt;/span&gt;        &lt;span class="k"&gt;Phase&lt;/span&gt;          &lt;span class="k"&gt;rate&lt;/span&gt;    &lt;span class="k"&gt;S&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;      &lt;span class="k"&gt;note&lt;/span&gt;
&lt;span class="ld"&gt;2025-08-03&lt;/span&gt;  &lt;span class="k"&gt;Stable&lt;/span&gt;         &lt;span class="mf"&gt;0.9902&lt;/span&gt;  &lt;span class="mf"&gt;0.000&lt;/span&gt;
&lt;span class="ld"&gt;2025-08-04&lt;/span&gt;  &lt;span class="k"&gt;Stable&lt;/span&gt;         &lt;span class="mf"&gt;0.9881&lt;/span&gt;  &lt;span class="mf"&gt;0.000&lt;/span&gt;
&lt;span class="ld"&gt;2025-08-05&lt;/span&gt;  &lt;span class="k"&gt;Phase&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="mf"&gt;0.9855&lt;/span&gt;  &lt;span class="mf"&gt;0.598&lt;/span&gt;   &lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="k"&gt;bug&lt;/span&gt; &lt;span class="k"&gt;introduced&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;
&lt;span class="ld"&gt;2025-08-06&lt;/span&gt;  &lt;span class="k"&gt;Phase&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="mf"&gt;0.9857&lt;/span&gt;  &lt;span class="mf"&gt;1.142&lt;/span&gt;
&lt;span class="ld"&gt;2025-08-07&lt;/span&gt;  &lt;span class="k"&gt;Phase&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="mf"&gt;0.9786&lt;/span&gt;  &lt;span class="mf"&gt;3.309&lt;/span&gt;
&lt;span class="ld"&gt;2025-08-08&lt;/span&gt;  &lt;span class="k"&gt;Phase&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="mf"&gt;0.9877&lt;/span&gt;  &lt;span class="mf"&gt;3.396&lt;/span&gt;
&lt;span class="ld"&gt;2025-08-09&lt;/span&gt;  &lt;span class="k"&gt;Phase&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="mf"&gt;0.9816&lt;/span&gt;  &lt;span class="mf"&gt;4.889&lt;/span&gt;
&lt;span class="ld"&gt;2025-08-10&lt;/span&gt;  &lt;span class="k"&gt;Phase&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="mf"&gt;0.9777&lt;/span&gt;  &lt;span class="mf"&gt;7.278&lt;/span&gt;   &lt;span class="err"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="k"&gt;ALERT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First alert: &lt;strong&gt;August 10&lt;/strong&gt; — five days into the subtle 0.8% window.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;19 days before&lt;/strong&gt; the escalation made the problem visible to users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;38 days before&lt;/strong&gt; the official postmortem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can re-run this yourself: &lt;code&gt;python scripts/anthropic_backtest.py&lt;/code&gt; in the repo regenerates the full report and asserts the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this does and doesn't prove
&lt;/h3&gt;

&lt;p&gt;I want to be precise here, because this is where drift-detection claims usually get slippery:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This is a backtest on synthetic data&lt;/strong&gt;, not a live catch. The timeline and magnitudes come from the public postmortem; real probe noise may differ, and real lead time could be shorter or longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a single simulated observer.&lt;/strong&gt; A real public alert requires the ≥2-org quorum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The simulation omits live DP noise&lt;/strong&gt;, which adds variance and may delay an alert by a few days.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What it &lt;em&gt;does&lt;/em&gt; prove: a metric as cheap as "did the JSON parse?" — measured consistently, at temperature 0, against a fixed suite — carries enough signal to surface a 0.8%-of-traffic infrastructure fault weeks before it becomes user-visible. Conventional monitoring is structurally blind to this class of failure. A $0.10/day canary is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture in one pass
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your infra                          public
┌─────────────────────────┐
│ probe SDK               │
│  canary suite (≤200,    │
│  temp 0, hash-addressed)│
│  → features + DP noise  │
│  → Ed25519 signature    │
└──────────┬──────────────┘
           ▼
   ingestion gateway (FastAPI)
   schema validation, signature check
           ▼
   CUSUM per (model, metric)  →  local alert (private)
           ▼
   quorum scorer (≥2 orgs)    →  public drift alert
           ▼
   "model weather" dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack: Python probe SDK (OpenTelemetry GenAI semantic conventions), FastAPI gateway, SQLAlchemy storage, CUSUM + Bayesian online change-point detection, 127 tests, ruff-clean CI. Apache-2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The public &lt;strong&gt;"model weather" dashboard&lt;/strong&gt; (no login) shows live drift status for four production models:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://seismograph-weather.onrender.com/dashboard" rel="noopener noreferrer"&gt;seismograph-weather.onrender.com/dashboard&lt;/a&gt;&lt;/strong&gt; &lt;em&gt;(free host — first load may take ~30s)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Run the probe against your own endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;seismograph-probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or clone the whole stack — gateway, dashboard, the federated quorum demo (&lt;code&gt;scripts/demo_simulation.py&lt;/code&gt; shows two orgs independently confirming a drift and the public dashboard flipping only when quorum is met):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Tania-coder/SEISMOGRAPH" rel="noopener noreferrer"&gt;github.com/Tania-coder/SEISMOGRAPH&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've been burned by a silent model change — I'd like to hear the story. That failure mode is exactly what this network exists to catch, and every additional independent observer makes the signal stronger.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;SEISMOGRAPH is open source (Apache-2.0), archived at &lt;a href="https://doi.org/10.5281/zenodo.21045517" rel="noopener noreferrer"&gt;DOI 10.5281/zenodo.21045517&lt;/a&gt;. Built by &lt;a href="https://github.com/Tania-coder" rel="noopener noreferrer"&gt;Tatiana Radchenko&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>observability</category>
    </item>
    <item>
      <title>SEISMOGRAPH now has a live public dashboard</title>
      <dc:creator>Tatiana Radchenko 🇩🇰</dc:creator>
      <pubDate>Wed, 24 Jun 2026 12:04:48 +0000</pubDate>
      <link>https://dev.to/taniacoder/seismograph-now-has-a-live-public-dashboard-nhj</link>
      <guid>https://dev.to/taniacoder/seismograph-now-has-a-live-public-dashboard-nhj</guid>
      <description>&lt;p&gt;When I wrote about detecting silent LLM provider drift, the dashboard was localhost-only — you had to take my word for it. Now it's live and you can touch it:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://seismograph-weather.onrender.com/dashboard" rel="noopener noreferrer"&gt;https://seismograph-weather.onrender.com/dashboard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It shows real-time "model weather" for 4 production models (GPT-4o, GPT-4o-mini, Claude 3.5 Sonnet, Claude 3 Haiku) — no login. The same engine flagged the Claude 3.5 Sonnet degradation 38 days before the official postmortem on a reproducible backtest.&lt;/p&gt;

&lt;p&gt;Open-source, Apache-2.0, 107 tests.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install seismograph-probe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Tania-coder/SEISMOGRAPH" rel="noopener noreferrer"&gt;https://github.com/Tania-coder/SEISMOGRAPH&lt;/a&gt; — building in public, feedback welcome.&lt;br&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%2Fl327p4l00scdk8ag9fue.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%2Fl327p4l00scdk8ag9fue.png" alt=" " width="799" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>I've been building SEISMOGRAPH for 3 weeks. Here's what shipped today</title>
      <dc:creator>Tatiana Radchenko 🇩🇰</dc:creator>
      <pubDate>Tue, 23 Jun 2026 20:42:47 +0000</pubDate>
      <link>https://dev.to/taniacoder/ive-been-building-seismograph-for-3-weeks-heres-what-shipped-today-401h</link>
      <guid>https://dev.to/taniacoder/ive-been-building-seismograph-for-3-weeks-heres-what-shipped-today-401h</guid>
      <description>&lt;p&gt;tl;dr: pip install seismograph-probe — a Python probe that detects silent LLM API drift using CUSUM change-point detection, with privacy-preserving signal aggregation. 103 tests passing. Dashboard live. Open source.&lt;/p&gt;

&lt;p&gt;Three weeks ago I asked a question I couldn't answer:&lt;/p&gt;

&lt;p&gt;"Did GPT-4 just change underneath me, or is it my prompt?"&lt;/p&gt;

&lt;p&gt;No latency spike. No downtime. Just subtly different outputs from the same prompts, same parameters, same everything. I spent days debugging something that wasn't my fault.&lt;/p&gt;

&lt;p&gt;So I built a detector.&lt;/p&gt;

&lt;p&gt;Today I'm shipping it publicly.&lt;/p&gt;

&lt;p&gt;What's actually working right now&lt;br&gt;
This isn't a concept post. Here's what's live:&lt;br&gt;
The probe SDK — on PyPI today&lt;br&gt;
pip install seismograph-probe&lt;/p&gt;

&lt;p&gt;from probe.sdk import ProbeSDK&lt;/p&gt;

&lt;p&gt;sdk = ProbeSDK(provider="openai", model="gpt-4-turbo")&lt;/p&gt;

&lt;p&gt;result = sdk.run_canary_suite()&lt;/p&gt;

&lt;p&gt;print(result.drift_score)  # 0.0 stable → 1.0 significant shift&lt;/p&gt;

&lt;p&gt;The probe runs ≤200 canary prompts at temperature=0 daily. These are semantically stable tasks — deterministic questions, structured reasoning, format-adherence checks. The goal is a reliable behavioral baseline, not a capability benchmark.&lt;/p&gt;

&lt;p&gt;Privacy boundary: raw prompts and model outputs never leave your machine. The probe extracts SHA-256 feature hashes, distributional stats, and DP-noised aggregates. That's all that transmits.&lt;br&gt;
CUSUM change-point detection — running&lt;br&gt;
The correlation engine uses CUSUM (Cumulative Sum) — a sequential statistical test that's sensitive to gradual drift, not just threshold crossings.&lt;/p&gt;

&lt;p&gt;When I backtest against a known LLM behavioral shift event (Aug–Sep 2025):&lt;/p&gt;

&lt;p&gt;Day 0:   CUSUM statistic: 0.12  (stable baseline)&lt;/p&gt;

&lt;p&gt;Day 11:  First elevation detected&lt;/p&gt;

&lt;p&gt;Day 19:  Alert threshold crossed  ← SEISMOGRAPH fires&lt;/p&gt;

&lt;p&gt;Day 57:  Public postmortem published&lt;/p&gt;

&lt;p&gt;38-day lead time. That's the number I keep coming back to.&lt;br&gt;
Ingestion gateway — deployed&lt;br&gt;
FastAPI gateway with:&lt;/p&gt;

&lt;p&gt;Ed25519-signed batch verification (unsigned batches rejected atomically)&lt;br&gt;
Pydantic v2 schema validation&lt;br&gt;
SQLAlchemy ORM + SQLite (ClickHouse migration planned for Phase 2)&lt;br&gt;
Bearer token auth on audit export endpoint&lt;br&gt;
Public dashboard — live at localhost, hosted version coming&lt;br&gt;
Dark-mode model weather dashboard. Polls /v1/weather every 60 seconds. Shows per-model drift status across your fleet.&lt;/p&gt;

&lt;p&gt;GET /v1/weather&lt;/p&gt;

&lt;p&gt;→ [{ "model": "gpt-4-turbo", "status": "STABLE", ... },&lt;/p&gt;

&lt;p&gt;{ "model": "claude-3-5-sonnet", "status": "STABLE", ... }]&lt;br&gt;
Test suite — 103/103 passing&lt;br&gt;
Not "it works on my machine." 103 tests across probe SDK, storage layer, gateway, CUSUM detector, privacy boundary, and auth. Zero ruff violations across 22 Python files.&lt;br&gt;
Provider ToS compliance — checked&lt;br&gt;
Before adding any provider to the canary suite, I verify it doesn't violate their Terms of Service. Done for: OpenAI ✅, Anthropic ✅, Google Gemini ✅, Mistral ✅, Cohere ✅. Documented in docs/PROVIDER_TOS_CHECKS.md.&lt;/p&gt;

&lt;p&gt;What's NOT done yet (being honest)&lt;br&gt;
No hosted gateway yet. The gateway runs locally. Public ingestion endpoint is Phase 1.&lt;br&gt;
No Bayesian online detector yet. CUSUM is running. BayesianOnlineDetector.update() is deferred — it's on the backlog.&lt;br&gt;
No federation yet. Right now it's single-org. The cross-observer agreement scoring that makes it genuinely valuable is Phase 2.&lt;br&gt;
No cloud dashboard. localhost:8000 only for now.&lt;/p&gt;

&lt;p&gt;This is Phase 0: I'm proving the detection logic works before scaling it.&lt;/p&gt;

&lt;p&gt;The architecture in one diagram&lt;br&gt;
Your app&lt;/p&gt;

&lt;p&gt;│  (gen_ai.* OTel spans)&lt;/p&gt;

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

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

&lt;p&gt;│  SHA-256 hashes + DP-noised stats only&lt;/p&gt;

&lt;p&gt;│  Ed25519-signed batch&lt;/p&gt;

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

&lt;p&gt;Ingestion Gateway (FastAPI)&lt;/p&gt;

&lt;p&gt;│  signature check → schema validation → store&lt;/p&gt;

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

&lt;p&gt;SQLite / ClickHouse&lt;/p&gt;

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

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

&lt;p&gt;CUSUM Detector ──► DriftAlert&lt;/p&gt;

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

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

&lt;p&gt;/v1/weather dashboard&lt;/p&gt;

&lt;p&gt;OTel-native throughout. If you're already emitting gen_ai.* spans, the adapter plugs straight in.&lt;/p&gt;

&lt;p&gt;Why this matters (and why it has to be federated)&lt;br&gt;
A single organization's drift signal is almost useless. Your outputs change because your users change. Your prompts change. Your context windows change.&lt;/p&gt;

&lt;p&gt;But if 15 independent organizations running the same canary suite all see correlated semantic drift on the same day — that's a model change. That's the signal you can act on.&lt;/p&gt;

&lt;p&gt;Single-org signal = private fleet data (yours only).&lt;br&gt;
Multi-org correlated signal = public drift alert.&lt;/p&gt;

&lt;p&gt;That's the design. Federation is Phase 2. The local probe is shippable today.&lt;/p&gt;

&lt;p&gt;Try it / follow along&lt;br&gt;
GitHub: github.com/Tania-coder/SEISMOGRAPH&lt;br&gt;
PyPI: pypi.org/project/seismograph-probe&lt;/p&gt;

&lt;p&gt;If you've been burned by a silent model change — I want to hear about it. Open an issue, or find me on Twitter @tatyanti.&lt;/p&gt;

&lt;p&gt;The probe is Apache 2.0. The gateway will be too.&lt;/p&gt;

&lt;p&gt;Tatiana Radchenko · AI Infrastructure · Aarhus, Denmark&lt;br&gt;
Building in public. Phase 0 of 3.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
