opencode-cache-hit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/CONTRIBUTING.md +24 -8
- package/README.md +51 -5
- package/README.zh-CN.md +8 -4
- package/cache-hit.config.example.json +7 -2
- package/docs/assets/.gitkeep +0 -0
- package/docs/en/design.md +8 -4
- package/docs/en/timeline.md +9 -8
- package/docs/zh-CN/design.md +8 -4
- package/docs/zh-CN/timeline.md +10 -9
- package/package.json +1 -2
- package/scripts/README.md +1 -1
- package/scripts/plot-hit-rate.ts +4 -3
- package/src/agents-view.tsx +16 -2
- package/src/cache-ttl-view.tsx +133 -0
- package/src/format-cost.ts +13 -1
- package/src/i18n.ts +18 -3
- package/src/load-config.ts +24 -5
- package/src/main-session-view.tsx +43 -3
- package/src/plugin-config.ts +59 -1
- package/src/plugin.tsx +4 -1
- package/src/pricing.ts +57 -0
- package/src/sidebar-host.tsx +7 -1
- package/src/stats.ts +6 -15
- package/src/timeline/records.ts +27 -8
- package/src/timeline/types.ts +3 -3
- package/src/timeline/writer.ts +5 -4
- package/src/types.ts +16 -0
- package/src/use-cache-hit-metrics.ts +8 -9
- package/src/widget.tsx +11 -3
package/AGENTS.md
CHANGED
|
@@ -40,8 +40,9 @@ After moving or renaming exports: run full `bun test`; `tests/module-load.test.t
|
|
|
40
40
|
## Configuration
|
|
41
41
|
|
|
42
42
|
- Example: [cache-hit.config.example.json](cache-hit.config.example.json) — **included in npm** `files`.
|
|
43
|
-
- Runtime: `cache-hit.config.json` beside package root (
|
|
43
|
+
- Runtime: `~/.config/opencode/cache-hit.json` (preferred) or `cache-hit.config.json` beside package root (legacy fallback); **not** published; gitignored.
|
|
44
44
|
- Defaults: [src/plugin-config.ts](src/plugin-config.ts).
|
|
45
|
+
- Timeline log dir default: `~/.local/share/opencode/logs/cache-hit/`. Supports `~/` expansion in `timeline.dir`.
|
|
45
46
|
|
|
46
47
|
## npm publish
|
|
47
48
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -16,25 +16,41 @@ Coding agents: [AGENTS.md](AGENTS.md).
|
|
|
16
16
|
|
|
17
17
|
After `bun install`, a **pre-push** hook runs `bun test` (skip with `git push --no-verify`). **CI** (GitHub Actions on `main` and PRs) runs the same tests on the server.
|
|
18
18
|
|
|
19
|
-
## Configuration file
|
|
19
|
+
## Configuration file
|
|
20
20
|
|
|
21
|
-
The plugin reads
|
|
21
|
+
The plugin reads config from two locations (priority order):
|
|
22
|
+
|
|
23
|
+
1. **XDG**: `~/.config/opencode/cache-hit.json` (preferred — persists across plugin updates)
|
|
24
|
+
2. **Legacy**: `cache-hit.config.json` beside plugin root (backward compatible)
|
|
25
|
+
|
|
26
|
+
The XDG path is recommended for npm global installs; the legacy path still works for local installs and existing setups.
|
|
22
27
|
|
|
23
28
|
| File | In npm tarball? | Purpose |
|
|
24
29
|
|------|-----------------|--------|
|
|
25
30
|
| `cache-hit.config.example.json` | **Yes** | Template; copy and edit |
|
|
26
|
-
| `cache-hit.config.json` | **No** |
|
|
27
|
-
| `logs/` | **No** | Timeline output
|
|
31
|
+
| `cache-hit.config.json` | **No** | Legacy plugin-root overrides |
|
|
32
|
+
| `logs/` | **No** | Timeline output (default: `~/.local/share/opencode/logs/cache-hit/`) |
|
|
28
33
|
|
|
29
|
-
After
|
|
34
|
+
After installing:
|
|
30
35
|
|
|
31
36
|
```bash
|
|
32
|
-
|
|
33
|
-
cp cache-hit.config.example.json cache-hit.config.json
|
|
37
|
+
cp cache-hit.config.example.json ~/.config/opencode/cache-hit.json
|
|
34
38
|
# edit, then restart OpenCode
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
For a **local path** in
|
|
41
|
+
For a **local path** plugin in `~/.config/opencode/plugins/opencode-cache-hit/`, placing `cache-hit.config.json` in that folder also works (matches the legacy fallback).
|
|
42
|
+
|
|
43
|
+
## Versioning
|
|
44
|
+
|
|
45
|
+
Follow [Semantic Versioning](https://semver.org/):
|
|
46
|
+
|
|
47
|
+
| Change | Bump | Example |
|
|
48
|
+
|--------|------|---------|
|
|
49
|
+
| Bug fix (backward-compatible) | **patch** | `0.1.0` → `0.1.1` |
|
|
50
|
+
| New feature (backward-compatible) | **minor** | `0.1.0` → `0.2.0` |
|
|
51
|
+
| Breaking change | **major** | `0.2.0` → `1.0.0` |
|
|
52
|
+
|
|
53
|
+
Default changes (rate, paths) accompanied by new features → minor. Pure bugfixes only → patch.
|
|
38
54
|
|
|
39
55
|
## Publishing to npm
|
|
40
56
|
|
package/README.md
CHANGED
|
@@ -19,7 +19,9 @@ Roadmap items (sidebar Timeline section, metric windows, nested sub-agents) are
|
|
|
19
19
|
|
|
20
20
|
## Acknowledgments
|
|
21
21
|
|
|
22
|
-
This plugin is **not** part of opencode-visual-cache. Its sidebar layout, panel components (`src/tui-panel/`), and coexistence patterns are **heavily inspired by** [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache). visual-cache focuses on **main-session context / token distribution**; cache-hit focuses on **per-turn metrics and sub-agent totals**.
|
|
22
|
+
This plugin is **not** part of opencode-visual-cache. Its sidebar layout, panel components (`src/tui-panel/`), and coexistence patterns are **heavily inspired by** [opencode-visual-cache](https://www.npmjs.com/package/opencode-visual-cache). visual-cache focuses on **main-session context / token distribution**; cache-hit focuses on **per-turn metrics and sub-agent totals**.
|
|
23
|
+
|
|
24
|
+
The **cache TTL** feature (elapsed time display with color-coded status) is inspired by [opencode-cache-timer](https://github.com/nero-sensei/opencode-cache-timer) by nero-sensei. The original plugin provides a standalone sidebar countdown for prompt cache expiration; this plugin integrates the concept directly into the cache-hit panel.
|
|
23
25
|
|
|
24
26
|
## Screenshots
|
|
25
27
|
|
|
@@ -29,7 +31,7 @@ This plugin is **not** part of opencode-visual-cache. Its sidebar layout, panel
|
|
|
29
31
|
|
|
30
32
|
- **Cache hit rate**: session total + **per-turn** rate with trend (↑ / ↓ / `-`) on the main block
|
|
31
33
|
- **Token breakdown**: cache read / write / miss / output (aligned rows with visual-cache)
|
|
32
|
-
- **Cost**: session cost with multi-currency config (`USD`, `CNY`, `EUR`, `GBP`, `JPY`)
|
|
34
|
+
- **Cost**: session cost with multi-currency config (`USD`, `CNY`, `EUR`, `GBP`, `JPY`); per-million rates and cache savings from provider config
|
|
33
35
|
- **Sub-agents**: **Agents** section rolls up **child sessions only** (scope labeled in UI)
|
|
34
36
|
- **Main + Agents**: main block always shown; **Agents** section when sub-agents exist (foldable)
|
|
35
37
|
- **Collapsible sections**: Detail / Model (and Agents); theme-adaptive hit bar colors
|
|
@@ -44,8 +46,8 @@ This plugin is **not** part of opencode-visual-cache. Its sidebar layout, panel
|
|
|
44
46
|
|---|----------------|-------------------|
|
|
45
47
|
| Main session context / token **distribution** estimate | Yes | No — use visual-cache |
|
|
46
48
|
| Per-role token breakdown (system / tools / …) | Yes | No |
|
|
47
|
-
| Cache **savings** estimate | Yes |
|
|
48
|
-
| Model **per-million** pricing from provider | Yes |
|
|
49
|
+
| Cache **savings** estimate | Yes | Yes (from provider pricing) |
|
|
50
|
+
| Model **per-million** pricing from provider | Yes | Yes (from SDK provider config) |
|
|
49
51
|
| **Slash commands** (`/cache-lang`, `/cache-currency`, …) | Yes | Config file only |
|
|
50
52
|
| Fold state in `api.kv` | Yes | In-session (not persisted) |
|
|
51
53
|
| Loaded **skills** panel | Yes | No |
|
|
@@ -57,7 +59,9 @@ This plugin is **not** part of opencode-visual-cache. Its sidebar layout, panel
|
|
|
57
59
|
|
|
58
60
|
### Option A: OpenCode command palette (recommended)
|
|
59
61
|
|
|
60
|
-
`Ctrl+P` → **install plugin** → `opencode-cache-hit@latest`
|
|
62
|
+
`Ctrl+P` → type **install plugin** → press `Tab` to switch scope to **global** (default is local) → type `opencode-cache-hit@latest` → press Enter.
|
|
63
|
+
|
|
64
|
+
Global plugins install to `~/.cache/opencode/packages/opencode-cache-hit@latest/`. Create config at `~/.config/opencode/cache-hit.json`:
|
|
61
65
|
|
|
62
66
|
### Option B: Manual
|
|
63
67
|
|
|
@@ -146,11 +150,53 @@ Per assistant turn → JSONL. [docs/en/timeline.md](docs/en/timeline.md) · [中
|
|
|
146
150
|
```bash
|
|
147
151
|
LOG=~/.config/opencode/plugins/opencode-cache-hit/logs/timeline-$(date +%Y-%m-%d).jsonl
|
|
148
152
|
tail -f "$LOG"
|
|
153
|
+
# time fields are ISO 8601 strings with local timezone (e.g. "2024-05-30T08:00:00.000+08:00")
|
|
149
154
|
jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]|@tsv' "$LOG"
|
|
150
155
|
```
|
|
151
156
|
|
|
152
157
|
Retention details: [Rotation and retention](docs/en/timeline.md#rotation-and-retention). Charts: [scripts/README.md](scripts/README.md).
|
|
153
158
|
|
|
159
|
+
### Cache TTL (`cacheTTL`, default on)
|
|
160
|
+
|
|
161
|
+
Shows how long the prompt cache has been alive. Color changes when exceeding TTL:
|
|
162
|
+
|
|
163
|
+
- Green: elapsed < TTL
|
|
164
|
+
- Yellow: TTL ≤ elapsed < 2×TTL
|
|
165
|
+
- Red: elapsed ≥ 2×TTL
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
"cacheTTL": {
|
|
169
|
+
"enabled": true,
|
|
170
|
+
"providers": {
|
|
171
|
+
"anthropic": "5m",
|
|
172
|
+
"openai": "5m",
|
|
173
|
+
"deepseek": "2h",
|
|
174
|
+
"google": "1h"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
| Field | Default | Meaning |
|
|
180
|
+
|-------|---------|---------|
|
|
181
|
+
| `enabled` | `true` | Master switch |
|
|
182
|
+
| `providers` | `{}` | TTL per provider (or `provider:model`). Human-readable: `30s`, `5m`, `1.5h` |
|
|
183
|
+
|
|
184
|
+
**Built-in defaults** (used when provider not in config):
|
|
185
|
+
|
|
186
|
+
| Provider | Default TTL | Source |
|
|
187
|
+
|----------|-------------|--------|
|
|
188
|
+
| anthropic | 5 min | [Anthropic docs](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) |
|
|
189
|
+
| openai | 5 min | [OpenAI docs](https://platform.openai.com/docs/guides/prompt-caching) |
|
|
190
|
+
| deepseek | 2 hours | [DeepSeek docs](https://api-docs.deepseek.com/guides/kv_cache) |
|
|
191
|
+
| google | 1 hour | [Google docs](https://ai.google.dev/api/caching) |
|
|
192
|
+
| xai | 5 min | [xAI docs](https://docs.x.ai/developers/advanced-api-usage/prompt-caching) |
|
|
193
|
+
| minimax | 5 min | [MiniMax docs](https://platform.minimax.io/docs/api-reference/text-prompt-caching) |
|
|
194
|
+
| xiaomi | 5 min | Implicit caching |
|
|
195
|
+
| qwen | 5 min | Implicit caching |
|
|
196
|
+
| moonshot | 5 min | Implicit caching |
|
|
197
|
+
|
|
198
|
+
**Default TTL**: 5 minutes for all providers not listed above. Color changes based on elapsed time vs TTL: green (< TTL), yellow (TTL-2x TTL), red (≥ 2x TTL).
|
|
199
|
+
|
|
154
200
|
## Updating
|
|
155
201
|
|
|
156
202
|
OpenCode may [cache plugins at first install](https://github.com/anomalyco/opencode/issues/6774) and not auto-refresh npm versions.
|
package/README.zh-CN.md
CHANGED
|
@@ -24,7 +24,7 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量
|
|
|
24
24
|
- visual-cache:主 session **上下文 / token 分布预估**
|
|
25
25
|
- cache-hit:**按轮次指标、成本、子 agent 汇总**
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
**缓存存活时间**功能(显示已存活时间 + 颜色状态)借鉴自 [opencode-cache-timer](https://github.com/nero-sensei/opencode-cache-timer)(作者:nero-sensei)。原插件提供独立的侧边栏倒计时;本插件将该概念直接集成到缓存命中面板中。
|
|
28
28
|
|
|
29
29
|
## 截图
|
|
30
30
|
|
|
@@ -46,7 +46,7 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量
|
|
|
46
46
|
|
|
47
47
|
- **命中率**:会话累计 + 主块**单轮**命中率与趋势
|
|
48
48
|
- **Token 明细**:缓存读/写/未命中/输出
|
|
49
|
-
- **费用**:多币种配置(`USD` / `CNY` / `EUR` / `GBP` / `JPY
|
|
49
|
+
- **费用**:多币种配置(`USD` / `CNY` / `EUR` / `GBP` / `JPY`);从 provider 配置读取百万 token 单价及缓存节省
|
|
50
50
|
- **子 agent**:**Agents** 段仅汇总**子 session**(UI 有范围提示)
|
|
51
51
|
- **主 session + Agents**:主块始终显示;有子 agent 时出现可折叠的 **Agents** 段
|
|
52
52
|
- **可选时间轴**:按天 JSONL 落盘
|
|
@@ -58,7 +58,8 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量
|
|
|
58
58
|
| | visual-cache | cache-hit |
|
|
59
59
|
|---|----------------|-----------|
|
|
60
60
|
| 主 session 上下文 / Token **分布**估算 | 有 | 无 |
|
|
61
|
-
| 按角色 Token
|
|
61
|
+
| 按角色 Token 分布 | 有 | 无 |
|
|
62
|
+
| 缓存**节省**、百万 token **单价** | 有 | 有(读 provider 配置) |
|
|
62
63
|
| **斜杠命令**改配置 | 有 | 仅配置文件 |
|
|
63
64
|
| **子 agent** 汇总 | 无 | **有** |
|
|
64
65
|
| 按次 **JSONL** | 无 | 可选 |
|
|
@@ -80,7 +81,9 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量
|
|
|
80
81
|
|
|
81
82
|
## 安装
|
|
82
83
|
|
|
83
|
-
**方式一:** `Ctrl+P` → install plugin → `opencode-cache-hit@latest
|
|
84
|
+
**方式一:** `Ctrl+P` → 输入 **install plugin** → 按 `Tab` 将范围切换为 **global**(默认是 local)→ 输入 `opencode-cache-hit@latest` → 回车。
|
|
85
|
+
|
|
86
|
+
全局插件安装到 `~/.cache/opencode/packages/opencode-cache-hit@latest/`。在 `~/.config/opencode/cache-hit.json` 创建配置:
|
|
84
87
|
|
|
85
88
|
**方式二:** 编辑 `~/.config/opencode/tui.json` / `tui.jsonc`:
|
|
86
89
|
|
|
@@ -153,6 +156,7 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量
|
|
|
153
156
|
```fish
|
|
154
157
|
set log ~/.config/opencode/plugins/opencode-cache-hit/logs/timeline-(date +%Y-%m-%d).jsonl
|
|
155
158
|
tail -f $log
|
|
159
|
+
# 时间字段为 ISO 8601 含本地时区(如 "2024-05-30T08:00:00.000+08:00")
|
|
156
160
|
jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]|@tsv' $log
|
|
157
161
|
```
|
|
158
162
|
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"$comment": "lang: en | zh | auto (system locale). Default en.",
|
|
3
3
|
"currency": "CNY",
|
|
4
4
|
"costUnit": "USD",
|
|
5
|
-
"rate":
|
|
5
|
+
"rate": 6.77,
|
|
6
6
|
"display": {
|
|
7
7
|
"lang": "en",
|
|
8
8
|
"panelBorder": true
|
|
9
9
|
},
|
|
10
10
|
"timeline": {
|
|
11
11
|
"enabled": false,
|
|
12
|
-
"dir": "",
|
|
12
|
+
"dir": "~/.local/share/opencode/logs/cache-hit",
|
|
13
13
|
"flushIncomplete": false,
|
|
14
14
|
"logSummaryMessages": true,
|
|
15
15
|
"maxMemoryRows": 50,
|
|
@@ -18,5 +18,10 @@
|
|
|
18
18
|
"retainRotated": 5,
|
|
19
19
|
"maxAgeDays": 30,
|
|
20
20
|
"maxLogFiles": 20
|
|
21
|
+
},
|
|
22
|
+
"cacheTTL": {
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"providers": {
|
|
25
|
+
}
|
|
21
26
|
}
|
|
22
27
|
}
|
|
File without changes
|
package/docs/en/design.md
CHANGED
|
@@ -43,8 +43,8 @@ flowchart TB
|
|
|
43
43
|
## Cost model
|
|
44
44
|
|
|
45
45
|
- OpenCode: `msg.cost` accumulates assistant messages using **USD** list prices from `opencode.json`.
|
|
46
|
-
- Plugin: `createCostFormatter(loadPluginConfig().cost)`; default `costUnit: USD` → `currency: CNY`, `rate:
|
|
47
|
-
- Config file: `cache-hit.config.json` at plugin root (
|
|
46
|
+
- Plugin: `createCostFormatter(loadPluginConfig().cost)`; default `costUnit: USD` → `currency: CNY`, `rate: 6.77`.
|
|
47
|
+
- Config file: `~/.config/opencode/cache-hit.json` (preferred) or `cache-hit.config.json` at plugin root (legacy). Defaults in `plugin-config.ts`.
|
|
48
48
|
|
|
49
49
|
## Runtime architecture
|
|
50
50
|
|
|
@@ -184,9 +184,13 @@ Sum input and cache.read over assistant messages
|
|
|
184
184
|
- `computePerCallHitTrend`: one rate per assistant turn; skip `summary: true`.
|
|
185
185
|
- Display the **last** non-summary turn; compare to previous for ↑ / ↓ / `-`.
|
|
186
186
|
|
|
187
|
-
**
|
|
187
|
+
**Pricing & Saved**
|
|
188
188
|
|
|
189
|
-
-
|
|
189
|
+
- Provider pricing is read from `api.state.provider` (SDK runtime data, not hardcoded).
|
|
190
|
+
- `computePricing` looks up per-million rates by `providerID` + `modelID`; computes `saved = (inputRate - cacheReadRate) * cacheRead / 1M`.
|
|
191
|
+
- Main session: **Saved** row in Detail section; per-million rates (`/M in`, `/M cache`, `/M out`) in Model section.
|
|
192
|
+
- Agents section: **Saved** row sums savings across all child sessions (`computeSubsSaved`).
|
|
193
|
+
- All pricing rows hidden when rates are unavailable or saved is zero.
|
|
190
194
|
|
|
191
195
|
## Time fields (OpenCode SDK v2)
|
|
192
196
|
|
package/docs/en/timeline.md
CHANGED
|
@@ -36,14 +36,14 @@ flowchart LR
|
|
|
36
36
|
```typescript
|
|
37
37
|
export type LlmCallRecord = {
|
|
38
38
|
schema: 1
|
|
39
|
-
recordedAt:
|
|
39
|
+
recordedAt: string // ISO 8601 with local timezone offset
|
|
40
40
|
sessionId: string
|
|
41
41
|
rootSessionId: string // main session; differs for child scope
|
|
42
42
|
scope: "main" | "child"
|
|
43
43
|
messageKey: string
|
|
44
44
|
modelId: string
|
|
45
|
-
created:
|
|
46
|
-
completedAt?:
|
|
45
|
+
created: string
|
|
46
|
+
completedAt?: string
|
|
47
47
|
durationMs?: number
|
|
48
48
|
isComplete: boolean
|
|
49
49
|
input: number
|
|
@@ -81,14 +81,14 @@ Module: `src/timeline/records.ts` — `buildCallRecords(sessionId, rootSessionId
|
|
|
81
81
|
**Default layout**
|
|
82
82
|
|
|
83
83
|
```
|
|
84
|
-
~/.
|
|
84
|
+
~/.local/share/opencode/logs/cache-hit/
|
|
85
85
|
timeline-2026-05-31.jsonl # one active file per local calendar day
|
|
86
86
|
timeline-2026-05-31.jsonl.1 # size rotation backup for that day
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
All main and child sessions for a day share one file; filter by `rootSessionId` / `sessionId` / `scope`. A new date gets a new filename at midnight.
|
|
90
90
|
|
|
91
|
-
Optional `dir` (e.g.
|
|
91
|
+
Optional `dir` (e.g. `~/my-logs/`). Supports `~/` expansion to home directory.
|
|
92
92
|
|
|
93
93
|
**Legacy**: older builds used `<rootSessionId>.jsonl` per main session; not migrated automatically.
|
|
94
94
|
|
|
@@ -116,7 +116,7 @@ Example values above; code defaults below (`enabled: false`, rotation `0` except
|
|
|
116
116
|
| Field | Code default | Description |
|
|
117
117
|
|-------|--------------|-------------|
|
|
118
118
|
| `enabled` | `false` | No IO when off |
|
|
119
|
-
| `dir` | `""` | Empty →
|
|
119
|
+
| `dir` | `""` | Empty → `~/.local/share/opencode/logs/cache-hit` |
|
|
120
120
|
| `flushIncomplete` | `false` | Write only completed turns |
|
|
121
121
|
| `logSummaryMessages` | `true` | Include summary rows (flagged) |
|
|
122
122
|
| `maxMemoryRows` | `50` | In-memory rows for future UI |
|
|
@@ -200,8 +200,9 @@ sequenceDiagram
|
|
|
200
200
|
### Phase 1 — disk only (current)
|
|
201
201
|
|
|
202
202
|
```bash
|
|
203
|
-
LOG=~/.
|
|
203
|
+
LOG=~/.local/share/opencode/logs/cache-hit/timeline-$(date +%Y-%m-%d).jsonl
|
|
204
204
|
tail -f "$LOG"
|
|
205
|
+
# time fields are ISO 8601 strings with local timezone (e.g. "2024-05-30T08:00:00.000+08:00")
|
|
205
206
|
jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]|@tsv' "$LOG"
|
|
206
207
|
```
|
|
207
208
|
|
|
@@ -249,5 +250,5 @@ bun scripts/plot-hit-rate.ts "$LOG" --by-root -o /tmp/hit-multi.svg
|
|
|
249
250
|
## Example line
|
|
250
251
|
|
|
251
252
|
```json
|
|
252
|
-
{"schema":1,"recordedAt":
|
|
253
|
+
{"schema":1,"recordedAt":"2024-05-30T08:00:00.000+08:00","sessionId":"sess_main","rootSessionId":"sess_main","scope":"main","messageKey":"sess_main:1716999990000:deepseek/v4","modelId":"deepseek/v4","created":"2024-05-30T07:59:50.000+08:00","completedAt":"2024-05-30T08:00:00.000+08:00","durationMs":10000,"isComplete":true,"input":1200,"output":80,"reasoning":0,"cacheRead":38000,"cacheWrite":0,"cost":0.012,"hitPercent":96.9,"skippedForHit":false}
|
|
253
254
|
```
|
package/docs/zh-CN/design.md
CHANGED
|
@@ -43,8 +43,8 @@ flowchart TB
|
|
|
43
43
|
## 成本模型
|
|
44
44
|
|
|
45
45
|
- OpenCode:`msg.cost` = 按 `opencode.json` 中**美元**单价对 assistant 消息累加。
|
|
46
|
-
- 插件:`createCostFormatter(loadPluginConfig().cost)`;默认 `costUnit: USD` → `currency: CNY`,`rate:
|
|
47
|
-
-
|
|
46
|
+
- 插件:`createCostFormatter(loadPluginConfig().cost)`;默认 `costUnit: USD` → `currency: CNY`,`rate: 6.77`。
|
|
47
|
+
- 配置路径:优先 `~/.config/opencode/cache-hit.json`,兜底插件根目录 `cache-hit.config.json`。缺省见 `plugin-config.ts` 的 `DEFAULT_PLUGIN_CONFIG`。
|
|
48
48
|
|
|
49
49
|
## 运行时架构
|
|
50
50
|
|
|
@@ -185,9 +185,13 @@ flowchart TD
|
|
|
185
185
|
- `computePerCallHitTrend(messages)`:每条 assistant 一轮命中率;`summary: true` 跳过。
|
|
186
186
|
- 展示**最后一条**非 summary 轮的命中率;与前一条比较得趋势(↑ / ↓ / `-`)。
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
**单价与节省(Pricing & Saved)**
|
|
189
189
|
|
|
190
|
-
-
|
|
190
|
+
- Provider 单价从 `api.state.provider`(SDK 运行时数据)读取,非硬编码。
|
|
191
|
+
- `computePricing` 根据 `providerID` + `modelID` 查找百万 token 单价;计算 `saved = (inputRate - cacheReadRate) * cacheRead / 1M`。
|
|
192
|
+
- 主 session:Detail 段展示 **Saved** 行;Model 段展示百万 token 单价(`/M 输入`、`/M 缓存`、`/M 输出`)。
|
|
193
|
+
- Agents 段:**Saved** 行汇总所有子 session 的节省金额(`computeSubsSaved`)。
|
|
194
|
+
- 单价不可用或节省为零时,所有 pricing 行隐藏。
|
|
191
195
|
|
|
192
196
|
## 时间字段(OpenCode SDK v2)
|
|
193
197
|
|
package/docs/zh-CN/timeline.md
CHANGED
|
@@ -37,8 +37,8 @@ flowchart LR
|
|
|
37
37
|
/** 单条记录;JSONL 一行一个 */
|
|
38
38
|
export type LlmCallRecord = {
|
|
39
39
|
schema: 1
|
|
40
|
-
/**
|
|
41
|
-
recordedAt:
|
|
40
|
+
/** 写入时间(ISO 8601,含时区),非 LLM 时间 */
|
|
41
|
+
recordedAt: string
|
|
42
42
|
/** 所属 session */
|
|
43
43
|
sessionId: string
|
|
44
44
|
/** 主 session id;子 session 时与 sessionId 不同 */
|
|
@@ -47,8 +47,8 @@ export type LlmCallRecord = {
|
|
|
47
47
|
/** OpenCode message id;SDK 若无则用稳定合成键,见下文 */
|
|
48
48
|
messageKey: string
|
|
49
49
|
modelId: string
|
|
50
|
-
created:
|
|
51
|
-
completedAt?:
|
|
50
|
+
created: string
|
|
51
|
+
completedAt?: string
|
|
52
52
|
durationMs?: number
|
|
53
53
|
isComplete: boolean
|
|
54
54
|
input: number
|
|
@@ -101,14 +101,14 @@ export function buildCallRecords(
|
|
|
101
101
|
**默认路径(可配置)**
|
|
102
102
|
|
|
103
103
|
```
|
|
104
|
-
~/.
|
|
104
|
+
~/.local/share/opencode/logs/cache-hit/
|
|
105
105
|
timeline-2026-05-31.jsonl # 按本地日历日一个活跃文件
|
|
106
106
|
timeline-2026-05-31.jsonl.1 # 当日超过 rotateMaxBytes 时链式备份
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
所有主/子 session 的调用写入**同一天**的同一文件;用行内 `rootSessionId` / `sessionId` / `scope` 筛某场对话。跨日自动切到新文件名。
|
|
110
110
|
|
|
111
|
-
`dir` 非空时可改到例如
|
|
111
|
+
`dir` 非空时可改到例如 `~/my-logs/`,支持 `~/` 展开为 home 目录。
|
|
112
112
|
|
|
113
113
|
推荐 **JSONL** 第一期:实现简单、`tail -f` / `jq` 友好;SQLite 留给第二期索引查询。
|
|
114
114
|
|
|
@@ -138,7 +138,7 @@ export function buildCallRecords(
|
|
|
138
138
|
| 字段 | 代码默认 | 说明 |
|
|
139
139
|
|------|----------|------|
|
|
140
140
|
| `enabled` | `false` | 关闭时零 IO,不影响侧栏 |
|
|
141
|
-
| `dir` | `""` |
|
|
141
|
+
| `dir` | `""` | 空则用 `~/.local/share/opencode/logs/cache-hit` |
|
|
142
142
|
| `flushIncomplete` | `false` | 是否在未完成时写 JSONL |
|
|
143
143
|
| `logSummaryMessages` | `true` | 是否记录 summary 行 |
|
|
144
144
|
| `maxMemoryRows` | `50` | TUI 内存中保留条数(全量仍可从文件读) |
|
|
@@ -228,8 +228,9 @@ sequenceDiagram
|
|
|
228
228
|
- 文档示例:
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
|
-
LOG=~/.
|
|
231
|
+
LOG=~/.local/share/opencode/logs/cache-hit/timeline-$(date +%Y-%m-%d).jsonl
|
|
232
232
|
tail -f $LOG
|
|
233
|
+
# 时间字段为 ISO 8601 含本地时区(如 "2024-05-30T08:00:00.000+08:00")
|
|
233
234
|
jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]|@tsv' $LOG
|
|
234
235
|
```
|
|
235
236
|
|
|
@@ -293,7 +294,7 @@ bun scripts/plot-hit-rate.ts $LOG --by-root -o /tmp/hit-multi.svg
|
|
|
293
294
|
## 示例 JSONL 行
|
|
294
295
|
|
|
295
296
|
```json
|
|
296
|
-
{"schema":1,"recordedAt":
|
|
297
|
+
{"schema":1,"recordedAt":"2024-05-30T08:00:00.000+08:00","sessionId":"sess_main","rootSessionId":"sess_main","scope":"main","messageKey":"sess_main:1716999990000:deepseek/v4","modelId":"deepseek/v4","created":"2024-05-30T07:59:50.000+08:00","completedAt":"2024-05-30T08:00:00.000+08:00","durationMs":10000,"isComplete":true,"input":1200,"output":80,"reasoning":0,"cacheRead":38000,"cacheWrite":0,"cost":0.012,"hitPercent":96.9,"skippedForHit":false}
|
|
297
298
|
```
|
|
298
299
|
|
|
299
300
|
---
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-cache-hit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenCode TUI sidebar: prompt cache hit rate, tokens & cost with sub-agent rollup. Works with opencode-visual-cache; optional per-call JSONL timeline.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"author": "zhumengzhu <mengzhu.loveyou@gmail.com>",
|
|
8
7
|
"repository": {
|
|
9
8
|
"type": "git",
|
|
10
9
|
"url": "git+https://github.com/zhumengzhu/opencode-cache-hit.git"
|
package/scripts/README.md
CHANGED
|
@@ -14,7 +14,7 @@ python3 -c "import json,sys; r=[json.loads(x) for x in open(sys.argv[1]) if x.st
|
|
|
14
14
|
bun -e "const t=await Bun.file(process.argv[1]).text();const rows=t.trim().split('\n').filter(Boolean).map(l=>JSON.parse(l));const h=rows.map(r=>r.hitPercent).filter((p):p is number=>p!=null);console.log(rows.length+' calls, avg hit '+(h.reduce((a,b)=>a+b,0)/h.length).toFixed(1)+'%')" logs/timeline-2026-05-31.jsonl
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Export TSV for spreadsheets:
|
|
17
|
+
Export TSV for spreadsheets (time fields are ISO 8601 strings):
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
jq -r 'select(.hitPercent!=null) | [.completedAt,.scope,.hitPercent,.cost]|@tsv' logs/timeline-2026-05-31.jsonl
|
package/scripts/plot-hit-rate.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
type Row = {
|
|
11
11
|
rootSessionId?: string
|
|
12
12
|
scope?: string
|
|
13
|
-
created?:
|
|
14
|
-
completedAt?:
|
|
13
|
+
created?: string
|
|
14
|
+
completedAt?: string
|
|
15
15
|
hitPercent?: number | null
|
|
16
16
|
skippedForHit?: boolean
|
|
17
17
|
}
|
|
@@ -57,7 +57,8 @@ async function loadRecords(path: string, root?: string): Promise<Row[]> {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function timeOf(r: Row): number {
|
|
60
|
-
|
|
60
|
+
const ts = r.completedAt ?? r.created
|
|
61
|
+
return ts ? new Date(ts).getTime() : 0
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
function groupByRoot(rows: Row[]): Map<string, Row[]> {
|
package/src/agents-view.tsx
CHANGED
|
@@ -3,9 +3,10 @@ import { For, Show } from "solid-js"
|
|
|
3
3
|
import { TokenDetailRows } from "./cache-hit-rows.tsx"
|
|
4
4
|
import type { CacheHitMetrics } from "./use-cache-hit-metrics.ts"
|
|
5
5
|
import { aggregateSubAgents } from "./stats.ts"
|
|
6
|
+
import { computeSubsSaved } from "./pricing.ts"
|
|
6
7
|
import { formatTokenCount } from "./format-tokens.ts"
|
|
7
8
|
import { TuiMetricRow, truncateVisual, type PanelLayout } from "./tui-panel/index.ts"
|
|
8
|
-
import type { SubAgentSummary } from "./types.ts"
|
|
9
|
+
import type { ProviderInfo, SubAgentSummary } from "./types.ts"
|
|
9
10
|
|
|
10
11
|
function agentRowLabel(id: string, gauge: number): string {
|
|
11
12
|
const tail = id.length > 10 ? id.slice(-8) : id
|
|
@@ -20,14 +21,27 @@ function subHasActivity(sub: SubAgentSummary): boolean {
|
|
|
20
21
|
export function AgentsView(props: {
|
|
21
22
|
m: CacheHitMetrics
|
|
22
23
|
layout: PanelLayout
|
|
24
|
+
providers: ReadonlyArray<ProviderInfo>
|
|
23
25
|
formatCost: (n: number) => string
|
|
24
26
|
}) {
|
|
25
27
|
const { m, layout } = props
|
|
26
28
|
const total = () => aggregateSubAgents(m.subs())
|
|
27
29
|
|
|
30
|
+
const subsSaved = () => computeSubsSaved(m.subs(), props.providers)
|
|
31
|
+
|
|
28
32
|
return (
|
|
29
33
|
<>
|
|
30
|
-
<TokenDetailRows pal={m.pal()} layout={layout} t={m.t()} snap={total()}
|
|
34
|
+
<TokenDetailRows pal={m.pal()} layout={layout} t={m.t()} snap={total()}>
|
|
35
|
+
<Show when={subsSaved() > 0}>
|
|
36
|
+
<TuiMetricRow
|
|
37
|
+
pal={m.pal()}
|
|
38
|
+
layout={layout}
|
|
39
|
+
label={m.t().saved}
|
|
40
|
+
value={props.formatCost(subsSaved())}
|
|
41
|
+
fg={m.pal().success}
|
|
42
|
+
/>
|
|
43
|
+
</Show>
|
|
44
|
+
</TokenDetailRows>
|
|
31
45
|
<Show when={total().cost > 0}>
|
|
32
46
|
<TuiMetricRow
|
|
33
47
|
pal={m.pal()}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache TTL elapsed time display.
|
|
3
|
+
* Inspired by opencode-cache-timer (https://github.com/nero-sensei/opencode-cache-timer)
|
|
4
|
+
* by nero-sensei.
|
|
5
|
+
*/
|
|
6
|
+
/** @jsxImportSource @opentui/solid */
|
|
7
|
+
import { createMemo, createSignal, onCleanup, Show, type Accessor } from "solid-js"
|
|
8
|
+
import type { AssistantMessage } from "./types.ts"
|
|
9
|
+
import type { CacheTTLConfig } from "./plugin-config.ts"
|
|
10
|
+
import { parseDuration } from "./plugin-config.ts"
|
|
11
|
+
import type { PanelPalette, PanelLayout } from "./tui-panel/index.ts"
|
|
12
|
+
import { TuiMetricRow } from "./tui-panel/index.ts"
|
|
13
|
+
|
|
14
|
+
const SECOND = 1000
|
|
15
|
+
const MINUTE = 60 * SECOND
|
|
16
|
+
const HOUR = 60 * MINUTE
|
|
17
|
+
|
|
18
|
+
const DEFAULT_TTL_MS = 5 * MINUTE
|
|
19
|
+
|
|
20
|
+
const BUILT_IN_TTL: Record<string, number> = {
|
|
21
|
+
anthropic: 5 * MINUTE,
|
|
22
|
+
openai: 5 * MINUTE,
|
|
23
|
+
deepseek: 2 * HOUR,
|
|
24
|
+
google: 1 * HOUR,
|
|
25
|
+
xai: 5 * MINUTE,
|
|
26
|
+
minimax: 5 * MINUTE,
|
|
27
|
+
xiaomi: 5 * MINUTE,
|
|
28
|
+
qwen: 5 * MINUTE,
|
|
29
|
+
moonshot: 5 * MINUTE,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findLastCacheActivity(messages: Accessor<AssistantMessage[]>): AssistantMessage | null {
|
|
33
|
+
const msgs = messages()
|
|
34
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
35
|
+
const m = msgs[i]
|
|
36
|
+
if (
|
|
37
|
+
m.role === "assistant" &&
|
|
38
|
+
m.time?.completed !== undefined &&
|
|
39
|
+
((m.tokens?.cache?.read ?? 0) > 0 || (m.tokens?.cache?.write ?? 0) > 0)
|
|
40
|
+
) {
|
|
41
|
+
return m
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getTTL(
|
|
48
|
+
providerID: string,
|
|
49
|
+
modelID: string,
|
|
50
|
+
config: CacheTTLConfig,
|
|
51
|
+
): number {
|
|
52
|
+
const userProviders = config.providers
|
|
53
|
+
const specific = userProviders[`${providerID}:${modelID}`]
|
|
54
|
+
if (specific !== undefined) {
|
|
55
|
+
const parsed = parseDuration(specific)
|
|
56
|
+
if (parsed !== null) return parsed
|
|
57
|
+
}
|
|
58
|
+
const userProvider = userProviders[providerID]
|
|
59
|
+
if (userProvider !== undefined) {
|
|
60
|
+
const parsed = parseDuration(userProvider)
|
|
61
|
+
if (parsed !== null) return parsed
|
|
62
|
+
}
|
|
63
|
+
const builtIn = BUILT_IN_TTL[providerID]
|
|
64
|
+
if (builtIn !== undefined) return builtIn
|
|
65
|
+
return DEFAULT_TTL_MS
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatElapsed(ms: number): string {
|
|
69
|
+
if (ms <= 0) return "0s"
|
|
70
|
+
const totalSeconds = Math.floor(ms / 1000)
|
|
71
|
+
const hours = Math.floor(totalSeconds / 3600)
|
|
72
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
|
73
|
+
const seconds = totalSeconds % 60
|
|
74
|
+
if (hours > 0) return `${hours}h ${minutes}m`
|
|
75
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`
|
|
76
|
+
return `${seconds}s`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function CacheTTLView(props: {
|
|
80
|
+
messages: Accessor<AssistantMessage[]>
|
|
81
|
+
config: CacheTTLConfig
|
|
82
|
+
pal: PanelPalette
|
|
83
|
+
layout: PanelLayout
|
|
84
|
+
label: string
|
|
85
|
+
}) {
|
|
86
|
+
const [now, setNow] = createSignal(Date.now())
|
|
87
|
+
const tick = setInterval(() => setNow(Date.now()), 1000)
|
|
88
|
+
onCleanup(() => clearInterval(tick))
|
|
89
|
+
|
|
90
|
+
const lastCache = createMemo(() => findLastCacheActivity(props.messages))
|
|
91
|
+
|
|
92
|
+
const ttlMs = createMemo(() => {
|
|
93
|
+
const m = lastCache()
|
|
94
|
+
if (!m || !m.providerID) return DEFAULT_TTL_MS
|
|
95
|
+
return getTTL(m.providerID, m.modelID ?? "", props.config)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const elapsed = createMemo(() => {
|
|
99
|
+
const m = lastCache()
|
|
100
|
+
if (!m || m.time.completed === undefined) return null
|
|
101
|
+
return now() - m.time.completed
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const statusIcon = createMemo(() => {
|
|
105
|
+
const e = elapsed()
|
|
106
|
+
const ttl = ttlMs()
|
|
107
|
+
if (e === null) return ""
|
|
108
|
+
if (e < ttl) return "●"
|
|
109
|
+
if (e < ttl * 2) return "◐"
|
|
110
|
+
return "○"
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const statusColor = createMemo(() => {
|
|
114
|
+
const e = elapsed()
|
|
115
|
+
const ttl = ttlMs()
|
|
116
|
+
if (e === null) return props.pal.textMuted
|
|
117
|
+
if (e < ttl) return props.pal.success
|
|
118
|
+
if (e < ttl * 2) return props.pal.warning
|
|
119
|
+
return props.pal.error
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Show when={elapsed() !== null}>
|
|
124
|
+
<TuiMetricRow
|
|
125
|
+
pal={props.pal}
|
|
126
|
+
layout={props.layout}
|
|
127
|
+
label={props.label}
|
|
128
|
+
value={`${statusIcon()} ${formatElapsed(elapsed()!)}`}
|
|
129
|
+
fg={statusColor()}
|
|
130
|
+
/>
|
|
131
|
+
</Show>
|
|
132
|
+
)
|
|
133
|
+
}
|