feat(aa-sync): daily Artificial Analysis acquisition workload - #86
Draft
DiTo97 wants to merge 2 commits into
Draft
feat(aa-sync): daily Artificial Analysis acquisition workload#86DiTo97 wants to merge 2 commits into
DiTo97 wants to merge 2 commits into
Conversation
Turns the experiments/aa-scrape-spike proof-of-concept into a production service: a provider x model offering dataset joined to the Artificial Analysis Openness Index, refreshed weekly. Sources both datasets over plain HTTP — no browser. The spike recommended Playwright, but that was only needed for the *expanded* column view of the aggregate leaderboard, which this service does not use. Per-provider pages and the Openness Index table are fully server-rendered on a plain GET. Uses the individual /providers/<slug> pages rather than the aggregate leaderboard: the leaderboard is filtered (Status: Current) and yields 512 rows against 1045 from the provider pages across 51 providers. The join normalises display names rather than string-matching them. The parenthetical suffix space carries more than reasoning effort — quantisation, serving tier, hosting platform and snapshot dates — so serving-side tokens are dropped from the key while reasoning identity is kept, read from the display name first and the /models/ slug second. Matching runs in three confidence tiers and never guesses: candidates that disagree are recorded as ambiguous, models with no openness row as unmatched, and absent openness stays null rather than becoming zero. Refs #85, #78, #76 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The original design rested on a wrong assumption: that AA's public pages carry no per-token pricing, and that the per-provider pages are the only place the superseded offerings the leaderboard hides can be found. Both are false, and checking them properly collapses the acquisition to two GETs. AA is a Next.js app; every page streams its data into the initial HTML as self.__next_f.push([1,"..."]) chunks. The rendered table is a lossy view of that payload in two directions. "Expand Columns" is a client-side toggle -- expanding issues no network request -- so the 50-column view was always in the HTML, per-token and cache pricing included. And the leaderboard renders only non-deprecated rows while its payload holds every row: measured 2026-08-14, Nebius rendered 25 of 35 and Azure 20 of 84, with rendered + deprecated == payload exactly for both. Status: Current *is* deprecated == false. Every offering id on /providers/nebius is in the leaderboard payload, and a per-key diff of the two found zero fields with a real value on the provider page but missing from the leaderboard, zero the reverse, and zero conflicts -- the only differences were React's "$undefined" sentinel versus an omitted key. So 58 provider-page requests bought a strict subset of one, and are gone. Keyed on AA's offering id, because no composite of the readable fields is unique. Over the full 1082 rows, (provider, model_slug, host_api_id, display_name) still collides on 4 groups: openai/o3 appears twice under one host_api_id with the same label, differing only in price ($10/$40 vs $2/$8) and whether performance was measured. normalize_sources asserts id uniqueness. The openness join is now an exact model_slug match. Openness records live in their own id space and point at a model by modelId, a uuid that appears nowhere in the leaderboard payload (0 of 298), so id cannot anchor that join -- but the openness page supplies a slug for all 298 scored models. Name normalisation, the creator filter, the three confidence tiers and the serving-token vocabulary all go away, and coverage improves: 698 matched against 563, 0 ambiguous vs 1. Since the payload is typed JSON, the em-dash / U+2212 / currency / thousands- separator / trailing-asterisk cleaning is gone with the table parsing that needed it. intelligence_index is 45.1382483763163 rather than 45, and intelligence_index_estimated is a boolean rather than a trailing asterisk. Also adds three named alerts, each opening or commenting on one aa-sync-failure issue rather than duplicating: payload-not-found (AA stopped shipping a flight payload), request-blocked (persistent non-200 after retries; 403 is never retried), and schema-drift, checked against a tracked schema/offering-manifest.json because "it did not crash" is not evidence the source is unchanged. Drift separates a vanished or retyped field from a >10x range shift -- how a unit change looks from outside -- from a merely new field, which is informational and never fails a run. Breaking drift publishes nothing: a stale snapshot is recoverable, a silently wrong one is not. Delivery is one function, deliver_via_github_issue; any (Alert) -> str callable is a channel, so Teams or email is a single new function and no other module knows the difference. Sync moves to daily now that a run is two requests. 1082 offerings over 58 providers, against 1045 over 51. Refs #85, #78, #76 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Turns the
experiments/aa-scrape-spikeproof-of-concept into a production service: a provider × model × reasoning-effort offering dataset joined to the Artificial Analysis Openness Index, refreshed daily.Correction: the original design assumption was wrong
This PR originally shipped on two premises that turned out to be false, and the follow-up commit rewrites the acquisition around what the pages actually serve.
What it assumed: AA's public pages carry no per-token pricing (only
Cost per Task USD), and the per-provider/providers/<slug>pages are the only place to get the superseded offerings the aggregate leaderboard hides. So the service fetched ~58 provider pages and parsed their 12-column HTML tables.What is actually true: AA is a Next.js app, and every page streams its data into the initial HTML as
self.__next_f.push([1,"…")chunks. The rendered table is a lossy view of that payload in both directions:Columns. "Expand Columns" is a client-side toggle over data that already shipped — expanding issues no network request. The 50-column view, per-token pricing and cache pricing included, was in the initial HTML all along. There was never a leaderboard-column-backfill to remove: the leaderboard was only ever used for slug discovery. The 12-column limit was a property of the default render, not of the response.
Rows. The leaderboard renders only non-deprecated offerings (
Status: Current) while its payload carries every offering:rendered + deprecated == payloadexactly for both, soStatus: Currentisdeprecated == false— applied client-side to a payload that already contains everything.So the provider pages were the redundant path, not the leaderboard. Every offering id on
/providers/nebiusis present in the leaderboard payload, and a per-key comparison found zero fields with a real value on the provider page but missing from the leaderboard, zero the reverse, and zero conflicts — the only differences were React's"$undefined"sentinel versus an omitted key, which normalise to the sameNone. 58 requests bought a strict subset of one.Acquisition is now two GETs: the provider leaderboard and the Openness Index.
What the service does
1082 offerings across 58 providers, 62 published fields each, up from 1045 across 51. Superseded offerings are kept and flagged
deprecatedrather than filtered at acquisition time, so a model being replaced never punches a hole in price history; filter at query time instead.Keyed on
offering_id— AA's own uuid — because no composite of the human-readable fields is unique. Tested over all 1082 rows:offering_id(provider, model_slug, host_api_id, display_name)(provider, model_slug, display_name)(provider, model_slug)(provider, host_api_id)The irreducible collisions are real distinct endpoints AA does not name apart:
openai/o3appears twice under onehost_api_idwith the same label, differing only in price ($10/$40 vs $2/$8 per 1M) and whether performance was measured. There is no tier, region or variant field to separate them.normalize_sourcesasserts id uniqueness every run.The openness join is an exact
model_slugmatch. Openness records live in their own id space and reference a model bymodelId, a uuid that appears nowhere in the leaderboard payload (0 of 298) — sooffering_idcannot anchor this join. The openness page does supply a slug for all 298 scored models, and the leaderboard carriesmodel_slugon every offering. That removes the name normalisation, the creator filter, the three confidence tiers and the serving-token vocabulary the display-name join required, and coverage improves: 698 matched against 563, with 0 ambiguous instead of 1.Reasoning-effort grain is native. 299 labels carry an explicit effort token and 78 (provider, base model) groups span more than one effort. Each variant is its own offering with its own metrics;
maxowns the unsuffixed slug. Per-token price is constant across efforts whilecost_per_task_usdvaries, so effort matters economically even though pricing does not change with it.No value cleaning. The payload is typed JSON, so the em-dash / U+2212 minus / currency-symbol / thousands-separator / trailing-asterisk handling is gone along with the table parsing that needed it.
intelligence_indexis45.1382483763163rather than45, andintelligence_index_estimatedis a real boolean rather than a trailing*.Per-token pricing is no longer the blocker on a
PricingInfocrosswalk that this PR originally described —input_price_usd_per_1m,output_price_usd_per_1mand both cache prices are now published. The remaining reason the dataset stays separate fromprices.jsonis grain, not missing fields.Failure and drift alerting
Three failure modes alert under their own names, each opening — or commenting on, never duplicating — one issue labelled
aa-sync-failure:payload-not-foundflight.reconstruct_payloadrequest-blockedfetch.get_pageschema-driftschema.check_driftRetryable statuses (408/425/429/5xx) get 4 attempts with 2s/8s/30s backoff; a refusal such as 403 is not retried, because repeating it will not change the answer.
Drift is checked against a tracked
schema/offering-manifest.json, because "the pipeline did not crash" is not evidence the source is unchanged. A vanished or retyped field is breaking; so is a numeric field whose values move >10× outside their recorded span, which is what a unit change looks like from outside (fractions rescaled to percentages, per-1M prices restated per-1k) and which parses perfectly while meaning something else. A new field is informational only — additive change is how AA ships new benchmarks and never fails a run. On breaking drift the run alerts and publishes nothing: a stale snapshot is recoverable, a silently wrong one is not.Delivery is one function,
deliver_via_github_issue. Any(Alert) -> strcallable is a channel, so routing to Teams or email later is a single new function and no other module changes.Verification
89 tests. Each of the three failure modes is driven end-to-end against a deliberately broken input — a page with its 36 flight chunks stripped, a mocked persistent 403, and the real 1082-row payload with
price1mInputTokensrescaled 1000× — asserting the alert kind, title and body that would be delivered. Delivery itself is injected in tests, so no test touches the network.Sync moves to daily now that a run is two requests instead of ~60.
Refs #85, #78, #76