Skip to content

feat(aa-sync): daily Artificial Analysis acquisition workload - #86

Draft
DiTo97 wants to merge 2 commits into
mainfrom
feat/aa-acquisition-weekly
Draft

feat(aa-sync): daily Artificial Analysis acquisition workload#86
DiTo97 wants to merge 2 commits into
mainfrom
feat/aa-acquisition-weekly

Conversation

@DiTo97

@DiTo97 DiTo97 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Turns the experiments/aa-scrape-spike proof-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 payload deprecated
    Nebius 25 35 10
    Azure 20 84 64
    all providers 516 1082 566

    rendered + deprecated == payload exactly for both, so Status: Current is deprecated == 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/nebius is 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 same None. 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 deprecated rather 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:

Candidate key Duplicate groups
offering_id 0
(provider, model_slug, host_api_id, display_name) 4
(provider, model_slug, display_name) 6
(provider, model_slug) 23
(provider, host_api_id) 248

The irreducible collisions are real distinct endpoints AA does not name apart: openai/o3 appears twice under one host_api_id with 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_sources asserts id uniqueness every run.

The openness join is an exact model_slug match. Openness records live in their own id space and reference a model by modelId, a uuid that appears nowhere in the leaderboard payload (0 of 298) — so offering_id cannot anchor this join. The openness page does supply a slug for all 298 scored models, and the leaderboard carries model_slug on 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; max owns the unsuffixed slug. Per-token price is constant across efforts while cost_per_task_usd varies, 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_index is 45.1382483763163 rather than 45, and intelligence_index_estimated is a real boolean rather than a trailing *.

Per-token pricing is no longer the blocker on a PricingInfo crosswalk that this PR originally described — input_price_usd_per_1m, output_price_usd_per_1m and both cache prices are now published. The remaining reason the dataset stays separate from prices.json is 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:

Alert Means Detected by
payload-not-found AA no longer ships data as a flight payload in the initial HTML flight.reconstruct_payload
request-blocked persistent non-200 or transport failure after retries fetch.get_page
schema-drift payload parsed but a field vanished, changed type, or changed units schema.check_drift

Retryable 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) -> str callable 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 price1mInputTokens rescaled 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

DiTo97 and others added 2 commits August 14, 2026 11:04
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>
@DiTo97 DiTo97 changed the title feat(aa-sync): weekly Artificial Analysis acquisition workload feat(aa-sync): daily Artificial Analysis acquisition workload Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant