计量、额度与限流
本页说明一次请求计多少点、免费档包含什么,以及触发限流时的 429 响应。API 调用目前处于免费计量阶段:会记录用量,但不收费。
1. 计费点数
只有成功的 /v1/forecast 请求计点:
计费点数 = usage.input_data_points + usage.forecast_data_points| 字段 | 统计口径 |
|---|---|
usage.input_data_points | 实际喂给模型的观测点数。长缺口切分丢掉的点不计;插值补出的点与补入的时间戳也不计。 |
usage.forecast_data_points | 预测点数,即 h × 实体数。 |
例如一个实体、48 个逐小时观测值、h 为 24:计 48 + 24 = 72 点。若其中有 2 个时间戳缺失并被补齐,仍按 46 个观测值计,即 46 + 24 = 70 点。
- 错误响应不计点,包括 4xx、429 与 5xx。
/v1/clean不计点,也不占用每日额度。- 客户端超时:如果请求在服务端已经成功,仍会计点,见认证与请求头中关于重试的说明。
2. 免费档
| 项目 | 免费档 |
|---|---|
/v1/forecast 每分钟请求数(RPM) | 10 |
/v1/forecast 每日点数 | 1,000,000(每天 UTC 零点、即北京时间 8:00 恢复) |
/v1/forecast 单次请求 | 最多 200,000 行 |
/v1/clean | 免费,每个账号每分钟 60 次,单次最多 200,000 行 |
账号实际生效的请求数上限,以任一密钥响应中的 X-RateLimit-Limit 为准(见第 4 节)。
3. 当前执行的限制
所有限制都按账号计算:同一账号下的所有密钥共用同一个每分钟请求窗口和同一份每日点数额度,多建密钥不会增加额度。不同账号之间互不占用。
X-RateLimit-Policy | 适用端点 | 计什么 | 窗口与恢复 |
|---|---|---|---|
forecast-rpm | /v1/forecast | 请求数,上限 10 | 60 秒滑动窗口 |
clean-rpm | /v1/clean | 请求数,上限 60 | 60 秒滑动窗口 |
forecast-daily-points | /v1/forecast | 点数,上限 1,000,000 | 按 UTC 自然日计算,每天 UTC 零点(北京时间 8:00)恢复 |
- 请求数限制在认证之后、解析请求体之前检查,所以请求体有误的调用也会占用次数。被限流的请求本身不记录。
- 每日点数在请求开始时按「行数 +
h× 实体数」预占;请求成功后改按实际计费点数结算,失败则全部释放。所以一个请求能否通过,取决于它的预估点数能否放进今天剩余的额度。 - 单次请求上限不属于限流:
/v1/forecast最多 200,000 行(series与future_covariates合计),/v1/clean最多 200,000 行,超出立即返回 422POINTS_EXCEEDED,不清洗、不占额度。该上限按清洗主机的内存设定,超大的单个请求会被直接拒绝而不是排队;请拆成多次请求。 - 单条序列的上下文也不属于限流:某个实体长于所选模型的最大上下文时返回 422
SERIES_TOO_LONG,不计费。服务端不会替你截断历史,见模型与能力上限。 - 清洗容量所有账号共享,也不属于限流:同一时刻在清洗的数据量有上限。容量满时
/v1/forecast会短暂排队;/v1/clean不排队,直接返回 503SERVICE_UNAVAILABLE并带Retry-After(这次调用仍占用一次clean-rpm),按Retry-After重试即可。
4. 429 响应
超出请求数限制时:
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1790665223
X-RateLimit-Limit-Type: rpm
X-RateLimit-Policy: clean-rpm
X-Request-Id: req_7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"category": "rate_limited",
"retryable": true,
"message": "Too many requests for this account; please respect the Retry-After interval",
"request_id": "req_7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d",
"details": {"limit_type": "rpm", "limit_scope": "account", "limit_value": 60, "retry_after_seconds": 23}
}
}| 响应头 | 请求数限制(rpm) | 每日点数(daily) |
|---|---|---|
Retry-After | 窗口中出现空位还需的秒数 | 距下一个 UTC 零点的秒数 |
X-RateLimit-Limit | 每分钟请求上限 | 每日点数上限 |
X-RateLimit-Remaining | 0 | 今天剩余的点数 |
X-RateLimit-Reset | 窗口出现空位的 Unix 时间(秒) | 下一个 UTC 零点的 Unix 时间(秒) |
X-RateLimit-Limit-Type | rpm | daily |
X-RateLimit-Policy | forecast-rpm 或 clean-rpm | forecast-daily-points |
X-RateLimit-* 响应头不只出现在 429 中:密钥通过认证后,/v1/forecast 与 /v1/clean 的每个响应(包括成功响应和其他错误)都会携带,描述该密钥所属账号在本次请求之后的请求数限制(rpm);同一账号的其他密钥发出的请求也会让 X-RateLimit-Remaining 减少。X-RateLimit-Remaining 是当前 60 秒窗口内还能发起的请求数,可据此在被拒绝之前放慢速度。只有 429 描述的是被耗尽的那个桶,可能是 daily。密钥无效(401)的响应不携带这些响应头。
429 的 details.limit_scope 固定为 account,表示耗尽的是账号级限额:换用同一账号的另一个密钥重试不会通过。
每日额度不足时,details 还会给出原因与本次预估的点数:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"category": "rate_limited",
"retryable": true,
"message": "Daily point quota of this account exhausted",
"request_id": "req_2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e",
"details": {
"limit_type": "daily",
"limit_scope": "account",
"limit_value": 1000000,
"retry_after_seconds": 30512,
"reason": "daily_quota",
"requested_points": 2400
}
}
}requested_points 大于 X-RateLimit-Remaining 时,可以把请求拆小(减少实体数或历史长度)后在今天的剩余额度内完成。
5. 客户端重试
请求数限制适合按 Retry-After 等待后自动重试;每日额度要到 UTC 零点才恢复,不要在进程内等待。
import time
import requests
def retryable(resp):
"""error.retryable: whether the same request may succeed when sent again."""
try:
return resp.json()["error"]["retryable"] is True
except (ValueError, KeyError, TypeError):
return False
def post_with_retry(url, payload, headers, attempts=4):
for attempt in range(attempts):
resp = requests.post(url, json=payload, headers=headers, timeout=180)
if resp.ok or not retryable(resp) or attempt == attempts - 1:
return resp
# The daily point quota only frees up at the next UTC midnight: surface it, do not sleep.
if resp.headers.get("X-RateLimit-Limit-Type") == "daily":
return resp
# 429 and MODEL_BUSY carry Retry-After; the other retryable errors usually do not.
time.sleep(int(resp.headers.get("Retry-After", 2 ** attempt)))
return resp