fix: restore Anthropic token accounting
This commit is contained in:
@@ -71,6 +71,7 @@
|
||||
- Control UI: standardize UI build instructions on `bun run ui:*` (fallback supported).
|
||||
- Status: show runtime (docker/direct) and move shortcuts to `/help`.
|
||||
- Status: show model auth source (api-key/oauth).
|
||||
- Status: fix zero token counters for Anthropic (Opus) sessions by normalizing usage fields and ignoring empty usage updates.
|
||||
- Block streaming: avoid splitting Markdown fenced blocks and reopen fences when forced to split.
|
||||
- Block streaming: preserve leading indentation in block replies (lists, indented fences).
|
||||
- Docs: document systemd lingering and logged-in session requirements on macOS/Windows.
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
type SkillSnapshot,
|
||||
} from "./skills.js";
|
||||
import { buildAgentSystemPromptAppend } from "./system-prompt.js";
|
||||
import { normalizeUsage, type UsageLike } from "./usage.js";
|
||||
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||
|
||||
export type EmbeddedPiAgentMeta = {
|
||||
@@ -1000,20 +1001,12 @@ export async function runEmbeddedPiAgent(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const usage = lastAssistant?.usage;
|
||||
const usage = normalizeUsage(lastAssistant?.usage as UsageLike);
|
||||
const agentMeta: EmbeddedPiAgentMeta = {
|
||||
sessionId: sessionIdUsed,
|
||||
provider: lastAssistant?.provider ?? provider,
|
||||
model: lastAssistant?.model ?? model.id,
|
||||
usage: usage
|
||||
? {
|
||||
input: usage.input,
|
||||
output: usage.output,
|
||||
cacheRead: usage.cacheRead,
|
||||
cacheWrite: usage.cacheWrite,
|
||||
total: usage.totalTokens,
|
||||
}
|
||||
: undefined,
|
||||
usage,
|
||||
};
|
||||
|
||||
const replyItems: Array<{ text: string; media?: string[] }> = [];
|
||||
|
||||
50
src/agents/usage.test.ts
Normal file
50
src/agents/usage.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { hasNonzeroUsage, normalizeUsage } from "./usage.js";
|
||||
|
||||
describe("normalizeUsage", () => {
|
||||
it("normalizes Anthropic-style snake_case usage", () => {
|
||||
const usage = normalizeUsage({
|
||||
input_tokens: 1200,
|
||||
output_tokens: 340,
|
||||
cache_creation_input_tokens: 200,
|
||||
cache_read_input_tokens: 50,
|
||||
total_tokens: 1790,
|
||||
});
|
||||
expect(usage).toEqual({
|
||||
input: 1200,
|
||||
output: 340,
|
||||
cacheRead: 50,
|
||||
cacheWrite: 200,
|
||||
total: 1790,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes OpenAI-style prompt/completion usage", () => {
|
||||
const usage = normalizeUsage({
|
||||
prompt_tokens: 987,
|
||||
completion_tokens: 123,
|
||||
total_tokens: 1110,
|
||||
});
|
||||
expect(usage).toEqual({
|
||||
input: 987,
|
||||
output: 123,
|
||||
cacheRead: undefined,
|
||||
cacheWrite: undefined,
|
||||
total: 1110,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined for empty usage objects", () => {
|
||||
expect(normalizeUsage({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("guards against empty/zero usage overwrites", () => {
|
||||
expect(hasNonzeroUsage(undefined)).toBe(false);
|
||||
expect(hasNonzeroUsage(null)).toBe(false);
|
||||
expect(hasNonzeroUsage({})).toBe(false);
|
||||
expect(hasNonzeroUsage({ input: 0, output: 0 })).toBe(false);
|
||||
expect(hasNonzeroUsage({ input: 1 })).toBe(true);
|
||||
expect(hasNonzeroUsage({ total: 1 })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,17 @@ export type UsageLike = {
|
||||
cacheRead?: number;
|
||||
cacheWrite?: number;
|
||||
total?: number;
|
||||
// Common alternates across providers/SDKs.
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
cache_creation_input_tokens?: number;
|
||||
// Some agents/logs emit alternate naming.
|
||||
totalTokens?: number;
|
||||
total_tokens?: number;
|
||||
@@ -11,27 +22,58 @@ export type UsageLike = {
|
||||
cache_write?: number;
|
||||
};
|
||||
|
||||
export type NormalizedUsage = {
|
||||
input?: number;
|
||||
output?: number;
|
||||
cacheRead?: number;
|
||||
cacheWrite?: number;
|
||||
total?: number;
|
||||
};
|
||||
|
||||
const asFiniteNumber = (value: unknown): number | undefined => {
|
||||
if (typeof value !== "number") return undefined;
|
||||
if (!Number.isFinite(value)) return undefined;
|
||||
return value;
|
||||
};
|
||||
|
||||
export function hasNonzeroUsage(
|
||||
usage?: NormalizedUsage | null,
|
||||
): usage is NormalizedUsage {
|
||||
if (!usage) return false;
|
||||
return [
|
||||
usage.input,
|
||||
usage.output,
|
||||
usage.cacheRead,
|
||||
usage.cacheWrite,
|
||||
usage.total,
|
||||
].some((v) => typeof v === "number" && Number.isFinite(v) && v > 0);
|
||||
}
|
||||
|
||||
export function normalizeUsage(raw?: UsageLike | null):
|
||||
| {
|
||||
input?: number;
|
||||
output?: number;
|
||||
cacheRead?: number;
|
||||
cacheWrite?: number;
|
||||
total?: number;
|
||||
}
|
||||
| NormalizedUsage
|
||||
| undefined {
|
||||
if (!raw) return undefined;
|
||||
|
||||
const input = asFiniteNumber(raw.input);
|
||||
const output = asFiniteNumber(raw.output);
|
||||
const cacheRead = asFiniteNumber(raw.cacheRead ?? raw.cache_read);
|
||||
const cacheWrite = asFiniteNumber(raw.cacheWrite ?? raw.cache_write);
|
||||
const input = asFiniteNumber(
|
||||
raw.input ??
|
||||
raw.inputTokens ??
|
||||
raw.input_tokens ??
|
||||
raw.promptTokens ??
|
||||
raw.prompt_tokens,
|
||||
);
|
||||
const output = asFiniteNumber(
|
||||
raw.output ??
|
||||
raw.outputTokens ??
|
||||
raw.output_tokens ??
|
||||
raw.completionTokens ??
|
||||
raw.completion_tokens,
|
||||
);
|
||||
const cacheRead = asFiniteNumber(
|
||||
raw.cacheRead ?? raw.cache_read ?? raw.cache_read_input_tokens,
|
||||
);
|
||||
const cacheWrite = asFiniteNumber(
|
||||
raw.cacheWrite ?? raw.cache_write ?? raw.cache_creation_input_tokens,
|
||||
);
|
||||
const total = asFiniteNumber(
|
||||
raw.total ?? raw.totalTokens ?? raw.total_tokens,
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
queueEmbeddedPiMessage,
|
||||
runEmbeddedPiAgent,
|
||||
} from "../../agents/pi-embedded.js";
|
||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
type SessionEntry,
|
||||
@@ -450,7 +451,7 @@ export async function runReplyAgent(params: {
|
||||
sessionEntry?.contextTokens ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
if (usage) {
|
||||
if (hasNonzeroUsage(usage)) {
|
||||
const entry = sessionEntry ?? sessionStore[sessionKey];
|
||||
if (entry) {
|
||||
const input = usage.input ?? 0;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { lookupContextTokens } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
|
||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
@@ -171,7 +172,7 @@ export function createFollowupRunner(params: {
|
||||
sessionEntry?.contextTokens ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
if (usage) {
|
||||
if (hasNonzeroUsage(usage)) {
|
||||
const entry = sessionStore[sessionKey];
|
||||
if (entry) {
|
||||
const input = usage.input ?? 0;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
|
||||
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
||||
import { hasNonzeroUsage } from "../agents/usage.js";
|
||||
import {
|
||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||
ensureAgentWorkspace,
|
||||
@@ -482,7 +483,7 @@ export async function agentCommand(
|
||||
contextTokens,
|
||||
};
|
||||
next.abortedLastRun = result.meta.aborted ?? false;
|
||||
if (usage) {
|
||||
if (hasNonzeroUsage(usage)) {
|
||||
const input = usage.input ?? 0;
|
||||
const output = usage.output ?? 0;
|
||||
const promptTokens =
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
|
||||
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
||||
import { hasNonzeroUsage } from "../agents/usage.js";
|
||||
import {
|
||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||
ensureAgentWorkspace,
|
||||
@@ -357,7 +358,7 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
cronSession.sessionEntry.modelProvider = providerUsed;
|
||||
cronSession.sessionEntry.model = modelUsed;
|
||||
cronSession.sessionEntry.contextTokens = contextTokens;
|
||||
if (usage) {
|
||||
if (hasNonzeroUsage(usage)) {
|
||||
const input = usage.input ?? 0;
|
||||
const output = usage.output ?? 0;
|
||||
const promptTokens =
|
||||
|
||||
Reference in New Issue
Block a user