fix(ai): require OpenAI Responses terminal events
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed context usage and compaction estimates to ignore malformed all-zero assistant usage after truncated responses ([#5526](https://github.com/earendil-works/pi/pull/5526) by [@dmmulroy](https://github.com/dmmulroy)).
|
||||
- Fixed `pi --resume` to load user package themes and resolve automatic light/dark theme settings.
|
||||
- Fixed `models.json` custom providers so stored credentials can satisfy auth without a redundant provider-level `apiKey` ([#5953](https://github.com/earendil-works/pi/issues/5953)).
|
||||
|
||||
|
||||
@@ -1874,10 +1874,12 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
// Case 2: Threshold - context is getting large
|
||||
// For error messages (no usage data), estimate from last successful response.
|
||||
// This ensures sessions that hit persistent API errors (e.g. 529) can still compact.
|
||||
// For error messages or all-zero usage messages, estimate from the last valid response.
|
||||
// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
|
||||
// responses can still compact and do not reset context accounting.
|
||||
let contextTokens: number;
|
||||
if (assistantMessage.stopReason === "error") {
|
||||
const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
|
||||
if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
|
||||
const messages = this.agent.state.messages;
|
||||
const estimate = estimateContextTokens(messages);
|
||||
if (estimate.lastUsageIndex === null) return false; // No usage data at all
|
||||
@@ -1894,7 +1896,7 @@ export class AgentSession {
|
||||
}
|
||||
contextTokens = estimate.tokens;
|
||||
} else {
|
||||
contextTokens = calculateContextTokens(assistantMessage.usage);
|
||||
contextTokens = directContextTokens;
|
||||
}
|
||||
if (shouldCompact(contextTokens, contextWindow, settings)) {
|
||||
return await this._runAutoCompaction("threshold", false);
|
||||
@@ -3011,8 +3013,8 @@ export class AgentSession {
|
||||
const contextTokens = calculateContextTokens(assistant.usage);
|
||||
if (contextTokens > 0) {
|
||||
hasPostCompactionUsage = true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +139,17 @@ export function calculateContextTokens(usage: Usage): number {
|
||||
|
||||
/**
|
||||
* Get usage from an assistant message if available.
|
||||
* Skips aborted and error messages as they don't have valid usage data.
|
||||
* Skips aborted, error, and all-zero usage messages as they don't have valid usage data.
|
||||
*/
|
||||
function getAssistantUsage(msg: AgentMessage): Usage | undefined {
|
||||
if (msg.role === "assistant" && "usage" in msg) {
|
||||
const assistantMsg = msg as AssistantMessage;
|
||||
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage) {
|
||||
if (
|
||||
assistantMsg.stopReason !== "aborted" &&
|
||||
assistantMsg.stopReason !== "error" &&
|
||||
assistantMsg.usage &&
|
||||
calculateContextTokens(assistantMsg.usage) > 0
|
||||
) {
|
||||
return assistantMsg.usage;
|
||||
}
|
||||
}
|
||||
@@ -152,7 +157,7 @@ function getAssistantUsage(msg: AgentMessage): Usage | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the last non-aborted assistant message usage from session entries.
|
||||
* Find the last valid assistant message usage from session entries.
|
||||
*/
|
||||
export function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
|
||||
@@ -140,4 +140,28 @@ describe("AgentSession.getSessionStats", () => {
|
||||
session.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores zero-usage messages when checking for post-compaction context usage", () => {
|
||||
const { session, sessionManager } = createSession();
|
||||
|
||||
try {
|
||||
sessionManager.appendMessage(createUserMessage("first", 1));
|
||||
sessionManager.appendMessage(createAssistantMessage("response1", 180_000, 2));
|
||||
const keptUserId = sessionManager.appendMessage(createUserMessage("second", 3));
|
||||
sessionManager.appendMessage(createAssistantMessage("response2", 195_000, 4));
|
||||
sessionManager.appendCompaction("summary", keptUserId, 195_000);
|
||||
sessionManager.appendMessage(createUserMessage("third", 5));
|
||||
sessionManager.appendMessage(createAssistantMessage("response3", 25_000, 6));
|
||||
sessionManager.appendMessage(createUserMessage("continue", 7));
|
||||
sessionManager.appendMessage(createAssistantMessage("partial", 0, 8));
|
||||
syncAgentMessages(session, sessionManager);
|
||||
|
||||
const stats = session.getSessionStats();
|
||||
expect(stats.contextUsage).toBeDefined();
|
||||
expect(stats.contextUsage?.tokens).not.toBeNull();
|
||||
expect(stats.contextUsage?.tokens ?? 0).toBeGreaterThan(25_000);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
calculateContextTokens,
|
||||
compact,
|
||||
DEFAULT_COMPACTION_SETTINGS,
|
||||
estimateContextTokens,
|
||||
findCutPoint,
|
||||
getLastAssistantUsage,
|
||||
prepareCompaction,
|
||||
@@ -217,12 +218,43 @@ describe("getLastAssistantUsage", () => {
|
||||
expect(usage!.input).toBe(100);
|
||||
});
|
||||
|
||||
it("should skip all-zero assistant usage", () => {
|
||||
const entries: SessionEntry[] = [
|
||||
createMessageEntry(createUserMessage("Hello")),
|
||||
createMessageEntry(createAssistantMessage("Hi", createMockUsage(100, 50))),
|
||||
createMessageEntry(createUserMessage("continue")),
|
||||
createMessageEntry(createAssistantMessage("Partial", createMockUsage(0, 0))),
|
||||
];
|
||||
|
||||
const usage = getLastAssistantUsage(entries);
|
||||
expect(usage).not.toBeNull();
|
||||
expect(usage!.input).toBe(100);
|
||||
});
|
||||
|
||||
it("should return undefined if no assistant messages", () => {
|
||||
const entries: SessionEntry[] = [createMessageEntry(createUserMessage("Hello"))];
|
||||
expect(getLastAssistantUsage(entries)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("estimateContextTokens", () => {
|
||||
it("uses the last non-zero assistant usage as the context anchor", () => {
|
||||
const messages: AgentMessage[] = [
|
||||
createUserMessage("Hello"),
|
||||
createAssistantMessage("Hi", createMockUsage(100, 50)),
|
||||
createUserMessage("continue"),
|
||||
createAssistantMessage("Partial thinking", createMockUsage(0, 0)),
|
||||
];
|
||||
|
||||
const estimate = estimateContextTokens(messages);
|
||||
|
||||
expect(estimate.usageTokens).toBe(150);
|
||||
expect(estimate.lastUsageIndex).toBe(1);
|
||||
expect(estimate.trailingTokens).toBeGreaterThan(0);
|
||||
expect(estimate.tokens).toBe(150 + estimate.trailingTokens);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldCompact", () => {
|
||||
it("should return true when context exceeds threshold", () => {
|
||||
const settings: CompactionSettings = {
|
||||
|
||||
Reference in New Issue
Block a user