diff --git a/apps/desktop/src/screens/Repl.tsx b/apps/desktop/src/screens/Repl.tsx index a833980..69ae1bb 100644 --- a/apps/desktop/src/screens/Repl.tsx +++ b/apps/desktop/src/screens/Repl.tsx @@ -25,6 +25,7 @@ import { type VimMode, } from '@deepcode/core/dist/keybindings/vim.js'; import { contextWindowFor } from '@deepcode/core/dist/providers/deepseek.js'; +import { estimateCost } from '@deepcode/core/dist/providers/pricing.js'; import { Dropdown, type DropdownOption } from '../components/Dropdown.js'; import { Pill } from '../components/Pill.js'; import { PlusMenu } from '../components/PlusMenu.js'; @@ -177,6 +178,7 @@ interface AgentEvt { inputTokens?: number; outputTokens?: number; reasoningTokens?: number; + cacheReadTokens?: number; // permission_request fields requestId?: string; toolName?: string; @@ -236,6 +238,11 @@ export function ReplScreen({ // Overridden by a persisted effortLevel in settings on mount. const [effort, setEffort] = useState('high'); const [model, setModel] = useState('deepseek-chat'); + // The agent.onEvent subscription below is created once on mount, so it would + // close over a stale `model`. A ref keeps the per-turn cost calc on the + // current model (deepseek-reasoner output is ¥16/M vs chat's ¥2/M). + const modelRef = useRef(model); + modelRef.current = model; const [mode, setMode] = useState< 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions' >('default'); @@ -349,7 +356,18 @@ export function ReplScreen({ // drives the context bar (overwrite, not accumulate). setUsage({ inputTokens: inTok, outputTokens: outTok }); // Cost bills every turn, so it accrues across the whole conversation. - setCostYuan((c) => c + (inTok / 1_000_000) * 1.0 + (outTok / 1_000_000) * 2.0); + // estimateCost credits cheaper cache-hit input tokens (¥0.1/M vs ¥1/M) + // and applies the right per-model output + reasoning rates. + const turnCost = estimateCost( + { + inputTokens: inTok, + outputTokens: outTok, + reasoningTokens: e.reasoningTokens ?? 0, + cacheReadTokens: e.cacheReadTokens ?? 0, + }, + modelRef.current, + ).totalYuan; + setCostYuan((c) => c + turnCost); break; } case 'error': diff --git a/packages/core/package.json b/packages/core/package.json index 3be4117..6d5a1e1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,6 +20,10 @@ "types": "./dist/providers/deepseek.d.ts", "import": "./dist/providers/deepseek.js" }, + "./dist/providers/pricing.js": { + "types": "./dist/providers/pricing.d.ts", + "import": "./dist/providers/pricing.js" + }, "./dist/types.js": { "types": "./dist/types.d.ts", "import": "./dist/types.js" diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index bc1b02b..324102a 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -448,6 +448,7 @@ export async function runAgent(opts: RunAgentOptions): Promise { inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens, reasoningTokens: result.usage.reasoningTokens, + cacheReadTokens: result.usage.cacheReadTokens, }); const assistantMsg: StoredMessage = { @@ -685,6 +686,7 @@ export async function runAgent(opts: RunAgentOptions): Promise { inputTokens: compactResult.usage.inputTokens, outputTokens: compactResult.usage.outputTokens, reasoningTokens: 0, + cacheReadTokens: 0, }); if (opts.hooks) { await opts.hooks.dispatch({ diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 13c2b75..f49507d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -182,7 +182,13 @@ export type AgentEvent = | { type: 'tool_use'; id: string; name: string; input: Record } | { type: 'tool_result'; id: string; result: ToolResult } | { type: 'turn_complete'; message: StoredMessage } - | { type: 'usage'; inputTokens: number; outputTokens: number; reasoningTokens: number } + | { + type: 'usage'; + inputTokens: number; + outputTokens: number; + reasoningTokens: number; + cacheReadTokens: number; + } | { type: 'error'; error: string }; /**