mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(agents): prevent false billing error replacing valid response text (#40616)
Merged via squash.
Prepared head SHA: 05179362b4
Co-authored-by: ingyukoh <6015960+ingyukoh@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
@@ -107,6 +107,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram/config schema: accept `channels.telegram.actions.editMessage` and `createForumTopic` in strict config validation so existing Telegram action toggles no longer fail as unrecognized keys. (#35498) Thanks @ingyukoh.
|
- Telegram/config schema: accept `channels.telegram.actions.editMessage` and `createForumTopic` in strict config validation so existing Telegram action toggles no longer fail as unrecognized keys. (#35498) Thanks @ingyukoh.
|
||||||
- Agents/cooldowns: default cooldown windows with no recorded failure history to `unknown` instead of `rate_limit`, avoiding false API rate-limit warnings while preserving cooldown recovery probes. (#42911) Thanks @VibhorGautam.
|
- Agents/cooldowns: default cooldown windows with no recorded failure history to `unknown` instead of `rate_limit`, avoiding false API rate-limit warnings while preserving cooldown recovery probes. (#42911) Thanks @VibhorGautam.
|
||||||
- Discord/config typing: expose channel-level `autoThread` on the canonical guild-channel config type so strict config loading matches the existing Discord schema and runtime behavior. (#35608) Thanks @ingyukoh.
|
- Discord/config typing: expose channel-level `autoThread` on the canonical guild-channel config type so strict config loading matches the existing Discord schema and runtime behavior. (#35608) Thanks @ingyukoh.
|
||||||
|
- Agents/error rendering: ignore stale assistant `errorMessage` fields on successful turns so background/tool-side failures no longer prepend synthetic billing errors over valid replies. (#40616) Thanks @ingyukoh.
|
||||||
|
|
||||||
## 2026.3.8
|
## 2026.3.8
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,18 @@ describe("buildEmbeddedRunPayloads", () => {
|
|||||||
expect(payloads[0]?.isError).toBe(true);
|
expect(payloads[0]?.isError).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not emit a synthetic billing error for successful turns with stale errorMessage", () => {
|
||||||
|
const payloads = buildPayloads({
|
||||||
|
lastAssistant: makeAssistant({
|
||||||
|
stopReason: "stop",
|
||||||
|
errorMessage: "insufficient credits for embedding model",
|
||||||
|
content: [{ type: "text", text: "Handle payment required errors in your API." }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSinglePayloadText(payloads, "Handle payment required errors in your API.");
|
||||||
|
});
|
||||||
|
|
||||||
it("suppresses raw error JSON even when errorMessage is missing", () => {
|
it("suppresses raw error JSON even when errorMessage is missing", () => {
|
||||||
const payloads = buildPayloads({
|
const payloads = buildPayloads({
|
||||||
assistantTexts: [errorJsonPretty],
|
assistantTexts: [errorJsonPretty],
|
||||||
|
|||||||
@@ -128,16 +128,17 @@ export function buildEmbeddedRunPayloads(params: {
|
|||||||
const useMarkdown = params.toolResultFormat === "markdown";
|
const useMarkdown = params.toolResultFormat === "markdown";
|
||||||
const suppressAssistantArtifacts = params.didSendDeterministicApprovalPrompt === true;
|
const suppressAssistantArtifacts = params.didSendDeterministicApprovalPrompt === true;
|
||||||
const lastAssistantErrored = params.lastAssistant?.stopReason === "error";
|
const lastAssistantErrored = params.lastAssistant?.stopReason === "error";
|
||||||
const errorText = params.lastAssistant
|
const errorText =
|
||||||
? suppressAssistantArtifacts
|
params.lastAssistant && lastAssistantErrored
|
||||||
? undefined
|
? suppressAssistantArtifacts
|
||||||
: formatAssistantErrorText(params.lastAssistant, {
|
? undefined
|
||||||
cfg: params.config,
|
: formatAssistantErrorText(params.lastAssistant, {
|
||||||
sessionKey: params.sessionKey,
|
cfg: params.config,
|
||||||
provider: params.provider,
|
sessionKey: params.sessionKey,
|
||||||
model: params.model,
|
provider: params.provider,
|
||||||
})
|
model: params.model,
|
||||||
: undefined;
|
})
|
||||||
|
: undefined;
|
||||||
const rawErrorMessage = lastAssistantErrored
|
const rawErrorMessage = lastAssistantErrored
|
||||||
? params.lastAssistant?.errorMessage?.trim() || undefined
|
? params.lastAssistant?.errorMessage?.trim() || undefined
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -134,6 +134,20 @@ describe("extractAssistantText", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves response when errorMessage set from background failure (#13935)", () => {
|
||||||
|
const responseText = "Handle payment required errors in your API.";
|
||||||
|
const msg = makeAssistantMessage({
|
||||||
|
role: "assistant",
|
||||||
|
errorMessage: "insufficient credits for embedding model",
|
||||||
|
stopReason: "stop",
|
||||||
|
content: [{ type: "text", text: responseText }],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe(responseText);
|
||||||
|
});
|
||||||
|
|
||||||
it("strips Minimax tool invocations with extra attributes", () => {
|
it("strips Minimax tool invocations with extra attributes", () => {
|
||||||
const msg = makeAssistantMessage({
|
const msg = makeAssistantMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
|
|||||||
@@ -245,7 +245,9 @@ export function extractAssistantText(msg: AssistantMessage): string {
|
|||||||
}) ?? "";
|
}) ?? "";
|
||||||
// Only apply keyword-based error rewrites when the assistant message is actually an error.
|
// Only apply keyword-based error rewrites when the assistant message is actually an error.
|
||||||
// Otherwise normal prose that *mentions* errors (e.g. "context overflow") can get clobbered.
|
// Otherwise normal prose that *mentions* errors (e.g. "context overflow") can get clobbered.
|
||||||
const errorContext = msg.stopReason === "error" || Boolean(msg.errorMessage?.trim());
|
// Gate on stopReason only — a non-error response with an errorMessage set (e.g. from a
|
||||||
|
// background tool failure) should not have its content rewritten (#13935).
|
||||||
|
const errorContext = msg.stopReason === "error";
|
||||||
return sanitizeUserFacingText(extracted, { errorContext });
|
return sanitizeUserFacingText(extracted, { errorContext });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,9 +166,9 @@ export function extractAssistantText(message: unknown): string | undefined {
|
|||||||
normalizeText: (text) => text.trim(),
|
normalizeText: (text) => text.trim(),
|
||||||
}) ?? "";
|
}) ?? "";
|
||||||
const stopReason = (message as { stopReason?: unknown }).stopReason;
|
const stopReason = (message as { stopReason?: unknown }).stopReason;
|
||||||
const errorMessage = (message as { errorMessage?: unknown }).errorMessage;
|
// Gate on stopReason only — a non-error response with a stale/background errorMessage
|
||||||
const errorContext =
|
// should not have its content rewritten with error templates (#13935).
|
||||||
stopReason === "error" || (typeof errorMessage === "string" && Boolean(errorMessage.trim()));
|
const errorContext = stopReason === "error";
|
||||||
|
|
||||||
return joined ? sanitizeUserFacingText(joined, { errorContext }) : undefined;
|
return joined ? sanitizeUserFacingText(joined, { errorContext }) : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,16 @@ describe("extractAssistantText", () => {
|
|||||||
"Firebase downgraded us to the free Spark plan. Check whether billing should be re-enabled.",
|
"Firebase downgraded us to the free Spark plan. Check whether billing should be re-enabled.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves successful turns with stale background errorMessage", () => {
|
||||||
|
const message = {
|
||||||
|
role: "assistant",
|
||||||
|
stopReason: "end_turn",
|
||||||
|
errorMessage: "insufficient credits for embedding model",
|
||||||
|
content: [{ type: "text", text: "Handle payment required errors in your API." }],
|
||||||
|
};
|
||||||
|
expect(extractAssistantText(message)).toBe("Handle payment required errors in your API.");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveAnnounceTarget", () => {
|
describe("resolveAnnounceTarget", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user