Skip to content

Commit 43f56cc

Browse files
author
Shaw
committed
Merge branch 'pr-7397' into shaw/toon-all-plugins-build-checks
2 parents a1fd332 + 6148a8d commit 43f56cc

1 file changed

Lines changed: 69 additions & 6 deletions

File tree

  • plugins/plugin-elizacloud/models

plugins/plugin-elizacloud/models/image.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,29 @@ export async function handleImageDescription(
4747
runtime: IAgentRuntime,
4848
params: ImageDescriptionParams | string
4949
): Promise<{ title: string; description: string }> {
50+
// Honour `DISABLE_IMAGE_DESCRIPTION` (set by the runtime when
51+
// `features.vision === false`). The runtime exposes it via getSetting; some
52+
// hosts only set it in process.env. Check both before burning a quota slot.
53+
// The docs (`docs/runtime/core.md`) already promise this behaviour, but
54+
// historically only `plugin-discord` honoured it at the call site, leaving
55+
// every other caller (agent-orchestrator's task validator, vision, lifeops,
56+
// farcaster, telegram) free to spend the rate-limit budget.
57+
const disableSetting = getSetting(runtime, "DISABLE_IMAGE_DESCRIPTION", "");
58+
const disabled =
59+
disableSetting === "true" ||
60+
disableSetting === "1" ||
61+
process.env.DISABLE_IMAGE_DESCRIPTION === "true" ||
62+
process.env.DISABLE_IMAGE_DESCRIPTION === "1";
63+
if (disabled) {
64+
logger.debug(
65+
"[ELIZAOS_CLOUD] IMAGE_DESCRIPTION skipped — DISABLE_IMAGE_DESCRIPTION is set"
66+
);
67+
return {
68+
title: "Image description disabled",
69+
description: "Image description is disabled by configuration.",
70+
};
71+
}
72+
5073
let imageUrl: string;
5174
let promptText: string | undefined;
5275
const modelName = getImageDescriptionModel(runtime);
@@ -84,21 +107,56 @@ export async function handleImageDescription(
84107
max_tokens: maxTokens,
85108
};
86109

87-
// Retry with exponential backoff for transient errors (429 rate limit)
110+
// On 429, honour the upstream's `retryAfter` instead of retrying on a
111+
// hardcoded backoff. Hardcoded retries inside the rate-limit window add
112+
// wasted requests to the same bucket and make the problem worse — see
113+
// #7374's billing render-loop fix and S33's dashboard 429-aware UX.
114+
// Strategy: only retry once, only if the upstream signals a short wait
115+
// (≤5s, i.e. transient burst). Anything longer, bail immediately and let
116+
// the caller fail fast.
88117
let response: Response | null = null;
89-
for (let attempt = 0; attempt < 3; attempt++) {
118+
let attemptedRetry = false;
119+
for (let attempt = 0; attempt < 2; attempt++) {
90120
response = await client.routes.postApiV1ChatCompletionsRaw({
91121
json: requestBody,
92122
});
123+
if (response.status !== 429 || attemptedRetry) break;
124+
125+
// `Number(null) === 0`, so guard against a missing header before
126+
// calling `Number(...)` — otherwise the header path always wins with a
127+
// bogus `0` and the body fallback becomes unreachable.
128+
const headerValue = response.headers.get("retry-after");
129+
const headerRetryAfter =
130+
headerValue !== null && Number.isFinite(Number(headerValue))
131+
? Number(headerValue)
132+
: undefined;
133+
let bodyRetryAfter: number | undefined;
134+
try {
135+
const peek = (await response.clone().json()) as {
136+
retryAfter?: unknown;
137+
};
138+
bodyRetryAfter =
139+
typeof peek?.retryAfter === "number" &&
140+
Number.isFinite(peek.retryAfter)
141+
? peek.retryAfter
142+
: undefined;
143+
} catch {
144+
// Body wasn't JSON — fall through to header value.
145+
}
146+
const retryAfter = headerRetryAfter ?? bodyRetryAfter ?? 0;
93147

94-
if (response.status === 429 && attempt < 2) {
95-
const wait = (attempt + 1) * 2000; // 2s, 4s
148+
if (retryAfter > 0 && retryAfter <= 5) {
96149
logger.warn(
97-
`[ELIZAOS_CLOUD] Image analysis rate-limited (429), retrying in ${wait / 1000}s...`
150+
`[ELIZAOS_CLOUD] Image analysis rate-limited (429), retrying once after ${retryAfter}s...`
98151
);
99-
await new Promise((r) => setTimeout(r, wait));
152+
await new Promise((r) => setTimeout(r, retryAfter * 1000));
153+
attemptedRetry = true;
100154
continue;
101155
}
156+
// Long rate-limit window: don't burn another bucket slot retrying inside it.
157+
logger.warn(
158+
`[ELIZAOS_CLOUD] Image analysis rate-limited (429); upstream retryAfter=${retryAfter || "unknown"}s — failing fast`
159+
);
102160
break;
103161
}
104162

@@ -109,6 +167,11 @@ export async function handleImageDescription(
109167
"Eliza Cloud credits exhausted — top up at https://www.elizacloud.ai/dashboard/settings?tab=billing"
110168
);
111169
}
170+
if (status === 429) {
171+
throw new Error(
172+
"Eliza Cloud rate limit exceeded for image description — try again in a minute"
173+
);
174+
}
112175
throw new Error(`ElizaOS Cloud API error: ${status}`);
113176
}
114177

0 commit comments

Comments
 (0)