Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

HTTP API

Soma 在 /v1 下提供参赛者 API。请使用比赛面向浏览器的源(origin);本地环境通过 http://localhost:5173 代理这些路由。除非路由做重定向,或清除会话且不带响应体,否则响应都是 JSON。

下面的契约描述的是 Soma 的工作流。参赛者需要的一切在网站上也都有对应的页面;当你要写脚本调用 API 时,再来查这份参考。

Soma 会缓存公开读取(排行榜、对局、模型、版本、个人主页、赛季、游戏、地图,以及网站的社区页面),最长十分钟;一旦有写入改动了底下的数据,对应的缓存立即清除,所以你拿到的公开读取总是最新的。/v1/me 下关于你自己账户的读取,以及 /v1/private 下的成员读取,从不缓存。

登录

让浏览器访问 GET /v1/auth/{provider}(例如 /v1/auth/github)。登录服务商会经由 GET /v1/auth/{provider}/callback 返回,Soma 随即设置一个 HttpOnly 的 soma_session cookie。会话有效期 30 天,Soma 在每个需要认证的请求上都会检查服务端的撤销状态。请使用同源请求,浏览器才会带上 cookie。GET /v1/auth-providers 列出本部署支持的登录方式,登录页正是据此为每一种画出一个按钮。

方法路径认证结果
GET/v1/auth-providers公开登录方式列表 [{slug,label}]
GET/v1/auth/{provider}公开通过重定向开始 OAuth
GET/v1/auth/{provider}/callbackOAuth 回调完成登录
GET/v1/me会话当前账户
PATCH/v1/me会话更新你的显示名称或简介
GET/v1/me/candidates会话你仍在审核或试赛中的版本
GET/v1/sessions会话你当前有效的会话,每个一行
DELETE/v1/sessions/{sid}会话按 id 撤销其中一个。其余的要逐个撤销;没有批量路由
DELETE/v1/session会话撤销当前会话并清除 cookie
GET/v1/profiles/{username}公开参赛者的公开页面
GET/v1/status公开平台状态,含 admitters:此刻有多少台机器可以处理审核队列

Soma 没有供 SDK 或 CLI 使用的 API bearer 令牌。不要用 GitHub 个人访问令牌来代替 Soma 会话。

游戏、赛季与天梯

方法路径查询参数结果
GET/v1/games无已注册游戏的数组
GET/v1/games/{game}无一个游戏,连同它的精选赛季
GET/v1/games/{game}/seasons无公开赛季,最新的在前
GET/v1/games/{game}/seasons/{slug}/mapsenabled, boards赛季的地图,按添加顺序排列
GET/v1/games/{game}/seasons/{slug}/maps/{map_id}无一张地图、地图文件本身,以及它启用的时段
GET/v1/games/{game}/seasons/{slug}/podium无每个天梯的前三名,赛季结束后即定格
GET/v1/games/{game}/seasons/{slug}/playing无此刻正在进行的对局数;从不缓存
GET/v1/games/{game}/leaderboardladder, season, limit, cursor一页排名
GET/v1/games/{game}/leaderboard/seriesladder, season每个上榜条目在赛季内等距时间点上的名次与评分

这些读取都是公开的。game 是一个 slug,例如 ants。天梯的取值为 nano、micro、mini、small、large 和 open,默认是 open。season 接受赛季的 slug,例如 summer-2026;数字或 UUID 都不行。省略它就是该游戏的精选赛季:平台管理员设为精选的公开赛季,否则是最新的进行中的公开赛季,再否则是最新的公开赛季。赛季之间会重叠,所以请指明你要的那一个。私有赛季在这里的回答与不存在的赛季无异;见私有赛季。

排行榜的 limit 默认为 50,最大 200;cursor 默认为 "0"。游标是一个偏移量字符串。沿着返回的 next_cursor 翻页,直到它为 null。实时评分可能在两次请求之间改变榜单顺序,所以分页得不到稳定的快照。

curl --fail-with-body -sS   'http://localhost:5173/v1/games/ants/leaderboard?ladder=open&limit=10'

响应体包含 season(slug)、season_name、closed、round(赛季当前的轮次,{n, kind, games},没有则为 null)、total、entries 和 next_cursor。每个条目包含 rank、version_id、model_id、model、owner、version、class、size_bytes、rating、provisional、matches、round_matches(当前轮次中已计分的对局数,没有轮次时为 null)、baseline(是否为平台提供的条目)、trend(最近一场计分对局带来的评分变动,第一场之前为 null)和 history(这个天梯上最近十二次评分,最早的在前,含晋级时的初始评分,保留两位小数:足够画一条迷你走势图)。评分相同的条目按版本 id 排序,每次都一样。

赛季条目包含 name、slug、state、submissions_open_at、submissions_close_at、closed_at、close_requested_at、visibility(public 或 private)、entry(open 或 restricted)、engine_digest、rules,以及 weight_classes:赛季所采用的大小分界,读懂排名离不开它,每一级都带有该级允许的记忆(memory_flat_bytes 和 memory_cell_bytes,没有时为 0)。它还带有五个计数:entries(参赛的模型数)、active_versions(天梯的规模)、entered_versions(历来提交的全部版本数)、in_flight_versions(正在审核中的数量)和 matches_played,后者不含试赛,因此与 GET /v1/matches 能查到的数目一致。maps 汇总赛季的地图:多少张启用、多少张未启用,以及启用的地图所覆盖的席位数和边长;baselines 以同样的方式汇总它的基线。round 是它所处的轮次({n, kind, games, started_at},kind 为 round 或 finals),next_round 是等待开始的下一次重置({n, kind, games, starts_at}),没有时各为 null。见赛季。

赛季的地图列表涵盖该赛季的每一张地图,无论启用与否,因为对局会引用它们。每张给出 map_id、players、rows、cols、是否 enabled(启用中)、added_at,以及在它上面进行过的计分 matches 数。?enabled=true 把结果收窄到启用中的地图,?boards=true 则为每张附上 board:上传时的地图文件原样不动,它与回放中携带的 JSON、以及 tinybrains 从路径读取并运行的文件是同一个。地图从管理员上传那一刻起就是公开的。

rules 是赛季的规则文档,整体公开:它就是你要参加的比赛。谁可以参赛不属于规则。entry 说明赛季是否仅限其参与者参加;而名单本身写着具体的人,只有赛季管理员能读取。

私有赛季

私有赛季只对它的参与者、它的赛季管理员和平台管理员可见。对其他任何人,每条路由的回答都与赛季不存在时一样,它的版本和对局都回答 404。成员通过公开读取的登录版来读它:参数相同、响应形状相同,位于 /v1/private 之下,从不缓存。对于调用者无权看到的赛季,每条路由的回答都与公开路由完全一致。

方法路径对应的公开路由
GET/v1/private/games/{game}/seasons/v1/games/{game}/seasons,外加你有权看到的私有赛季
GET/v1/private/games/{game}/leaderboard/v1/games/{game}/leaderboard
GET/v1/private/games/{game}/leaderboard/series/v1/games/{game}/leaderboard/series
GET/v1/private/games/{game}/seasons/{slug}/maps.../seasons/{slug}/maps
GET/v1/private/games/{game}/seasons/{slug}/maps/{map_id}.../seasons/{slug}/maps/{map_id}
GET/v1/private/games/{game}/seasons/{slug}/podium.../seasons/{slug}/podium
GET/v1/private/games/{game}/seasons/{slug}/playing.../seasons/{slug}/playing
GET/v1/private/models/{id}/v1/models/{id}
GET/v1/private/versions/{id}/v1/versions/{id}
GET/v1/private/matches/v1/matches
GET/v1/private/matches/{id}/v1/matches/{id}
GET/v1/private/matches/{id}/frame/v1/matches/{id}/frame
GET/v1/private/matches/{id}/related/v1/matches/{id}/related
GET/v1/private/threads/v1/threads:私有赛季的对局上的评论,只在能看到该赛季的人之间

不带 season 时,成员读取仍然解析为精选赛季:私有赛季永远不会是默认值,所以请指明它。

模型、版本与对局

API 用 UUID 来指代模型,版本则用它自己的 UUID。模型与版本解释了原因。

方法路径认证参数
POST/v1/games/{game}/models会话请求体 {name}
GET/v1/models会话可选的 game 查询参数;调用者的模型
GET/v1/models/{id}公开模型 UUID
PATCH/v1/models/{id}会话,所有者请求体 {name?, retired?}
GET/v1/models/{id}/season公开本赛季的战绩:最近五场结果、最佳与最差、当前名次和一周前的名次
GET/v1/models/{id}/rivals公开与交手过的每个模型的对战记录
GET/v1/versions/{id}公开版本 UUID;仅当版本处于参赛中、已被取代或 disabled(被停用的基线)时可读,且仅限公开赛季
GET/v1/me/versions/{id}会话,所有者你的某个版本,任何状态都可以;从不缓存
PATCH/v1/versions/{id}会话,所有者请求体 {note}
GET/v1/games/{game}/submission会话发起请求之前,你相对于赛季提交限制的状况;可选 model 和 season(不带时为精选赛季)
GET/v1/matches公开筛选条件见下;可选 limit,默认 25,最大 60,以及 cursor
GET/v1/matches/{id}公开对局 UUID;仅限公开赛季中已结束的公开对局
GET/v1/matches/{id}/frame公开对局的最后一帧,带每个席位的得分
GET/v1/matches/{id}/related公开与它相关的十二场对局
GET/v1/me/matches会话你的每一场对局,任何状态;可选 game、limit、cursor
GET/v1/me/matches/{id}会话,所有者你的某一场对局,任何状态都可以

模型详情给出它的名称、所有者、是否已退役,以及它的每一个公开版本,最新的在前。

版本详情给出它的模型、所有者、游戏、版本号、class 和 class_max_bytes、该级允许的记忆和 memory_bytes、size_bytes、param_count、实测的 infer_us、两个哈希、orion_version(审核它的运行时)、season、status、phase、admit_attempt、successor、reject_reason、note、最近一次 trial、ratings、baseline、created_at 和 last_played_at。许多字段在审核产出它们之前一直为 null。ratings 按天梯为键。successor 是同一模型的下一个版本号,只在这个版本被取代后才出现。

公开的版本路由只在版本上了天梯之后才显示它。当你的版本还在审核中、等待试赛或已被拒绝时,GET /v1/versions/{id} 回答 404,要读它的阶段、尝试次数和原因,去 GET /v1/me/versions/{id}。对局也是同样的划分:GET /v1/matches/{id} 回答已结束的公开对局,对试赛或排队中的配对回答 404,而 GET /v1/me/matches/{id} 会把它们返回给所有者。

GET /v1/matches 回答一个对象:season、total、matches、sort 和 next_cursor,每场对局是一张卡片,带每个席位的模型、所有者、名次和得分。它列出已结束和已计分的对局,最新的在前,以及通过了试赛的候选版本的试赛。用 model(某个模型的每个版本)或 version(某一个版本)、game、season、ladder、class、map、outcome、owner、players_min、players_max、vs(交手过的两个模型)、since 和 top 来收窄,用 sort 排序:newest、closest、upset、longest 或 discussed。沿 next_cursor 翻到下一页。

GET /v1/me/matches 补上那个列表漏掉的部分。对你自己的对局,它还会返回排队中、已取消和失败的行,以及你的试赛,按各自进行的时间排序,最新的在前;从未进行过的则按创建时间。每一行为取消的对局加上 withdrawn_reason 和 successor,为失败的对局加上 fault_reason;每个席位带有 mine,这样在你自己的两个模型之间的对局里,你也能分清哪一边是哪一个。它用 total 和 next_cursor 分页。

对局详情包含 seats、is_trial、engine_digest 和 orion_version、seed、map(地图在其赛季中的 id)、reason、turns、played_at、played_ms、status、取消和失败相关的字段、它计分所在的天梯,以及存在回放时的临时 replay_url,签名有效期一小时。每个席位记录它的模型和版本、所有者、重量级、名次、得分、判罚次数、结果和它的 rating_change。版本的 trial 字段报告试赛进度。

GET /v1/models/{id} 对未知的 id 回答一个 null 响应体而不是 404;版本、对局和个人主页路由则回答 404 not_found。两种情况都要处理。

提交

先问再提交。GET /v1/games/{game}/submission?season=<slug> 用拒绝时会用的措辞,报告你相对于该赛季提交限制的状况(你在各项上限下持有多少模型和版本、你是否已有候选版本在处理中、冷却期何时结束)。读一次就能在撞上 409 之前知道它,只有 weights_already_entered 例外,它需要你提交的哈希。

POST /v1/submissions 需要会话,以及这样的请求体:

{
  "game": "ants",
  "season": "summer-2026",
  "model": "<model UUID>",
  "weights_hash": "sha256:<64 位十六进制数字>",
  "manifest_hash": "sha256:<64 位十六进制数字>",
  "note": "可选:这个版本改了什么"
}

season 是版本要进入的赛季的 slug,省略时是该游戏的精选赛季。model 是你已有的某个模型的 model_id。Soma 对未知的模型回答 404 unknown_model;提交从不会创建模型。note 可选,最多一小段,会以 400 note_too_long 或 422 note_word_listed 拒绝。

把示例值换成你的模型 id 和你的哈希。响应是 201,带 version_id、model_id、model、version、status、season、两个哈希和 upload。Soma 记录下一个审核中的版本,它在上天梯之前还要经过审核和试赛。再次提交同样的两个哈希,会对同一版本回答 200 并给出新的上传 URL,有效期为该版本三十分钟上传窗口的剩余时间;用它来补救失败的上传。基于会话的示例见提交版本。

POST /v1/submissions/reenter 需要会话和请求体 {game, model, season, from}。它把你的模型 model 以它在赛季 from 中的状态(它在那里参赛中的版本,或它最近一个参过赛的版本)带入赛季 season,字节完全相同:不上传任何东西,回答中也没有 upload。新版本为 testing,审核和试赛会按新赛季的规则再判一次。它回答 201 和新版本,若该版本已在处理中则回答 200。缺少字段时以 400 reenter_invalid 拒绝,在 from 中没有参赛版本时以 404 nothing_to_reenter 拒绝,其余情况与提交相同。见转入新赛季。

网站的社区路由

网站的页面读取这些路由;脚本也可以。除非另有标注,它们都是公开的。

方法路径结果
GET/v1/announcements网站上显示的公告
GET/v1/stories已发布的博客文章与精选模型文章的信息流
GET/v1/posts/{slug}一篇已发布的博客文章
GET/v1/models/{id}/story模型已批准的模型文章;所有者在 /v1/me/models/{id}/story 读取草稿,并用 PUT /v1/models/{id}/story 写入
GET/v1/games/{game}/picks管理员加入精选的对局
GET/v1/threads?match= 或 ?model=某场对局或某个模型下的评论区
POST/v1/threads/comments会话:发表评论或回复
DELETE/v1/comments/{id}会话:删除你自己的评论
POST/v1/comments/{id}/reports会话:举报评论
GET/v1/me/comments会话:你仍在待审中的评论
GET/v1/profiles/{username}/comments参赛者的公开评论
GET, POST/v1/me/notifications、/v1/me/notifications/read会话:你的通知流,以及标为已读
GET, PATCH/v1/me/notification-settings会话:你会收到哪些通知
POST/v1/events匿名观看计数;回答 204

错误与速率限制

先检查 HTTP 状态码,再去读成功的响应体。Soma 拒绝请求时:缺少哈希、缺少名称或备注过长为 400,会话无效为 401,不属于你的模型为 404,赛季、资格、配额、冷却、重复权重或候选版本正在处理中的冲突为 409,备注或简介含有网站列入名单的词为 422。错误详情取决于响应是 Soma 还是它底下的运行时产生的,两者有所不同。拒绝原因参考把请求错误与之后的版本裁定分开说明。

Soma 在两个层面声明速率限制:按路由,以及在有账户参与的路由上按已登录用户。

范围限制
每条公开路由每秒 30 个请求,突发 60
登录每秒 5 个请求,突发 10
每条会话路由每秒 20 个请求,突发 40
按用户,读取:/me、/models、/me/matches、提交预检、通知流每秒 10 个请求,突发 20
按用户,写入:提交、创建和编辑模型、你的个人主页、撤销会话、赛季管理每秒 1 个请求,突发 5
按管理员,上传和切换地图与基线每秒 5 个请求,突发 40

遇到 429 就退避,并遵守响应给出的重试时间。这些频道没有声明每日提交额度。

管理路由

这些都不是参赛者的操作。每一条都需要有效的会话,而且 Soma 在每个请求上都会检查调用者的身份,所以被移除的管理员在下一次调用时就会失去这条路由。

平台管理员创建赛季,并决定平台范围内的事项;其他任何人都会得到 403 admin_only:

方法路径作用
POST/v1/games/{game}/seasons创建赛季:name、提交窗口、rules、weight_classes、visibility、entry、fleet、providers,以及可选的 admins(用户名)
POST/v1/games/{game}/seasons/{slug}/featured将公开赛季设为精选:所有未指明赛季的读取都会解析到它
PATCH/v1/games/{game}/seasons/{slug}/fleet{matches, admissions},各为 own、platform 或 both,赛季进行期间可改
PATCH/v1/games/{game}/seasons/{slug}/fill空闲补赛,赛季进行期间可改
GET, POST, PATCH/v1/games/{game}/seasons/{slug}/rounds、.../rounds/{n}轮次与决赛
POST, DELETE/v1/games/{game}/seasons/{slug}/admins按 handle 指派或移除赛季管理员

平台管理台的其余部分位于 /v1/admin/* 下(公告、审计日志、评论审核、事件、通知、精选、博客、模型文章、评论区、用户和词表),/v1/runner-keys 和 /v1/runners 则管理平台机群。

赛季管理员和平台管理员通过这些路由运营一个赛季。其他任何调用者都会被以 403 season_admin_only 拒绝;调用者无权看到的私有赛季回答 404 unknown_season,与不存在的赛季一样。

方法路径作用
PATCH/v1/games/{game}/seasons/{slug}编辑 scheduled 赛季的提交窗口、规则和重量级;名称、可见性、参赛资格和机群永远不能改
PATCH/v1/games/{game}/seasons/{slug}/entry{"entry": "restricted"}:收窄 scheduled 赛季的参赛资格。只能单向、只能一次,而且不能用于私有赛季,它本来就是受限的
POST/v1/games/{game}/seasons/{slug}/close请求结束赛季,撤回时钟会在一分钟内执行
GET, POST, DELETE/v1/games/{game}/seasons/{slug}/participants参与者名单:添加 {logins, provider?}(一个或多个,默认 github),移除 {id}
GET/v1/games/{game}/seasons/{slug}/admins赛季的管理员
POST, PATCH/v1/games/{game}/seasons/{slug}/maps、.../maps/{map_id}上传一张地图,由游戏自己的引擎检查,Soma 以停用状态存储;{"enabled": true} 或 false 将它启用或停用
POST/v1/games/{game}/seasons/{slug}/maps/import{from, maps?}:把另一个赛季的地图复制进来,处于停用状态
GET, POST, PATCH/v1/games/{game}/seasons/{slug}/baselines、.../baselines/{baseline}列出、上传(一个 name 和两个哈希,回答两个上传 URL;审核把它当作提交来处理,通过后处于停用状态)以及切换基线
POST/v1/games/{game}/seasons/{slug}/baselines/import{from, baselines?}:按本赛季的规则重新审核另一个赛季的基线,处于停用状态
GET, POST/v1/games/{game}/seasons/{slug}/runner-keys{admissions, keys}:赛季的审核队列是否有运行器可以处理它(queued、admitters、reach),以及它自己的运行器密钥和各自的运行器;POST 生成一个新密钥,只显示一次,仅服务于本赛季
DELETE/v1/games/{game}/seasons/{slug}/runner-keys/{key}吊销本赛季的一个密钥
DELETE/v1/games/{game}/seasons/{slug}/runners/{runner}停止本赛季的一个运行器;它当前的对局会打完
GET, POST/v1/games/{game}/seasons/{slug}/notify向赛季的参与者发送通知({subject, link?}),并读取过往的发送记录
GET/v1/games/{game}/seasons/{slug}/audit对该赛季的每一项管理操作,最新的在前

请求契约在 Soma 的工作流里。没有任何路由能强制发起对局、晋级版本或撤回你自己的版本。

GET /v1/admin-check 是一个授权探测:反向代理调用它来决定是否把请求放行到运维控制台,参赛者用不到它。