F1verse

io.github.jinsim由 GitHub 帳號 jinsim 發布v0.15.0更新於 Nov 7, 2026

Formula 1 race data with the domain rules applied: stints, tyre wear, publish-safety. Zero deps.

已驗證STDIO僅桌面位置與生活資料與分析

概覽

AI 產生的概覽

F1verse 讓助理取得已套用領域規則的一級方程式賽事資料:比賽故事、配速、進站策略、資料品質檢查與冠軍預測。

可以做什麼
  • 載入比賽與各階段賽程,取得成績、逐圈排名、領先變化、纏鬥與比賽配速
  • 檢視進站段、輪胎衰退、進站交換、圈速取消以及遙測與天氣取樣
  • 檢查資料品質、交叉核對、修訂紀錄,以及成績是否適合發布
  • 比較賽季、車手生涯、賽道與隊友對決,並取得賽道幾何與實測資料
  • 計算冠軍數學可能性,並執行附回測的賽季模擬預測
  • 取得即時計時資料來源,如車隊無線電連結、計時統計與 FIA 文件
適用情境
適合讓助理用計算得出的數字而非原始計時表來回答關於 F1 比賽、賽季或賽道的問題,或產生有依據的比賽摘要與冠軍預測。
執行需求
以 stdio 在本機執行,透過 uvx 從 f1verse 的 PyPI 套件啟動,因此需要 Python 與 uv。未宣告任何 API 金鑰或帳號。執行時讀取公開的 F1 介面與官方即時計時存檔,需要網路存取。
安裝前請注意
執行時會向第三方公開介面請求資料,請求會送往這些主機。成績在賽後可能變動,伺服器記錄修訂而非凍結分類。預測與輪胎展望屬於模型情境,並非實測。車隊無線電僅回傳連結。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 F1verse,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

f1verse

[PyPI] [Python] [Tests] [Dependencies] [License]

Documentation · MCP server · Agent skill · llms.txt · Changelog · Contributing · Security · License

Methods and evidence explains which answers are observations, model scenarios or retrospective evaluations, and how to reproduce their checks.

The story layer for Formula 1 data. Data libraries fetch and tidy — f1verse tells you what happened: lead changes, laps led, event timelines, stint strategy, race pace — and what happens next: title probabilities from a season simulation that ships with its own backtest.

Zero dependencies. Standard library only. Full live-timing coverage from 2023; lap-by-lap racing back to 1996, pit stops to 2011, and results, qualifying and standings to 1950 — each answer stating which era it came from and what that era does not hold.

bash
pip install f1verse
python
import f1verse
race = f1verse.load(2026, 12)        # year, round — no other library needed
race.laps_led()      # {'ANT': 32, 'NOR': 31, 'HAM': 9}race.leader_runs()   # [{'abbr': 'NOR', 'from': 1, 'to': 4}, ...]race.results()[7]    # {'abbr': 'HUL', 'gap': '+1 LAP', ...}race.race_pace()     # median pace — pit/SC/VSC laps excluded by defaultrace.story()         # one call, whole story, plain JSON
race.championship_prediction()   # per-lap "if it ended now" title projectionrace.team_radio()                # timestamped clip URLs (nothing downloaded)
f1verse.championship_projection(2026)   # who wins the title, 20,000 seasonsf1verse.title_scenarios(2026)           # and who is mathematically out

Give it to an AI agent

f1verse provides both an agent skill (analysis instructions) and an MCP server (callable data tools). They can be used together or separately.

The f1verse skill teaches event discovery, source checks, strategy interpretation and statistical limits. Codex discovers it in this checkout under .agents/skills/f1verse; invoke it with $f1verse. To use it in other projects, install that directory with a GitHub skill installer or copy it to ~/.agents/skills/f1verse. It works with the Python package or a connected MCP server. Installing the skill does not connect MCP.

For MCP, configure your client with:

json
{"mcpServers": {"f1verse": {"command": "uvx", "args": ["--from", "f1verse", "f1verse-mcp"]}}}

The uvx launcher installs the package on demand; the server itself has no runtime dependencies. Discover tools with tools/list, analysis instructions with resources/list, and the race-brief and strategy-review templates with prompts/list. Resources and prompts do not fetch race data. The tool catalogue includes input/output schemas and read-only hints; call f1verse.tools() for the current list.

The server manifest is server.json, and its official registry name is io.github.jinsim/f1verse. An MCP registry listing and a GitHub agent skill are separate distribution paths; publishing one does not publish the other. A clone runs local changes with python -m f1verse.mcp after pip install -e .; uvx runs the latest published PyPI version, which may differ from this checkout.

For any other LLM pipeline, the library describes itself:

python
f1verse.tools()                  # MCP-dialect JSON schemasf1verse.tools("openai")          # function-calling dialectf1verse.call_tool("f1_race_story", {"year": 2026, "round": 12})

Errors are written for the caller that has to fix them without reading this page:

python
f1verse.call_tool("f1_race_summary", {})# LookupError: unknown tool 'f1_race_summary' — available: f1_race_story, ...f1verse.load_session(2026, 12, "Qualy")# LookupError: ... listing the sessions that weekend actually had

The whole weekend, not just the race

python
f1verse.sessions(2026, 12)# Practice 1 · Sprint Qualifying · Sprint · Qualifying · Race
q = f1verse.load_session(2026, 12, "Qualifying")q.results()[0]# {'abbr': 'NOR', 'q1': 72.695, 'q1_gap': 0.085, 'q3': 71.163, 'q3_gap': 0.0,#  'best': 71.163, 'eliminated_in': None, ...}
q.segments()["q1"]# {'fastest': 'PIA', 'advanced': [...16 codes...], 'eliminated': [...],#  'cut_margin': 0.022}

Each kind gets the classification it actually has. Qualifying gaps are to the fastest lap of that segment — the pole-sitter above was 0.085 s off in Q1 — because a single "gap to leader" column would misreport the session. A sprint loads as a Race; practice is a best-lap table.

Is this data safe to publish?

python
race.quality_report()# {'state': 'final',            # provisional → settled → final, or corrected#  'coverage': {'overall': 0.9955, 'sectors': 0.9824, 'compound': 1.0},#  'missing': ['STR.lap_46.lap_duration', ...],#  'source_age_seconds': 312,#  'revisions': [],             # source rewrites this install has observed#  'crosscheck': {...},#  'publishable': True}

crosscheck answers do independent sources agree. quality_report adds the three things that verdict is silent about: how complete the data is, how old the copy is, and whether the classification is still provisional.

The chequered flag is not the final classification — scrutineering disqualifications and penalties land hours later and rewrite rows in place. So the rows the stewards can change are not cached forever until the session is final, and any change that is seen is recorded:

python
before = race.snapshot()          # hashed, comparable, JSON — you persist it...f1verse.diff(before, race.refresh().snapshot())# {'changed': True,#  'changes': [{'abbr': 'HAM', 'field': 'position', 'before': 4, 'after': None},#              {'abbr': 'HAM', 'field': 'gap', 'before': '+8.1s', 'after': 'DSQ'}]}
f1verse.revisions()               # every source rewrite observed, with thef1verse.vintage(rec)              # superseded body when it was small enough

There is deliberately no as_of= time travel: f1verse can tell you what it sees now and when it saw a value change, not reconstruct a value nobody here ever fetched.

How the race actually unfolded

python
race = f1verse.load(2025, 24)
race.running_order()[30]# ['PIA', 'VER', 'NOR', 'LEC', 'RUS', ...]   who was where, lap by lap
race.position_changes()[1]# {'lap': 2, 'moves': 18, 'biggest': {'abbr': 'PIA', 'gained': 17,#                                     'from': 19, 'to': 2}}
race.battles()[0]# {'ahead': 'HAD', 'behind': 'OCO', 'from': 2, 'to': 14, 'laps': 13,#  'closest': 0.529}      <- a thirteen-lap fight the results table hides

battles finds pairs that held consecutive positions within 1.5 s for at least three laps. A scrap for eighth that ran a third of the race never shows up in a classification; it is often the best part of the afternoon.

Races from before the live feeds

python
old = f1verse.load_archive(2008, 18)     # Brazil, the last-corner title
old.coverage# {'lap_times': True, 'pit_stops': False, 'stints': False,#  'note': 'lap times only'}
old.leader_runs()# [{'abbr': 'MAS', 'from': 1, 'to': 9}, {'abbr': 'TRU', 'from': 10, 'to': 11},#  {'abbr': 'MAS', 'from': 12, 'to': 38}, ...]
old.laps_led()          # {'MAS': 64, 'TRU': 2, 'ALO': 2, 'RAI': 3}

The coverage block is not decoration. 2008 has no stint data anywhere, so ArchiveRace has no stints() — rather than returning an empty dict that reads like "no pit stops happened". What the era recorded, you get; what it did not, it says.

Seasons against each other

python
f1verse.season_shape(2025)# {'rounds': 24, 'final_margin': 2.0,#  'lead_changes': [{'round': 5, 'from': 'NOR', 'to': 'PIA'},#                   {'round': 20, 'from': 'PIA', 'to': 'NOR'}], ...}
f1verse.title_margins(top=5)# 2025  NOR 423.0 vs VER 421.0   margin 2.0   (0.08 of a win)# 2008  HAM  98.0 vs MAS  97.0   margin 1.0   (0.10 of a win)# 2012  VET 281.0 vs ALO 278.0   margin 3.0   (0.12 of a win)

Points systems changed repeatedly, so a raw margin cannot compare eras — one point in 1958 was most of a win. Every row also carries the gap measured in wins, which is the comparison that survives the rule changes.

Where the passing happened

python
f1verse.overtake_hotspots(race)[0]# {'from_s': 3510.0, 'to_s': 3540.0, 'signals': 69, 'drivers': [...]}

The timing feed publishes an OvertakeState per car that almost never changes — about a hundred transitions in nineteen thousand records. That sparsity is the value: the transitions are a free index of the moments worth looking at, from the same feed that times the race. Read it as "something happened here", then confirm against running_order.

Beyond a single race

python
f1verse.career("max_verstappen")# {'starts': 245, 'wins': 71, 'podiums': 131, 'poles': 48, ...}  1950-present
f1verse.milestones("max_verstappen")# [{'stat': 'poles', 'current': 48, 'target': 50, 'remaining': 2}]
f1verse.circuit_profile(2026, 13)# corners, marshal sectors, track outline, and pit loss split by track state# {'normal': 25.43, 'sc': 16.11, 'vsc': 18.4}  <- what an undercut costs here# plus historic record: 75 races held, pole-to-win rate 0.30
# Geometry is also interpreted, but never overclaimed: every corner carries# its lap position, preceding-run share and local heading deflection; mini-# and marshal-sector boundaries carry lap percentages.profile = f1verse.circuit_profile(2026, 13)profile["layout"]["corners"][0]# {'number': 1, 'progress_pct': 8.412, 'local_deflection_deg': 72.6, ...}
# What a map cannot say, the cars can. This measures the circuit from the# session's own telemetry: the height profile, where overtakes actually# happen, how much of the lap is flat out and where it is braked, and every# numbered corner as the car experienced it.f1verse.circuit_survey(2026, 13)["corners"]["corners"][0]# {'corner': 1, 'apex_speed_kph': 103, 'radius_m': 40.0,#  'lateral_load_g': 2.09, 'gear_at_apex': 2, 'severity': 'medium'}
# Published specifications are stored rather than derived, and the# measurement audits them rather than replacing them.f1verse.circuit_profile(2026, 13, measure=True)["audit"]# {'verdict': 'agrees', 'checked_age_days': 0,#  'checks': [{'field': 'length_m', 'published': 4259, 'measured': 4274.4,#              'off_by_percent': 0.36, 'state': 'agrees'}, ...]}
# The published facts and the audit are reachable on their own, so a caller# can ask what is claimed without paying for a telemetry survey.f1verse.circuit_facts("Austin")# {'length_m': 5513, 'race_laps': 56, 'race_distance_km': 308.405,#  'provenance': {'length_m': 'curated', ...},#  'source': 'formula1.com official circuit page, 2026 united-states',#  'checked': '2026-09-01'}# None for a circuit nobody has curated yet - it does not invent a length
f1verse.circuit_audit(2026, 13)# the same verdict as profile(measure=True)["audit"], on its own
f1verse.circuit_directory()# every venue recorded in F1 results, with stable id, city, country and# coordinates; unlike a current map, it does not pretend an old layout has# today's geometry
f1verse.circuit_history("monza")# every F1 event at the venue, recent winners and starting positions,# pole-to-win conversion, and the drivers with the most wins
f1verse.head_to_head(2026)# teammate quali/race scores per constructor
f1verse.standings(2026)

Tyre life, and what the stewards struck out

Fuel load is not observed. For 2026 onward, stint_degradation returns a raw clean-lap trend and declines fuel-corrected wear unless you supply an explicit fuel_model scenario. Earlier seasons retain labelled legacy assumptions. tyre_outlook is a constant-trend scenario, not measured physical tyre life.

python
stints = f1verse.stint_degradation(race)# Each row includes raw_trend_s_per_lap, fuel_model, evidence and sample size.
f1verse.circuit_abrasion(race)# 2026+ without a fuel scenario: factor=None, verdict='unknown', with a reason.
f1verse.tyre_outlook(race)          # projected to the cliff

A car's fuel burn can mask tyre wear. Corrected rates use a declared fuel scenario and clean laps only. Assumptions and sensitivity accompany the number. A stint with too few laps returns {'degradation_s_per_lap': None, 'reason': 'too few clean laps'} rather than a number fitted to noise.

python
f1verse.lap_deletions(session.race_control)# [{'car_number': 55, 'lap_time': '1:25.773', 'stands': True,#   'reason': 'TRACK LIMITS AT TURN 3 LAP 3', ...}]

A reinstated lap keeps the reversal visible instead of being quietly dropped. Check this before treating a fastest lap or a qualifying position as settled.

Live timing, straight off the wire

python
from f1verse.sources import liveclient
with liveclient.LiveFeed() as feed:    for topic, patch, stamp in feed.messages():        ...
liveclient.run("session-{n}.jsonl")   # record, rotate on turnover, reconnect

The official SignalR feed over a WebSocket written in the standard library — no websocket package, no SignalR client. Connecting takes three undocumented courtesies (an affinity cookie only handed to a pre-flight request, the official application's user agent, an invocation id on the subscription); they live in one place so no caller rediscovers them.

Recorded frames carry a millisecond arrival stamp — the difference between an archive and a screenshot — so replay() runs a session back at true speed. The stream is not a lap table, so f1verse.sources.timing.laps_from_stream rebuilds one: arrival order lies, sector times land after the next lap has begun, and qualifying carries phantom lap times that are really the gap between two runs. Every rebuilt lap row carries its provenance.

Running this on a schedule

python
f1verse.status(2026)# {'latest_race': {'round': 12, 'meeting': 'Dutch Grand Prix'},#  'next': {'round': 13, 'session': 'Practice 1'}, 'next_in_hours': 125.7}
f1verse.due(2026, processed=[...session keys you already handled...])# sessions finished, settled (45 min past the flag) and not yet processed —# nothing published twice, nothing missed after downtime

Caching is policy-driven, not blanket: lap and telemetry data is immutable and cached forever, schedules expire every few hours — a calendar cached for a season would hide a cancelled round for the rest of the year — and rows the stewards can still rewrite expire until the session is final. f1verse.cache_info() and f1verse.clear_cache(older_than=...) are there for operators; clear_cache never drops the revision journal.

Telemetry, track position, conditions

python
f1verse.lap_telemetry(race, "NOR", 40)# per-sample speed, throttle, brake, gear, RPM and DRS state for one lap
f1verse.lap_trace(race, "NOR", 40)      # x/y/z coordinates of that lapf1verse.top_speeds(race)                # fastest reading per driver
f1verse.weather_summary(race)# {'track_c': {'min': 25.8, 'max': 38.2}, 'rain': True, 'samples': 191}

Telemetry is high-frequency, so these take a bounded window and filter server-side rather than downloading a session and trimming it locally.

Grounded narration

python
facts = f1verse.race_facts(race)       # all numbers computed and formatted heref1verse.brief(race)                    # deterministic text, no model required
result = f1verse.narrate(    race,    generate=lambda prompt: my_model(prompt),    cache_dir=".cache/narration",)# {'text': '...', 'source': 'generated' | 'cache' | 'template', ...}

narrate accepts any text-generation callback; f1verse has no model SDK dependency. The callback selects IDs from fact_catalog(facts) by returning JSON such as {"fact_ids": ["result.winner", "led.NOR"]}. Python renders the selected facts. A limited grammar of exact fact-bound phrases is also accepted; arbitrary free prose is not verified. Generation is retried at most twice, and a deterministic summary is returned if verification still fails. The optional cache is exact-match only and stores verified text.

Who wins the championship — and who still can

Two different questions, and mixing them is how a projection ends up implying somebody is out when the arithmetic says otherwise.

python
f1verse.title_scenarios(2026)# arithmetic, not a forecast — maximum points left is a fixed number# {'rounds_left': 11, 'sprints_left': 1, 'max_points_available': 283,#  'drivers': [{'driver': 'RUS', 'gap_to_leader': 59.0,#               'still_possible': True, 'needs_avg_per_round': 5.4}, ...],#  'still_alive': 23}
f1verse.championship_projection(2026)# the rest of the season, played out 20,000 times# {'drivers': [{'driver': 'ANT', 'title_probability': 0.954,#               'points_now': 242.0, 'projected_points_median': 446,#               'projected_points_p10': 390, 'projected_points_p90': 490,#               'races_in_sample': 11, 'measured_dnf_rate': 0.08}, ...],#  'assumptions': {'ignores': ['car development', 'weather', ...], ...}}

A simulated finish is resampled from the positions that driver has actually finished in, not drawn from a curve around their average. The distinction decides championships: alternating wins and retirements is a different proposition from finishing fourth every weekend, and the mean is the same for both. Retirement uncertainty uses a declared Beta prior; sprints draw their own outcomes and score on their own table. The model's countback and other limitations are listed in assumptions.

Every run also re-draws the driver's own level first, bootstrapping their results before playing the season against that version of them — twelve races is a small sample, and treating it as settled truth is how a forecast becomes more confident than anyone should be.

python
report = f1verse.backtest_projection(checkpoints=[4, 8, 12])report["scores"]                  # multiclass Brier, log loss and calibrationreport["uniform_baseline"]        # same cases and fieldreport["points_interval"]         # coverage, width and central 80% interval scorereport["excluded"]                # failed or incomplete cases remain visible

These are replays using today's corrected data and calendar, not a record of predictions issued at the time. Multiple checkpoints in one season are dependent; the effective sample is seasons, not the number of driver probabilities.

Evidence-linked changes

python
race.events()                      # stable IDs, revisions and source referencesrace.turning_points(limit=5)        # retrospective candidates; explicit heuristicf1verse.pace_changes(race)          # persistent shifts; no inferred causef1verse.retirement_risk([race])     # descriptive survival; finishers censoredf1verse.source_status()            # last observed access state, no network probe

pace_changes compares adjacent clean-lap windows without crossing excluded or missing laps. It uses future laps, so it is not a live detector. Neither a gap change nor a pace shift identifies a causal effect. Survival uses completed-lap exposure and all-cause DNF flags; it does not identify physical tyre life.

championship_prediction (in feeds) is a different thing — F1's own live "if it ended now" table. championship_projection is this model. One reports, the other forecasts.

Predictions, pit-stop verdicts, official documents

python
f1verse.win_probabilities({"NOR": 1, "ANT": 2, "RUS": 3},                          year=2026, upto_round=12, circuit_id="monza")# every probability ships with its own evidence:#   grid base rate measured over 233 real races (pole wins 54.1%)#   blended with that circuit's pole-to-win conversion (Monza: 0.30)#   scaled by recent form (average finish over the last 5 rounds)
f1verse.pit_exchanges(race, pit_loss_s=22.74)# [{'lap': 17, 'driver': 'RUS', 'rival': 'PIA', 'verdict': 'worked',#   'gain_s': 3.54}, ...]# neutralised laps (red flag / SC / VSC) and same-lap covering stops are# excluded — calling those undercuts would be wrong
f1verse.fia_documents(2026)          # stewards' decisions, classifiedf1verse.power_unit_documents(2026)   # "who changed which engine part"

Why this exists

Raw timing data needs a lot of domain knowledge before it means anything:

  • Classified gaps are not comparable across lapped cars. A car one lap down can show a smaller number than one that finished ahead on the lead lap. → format_gap applies the broadcast convention (+1 LAP).
  • "Who led the race" has to be derived. Lead changes, laps led and the moments they happened are not published as such. → leader_runs, laps_led, timeline.
  • Race pace needs rules, not just a threshold: in/out laps and laps run under SC/VSC have to go, or the number is meaningless. → race_pace applies them by default.
  • Web and video pipelines need plain JSON. Every f1verse output is JSON-safe Python, ready to serialise.
  • Data quality should be checkable in code, not read from logs. → crosscheck and quality_report return structured verdicts.
  • Results change after the flag. A cache that treats a classification as immutable makes a stewards' decision invisible. → revisable rows expire until the session is final; changes are recorded.

Additional live-timing feeds

The official live-timing archive publishes several feeds that are rarely surfaced. f1verse parses three of them, with the same caching and rate-limit etiquette as the rest of the library:

python
f1verse.championship_prediction(session)# per-lap "if the race ended now" projection of both championships,# including the moments the projected champion changed
f1verse.team_radio(session)# timestamped team-radio clips: [{'t', 'utc', 'driver_number', 'url'}]# URLs only — nothing is downloaded or redistributed
f1verse.timing_stats(session)# personal bests, best sectors, speed-trap figures

Running continuously

See OPERATIONS.md for caching policy, rate limits and scheduling.

Tests

bash
pip install -e ".[test]" && pytest -q

Sources

LayerWhat it gives
Race datalaps, stints, pits, positions, results, overtakes
Live-timing archivechampionship projection, team radio, timing stats
Historic recordscareers, circuit records, standings — 1950 onward
Circuit geometrytrack outline, corners, marshal sectors, pit loss

All are public endpoints, read at runtime. See src/f1verse/sources/ for the exact hosts, and NOTICE for how upstream data rights relate to this project's own license.

Design rules

  1. Zero required dependencies. The native loader reads public REST endpoints and the official live-timing archive directly, with its own on-disk cache and polite pacing.
  2. Everything returned is plain JSON-safe Python.
  3. F1 domain rules are defaults, not options.
  4. Cross-checked where possible — lapped-car gaps, for instance, are computed by convention and confirmed against a second source.
  5. Code only. No timing data, media, or images are bundled or redistributed; data is fetched by the end user.

License

Apache-2.0. See LICENSE and NOTICE.

Use it privately or commercially; read it, change it, redistribute it, and build products or hosted services on it. Preserve the license and notices, state significant changes, and observe the license's patent terms. There is no source-disclosure requirement for software or services that use f1verse.

This license covers f1verse's own source. The Formula 1 data it reads at runtime belongs to its respective rights holders under their own terms.

Roadmap

  • Broader cross-validation coverage
  • Deviation detection: expected range, actual, evidence
  • Localisation packages

Unofficial fan project. Not affiliated with, endorsed by, or associated with Formula 1, FIA, FOM, or any F1 team. F1, FORMULA 1 and related marks are trademarks of Formula One Licensing BV. This library contains code only — no timing data, media, or images are included or redistributed; data is fetched by the end user from publicly accessible endpoints, subject to the respective providers' terms.

來源:README.md,提交 ecb58ee

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

2
  1. v0.15.0最新Nov 7, 2026
  2. v0.14.0Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分