Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions apps/desktop/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,54 @@ Typical log locations:

The Diagnostics tab shows the current in-memory gateway log plus a bounded previous-session tail read from `main.log` at startup. First-run or unreadable log files must not block boot; return an empty previous-session tail and continue.

## Pricing Data

Model pricing rules baked into the generated agent-monitor `db.js` come from
two sources, merged at build time by `loadHostDefaultPricing()` in
`scripts/build-agent-monitor.mjs`:

1. **`scripts/litellm-pricing.json`** — auto-generated, sorted 6-tuples
(`pattern`, `display_name`, `input/Mtok`, `output/Mtok`,
`cache_read/Mtok`, `cache_write/Mtok`) derived from LiteLLM's
`model_prices_and_context_window.json`. Filtered to vendor prefixes
`claude-`, `gpt-`, `o1-`, `o3-`, `gemini-`. Committed.
2. **`HOST_ONLY_OVERRIDES`** in `scripts/build-agent-monitor.mjs` — rows
LiteLLM does not carry (fallback patterns like `cursor-default`,
OpenCode-hosted free models), plus any *temporary* upstream-deviation
overrides documented inline with a TODO + ticket reference.

The build runs two invariants. Both are hard build-time assertions —
violation throws with an actionable message and aborts the build:

- **OpenAI zero-write + sanity floor** — `gpt-*` / `o1-*` / `o3-*` rows
must have `cache_write = 0` AND `cache_write_1h = 0` (OpenAI publishes
no cache-write surcharge), AND `cache_read ≤ input` (cached input must
not cost more than uncached — a true sanity check, not a guess at the
discount ratio). LiteLLM is trusted for the actual cache_read rate
(currently 10% for the GPT-5 family, 50% for older GPT-4o-style models).
- **Anthropic 1h cache floor** — every `claude-*` row with positive input
must have `cache_write_1h ≥ input × 1.5` (sanity floor for the 2×
documented tier).

(A third invariant — an Opus 4.x input floor of $10/Mtok — was removed
during the FEA-1431 bugfix pass: Anthropic re-priced Opus starting at 4.5
down to $5/Mtok input, so the floor was rejecting correct data.)

### Refreshing pricing

```bash
just desktop-refresh-pricing # or: pnpm -C apps/desktop refresh:pricing
```

The wrapper fetches upstream, writes `litellm-pricing.json` +
`litellm-pricing.meta.json` (with SHA pin + ISO timestamp), and then runs the
build-time invariants. A regression in upstream rates (Opus floor) blocks the
refresh — review the diff, then either add a temporary override row to
`HOST_ONLY_OVERRIDES` or back out the refresh. Refresh roughly weekly.

The JSON sits inside the `currentStamp()` hash inputs so a fresh refresh
forces a rebuild of `agent-monitor/.generated/server/db.js`.

## Agent Monitor Sidecar

The desktop app bundles the MIT-licensed `Claude-Code-Agent-Monitor`
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "desktop",
"version": "0.15.93",
"version": "0.15.103",
"description": "ClosedLoop Desktop",
"author": "ClosedLoop AI <support@closedloop.ai>",
"private": true,
Expand All @@ -14,6 +14,7 @@
"prebuild": "node -e \"const{execSync:e}=require('child_process'),{writeFileSync:w}=require('fs');const h=e('git rev-parse HEAD').toString().trim();w('src/shared/build-info.ts','// AUTO-GENERATED — do not edit\\nexport const BUILD_COMMIT_HASH = \\\"'+h+'\\\";\\n');\"",
"build": "pnpm clean:dist && pnpm prebuild && tsc -p tsconfig.json && pnpm build:agent-monitor",
"build:agent-monitor": "node scripts/build-agent-monitor.mjs",
"refresh:pricing": "node scripts/refresh-litellm-pricing.mjs",
"dashboard:reset": "node scripts/reset-dashboard-db.mjs",
"dashboard:reset-packs": "node scripts/reset-dashboard-db.mjs --packs-only",
"stage:package": "node scripts/stage-packaging-app.mjs",
Expand Down
58 changes: 57 additions & 1 deletion apps/desktop/scripts/agent-monitor-client/Sessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,64 @@ export function Sessions() {
<td className="px-5 py-4 text-sm text-gray-400">
{session.agent_count ?? "-"}
</td>
{/*
FEA-1433: render three cost states (matching the
sidecar's calculateSessionCostFea1433 contract).
cost == null → EVERY model in the session is
unpriced (no model_pricing rule matched any
row). Render an em-dash with a tooltip pointing
at Settings → Pricing. This is the strong
diagnostic signal — never silently show $0.
cost > 0 && unpriced_models non-empty → MIXED:
some models priced, at least one not. Render
the partial $ value with an asterisk + tooltip
so the user sees the partial total AND knows
it understates true cost (FEA-1433 review fix).
cost > 0 && unpriced_models empty → fully priced.
Render the dollar value.
cost == 0 → no usage on a priced model. Render
"-" (distinct from the unpriced em-dash).
Sort column treats null as end-sort in the sidecar
(see calculateSessionCostFea1433 patch in
build-agent-monitor.mjs).
*/}
<td className="px-5 py-4 text-sm text-gray-400 font-mono">
{session.cost != null && session.cost > 0 ? fmtCost(session.cost) : "-"}
{session.cost == null ? (
<span
className="cursor-help text-gray-500"
title={(() => {
const first = session.unpriced_models?.[0];
return first
? `Model "${first}" not in pricing table — open Settings → Pricing to add a manual rate.`
: "Model not in pricing table — open Settings → Pricing to add a manual rate.";
})()}
>
</span>
) : session.cost > 0 ? (
(() => {
const unpricedCount =
session.unpriced_models?.length ?? 0;
if (unpricedCount === 0) return fmtCost(session.cost);
// Mixed: partial cost. Asterisk + tooltip.
const sample = session.unpriced_models?.[0];
const label =
unpricedCount === 1
? `Partial — model "${sample}" is not in the pricing table; this total excludes its tokens. Open Settings → Pricing to add a rate.`
: `Partial — ${unpricedCount} models (including "${sample}") are not in the pricing table; this total excludes their tokens. Open Settings → Pricing to add rates.`;
return (
<span
className="cursor-help"
title={label}
>
{fmtCost(session.cost)}
<span className="text-amber-400 ml-0.5">*</span>
</span>
);
})()
) : (
"-"
)}
</td>
<td
className="px-5 py-4 text-[11px] text-gray-500 font-mono"
Expand Down
Loading