惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
aimingoo的专栏
aimingoo的专栏
I
InfoQ
B
Blog RSS Feed
D
DataBreaches.Net
S
SegmentFault 最新的问题
P
Proofpoint News Feed
A
About on SuperTechFans
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
博客园 - 司徒正美
小众软件
小众软件
博客园 - Franky
有赞技术团队
有赞技术团队
D
Docker
T
Tailwind CSS Blog
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
酷 壳 – CoolShell
酷 壳 – CoolShell
B
Blog
V
Visual Studio Blog
宝玉的分享
宝玉的分享
爱范儿
爱范儿

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix: cap long SDK retry waits (#68474) (thanks @jetd1) · ...
steipete · 2026-04-23 · via Recent Commits to openclaw:main

@@ -1,5 +1,5 @@

11

import type { Model } from "@mariozechner/pi-ai";

2-

import { beforeEach, describe, expect, it, vi } from "vitest";

2+

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

3344

const {

55

fetchWithSsrFGuardMock,

@@ -38,6 +38,11 @@ describe("buildGuardedModelFetch", () => {

3838

.mockReturnValue({ allowPrivateNetwork: false });

3939

delete process.env.OPENCLAW_DEBUG_PROXY_ENABLED;

4040

delete process.env.OPENCLAW_DEBUG_PROXY_URL;

41+

delete process.env.OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS;

42+

});

43+44+

afterEach(() => {

45+

delete process.env.OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS;

4146

});

42474348

it("pushes provider capture metadata into the shared guarded fetch seam", async () => {

@@ -93,4 +98,177 @@ describe("buildGuardedModelFetch", () => {

9398

proxy: undefined,

9499

});

95100

});

101+102+

describe("long retry-after handling", () => {

103+

const anthropicModel = {

104+

id: "sonnet-4.6",

105+

provider: "anthropic",

106+

api: "anthropic-messages",

107+

baseUrl: "https://api.anthropic.com/v1",

108+

} as unknown as Model<"anthropic-messages">;

109+110+

const openaiModel = {

111+

id: "gpt-5.4",

112+

provider: "openai",

113+

api: "openai-responses",

114+

baseUrl: "https://api.openai.com/v1",

115+

} as unknown as Model<"openai-responses">;

116+117+

it("injects x-should-retry:false when a retryable response exceeds the default wait cap", async () => {

118+

fetchWithSsrFGuardMock.mockResolvedValue({

119+

response: new Response(null, {

120+

status: 429,

121+

headers: { "retry-after": "239" },

122+

}),

123+

finalUrl: "https://api.anthropic.com/v1/messages",

124+

release: vi.fn(async () => undefined),

125+

});

126+127+

const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");

128+

const response = await buildGuardedModelFetch(anthropicModel)(

129+

"https://api.anthropic.com/v1/messages",

130+

{ method: "POST" },

131+

);

132+133+

expect(response.status).toBe(429);

134+

expect(response.headers.get("retry-after")).toBe("239");

135+

expect(response.headers.get("x-should-retry")).toBe("false");

136+

});

137+138+

it("parses retry-after-ms from OpenAI-compatible responses", async () => {

139+

fetchWithSsrFGuardMock.mockResolvedValue({

140+

response: new Response(null, {

141+

status: 429,

142+

headers: { "retry-after-ms": "90000" },

143+

}),

144+

finalUrl: "https://api.openai.com/v1/responses",

145+

release: vi.fn(async () => undefined),

146+

});

147+148+

const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");

149+

const response = await buildGuardedModelFetch(openaiModel)(

150+

"https://api.openai.com/v1/responses",

151+

{ method: "POST" },

152+

);

153+154+

expect(response.headers.get("x-should-retry")).toBe("false");

155+

});

156+157+

it("parses HTTP-date retry-after values", async () => {

158+

const future = new Date(Date.now() + 120_000).toUTCString();

159+

fetchWithSsrFGuardMock.mockResolvedValue({

160+

response: new Response(null, {

161+

status: 503,

162+

headers: { "retry-after": future },

163+

}),

164+

finalUrl: "https://api.anthropic.com/v1/messages",

165+

release: vi.fn(async () => undefined),

166+

});

167+168+

const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");

169+

const response = await buildGuardedModelFetch(anthropicModel)(

170+

"https://api.anthropic.com/v1/messages",

171+

{ method: "POST" },

172+

);

173+174+

expect(response.headers.get("x-should-retry")).toBe("false");

175+

});

176+177+

it("respects OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS", async () => {

178+

process.env.OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS = "10";

179+

fetchWithSsrFGuardMock.mockResolvedValue({

180+

response: new Response(null, {

181+

status: 429,

182+

headers: { "retry-after": "30" },

183+

}),

184+

finalUrl: "https://api.anthropic.com/v1/messages",

185+

release: vi.fn(async () => undefined),

186+

});

187+188+

const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");

189+

const response = await buildGuardedModelFetch(anthropicModel)(

190+

"https://api.anthropic.com/v1/messages",

191+

{ method: "POST" },

192+

);

193+194+

expect(response.headers.get("x-should-retry")).toBe("false");

195+

});

196+197+

it("can be disabled with OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS=0", async () => {

198+

process.env.OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS = "0";

199+

fetchWithSsrFGuardMock.mockResolvedValue({

200+

response: new Response(null, {

201+

status: 429,

202+

headers: { "retry-after": "239" },

203+

}),

204+

finalUrl: "https://api.anthropic.com/v1/messages",

205+

release: vi.fn(async () => undefined),

206+

});

207+208+

const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");

209+

const response = await buildGuardedModelFetch(anthropicModel)(

210+

"https://api.anthropic.com/v1/messages",

211+

{ method: "POST" },

212+

);

213+214+

expect(response.headers.get("x-should-retry")).toBeNull();

215+

});

216+217+

it("leaves short retry-after values untouched", async () => {

218+

fetchWithSsrFGuardMock.mockResolvedValue({

219+

response: new Response(null, {

220+

status: 429,

221+

headers: { "retry-after": "30" },

222+

}),

223+

finalUrl: "https://api.anthropic.com/v1/messages",

224+

release: vi.fn(async () => undefined),

225+

});

226+227+

const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");

228+

const response = await buildGuardedModelFetch(anthropicModel)(

229+

"https://api.anthropic.com/v1/messages",

230+

{ method: "POST" },

231+

);

232+233+

expect(response.headers.get("x-should-retry")).toBeNull();

234+

});

235+236+

it("ignores malformed retry-after values", async () => {

237+

fetchWithSsrFGuardMock.mockResolvedValue({

238+

response: new Response(null, {

239+

status: 429,

240+

headers: { "retry-after": "soon" },

241+

}),

242+

finalUrl: "https://api.anthropic.com/v1/messages",

243+

release: vi.fn(async () => undefined),

244+

});

245+246+

const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");

247+

const response = await buildGuardedModelFetch(anthropicModel)(

248+

"https://api.anthropic.com/v1/messages",

249+

{ method: "POST" },

250+

);

251+252+

expect(response.headers.get("x-should-retry")).toBeNull();

253+

});

254+255+

it("ignores retry-after on non-retryable responses", async () => {

256+

fetchWithSsrFGuardMock.mockResolvedValue({

257+

response: new Response(null, {

258+

status: 400,

259+

headers: { "retry-after": "239" },

260+

}),

261+

finalUrl: "https://api.anthropic.com/v1/messages",

262+

release: vi.fn(async () => undefined),

263+

});

264+265+

const { buildGuardedModelFetch } = await import("./provider-transport-fetch.js");

266+

const response = await buildGuardedModelFetch(anthropicModel)(

267+

"https://api.anthropic.com/v1/messages",

268+

{ method: "POST" },

269+

);

270+271+

expect(response.headers.get("x-should-retry")).toBeNull();

272+

});

273+

});

96274

});