mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 13:47:15 +00:00
fix(agents): harden compaction usage accounting when usage is missing
This commit is contained in:
committed by
Josh Lehman
parent
48b3c4a043
commit
6bb2d9d79b
@@ -330,6 +330,55 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
expect(assistants[1]?.usage).toBeDefined();
|
expect(assistants[1]?.usage).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds a zeroed assistant usage snapshot when usage is missing", async () => {
|
||||||
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
|
const messages = castAgentMessages([
|
||||||
|
{ role: "user", content: "question" },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "answer without usage" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await sanitizeOpenAIHistory(messages);
|
||||||
|
const assistant = result.find((message) => message.role === "assistant") as
|
||||||
|
| (AgentMessage & { usage?: unknown })
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
expect(assistant?.usage).toEqual(makeZeroUsageSnapshot());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes mixed partial assistant usage fields to numeric totals", async () => {
|
||||||
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
|
const messages = castAgentMessages([
|
||||||
|
{ role: "user", content: "question" },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "answer with partial usage" }],
|
||||||
|
usage: {
|
||||||
|
output: 3,
|
||||||
|
cache_read_input_tokens: 9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await sanitizeOpenAIHistory(messages);
|
||||||
|
const assistant = result.find((message) => message.role === "assistant") as
|
||||||
|
| (AgentMessage & { usage?: unknown })
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
expect(assistant?.usage).toEqual({
|
||||||
|
...makeZeroUsageSnapshot(),
|
||||||
|
input: 0,
|
||||||
|
output: 3,
|
||||||
|
cacheRead: 9,
|
||||||
|
cacheWrite: 0,
|
||||||
|
totalTokens: 12,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("drops stale usage when compaction summary appears before kept assistant messages", async () => {
|
it("drops stale usage when compaction summary appears before kept assistant messages", async () => {
|
||||||
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from "../session-transcript-repair.js";
|
} from "../session-transcript-repair.js";
|
||||||
import type { TranscriptPolicy } from "../transcript-policy.js";
|
import type { TranscriptPolicy } from "../transcript-policy.js";
|
||||||
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
||||||
import { makeZeroUsageSnapshot } from "../usage.js";
|
import { makeZeroUsageSnapshot, normalizeUsage, type UsageLike } from "../usage.js";
|
||||||
import { log } from "./logger.js";
|
import { log } from "./logger.js";
|
||||||
import { dropThinkingBlocks } from "./thinking.js";
|
import { dropThinkingBlocks } from "./thinking.js";
|
||||||
import { describeUnknownError } from "./utils.js";
|
import { describeUnknownError } from "./utils.js";
|
||||||
@@ -200,6 +200,60 @@ function stripStaleAssistantUsageBeforeLatestCompaction(messages: AgentMessage[]
|
|||||||
return touched ? out : messages;
|
return touched ? out : messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAssistantUsageSnapshot(usage: unknown) {
|
||||||
|
const normalized = normalizeUsage((usage ?? undefined) as UsageLike | undefined);
|
||||||
|
if (!normalized) {
|
||||||
|
return makeZeroUsageSnapshot();
|
||||||
|
}
|
||||||
|
const input = normalized.input ?? 0;
|
||||||
|
const output = normalized.output ?? 0;
|
||||||
|
const cacheRead = normalized.cacheRead ?? 0;
|
||||||
|
const cacheWrite = normalized.cacheWrite ?? 0;
|
||||||
|
const totalTokens = normalized.total ?? input + output + cacheRead + cacheWrite;
|
||||||
|
return {
|
||||||
|
...makeZeroUsageSnapshot(),
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
cacheRead,
|
||||||
|
cacheWrite,
|
||||||
|
totalTokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAssistantUsageSnapshots(messages: AgentMessage[]): AgentMessage[] {
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
let touched = false;
|
||||||
|
const out = [...messages];
|
||||||
|
for (let i = 0; i < out.length; i += 1) {
|
||||||
|
const message = out[i] as (AgentMessage & { role?: unknown; usage?: unknown }) | undefined;
|
||||||
|
if (!message || message.role !== "assistant") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalizedUsage = normalizeAssistantUsageSnapshot(message.usage);
|
||||||
|
if (
|
||||||
|
message.usage &&
|
||||||
|
typeof message.usage === "object" &&
|
||||||
|
(message.usage as { input?: unknown }).input === normalizedUsage.input &&
|
||||||
|
(message.usage as { output?: unknown }).output === normalizedUsage.output &&
|
||||||
|
(message.usage as { cacheRead?: unknown }).cacheRead === normalizedUsage.cacheRead &&
|
||||||
|
(message.usage as { cacheWrite?: unknown }).cacheWrite === normalizedUsage.cacheWrite &&
|
||||||
|
(message.usage as { totalTokens?: unknown }).totalTokens === normalizedUsage.totalTokens
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out[i] = {
|
||||||
|
...(message as unknown as Record<string, unknown>),
|
||||||
|
usage: normalizedUsage,
|
||||||
|
} as AgentMessage;
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return touched ? out : messages;
|
||||||
|
}
|
||||||
|
|
||||||
export function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] {
|
export function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] {
|
||||||
if (!schema || typeof schema !== "object") {
|
if (!schema || typeof schema !== "object") {
|
||||||
return [];
|
return [];
|
||||||
@@ -449,8 +503,9 @@ export async function sanitizeSessionHistory(params: {
|
|||||||
? sanitizeToolUseResultPairing(sanitizedToolCalls)
|
? sanitizeToolUseResultPairing(sanitizedToolCalls)
|
||||||
: sanitizedToolCalls;
|
: sanitizedToolCalls;
|
||||||
const sanitizedToolResults = stripToolResultDetails(repairedTools);
|
const sanitizedToolResults = stripToolResultDetails(repairedTools);
|
||||||
const sanitizedCompactionUsage =
|
const sanitizedCompactionUsage = ensureAssistantUsageSnapshots(
|
||||||
stripStaleAssistantUsageBeforeLatestCompaction(sanitizedToolResults);
|
stripStaleAssistantUsageBeforeLatestCompaction(sanitizedToolResults),
|
||||||
|
);
|
||||||
|
|
||||||
const isOpenAIResponsesApi =
|
const isOpenAIResponsesApi =
|
||||||
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
|
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
|
||||||
|
|||||||
Reference in New Issue
Block a user