Skip to content

Commit 2592eb0

Browse files
authored
fix(gateway): guard openrouter auto pricing recursion (openclaw#53055)
1 parent 3fe2f0a commit 2592eb0

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
2121
- Plugins/message tool: make Discord `components` and Slack `blocks` optional again so pin/unpin/react flows stop failing schema validation and Slack media sends are no longer forced into an invalid blocks-plus-media payload. Fixes #52970 and #52962. Thanks @vincentkoc.
2222
- Plugins/Feishu: route `message(..., media=...)` sends through the Feishu outbound media path so file and image attachments actually send instead of being silently dropped. Fixes #52962. Thanks @vincentkoc.
2323
- ClawHub/skills: resolve the local ClawHub auth token for gateway skill browsing and switch browse-all requests to search so ClawControl stops falling into unauthenticated 429s and empty authenticated skill lists. Fixes #52949. Thanks @vincentkoc.
24+
- Gateway/model pricing: stop `openrouter/auto` pricing refresh from recursing indefinitely during bootstrap, so OpenRouter auto routes can populate cached pricing and `usage.cost` again. Fixes #53035. Thanks @vincentkoc.
2425

2526
## 2026.3.22
2627

src/gateway/model-pricing-cache.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,45 @@ describe("model-pricing-cache", () => {
201201
cacheWrite: 0,
202202
});
203203
});
204+
205+
it("does not recurse forever for native openrouter auto refs", async () => {
206+
const config = {
207+
agents: {
208+
defaults: {
209+
model: { primary: "openrouter/auto" },
210+
},
211+
},
212+
} as unknown as OpenClawConfig;
213+
214+
const fetchImpl = withFetchPreconnect(
215+
async () =>
216+
new Response(
217+
JSON.stringify({
218+
data: [
219+
{
220+
id: "openrouter/auto",
221+
pricing: {
222+
prompt: "0.000001",
223+
completion: "0.000002",
224+
},
225+
},
226+
],
227+
}),
228+
{
229+
status: 200,
230+
headers: { "Content-Type": "application/json" },
231+
},
232+
),
233+
);
234+
235+
await expect(refreshGatewayModelPricingCache({ config, fetchImpl })).resolves.toBeUndefined();
236+
expect(
237+
getCachedGatewayModelPricing({ provider: "openrouter", model: "openrouter/auto" }),
238+
).toEqual({
239+
input: 1,
240+
output: 2,
241+
cacheRead: 0,
242+
cacheWrite: 0,
243+
});
244+
});
204245
});

src/gateway/model-pricing-cache.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,14 @@ function canonicalizeOpenRouterLookupId(id: string): string {
161161
return `${provider}/${model}`;
162162
}
163163

164-
function buildOpenRouterExactCandidates(ref: ModelRef): string[] {
164+
function buildOpenRouterExactCandidates(ref: ModelRef, seen = new Set<string>()): string[] {
165+
const refKey = modelKey(ref.provider, ref.model);
166+
if (seen.has(refKey)) {
167+
return [];
168+
}
169+
const nextSeen = new Set(seen);
170+
nextSeen.add(refKey);
171+
165172
const candidates = new Set<string>();
166173
const canonicalProvider = canonicalizeOpenRouterProvider(ref.provider);
167174
const canonicalFullId = canonicalizeOpenRouterLookupId(modelKey(canonicalProvider, ref.model));
@@ -181,7 +188,7 @@ function buildOpenRouterExactCandidates(ref: ModelRef): string[] {
181188
if (WRAPPER_PROVIDERS.has(ref.provider) && ref.model.includes("/")) {
182189
const nestedRef = parseModelRef(ref.model, DEFAULT_PROVIDER);
183190
if (nestedRef) {
184-
for (const candidate of buildOpenRouterExactCandidates(nestedRef)) {
191+
for (const candidate of buildOpenRouterExactCandidates(nestedRef, nextSeen)) {
185192
candidates.add(candidate);
186193
}
187194
}

0 commit comments

Comments
 (0)