AMD 免费 API 白嫖指南:给 DeepSeek 搭 20 RPM 限流代理,彻底解决 429 报错(含完整代码)

📌 文章更新日志

  • 2026-09-11 重要更新:新增「并发闸门(信号量) + 429 就地退避重试」深度实战落地与排坑。彻底解决多线程穿透与重试风暴导致的 process_concurrency_rate_limit_exceeded 报错,并在文末补充容器权限运维避坑经验。
  • 2026-08-28 初始发布:梳理 AMD Radeon 免费 API 获取指引,实现基础令牌桶(20 RPM)与模型探测拦截代理。

免费 API 的 20 RPM 限制,让自主智能体(Agent)频繁报 429。深入排查发现,真正的问题不仅是限流本身,更是框架启动时的模型探测请求把宝贵的对话额度给挤占光了,且多线程并发穿透与客户端重试风暴更是导致 process_concurrency 报错的隐形推手。本文带来完整实战:AMD 免费 API 获取指引、多层限流原理、令牌桶 + 并发信号量代理设计、就地退避重试与容器排坑记录。


一、AMD Radeon 免费推理 API 简介与获取方法

1.1 什么是 AMD Radeon 免费推理 API?

为了推广 AMD ROCm 生态与 Radeon 硬件算力,AMD 中国开发者社区推出了面向开发者的免费大模型推理服务(AMD AI Developer Hub)。开发者无需自建显卡服务器,即可直接调用其托管的高性能推理端点。

平台目前重点开放了以下主流前沿模型:

  • DeepSeek-V4-Flash:极致响应速度、超强代码与逻辑推理能力,非常适合作为日常 Agent、代码助手的主力模型。
  • GLM-5.2:综合素质均衡的通用大语言模型。
  • MinerU2.5-Pro:针对复杂文档/PDF 解析与数据提取的高精度多模态模型。

1.2 核心特性与配额机制

  • 协议兼容:完全兼容标准 OpenAI API 格式(如 /chat/completions/models 等)。
  • 免费配额:单个开发者账号享有 20 RPM(每分钟 20 次请求) 的免费额度。
  • 调用端点(Base URL)https://developer.amd.com.cn/radeon/api/v1

1.3 手把手获取 API Key(保姆级步骤)

  1. 访问平台:浏览器打开 AMD 开发者社区 / 开发者平台
  2. 注册登录:使用手机号或邮箱完成注册,按提示完成基础开发者信息登记;
  3. 进入模型体验中心:导航至「模型体验 / AI 推理服务」专区,勾选开通 DeepSeek-V4-Flash 等模型服务权限;
  4. 生成 API Key:在控制台中点击「创建 API 密钥」,复制生成的 Token(即环境变量中的 CUSTOM_API_KEY);
  5. 基础配置信息
    • Base URLhttps://developer.amd.com.cn/radeon/api/v1
    • Model NameDeepSeek-V4-Flash
    • Auth HeaderAuthorization: Bearer YOUR_API_KEY

二、问题爆发:20 RPM 为什么会让 Agent 疯狂 429?

笔者将 Hermes Agent 接入该接口后,本以为 20 RPM 对单人日常对话绰绰有余,但 Agent 却频繁触发 429 Too Many Requests,任务屡屡中断

一开始直觉以为只要把请求调慢点就行,但加了简单的延时后依然频繁 429。排查捕获的原始报错信息揭示了真相:

1
2
3
4
5
6
7
{
"error": {
"message": "Model API rate limit exceeded; please retry later",
"type": "rate_limit_error",
"code": "process_concurrency_rate_limit_exceeded"
}
}

AMD 的两层独立限流机制

限流层 配额限制 触发条件 响应特征
RPM 限流 20 次/分钟 滑动窗口内总请求数超标 retry-after 按时间窗口重置
并发限流 (process_concurrency) 瞬时活跃连接数 同时在处理的 HTTP 连接过多 retry-after: 1(短冷却)

关键认知:这两层限流是互相独立的。即使你一分钟只发了 5 个请求,只要某一瞬间 Agent 建立了多个并发连接,就会直接被并发限制打回 429!


三、深层盲区:令牌桶只管“速率”不管“并发”——重试风暴才是 429 的隐形推手

上个版本只做了“探测拦截”,以为 20 RPM 就绪了。但把 Agent 正式跑起来后,429 依旧阴魂不散。这次捕获的报错换了个马甲:

1
2
3
4
5
6
7
{
"error": {
"message": "Model API rate limit exceeded; please retry later",
"type": "rate_limit_error",
"code": "process_concurrency_rate_limit_exceeded"
}
}

同一个 process_concurrency 错误码,但成因完全不同。逐层拆解后发现一个被多数限流教程忽略的盲区

限流层 令牌桶代理能否挡住 为什么漏
RPM(20/分钟) ✅ 能挡 令牌桶按 3s 补 1 匀速放行,速率上永不超过 20
并发(process_concurrency) 挡不住 令牌桶是“每个请求取 1 令牌、阻塞排队”的速率闸门,不是并发闸门。Hermes 用的是 ThreadingHTTPServer(每请求一线程),多个线程能同时穿过令牌桶(都在 sleep 排队,互不持锁),瞬间并发打到 AMD

这就是 429 风暴的完整链条:

1
2
3
4
5
6
多线程并发穿透令牌桶
→ 瞬时活跃连接数 > AMD process_concurrency 阈值
→ 上游返回 429
→ Hermes 客户端收到 429 立即重试
→ 多个重试再次并发穿透
→ 再次 429 …… 恶性循环

即使把 RPM 限到 20 以内,只要瞬时并发超标,AMD 依旧判你 process_concurrency_rate_limit_exceeded


3.1 修复 A:并发闸门(信号量)

在令牌桶后面串一个 BoundedSemaphore,限制同时只能有 N 个在途上游请求。这里 N=2 是实测安全值——既压住并发,又不至于把单条流式对话卡死。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading

# 放在 build_proxy_handler 外层
sem = threading.BoundedSemaphore(2) # 同时最多 2 个在途上游请求

# 计费请求:先过速率闸门,再过并发闸门
if is_billing:
bucket.acquire() # 速率闸门:3s 补 1,排队式
sem.acquire() # 并发闸门:最多 2 个在途
try:
self._send_upstream(url, body, max_retries)
finally:
if is_billing:
sem.release() # 无论成败都要释放槽位

3.2 修复 B:429 就地退避重试(切断重试风暴)

更狠的一招——别再让 429 抛回给客户端。Hermes 收到 429 会当成错误疯狂重试,而这正是风暴的燃料。改为代理在上游侧就地重试:读 Retry-After 头(并发限流给的是 retry-after: 1,很短),sleep 后再试,最多 3 次。这样客户端永远只看到 200 或最终的确定失败,429 永远不会触发客户端重试逻辑。

1
2
3
4
5
6
7
8
9
def _send_upstream(self, url, body, max_retries=3):
for attempt in range(max_retries + 1):
# ... 构造并发送请求 ...
except urllib.error.HTTPError as e:
if e.code == 429 and attempt < max_retries:
ra = float(e.headers.get("Retry-After", 1))
time.sleep(ra) # 就地退避,不抛回客户端
continue # 重试
# 非 429 或重试耗尽:如实回给客户端

3.3 实测效果验证与四道防线体系

打上 并发闸门(2) + 就地重试(3) 后,重启代理进程。健康检查正常,真实对话请求连续返回 200

1
2
[proxy 04:31:40] "POST /chat/completions" → 200   ← 真实对话稳定通过
[proxy 04:31:40] "GET /healthz" → 200

四条防线补全后,429 彻底绝迹:

防线 手段 拦住的敌人
第 1 道 令牌桶(速率) RPM 超限
第 2 道 GET 探测本地 Mock 探测挤占额度
第 3 道 信号量(并发) 并发穿透
第 4 道 429 就地重试 重试风暴

四、方案设计:打造四道防线的本地限流代理网关

解决 429 通常有两种实现思路:

  1. 进程内限流(在 Python 脚本内 time.sleep 限速):仅对单一会话或脚本有效,多进程、多会话并发(如 Matrix 机器人 + Web Dashboard)时彻底失效。
  2. 本地限流代理(HTTP 代理 + 令牌桶 + 信号量网关):所有 Agent 与应用统一将 base_url 指向本地代理,由代理统一管控限速、排队、并发与重试。

我们采用 方案 2,整体四重防线架构如下:

1
2
3
4
5
6
7
8
9
10
Hermes Agent (base_url → http://127.0.0.1:8077)


本地限流代理 :8077
├── [防线 2] 探测拦截:GET /models 等本地 Mock 极速返回(0 额度损耗)
├── [防线 1] 速率闸门:令牌桶 20/min(容量 20,每 3 秒补充 1 令牌)
├── [防线 3] 并发闸门:信号量 BoundedSemaphore(2)(严格限制在途并发数)
└── [防线 4] 就地重试:捕获上游 429 按 Retry-After 自动退避(切断客户端风暴)

└──► AMD 接口 https://developer.amd.com.cn/radeon/api/v1

代理网关的核心优势:

  • 全局统一限速:多会话共享 20 RPM 额度池,绝不超发。
  • 并发严格约束:信号量控制在途连接不超过 2 个,根绝并发限流。
  • 就地静默重试:上游 429 由代理内部退避消化,阻断客户端重试风暴。
  • 队列式阻塞排队:超出配额的请求在本地队列优雅排队,而不是直接被上游 429 拒绝。
  • 路径拦截与探测消除:本地响应模型探测,避免无谓消耗上游额度(核心破局点)。

五、核心实现:完整生产级限流代理源码

5.1 线程安全令牌桶算法

令牌桶是经典的流控算法:系统以恒定速率生成令牌,请求到达时必须取得令牌方可通行,无令牌则进入睡眠等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import threading
import time

class TokenBucket:
"""固定速率令牌桶: capacity 容量, rate 每秒补充量。线程安全。"""
def __init__(self, capacity: float, rate: float):
self.capacity = capacity
self.rate = rate
self.tokens = float(capacity)
self.last = time.monotonic()
self.lock = threading.Lock()

def acquire(self):
"""阻塞直到取到 1 个令牌。"""
while True:
with self.lock:
now = time.monotonic()
self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
self.last = now
if self.tokens >= 1.0:
self.tokens -= 1.0
return
wait = (1.0 - self.tokens) / self.rate
time.sleep(wait)

5.2 HTTP 代理服务(四道防线完整集成)

代理做了四件至关重要的事:

  1. 速率闸门:对写操作(POST /chat/completions)扣减令牌;
  2. 并发闸门:信号量限制同时在途的上游连接数不超过 2 个;
  3. 就地重试:遇到上游 429 自动读取 Retry-After 进行就地退避重试,不回抛给客户端;
  4. 路径归一化与探测拦截:剥离多余 /v1 前缀;将 GET /models 等探测请求在代理层直接 Mock 返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import argparse
import json
import os
import sys
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

DEFAULT_UPSTREAM = "https://developer.amd.com.cn/radeon/api/v1"

def build_proxy_handler(bucket, sem, upstream, api_key, max_retries=3):
class ProxyHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"

def log_message(self, fmt, *args):
sys.stderr.write("[proxy %s] %s\n" % (time.strftime("%H:%M:%S"), fmt % args))

def _send_upstream(self, url, body, max_retries=max_retries):
"""向上游发送请求,遇到 429 自动就地退避重试,切断客户端重试风暴"""
for attempt in range(max_retries + 1):
req = urllib.request.Request(url, data=body, method=self.command)
for h, v in self.headers.items():
low = h.lower()
if low in ("host", "content-length", "connection", "accept-encoding"):
continue
req.add_header(h, v)
if api_key:
req.add_header("Authorization", f"Bearer {api_key}")

try:
with urllib.request.urlopen(req, timeout=300) as resp:
out = resp.read()
self.send_response(resp.status)
for h, v in resp.headers.items():
if h.lower() in ("content-length", "connection", "transfer-encoding"):
continue
self.send_header(h, v)
self.send_header("Content-Length", str(len(out)))
self.end_headers()
self.wfile.write(out)
return
except urllib.error.HTTPError as e:
if e.code == 429 and attempt < max_retries:
ra = float(e.headers.get("Retry-After", 1) or 1)
self.log_message("收到 429 process_concurrency,触发就地退避重试 (attempt %d/%d, sleep %.1fs)",
attempt + 1, max_retries, ra)
time.sleep(ra)
continue
out = e.read()
self.send_response(e.code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(out)))
self.end_headers()
self.wfile.write(out)
return
except Exception as e:
payload = json.dumps({"error": {"message": f"proxy upstream error: {e}",
"type": "proxy_error"}}).encode()
self.send_response(502)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
return

def _forward(self):
is_billing = self.command in ("POST", "PUT", "PATCH", "DELETE")

# 计费写请求:先过速率闸门(令牌桶),再过并发闸门(信号量)
if is_billing:
bucket.acquire() # 速率闸门:3s 补 1,平滑排队
sem.acquire() # 并发闸门:限制在途上游连接数

try:
length = int(self.headers.get("Content-Length", 0) or 0)
body = self.rfile.read(length) if length else None

# 路径归一化:upstream 已包含 /api/v1,剥离请求路径多余的 /v1 前缀
path = self.path.split("?", 1)[0]
if path.startswith("/v1/") and not path.startswith("/v1.1/"):
path = path[3:] # /v1/chat/completions -> /chat/completions

url = upstream.rstrip("/") + path
self._send_upstream(url, body, max_retries)
finally:
if is_billing:
sem.release() # 无论成功失败必释放并发槽位

def do_GET(self):
path = self.path.split("?", 1)[0]
# 健康检查接口
if path == "/healthz":
body = b'{"status":"ok"}\n'
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return

# 模型探测拦截:本地响应,不透传 AMD —— 解决 429 的核心所在!
# AMD 对 404 的探测请求也计入 RPM,探测频繁会迅速吃光 20 次额度
if path in ("/v1/models", "/models", "/api/v1/models", "/api/tags",
"/v1/props", "/props", "/version",
"/v1/models/DeepSeek-V4-Flash", "/api/show"):
body = json.dumps({"object": "list", "data": [
{"id": "DeepSeek-V4-Flash", "object": "model"},
{"id": "GLM-5.2", "object": "model"},
{"id": "MinerU2.5-Pro", "object": "model"},
]}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return
self._forward()

do_POST = do_PUT = do_DELETE = do_PATCH = _forward

return ProxyHandler


def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=8077)
ap.add_argument("--upstream", default=DEFAULT_UPSTREAM)
ap.add_argument("--rpm", type=float, default=20.0, help="每分钟请求数限额 (默认 20)")
ap.add_argument("--concurrency", type=int, default=2, help="同时在途最大并发请求数 (默认 2)")
ap.add_argument("--max-retries", type=int, default=3, help="上游 429 就地退避最大重试次数 (默认 3)")
ap.add_argument("--api-key", default=None, help="缺省时读取环境变量 CUSTOM_API_KEY")
args = ap.parse_args()

api_key = args.api_key or os.environ.get("CUSTOM_API_KEY", "")
bucket = TokenBucket(capacity=args.rpm, rate=args.rpm / 60.0)
sem = threading.BoundedSemaphore(args.concurrency)
handler = build_proxy_handler(bucket, sem, args.upstream, api_key, max_retries=args.max_retries)
server = ThreadingHTTPServer(("127.0.0.1", args.port), handler)
print(f"[proxy] listening on http://127.0.0.1:{args.port} -> {args.upstream} (rpm={args.rpm:g}, conc={args.concurrency}, retry={args.max_retries})")
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()

if __name__ == "__main__":
main()

启动脚本:

1
2
export CUSTOM_API_KEY="你的_AMD_API_KEY"
python3 rate_limit_proxy.py --rpm 20 --concurrency 2 --max-retries 3

六、进程常驻:容器内自动守护

在无 systemd 权限的 Docker 容器环境下,通过 Shell 编写循环自守护脚本,确保代理意外崩溃时 3 秒内自愈重启:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env bash
# run_rate_limit_proxy.sh —— 常驻守护脚本
set -u
PORT=8077
PIDFILE="/opt/data/.proxy.pid"
LOG="/opt/data/proxy.log"

start() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "已在运行 (PID $(cat "$PIDFILE"))"; return 0
fi
nohup bash -c '
while true; do
echo "[$(date +%H:%M:%S)] 启动 rate_limit_proxy" >> /opt/data/proxy.log
python3 /opt/data/rate_limit_proxy.py --rpm 20 --concurrency 2 --max-retries 3 \
>> /opt/data/proxy.log 2>&1
echo "[$(date +%H:%M:%S)] 代理退出 (code=$?) 3s 后重启" >> /opt/data/proxy.log
sleep 3
done
' >> /opt/data/proxy.log 2>&1 &
echo "$!" > "$PIDFILE"
sleep 1
echo "健康检查: $(curl -s http://127.0.0.1:$PORT/healthz)"
}

stop() {
[ -f "$PIDFILE" ] && kill "$(cat "$PIDFILE")" 2>/dev/null && rm -f "$PIDFILE"
pkill -f "rate_limit_proxy.py" 2>/dev/null || true
}

status() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "运行中: PID $(cat "$PIDFILE")"
echo "健康检查: $(curl -s http://127.0.0.1:$PORT/healthz)"
else
echo "未运行"
fi
}

case "${1:-start}" in
start) start ;;
stop) stop ;;
restart) stop; sleep 1; start ;;
status) status ;;
*) echo "用法: $0 [start|stop|restart|status]"; exit 1 ;;
esac

将 Agent(如 Hermes)的 Base URL 切换至本地代理:

1
hermes config set model.base_url http://127.0.0.1:8077

七、深度排查的三大核心发现

发现 1:模型探测请求是“隐形额度杀手”(主因)

Hermes 等 Agent 框架在初始化或重连时,会默认发送一连串探活与模型列表请求:

1
2
/v1/models  /models  /api/v1/models  /api/tags
/v1/props /props /version /api/show

深坑机制:虽然这些路径在 AMD 端大多返回 404,但 AMD 的网关依然会按单次 HTTP 请求扣减 20 RPM 额度!
日志统计显示,Agent 仅启动阶段就发送了 6 次探测,瞬间消耗了近 1/3 的每分钟限额,紧随其后的真实对话请求自然直接撞上 429:

1
2
3
4
07:50:47 GET /v1/models          → 200 (探测占用配额)
07:50:48 POST /chat/completions → 429 ← 真实对话被挤出配额窗口
07:50:51 POST /chat/completions → 429
07:51:30 POST /chat/completions → 200 ← 窗口刷新后才恢复

修复效果:在本地代理端拦截 GET 探测并 Mock 响应后,透传给 AMD 的请求减少了 90% 以上,额度 100% 留给真实对话。

发现 2:并发连接过载限制与客户端重试风暴

错误码 process_concurrency_rate_limit_exceeded 证实了独立的并发限制。单靠令牌桶控制请求速率(RPM)无法限制瞬时多线程在途数量。

通过在代理网关串联 BoundedSemaphore(2)(并发闸门),严格把在途并发数压至安全阈值;同时在网关层就地截获 429 并依据 Retry-After 短暂 sleep 退避重试,彻底阻断了 429 向客户端抛出导致的疯狂重试风暴

发现 3:URL 前缀多重嵌套

AMD 的 Base URL 本身已包含 /api/v1。如果客户端又自动拼装 /v1/chat/completions,就会导致实际请求打到 /api/v1/v1/chat/completions 返回 404。代理做路径归一化后彻底消除了该隐患。


八、压测与实战验证效果

1. 令牌桶精度压测

使用 25 个并发请求冲击代理网关,观察延迟响应与流量分布:

1
2
3
4
总请求 25, 成功率 100% (HTTP 200), 总耗时 15.07s
#01 - #13 请求: 耗时 0.2s (初始突发桶容量内,瞬时平滑放行)
#14 - #20 请求: 耗时 1.2s (排队等待窗口补充)
#21 - #25 请求: 耗时 3.1s - 15.1s (精确按每 3 秒 1 令牌的速率平滑释放)

结论:完全符合 20/min 的流速规律,全程 0 次 429 报错,0 丢包。

2. 探测拦截压测

15 个高频 GET 探测并发打入,在 1.34s 内全部以 200 极速返回,且完全不消耗上游令牌。

3. 并发闸门与重试风暴阻断验证

打上 并发闸门(2) + 就地重试(3) 后,多线程对话与健康检查平稳运行,真实对话请求连续稳定返回 200:

1
2
[proxy 04:31:40] "POST /chat/completions" → 200   ← 真实对话稳定通过
[proxy 04:31:40] "GET /healthz" → 200

九、经验总结与最佳实践

  1. 限流设计应当“主动排队”而非“被动重试”:在客户端代理层进行优雅阻塞排队,比让服务端频频报错体验好数倍;
  2. 警惕框架隐蔽的探活请求:各类 AI 客户端(如 LiteLLM、Hermes、NextChat 等)普遍带有探测机制,代理层 Mock 是保护免费额度的最佳手段;
  3. 分清 RPM 与并发限制:不仅要控制单位时间请求总量(令牌桶),还要控制瞬时在途并发连接数(信号量);
  4. 切断客户端重试风暴:将 429 消化在代理网关层(读取 Retry-After 就地退避),防止客户端把错误放大成雪崩;
  5. 路径拼接务必注意归一化:注意 Base URL 末尾与客户端路由拼接规则,避免无效 404。

十、附录:运维踩坑——容器内换代码的权限陷阱

本案例跑在 Docker 容器里,Agent 进程是普通用户,代理进程是 root 属主。改完代码后 kill 旧代理进程被拒(Operation not permitted),新代码死活不生效。排查半天才发现是权限问题——容器内无 sudo、root 直接登录被禁。

最终解法是到宿主机:

1
2
3
4
5
6
7
# 方式一:最省事,重启整个容器(以 root 重新拉起守护脚本,自动用新参数)
docker restart <容器名>

# 方式二:精准杀旧进程,让守护循环用新代码自愈重启
docker exec -u root <容器名> pkill -f "rate_limit_proxy.py"
sleep 8 # 等守护循环 3s 后重启
tail -3 /opt/data/proxy.log # 确认新实例打印 (rpm=20, conc=2, retry=3)

⚠️ 避坑小贴士

  1. 改完代理代码,普通用户 kill 不了 root 属主进程,得走宿主机操作。
  2. 另外 pgrep -f rate_limit_proxy.py 会匹配到命令自身 shell 导致自杀,管理进程务必用脚本自带命令(run_rate_limit_proxy.sh)或按 PID 操作,不要在命令行直接写脚本名字面量。

附:独立脚本适用的轻量限速类(无代理版本)

如果你的业务场景只需在单一 Python 脚本中直连 AMD API,无需引入独立代理进程,可直接使用下方集成 RPM 与并发槽位的限速类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import time, random, threading, requests

class RateLimiter:
def __init__(self, rpm=15, max_concurrency=3): # 留余量,设置 15 RPM 避免触顶
self.interval = 60.0 / rpm
self.semaphore = threading.BoundedSemaphore(max_concurrency) # 控制并发槽
self.lock = threading.Lock()
self.next_time = 0.0

def wait(self):
with self.lock:
now = time.monotonic()
if now < self.next_time:
time.sleep(self.next_time - now)
self.next_time = max(time.monotonic(), self.next_time) + self.interval
self.semaphore.acquire() # 占用并发槽

def release(self):
self.semaphore.release()

limiter = RateLimiter(rpm=15, max_concurrency=3)

def call_api(payload, max_retries=6):
last_err = None
for attempt in range(max_retries):
limiter.wait()
try:
resp = requests.post(API_URL, json=payload, headers=HEADERS, timeout=120)
if resp.status_code == 429:
retry_after = resp.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 60) + random.uniform(0, 1)
last_err = f"429: {resp.text[:200]}"
time.sleep(delay)
continue
resp.raise_for_status()
return resp
except requests.exceptions.Timeout:
last_err = "timeout"
time.sleep(min(2 ** attempt, 30))
finally:
limiter.release() # 确保释放并发槽
raise RuntimeError(f"重试多次后仍失败: {last_err}")