This is a submission for the Kaggle Benchmarking Challenge
What I Benchmarked
TL;DR. I tested 17 AI models on writing, recalling and fixing OpenTelemetry instrumentation, graded in code against 26 releases of the spec.
- 16 of 17 models wrote retired names, and 83% of those names had been retired for more than a year.
- 12 of 13 models wrote names they had correctly replaced when asked directly.
- 7 of 17 "fixed" a metric by changing its unit and keeping its retired name, which breaks it both ways.
- One sentence naming the spec version cut retired names by 36%, and made models invent more.
Benchmark: Semconv Drift on Kaggle
I asked Claude Sonnet 5 what replaced the OpenTelemetry attribute db.system. It answered correctly: db.system.name, the name the spec has used since January 2025.
In the same benchmark, I asked it to add tracing to a go-redis call. It wrote this:
attribute.String("db.system", "redis"),
attribute.String("db.operation", cmdName),
attribute.String("db.statement", statement),
All three names are retired. The code compiles, the span exports, the backend stores it. Nothing fails. Weeks later, a dashboard that filters on db.system.name shows no Redis traffic, and nobody connects the empty panel to a line of generated code.
This year I have had 14 pull requests merged into OpenTelemetry's Go compile-time instrumentation project, and this is exactly the kind of bug I would wave through in review. The code looks right. It is only wrong against a spec version, and nobody reviews against a spec version. So I built a benchmark to measure how often it happens.
The task
OpenTelemetry's semantic conventions are the shared vocabulary of telemetry: which key holds the HTTP method, what a database span calls its query, which unit a duration metric uses. They are versioned, and many of the names people learned first have been renamed. http.method became http.request.method in 2023. db.statement became db.query.text in 2024. rpc.client.duration, in milliseconds, became rpc.client.call.duration, in seconds, in January 2026.
Semconv Drift has four tasks on Kaggle Benchmarks:
| Task | Cases | What the model does |
|---|---|---|
| Write | 12 | Instrument everyday code: Flask and Express servers, an HTTP client, psycopg, go-redis, kafkajs, gRPC, metrics and resources, in Python, Go and TypeScript |
| Know | 22 | Name what replaced a retired name. Includes span-kind traps and two controls whose right answer is "unchanged" |
| Fix | 8 | Modernize old snippets, including real code from redis/go-redis, converting values where a unit changed |
| Write with version hint | 12 | The Write requests plus one sentence naming the current spec version |
Every Write prompt already says "Follow the current OpenTelemetry semantic conventions." The models were told. The question is what "current" means to them.
Grading is code, not a judge. The grader extracts every semantic convention name from an answer (string keys, metric names, SDK constants in four languages, enum values) and looks each one up in 1,563 names from 26 releases of the spec, v1.21.0 to v1.44.0, plus every name from v1.0.0 to v1.20.0 for the oldest ones. Every retired name carries the release and date that retired it. Reasoning text is stripped first, so a model is judged on the code it wrote. The answer key is tested too: the build fails if any gold answer is not a current name in the ground truth.
The ground truth comes from Attrition, a dataset of every tagged release of open-telemetry/semantic-conventions that I built for DEV's Sanity challenge.
Models Tested
17 models through Kaggle's model proxy, picked to answer three questions:
- Does a newer generation fix it? Three same-family pairs: Claude Sonnet 4.5 and 5, GPT-5.4 and 5.5, Gemini 2.5 Pro and 3.1 Pro.
- Does spending more fix it? Frontier models (GPT-6 Astra, Claude Opus 5, GPT-5.5, Gemini 3.1 Pro) next to small, cheap ones (GPT-5.4 mini, Claude Haiku 4.5, Gemini 3.7 and 3.8 Flash).
- Do open-weight models differ? Qwen 3 235B, DeepSeek-R1, GLM-5, Gemma 4 31B and gpt-oss-120b.
That is 830 graded answers, plus 84 more from repeat runs. Four reasoning models (DeepSeek-R1, GLM-5, Gemma 4 and gpt-oss-120b) have no Know score: on some short factual questions they spent the whole answer budget thinking and never answered. Grok 4.5 and 4.6 returned 404 for my account, so they are not in the lineup.
Findings
1. The drift is old, not new
16 of 17 models wrote at least one retired name in Write, 357 uses in all. If this were about training cutoffs, the retired names would be recent ones the models never saw. They are not:
- 297 of 357 (83%) had been retired for more than a year when I ran the benchmark.
- 238 (67%) had been retired for more than two years, which means before October 2024. Every model in the lineup was released after that.
- The median retired name was retired 2.4 years earlier.
These models were not missing the news. They had read the changelog, and years of tutorials before it, and the tutorials won. db.system is the clearest case: 24 uses across 12 models, nearly two years after the rename.
The only names a training cutoff could excuse are the January 2026 rpc.* renames. 16 models wrote them. GPT-6 Astra was the only exception, and it also avoided every older retired name, so a late cutoff alone does not explain its perfect Write score.
2. It told me the new name, then wrote the old one
Because Know and Write ask about the same names, I can check each model against itself. For every retired name a model wrote in Write, did the same model give the correct replacement in Know?
12 of 13 models wrote at least one retired name they had correctly replaced when asked directly, 58 times in all. Claude Haiku 4.5 said http.method became http.request.method, then wrote http.method five times. Sonnet 5 did it with db.system, as in the opening. Only GPT-6 Astra never did it.
This is not a knowledge gap. Asked a question, a model retrieves the changelog. Asked for code, it retrieves the most common code, and the most common code is old.
3. The half fix
One Fix case hands the model this Go snippet and asks it to move to v1.44.0, converting values if a unit changed:
dur, _ := meter.Float64Histogram("rpc.client.duration", metric.WithUnit("ms"))
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
dur.Record(ctx, float64(time.Since(start).Milliseconds()))
The answer is rpc.client.call.duration, unit s, values in seconds. Only GPT-6 Astra got all of it.
7 of 17 models changed the unit to seconds, converted the values, and kept the retired name. That is worse than leaving the code alone. Unchanged code is at least consistent with the old spec. The half-fixed metric emits the old name with the new unit, so it breaks in both directions: queries on the old name read values 1,000 times smaller than expected, and queries on the new name find nothing. The diff looks like a fix, so it passes review.
Two more models, from two different vendors, did something stranger. GPT-5.5 and Gemini 3.7 Flash both renamed the metric to rpc.client.request.duration. No release of the spec has ever defined that name. It is a plausible guess built from the real http.client.request.duration, and both models made the same guess.
4. Why this domain drifts when others do not
Another entry in this challenge tested removed pandas and Express APIs and found that models used the current ones. I think the difference is how each change fails.
A removed function fails loudly. Call DataFrame.append on pandas 2 and you get an error, a Stack Overflow question and an answer, and the answer ends up in training data. A retired attribute name fails silently. Nothing throws, nobody asks, and the old tutorials are never corrected. The OpenTelemetry project knows this: its instrumentation libraries offer OTEL_SEMCONV_STABILITY_OPT_IN=http/dup, which emits both the old and the new HTTP names during a migration, because a rename does not propagate on its own.
That is a hypothesis, not a measurement. But it predicts where to look for drift: anywhere the old form still works.
5. One sentence of context helps, and it is not enough
The hint task repeats the 12 Write requests with one added sentence: "Target OpenTelemetry semantic conventions v1.44.0 (released August 2026). Many attribute and metric names were renamed or removed between 2023 and 2026, so do not rely on older names."
Before trusting any difference, I needed a noise floor. Kaggle's benchmark library accepts a temperature but drops it unless a model is flagged as supporting it, so the same prompt can give a different answer twice. I ran the plain Write task a second time on seven models. Their retired-name total moved from 156 to 166, about 6%, with nothing changed. The hint moved the same seven models from 166 to 94, a 43% drop.
Across all 17 models:
- Retired names fell by 36%, from 357 to 230. The mean Write score rose from 39% to 48%.
- Only one model reached zero, and it was the one that was already there.
- Invented names rose from 21 to 31. Told that names had changed, some models stopped reaching for old names and started guessing new ones. Claude Haiku 4.5 went from 1 invented name to 7.
Another entry in this challenge found that a one-sentence rule took clean answers from 64% to 96%. Here one sentence removed about a third of the drift and replaced part of it with guesses. A version number tells a model that names changed. It does not tell it what they changed to.
6. The leaderboard
Overall is the average of a model's task scores, the same rule the Kaggle leaderboard uses.
| Model | Write | Know | Fix | Write + hint | Overall | Retired names written |
|---|---|---|---|---|---|---|
| GPT-6 Astra | 100% | 100% | 100% | 92% | 98% | 0 |
| Claude Opus 5 | 67% | 95% | 88% | 75% | 81% | 6 |
| Claude Sonnet 5 | 67% | 82% | 75% | 83% | 77% | 9 |
| GPT-5.4 | 67% | 77% | 75% | 83% | 76% | 8 |
| GPT-5.5 | 67% | 91% | 75% | 67% | 75% | 5 |
| Gemini 3.7 Flash | 58% | 91% | 75% | 58% | 71% | 9 |
| Gemini 3.8 Flash | 58% | 91% | 75% | 58% | 71% | 8 |
| Claude Sonnet 4.5 | 42% | 68% | 50% | 50% | 52% | 21 |
| Gemini 3.1 Pro | 25% | 82% | 63% | 33% | 51% | 17 |
| GPT-5.4 mini | 33% | 64% | 38% | 58% | 48% | 19 |
| GLM-5 | 17% | n/a | 75% | 42% | 44% | 17 |
| Gemini 2.5 Pro | 17% | 50% | 13% | 25% | 26% | 27 |
| Gemma 4 31B | 33% | n/a | 13% | 25% | 24% | 24 |
| Claude Haiku 4.5 | 0% | 45% | 13% | 25% | 21% | 40 |
| Qwen 3 235B | 0% | 23% | 13% | 25% | 15% | 49 |
| DeepSeek-R1 | 8% | n/a | 0% | 8% | 6% | 46 |
| gpt-oss-120b | 0% | n/a | 0% | 0% | 0% | 52 |
Two patterns are weaker than the findings above. Two of the three family pairs improved clearly (Sonnet 4.5 to 5: 52% to 77%; Gemini 2.5 Pro to 3.1 Pro: 26% to 51%), and the GPT pair tied (GPT-5.4 76%, GPT-5.5 75%). Price was a poor guide: GPT-5.4 matched GPT-5.5 for under a tenth of the cost ($0.10 against $1.36).
What surprised me
I expected a cutoff story: new renames missed, old ones known. I got the opposite. The models know the renames, and they still write the names from the tutorials they read most. The half fix surprised me more. I wrote that case to catch models that forget the unit. Almost none forgot the unit. They forgot the name.
What this changed about how I review AI code
I used to treat "the model knows the current API" as the thing to check. It is the wrong check. Asking a model what changed tells you nothing about what it will write. For telemetry code, I now check the names against the spec, not against my memory, and I treat a diff that changes a unit but not a name as a bug.
What this means for you
-
Lint telemetry names in CI. The spec's registry marks every deprecated name and its replacement. A check of string literals against that list would flag every retired name counted in this post,
db.systemincluded. - Put the spec version in the prompt, then still lint. It cut drift by a third here, and it also made models invent names. If you can, paste the current names you need instead of the version number.
- Prefer generated constants pinned to a version. In Go the semconv version is in the import path. Bump it on purpose.
- Review units and names together. A histogram whose unit changed and whose name did not is the hardest drift to spot, because the diff looks like a fix.
Paste this prompt: Save the answer as Any hit is a retired name. The current ones are Try it on the model you use at work
Language: Python.
Wrap this psycopg query in an OpenTelemetry client span. Record which database system it is, the database name, the operation, the table and the query text.
def list_orders(conn, customer_id):
with conn.cursor() as cur:
cur.execute('SELECT * FROM orders WHERE customer_id = %s', (customer_id,))
return cur.fetchall()
Follow the current OpenTelemetry semantic conventions. Write attribute keys and metric names as string literals rather than SDK constants. Return one complete code block and nothing else.
answer.py and run:
grep -nE "[\"'](db\.system|db\.name|db\.operation|db\.statement|db\.sql\.table|net\.peer\.name)[\"']" answer.py
db.system.name, db.namespace, db.operation.name, db.query.text, db.collection.name and server.address. Tell me in the comments which model you used and what it wrote.
What I would measure next
- More repeats. Seven models ran Write twice. Every model should run every task several times, with a reported spread.
- Names instead of a version. Does pasting the current names into the prompt remove the drift and the guessing, or only the drift?
-
Units beyond time. The grader checks
msagainstsonly. Byte and count units can drift too. - The GenAI conventions. They moved to their own repository and are changing fast, which makes them the next place to look.
Limitations
- Small case counts. Write has 12 cases, Know 22 and Fix 8. Claude Sonnet 5's 67% on Write has a 95% Wilson interval of 39% to 86%. The repeat runs flipped up to 3 of 12 Write cases for one model, so I would not claim any order in the middle of the table.
- Know is missing for four reasoning models, which ran out of answer budget while thinking.
- Grok is missing. Grok 4.5 and 4.6 returned 404 through Kaggle's model proxy.
-
One strict call. Claude Opus 5 lost one Write case only for naming a custom key
app.ledger.entry_count. The spec has owned theapp.*namespace since its mobile conventions, so the grader counts it as invented, and the rule applies to every model the same way. - A moving target. Each semconv release retires more names. Re-running after the next release needs only a new ground-truth export, which is also what keeps this benchmark from going stale itself.
My Benchmark
Kaggle benchmark: https://www.kaggle.com/benchmarks/applesone/semconv-drift
The four tasks, each with its own leaderboard and the full task code:
| Task | Link |
|---|---|
| Write | semconv-drift-write |
| Know | semconv-drift-know |
| Fix | semconv-drift-fix |
| Write with version hint | semconv-drift-write-with-version-hint |
Interactive results: a sortable leaderboard with confidence intervals and every chart, at https://vignesh2027.github.io/semconv-drift/
Code, grader and ground truth:
vignesh2027
/
semconv-drift
Do AI models write OpenTelemetry code with names the spec already retired? A Kaggle benchmark graded in code against 1,563 names from 26 spec releases.
Semconv Drift
Do AI models write OpenTelemetry code with names the spec already retired?
OpenTelemetry's semantic conventions name everything telemetry records: http.request.method, db.system.name, http.server.request.duration. Those names change between releases. A model trained on older tutorials keeps writing the old ones, and nothing fails loudly: the data still flows, while dashboards and alerts quietly stop matching.
Website · Kaggle benchmark · Tasks: Write · Know · Fix · Write with version hint · Results
Semconv Drift measures that on Kaggle Benchmarks, with four tasks and no AI judge. Every answer is graded in code against 1,563 attribute, metric and event names from 26 releases of the specification (v1.21.0 to v1.44.0).
Task
Cases
What it asks
What counts as correct
Write
12
Instrument everyday code (HTTP server and client, Postgres, Redis, Kafka, gRPC, metrics, resources) in Python, Go and TypeScript
Only current names, no invented names under a spec
analyze.py and charts.py regenerate every number and chart in this post from the Kaggle runs. If you maintain OpenTelemetry instrumentation and want a case added, open an issue on the repo.









Top comments (17)
@apples_one_cd174284bffb , this is a masterclass in practical ai benchmarking. the finding that "12 of 13 models knew the new name but wrote the old one" perfectly mirrors the "knowing is not acting" phenomenon we're seeing across all llm evaluations.
your point about silent failures is the absolute crux of the problem. because a retired otel name doesn't throw a compilation error, it slips past human review. this is exactly why deterministic validation is non-negotiable in ai-generated code. if the code fails silently in production, the validator must fail loudly in the pipeline.
the "half-fix" observation (changing the unit but keeping the retired name) is terrifyingly accurate. it perfectly illustrates why we can't rely on free-text generation for critical instrumentation. we need strict, schema-enforced outputs where the metric name and unit are inextricably linked and validated against a live registry.
your practical takeaway to "lint telemetry names in ci" is the exact right path. you can't prompt-engineer away training data bias; you have to architect deterministic guardrails around it. fantastic, deeply necessary work!
Thanks, I really appreciate you reading it that closely.
You put it well: the validator has to fail loudly because nothing else will. One thing the data added for me is that schema enforcement alone doesn't fully close it. The half fix would pass a schema that only checks that the name and the unit are each valid. The check has to look at the pair against the registry, because rpc.client.duration recorded in seconds is a combination no version of the spec ever defined.
Agreed on prompting too. The one-sentence version hint cut retired names by about a third, which helped, but it also pushed some models to invent names that never existed. So I treat the prompt as a nudge and the CI check as the actual guarantee.
Curious whether you've seen the same pattern outside telemetry, anywhere an old form still compiles and nobody notices.
vigneshwar, that point about combinatorial validation is brilliant. you're absolutely right: checking individual fields against a schema isn't enough. the pair (name + unit) must be validated together against the registry. that is the exact difference between a naive linter and a true semantic guardrail.
to answer your question: yes, this pattern shows up everywhere backward compatibility is silently maintained. a classic example is renamed environment variables or config keys. if a team renames
API_TIMEOUT_MStoAPI_TIMEOUT_SECONDS, the new code might silently ignore the old key, fall back to a hardcoded default, and cause subtle production latency issues. the code "compiles" and runs perfectly, but the behavior is silently broken.another great example is graphql
@deprecatedfields. an old client keeps querying the deprecated field, the server returnsnullor stale data, and the ui silently degrades. no runtime errors, no compilation failures—just zombie data.it always comes back to the same rule: if the system doesn't fail loudly on invalid state, it will fail silently in production. treating the prompt as a nudge and the ci check as the actual guarantee is the only way to build resilient systems.
thanks for the continued deep dive on this! always a pleasure discussing architecture at this level. 🐯🛡️
Both of those are perfect examples, and the config key one is the nastiest, because the fallback default makes the bug look like a performance problem instead of a naming problem. Nobody goes looking for a rename when the symptom is latency.
The GraphQL case is interesting because the spec gives you @deprecated for free, and it still doesn't stop the drift. Same as here: OpenTelemetry marks every retired name in its registry, and models wrote them anyway. Marking something deprecated only helps if a tool reads the mark.
You've basically given me the next benchmark: renamed config keys and deprecated GraphQL fields, graded the same way, with the old form silently "working". If I build it, I'll tag you. Thanks for the great discussion!
vigneshwar, that is incredibly exciting to hear! the config key fallback is indeed the nastiest because it masks the root cause as a performance issue, completely throwing devs off the scent.
knowing that our discussion directly inspired your next benchmark is the ultimate compliment. i would be absolutely honored if you tag me when you build it. this is exactly how the industry gets better: one rigorous, community-driven benchmark at a time.
can't wait to see the results. thanks for keeping the bar so high! 🐯🛡️
Pinning the spec version in CLAUDE.md is the obvious fix for the one-sentence result, but a gotcha: per the Claude Code docs, editing CLAUDE.md mid-session doesn't apply. The session keeps the version loaded at start until /clear, /compact or restart. So a quick A/B on a pin inside one session tests nothing.
Good catch, and worth knowing for anyone trying this at home. In the benchmark itself every case ran as a separate fresh prompt through Kaggle's model proxy, so there was no session state to carry over, but in a Claude Code workflow that gotcha would quietly invalidate a quick A/B. Have you tried pinning the actual names rather than the version? My hint result suggests a version number tells a model that things changed without telling it what they changed to.
Seven of seventeen models "fixed" a metric by swapping its unit while keeping the retired name, and that is the result I would build the whole benchmark around. A schema validator waves it straight through, since the shape stays valid even though the meaning rotted out. Did current-spec-in-context change the recall failures, or did the retired names leak back the moment the model worked from memory?
For your exact question
Read my sample page again it's shows exactly what u looking for !
Instead I tell u can u take a look
The go-redis example is what makes this failure mode nasty: nothing fails at write time — the code compiles, the span exports, and the empty dashboard is downstream and weeks away. We see the exact same shape in collection pipelines: a parser keeps working against yesterday's page structure, and "0 rows today" only surfaces a week later.
One thing your numbers imply: naming the spec version in the prompt cut retired names 36% but made models invent more — prompt context is a blunt instrument. The part that stays deterministic is post-generation validation: the semconv registry is machine-readable, so a CI gate can diff generated code against the current name list and fail loudly on retired attributes. Model judgment for writing, a version-pinned spec check for gatekeeping — same split that keeps scraping pipelines sane.
Solid benchmark. "Changed the unit, kept the retired name" is a great catch.
The scraping parallel is spot on: "0 rows today" a week later is exactly the same shape as the empty dashboard. In both cases the system keeps reporting success.
And yes, the registry being machine-readable is what makes the deterministic gate cheap. The one thing I'd add is to validate name and unit as a pair, since the half fix passes a check that looks at each separately. "Model judgment for writing, a version-pinned spec check for gatekeeping" is a better summary of the post than any line I wrote, thank you.
Agreed on validating names against the registry itself - a name that resolves is a fact, a name that is memorized is a rumor. We keep hitting this in scraping: the cheapest reliability win is never a smarter model, it is a deterministic check at the boundary (schema assert, count floor, machine-readable manifest). Everything downstream then gets to be dumber and cheaper. Glad the go-redis parallel landed - that is exactly the failure class we are both chasing: success signals that survive their own emptiness.
The 12 of 13 split between
KnowandWritemaps directly to how next-token priors work inside a function body. When you ask a direct question aboutdb.system, the prompt puts the model into changelog-retrieval mode. Once it is inside ago-redisspan setup block, the surrounding tokens (attribute.String(,cmdName,redis) pull it back into the high-frequency tutorial distribution from 2022 to 2024, which easily overrides a soft "follow current conventions" instruction. TheOTEL_SEMCONV_STABILITY_OPT_IN=http/duppoint explains why that prior never decays on its own. Because OpenTelemetry collectors accept arbitrary string keys without throwing, deprecated attributes never generate stack traces or failing CI runs that would feed corrective diffs back into training corpora. In Python and TypeScript where you do not always get compile-time Gosemconvpackage imports to break the build, the only reliable guardrail I have found is running a pre-commit AST or regex check against the retired attribute list before the agent patch is allowed to commit.This is the clearest explanation of the Know vs Write split I've seen, thank you. The "changelog-retrieval mode vs tutorial distribution" framing also fits the hint result: one sentence nudged the prior but couldn't override a whole span-setup context, so retired names dropped by a third and never reached zero for 16 of 17 models.
Agreed on the pre-commit check. One addition from building the grader: a regex on string literals misses SDK constants like semconv.DBSystemKey or ATTR_DB_SYSTEM, so I map constants back to their names too. Do you run yours on the diff only, or on the whole file the agent touched?
Some comments may only be visible to logged-in visitors. Sign in to view all comments.