refactor(agents): Drop the session cost from the context usage

This commit is contained in:
l0ng-ai committed 2026-10-28 12:23:04 +08:00
1 parent 70f10e741c
commit 443bb51bba
8 files changed
+13 -41

No files matched your search

+6 -21
View File
@@ -104,17 +104,10 @@ pub(crate) fn commands_of(update: &Value) -> Vec<AgentCommand> {
.collect()
}
/// `usage_update`: `{ used, size, cost?: { amount, currency } }`. An update without a cost
/// keeps the last one (Claude also sends one for its rate limits, without the cost).
pub(crate) fn usage_of(update: &Value, prev: Option<AgentUsage>) -> Option<AgentUsage> {
/// `usage_update`: `{ used, size }`.
pub(crate) fn usage_of(update: &Value) -> Option<AgentUsage> {
let tokens = |key: &str| update.get(key)?.as_f64().map(|n| n.clamp(0.0, f64::from(u32::MAX)) as u32);
let (Some(used), Some(size)) = (tokens("used"), tokens("size").filter(|s| *s > 0)) else { return prev };
let (cost, currency) = match (update.pointer("/cost/amount").and_then(Value::as_f64), prev) {
(Some(cost), _) => (Some(cost), update.pointer("/cost/currency").and_then(Value::as_str).map(str::to_owned)),
(None, Some(p)) => (p.cost, p.currency),
(None, None) => (None, None),
};
Some(AgentUsage { used, size, cost, currency })
Some(AgentUsage { used: tokens("used")?, size: tokens("size").filter(|s| *s > 0)? })
}
fn legacy(
@@ -217,16 +210,8 @@ mod tests {
]}));
assert_eq!(cmds[0].hint.as_deref(), Some("what to review"));
assert_eq!((cmds[1].name.as_str(), cmds[1].hint.as_ref()), ("init", None));
let u = usage_of(&json!({"used": 1200, "size": 200000, "cost": {"amount": 0.42, "currency": "USD"}}), None)
.unwrap();
assert_eq!((u.used, u.cost, u.currency.as_deref()), (1200, Some(0.42), Some("USD")));
assert!(usage_of(&json!({"used": 1, "size": 0}), None).is_none());
}
#[test]
fn usage_without_a_cost_keeps_the_last_one() {
let u = usage_of(&json!({"used": 100, "size": 1000, "cost": {"amount": 1.5, "currency": "USD"}}), None);
let u = usage_of(&json!({"used": 200, "size": 1000, "_meta": {"_claude/rateLimit": {}}}), u).unwrap();
assert_eq!((u.used, u.cost, u.currency.as_deref()), (200, Some(1.5), Some("USD")));
let u = usage_of(&json!({"used": 1200, "size": 200000, "cost": {"amount": 0.42, "currency": "USD"}})).unwrap();
assert_eq!((u.used, u.size), (1200, 200000));
assert!(usage_of(&json!({"used": 1, "size": 0})).is_none());
}
}
+1 -1
View File
@@ -915,7 +915,7 @@ impl Task {
true
}
"usage_update" => {
s.task.usage = config::usage_of(update, s.task.usage.take());
s.task.usage = config::usage_of(update).or(s.task.usage.take());
true
}
_ => false,
-3
View File
@@ -114,9 +114,6 @@ pub struct AgentUsage {
pub used: u32,
/// Size of the context window.
pub size: u32,
/// Session cost so far.
pub cost: Option<f64>,
pub currency: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
-3
View File
@@ -1112,9 +1112,6 @@ export type AgentUsage = {
used: number,
/** Size of the context window. */
size: number,
/** Session cost so far. */
cost: number | null,
currency: string | null,
};
export type AppInfo = {
+1 -1
View File
@@ -172,7 +172,6 @@ function Header({ task, entries, head }: { task: AgentTask; entries: AgentEntry[
);
}
/** Context window use: "12%", the numbers (and cost) in the tooltip. */
/** Context use (when the agent reports it) and the time spent on the task. */
function Meters({ task, entries }: { task: AgentTask; entries: AgentEntry[] | null }) {
const tx = useTx();
@@ -188,6 +187,7 @@ function Meters({ task, entries }: { task: AgentTask; entries: AgentEntry[] | nu
);
}
/** Context window use: "12%", the numbers in the tooltip. */
function Usage({ usage }: { usage: AgentUsage }) {
const label = usageLabel(usage);
return (
+3 -3
View File
@@ -124,9 +124,9 @@ describe("agent composer", () => {
"eye",
"lock",
]);
expect(usageLabel({ used: 24_000, size: 200_000, cost: 0.4, currency: "USD" })).toEqual({
expect(usageLabel({ used: 24_000, size: 200_000 })).toEqual({
short: "12%",
detail: "24k / 200k tokens · $0.40",
detail: "24k / 200k tokens",
});
expect(imageFromDataUrl("data:image/png;base64,AAA", "a.png")).toEqual({ mimeType: "image/png", data: "AAA", name: "a.png" });
expect(imageFromDataUrl("data:text/plain;base64,AAA")).toBeNull();
@@ -135,7 +135,7 @@ describe("agent composer", () => {
describe("usage meters", () => {
it("formats token counts", () => {
expect(usageLabel({ used: 83_100, size: 1_000_000, cost: null, currency: null }).detail).toBe("83.1k / 1M tokens");
expect(usageLabel({ used: 83_100, size: 1_000_000 }).detail).toBe("83.1k / 1M tokens");
});
it("adds up the time spent on turns", () => {
+1 -8
View File
@@ -131,8 +131,7 @@ export function nextValue(option: AgentConfigOption, current = option.current):
/** "12%" of the context window and a tooltip with the numbers. */
export function usageLabel(u: AgentUsage): { short: string; detail: string } {
const pct = Math.min(100, Math.round((u.used / u.size) * 100));
const cost = u.cost != null ? ` · ${costLabel(u)}` : "";
return { short: `${pct}%`, detail: tx`${tokens(u.used)} / ${tokens(u.size)} tokens${cost}` };
return { short: `${pct}%`, detail: tx`${tokens(u.used)} / ${tokens(u.size)} tokens` };
}
/** `850`, `83.1k`, `1M`. */
@@ -142,12 +141,6 @@ function tokens(n: number): string {
return `${Math.round(n)}`;
}
/** Session cost (`$4.23`), when the agent reports one. */
function costLabel(u: AgentUsage): string | null {
if (u.cost == null) return null;
return `${u.currency === "USD" || !u.currency ? "$" : `${u.currency} `}${u.cost.toFixed(2)}`;
}
/** Time the agent spent on the task's turns; a running one counts up to `now` (`null` when idle). */
export function workTime(entries: readonly AgentEntry[], now: number | null): number {
const users = entries.filter((e): e is Extract<AgentEntry, { type: "user" }> => e.type === "user");
+1 -1
View File
@@ -3697,7 +3697,7 @@
"Delete “{title}”? Its branch {branch} is kept.": "是否删除“{title}”?其分支 {branch} 将保留。",
"Delete “{title}”? Files it changed are kept.": "是否删除“{title}”?它更改的文件将保留。",
"{name} is over 5 MB.": "{name} 超过 5 MB。",
"{used} / {size} tokens{cost}": "{used} / {size} 个令牌{cost}",
"{used} / {size} tokens": "{used} / {size} 个令牌",
"Replace in {path}:{line}": "在 {path}:{line} 中替换",
"Clean up {count} files?": "是否清理 {count} 个文件?",
"Leave empty to use the folder name ({folder}).": "留空则使用文件夹名称({folder})。",