Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ node_modules/
logs/
*.jsonl
cache-hit.config.json

# Runtime tool artifacts (e.g. omo run-continuation)
.omo/
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ OpenCode TUI sidebar plugin: **cache hit rate**, **tokens**, **cost**, with **su
| Token Speed | [docs/en/token-speed.md](docs/en/token-speed.md) | [docs/zh-CN/token-speed.md](docs/zh-CN/token-speed.md) |
| TTFT Hybrid | [docs/en/ttft-hybrid.md](docs/en/ttft-hybrid.md) | [docs/zh-CN/ttft-hybrid.md](docs/zh-CN/ttft-hybrid.md) |
| TTFT Troubleshooting | [docs/en/ttft-troubleshooting.md](docs/en/ttft-troubleshooting.md) | [docs/zh-CN/ttft-troubleshooting.md](docs/zh-CN/ttft-troubleshooting.md) |
| Dynamic pricing (plan) | — | [docs/zh-CN/dynamic-pricing-plan.md](docs/zh-CN/dynamic-pricing-plan.md) |
| TUI panel | [src/tui-panel/README.md](src/tui-panel/README.md) | [src/tui-panel/README.zh-CN.md](src/tui-panel/README.zh-CN.md) |
| Migration plan | [docs/en/frontend-migration-plan.md](docs/en/frontend-migration-plan.md) | [docs/zh-CN/frontend-migration-plan.md](docs/zh-CN/frontend-migration-plan.md) |
| Contributing / npm | [CONTRIBUTING.md](CONTRIBUTING.md) | — |
Expand Down Expand Up @@ -48,6 +49,7 @@ After moving or renaming exports: run full `bun test`; `tests/module-load.test.t
- Example: [cache-hit.config.example.json](cache-hit.config.example.json) — **included in npm** `files`.
- Runtime: `~/.config/opencode/cache-hit.json` (preferred) or `cache-hit.config.json` beside package root (legacy fallback); **not** published; gitignored.
- Defaults: [src/plugin-config.ts](src/plugin-config.ts).
- Dynamic pricing (`dynamicPricing`): time-of-day tiers + context tier (runtime `tiers`/`experimentalOver200K` normalized); see README § Dynamic pricing and [docs/zh-CN/dynamic-pricing-plan.md](docs/zh-CN/dynamic-pricing-plan.md). `levels` absolute prices default to USD/1M; non-USD prices convert via `cost.rate` (when the level currency matches the display currency) or the per-rule `"rate"` (USD → level currency).
- Timeline log dir default: `~/.local/share/opencode/logs/cache-hit/`. Supports `~/` expansion in `timeline.dir`.

## npm publish
Expand Down
56 changes: 53 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The **tool-part TTFT fallback** (capturing `tool.pending` as first-response time

- **Cache hit rate**: session total + **per-turn** rate with trend (↑ / ↓ / `-`) on the main block
- **Token breakdown**: cache read / write / miss / output (aligned rows with visual-cache)
- **Cost**: session cost with multi-currency config (`USD`, `CNY`, `EUR`, `GBP`, `JPY`); per-million rates and cache savings from provider config
- **Cost**: session cost with multi-currency config (`USD`, `CNY`, `EUR`, `GBP`, `JPY`); per-million rates and cache savings from provider config; **dynamic pricing** for time-of-day tiers (DeepSeek peak/off-peak) and context tiers (`context_over_200k`, e.g. GPT-5.6)
- **Sub-agents**: **Agents** section rolls up **child sessions only** (scope labeled in UI); each row shows model name + session ID suffix with **vendor-tinted** label (cost in muted gray)
- **Main + Agents**: main block always shown; **Agents** section when sub-agents exist (foldable)
- **Collapsible sections**: Detail / Model (and Agents); theme-adaptive hit bar colors
Expand Down Expand Up @@ -95,7 +95,7 @@ Load errors: `~/.local/share/opencode/log/` (search `cache-hit` or `failed to lo
{
"currency": "CNY",
"costUnit": "USD",
"rate": 7.2
"rate": 6.77
}
```

Expand Down Expand Up @@ -205,6 +205,56 @@ Shows how long the prompt cache has been alive. Color changes when exceeding TTL

**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).

### Dynamic pricing (`dynamicPricing`, default on)

Model rates are normally static per-million USD from OpenCode's provider registry. Some models price by **time of day** (DeepSeek V4: peak 09:00-12:00 / 14:00-18:00 Beijing time, off-peak half price) or by **context size** (`context_over_200k`, e.g. GPT-5.6: rates roughly double above 200k tokens).

Without config, the plugin already:

- Reads the model's context tier from `state.provider` (runtime `tiers` / `experimentalOver200K`, normalized internally) and shows the right tier based on total context (input + cache read) vs the threshold.
- Applies a built-in DeepSeek off-peak 0.5× multiplier when the schedule below matches.

```json
"dynamicPricing": {
"enabled": true,
"timezone": "Asia/Shanghai",
"schedule": [
{ "level": "peak", "windows": [{"start": "09:00", "end": "12:00"}, {"start": "14:00", "end": "18:00"}] },
{ "level": "offpeak", "windows": [{"start": "18:00", "end": "09:00"}, {"start": "12:00", "end": "14:00"}] }
],
"contextThreshold": 200000,
"providers": {
"deepseek": {
"models": {
"deepseek/deepseek-v4-flash": {
"multipliers": { "peak": 1, "offpeak": 0.5 }
}
}
}
}
}
```

| Field | Default | Meaning |
|-------|---------|---------|
| `enabled` | `true` | Master switch. Disable to restore fully static pricing |
| `timezone` | `Asia/Shanghai` | IANA zone used for schedule matching (DeepSeek prices are Beijing-time based) |
| `schedule` | DeepSeek peak/off-peak | `{level, windows:[{start,end}]}` list; `HH:MM`, cross-midnight windows allowed |
| `contextThreshold` | `200000` | Token threshold for the context tier; per-model `contextThreshold` wins over the runtime tier size from `state.provider` |
| `providers` | `{}` | Per `providerID` → `modelID` rules |

Per-model rules support two forms (explicit config wins over the built-in DeepSeek default):

- `multipliers`: factor applied to the static rates per schedule level (e.g. `{"offpeak": 0.5}`)
- `levels`: absolute rates per level, e.g. `{"peak": {"input": 0.44, "output": 0.88, "cacheRead": 0.01}, "offpeak": {"input": 0.22, ...}}`. Cache rates may be written as flat `cacheRead`/`cacheWrite` (or `cache_read`/`cache_write`) or nested `cache: {"read": …, "write": …}` (both are accepted; flat wins if both present). Default unit is **USD per 1M** (same as `state.provider`). To write prices in another currency, set `"currency": "CNY"` and either make it match the display `cost.currency` (converted via `cost.rate`) or provide the per-rule `"rate"` (USD → that currency, e.g. `"rate": 1.08` for EUR). If the currency cannot be converted (no `rate`, currency ≠ display currency), a warning is logged to stderr and the values are treated as USD. `multipliers` are ratios and have no currency.
- `contextThreshold`: per-model override of the global threshold (wins over the runtime tier size from `state.provider`).

Rates shown in the sidebar switch automatically at schedule boundaries (no polling). Session cost shown is recomputed per message from its request time + context tier when dynamic rules apply (marked `≈`); otherwise OpenCode's own `msg.cost` is used. Sub-agent rows use their **session creation time** (`session.list`) for time-of-day pricing (marked `≈` on the Agents total when any child was recomputed).

**Timeline dashboard** ([docs/en/timeline.md](docs/en/timeline.md)) also recomputes costs offline: it reads `~/.config/opencode/opencode.json` (JSONC-aware) for provider rates and injects `dynCost` per record (shown with `≈` and in charts/totals when it differs from the original).

**Refresh official DeepSeek rates**: `bun scripts/fetch-deepseek-pricing.ts` prints a ready-to-paste `dynamicPricing.providers` snippet (CNY by default with `"currency": "CNY"`, `--usd --rate 6.77` for USD).

## Updating

> [!IMPORTANT]
Expand All @@ -221,7 +271,7 @@ Then reinstall via `Ctrl+P` → install plugin, and **restart OpenCode**.
To avoid the pinning issue entirely, install a **pinned version** instead of `@latest`:

```jsonc
{ "plugin": ["opencode-cache-hit@0.6.4"] }
{ "plugin": ["opencode-cache-hit@0.7.0"] }
```

## Compatibility
Expand Down
54 changes: 52 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量

- **命中率**:会话累计 + **单轮**命中率与趋势(↑ / ↓ / `-`)
- **Token 明细**:缓存读 / 写 / 未命中 / 输出(对齐 visual-cache 的行布局)
- **费用**:多币种配置(`USD` / `CNY` / `EUR` / `GBP` / `JPY`);从 provider 配置读取百万 token 单价及缓存节省
- **费用**:多币种配置(`USD` / `CNY` / `EUR` / `GBP` / `JPY`);从 provider 配置读取百万 token 单价及缓存节省;**动态计价**:支持按时段(DeepSeek 高峰/空闲)与按上下文分档(`context_over_200k`,如 GPT-5.6)
- **子 agent**:**Agents** 段仅汇总**子 session**(UI 有范围标注);每行显示模型名 + session ID 后缀,**label 按厂商近似品牌色**,金额为灰色
- **主 + Agents**:主块始终显示;有子 agent 时出现可折叠的 **Agents** 段
- **可折叠段落**:Detail / Model(以及 Agents);主题自适应的命中率条颜色
Expand Down Expand Up @@ -95,7 +95,7 @@ OpenCode **TUI 侧边栏插件**:展示 prompt cache 命中率、token 用量
{
"currency": "CNY",
"costUnit": "USD",
"rate": 7.2
"rate": 6.77
}
```

Expand Down Expand Up @@ -205,6 +205,56 @@ jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]

**默认 TTL**:上表未列出的 provider 均为 5 分钟。颜色基于已存活时间与 TTL 的比值:绿色(< TTL)、黄色(TTL–2×TTL)、红色(≥ 2×TTL)。

### 动态计价(`dynamicPricing`,默认开启)

模型单价通常为 OpenCode provider 注册表中的静态 USD/百万 token。部分模型按**时段**计价(DeepSeek V4:高峰 09:00-12:00 / 14:00-18:00 北京时间,空闲半价),或按**上下文大小**分档(`context_over_200k`,如 GPT-5.6:超过 200k token 单价约翻倍)。

零配置时插件已自动:

- 读取 `state.provider` 中模型的上下文档位(运行时 `tiers` / `experimentalOver200K`,内部归一化),按总上下文(输入 + 缓存读)与阈值显示对应档位。
- 时段匹配时对 DeepSeek 模型应用内置空闲 0.5× 倍率。

```json
"dynamicPricing": {
"enabled": true,
"timezone": "Asia/Shanghai",
"schedule": [
{ "level": "peak", "windows": [{"start": "09:00", "end": "12:00"}, {"start": "14:00", "end": "18:00"}] },
{ "level": "offpeak", "windows": [{"start": "18:00", "end": "09:00"}, {"start": "12:00", "end": "14:00"}] }
],
"contextThreshold": 200000,
"providers": {
"deepseek": {
"models": {
"deepseek/deepseek-v4-flash": {
"multipliers": { "peak": 1, "offpeak": 0.5 }
}
}
}
}
}
```

| 字段 | 默认值 | 含义 |
|------|--------|------|
| `enabled` | `true` | 总开关。设为 `false` 恢复完全静态计价 |
| `timezone` | `Asia/Shanghai` | 时段匹配所用 IANA 时区(DeepSeek 按北京时间计价) |
| `schedule` | DeepSeek 高峰/空闲 | `{level, windows:[{start,end}]}` 列表;`HH:MM` 格式,支持跨天窗口 |
| `contextThreshold` | `200000` | 上下文档位的 token 阈值;模型级 `contextThreshold` 优先于 `state.provider` 的运行时档位阈值 |
| `providers` | `{}` | 按 `providerID` → `modelID` 的规则 |

单模型规则支持两种形式(显式配置优先于内置 DeepSeek 默认):

- `multipliers`:按时段档对静态价施加系数(如 `{"offpeak": 0.5}`)
- `levels`:各时段档的绝对单价,如 `{"peak": {"input": 0.44, "output": 0.88, "cacheRead": 0.01}, "offpeak": {"input": 0.22, ...}}`。缓存单价可写为扁平 `cacheRead`/`cacheWrite`(或 `cache_read`/`cache_write`),也可写为嵌套 `cache: {"read": …, "write": …}`(两种都接受;同时存在时扁平优先)。默认单位为 **USD/百万 token**(与 `state.provider` 一致)。想用其他币种写价时设 `"currency": "CNY"`:要么与展示币种 `cost.currency` 一致(按 `cost.rate` 换算),要么提供模型级 `"rate"`(USD → 该币种,如 EUR 填 1.08)。无法换算时(无 `rate` 且币种 ≠ 展示币种)向 stderr 告警并按 USD 处理。`multipliers` 是倍率,无币种概念。
- `contextThreshold`:覆盖全局阈值的模型级配置(优先于 `state.provider` 的运行时档位阈值)。

侧边栏单价会在时段边界自动切换(无需轮询)。会话成本在动态规则生效时按每条消息的请求时刻 + 上下文档位重算(标注 `≈`);否则使用 OpenCode 自身的 `msg.cost`。子 agent 行按其**会话创建时刻**(`session.list`)做时段计价(任一子会话被重算时 Agents 合计标注 `≈`)。

**时间轴看板**([docs/zh-CN/timeline.md](docs/zh-CN/timeline.md))也会离线重算成本:读取 `~/.config/opencode/opencode.json`(支持 JSONC 注释)获取 provider 单价,逐条注入 `dynCost`(与原值不同时以 `≈` 展示,并计入图表与合计)。

**刷新 DeepSeek 官方价**:`bun scripts/fetch-deepseek-pricing.ts` 输出可直接粘贴的 `dynamicPricing.providers` 片段(默认人民币并带 `"currency": "CNY"`,`--usd --rate 6.77` 转美元)。

## 更新

> [!IMPORTANT]
Expand Down
25 changes: 25 additions & 0 deletions cache-hit.config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,30 @@
"enabled": true,
"providers": {
}
},
"dynamicPricing": {
"enabled": true,
"timezone": "Asia/Shanghai",
"schedule": [
{ "level": "peak", "windows": [{"start": "09:00", "end": "12:00"}, {"start": "14:00", "end": "18:00"}] },
{ "level": "offpeak", "windows": [{"start": "18:00", "end": "09:00"}, {"start": "12:00", "end": "14:00"}] }
],
"contextThreshold": 200000,
"providers": {
"deepseek": {
"models": {
"deepseek/deepseek-v4-flash": {
"multipliers": { "peak": 1, "offpeak": 0.5 }
},
"deepseek/deepseek-v4-pro": {
"currency": "CNY",
"levels": {
"offpeak": { "input": 4.5, "output": 13.5, "cacheRead": 0.15, "cacheWrite": 0 },
"peak": { "input": 9.0, "output": 27.0, "cacheRead": 0.30, "cacheWrite": 0 }
}
}
}
}
}
}
}
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
| 中文 | [README.zh-CN.md](../README.zh-CN.md) | [zh-CN/design.md](./zh-CN/design.md) | [zh-CN/timeline.md](./zh-CN/timeline.md) | [zh-CN/token-speed.md](./zh-CN/token-speed.md) | [zh-CN/ttft-hybrid.md](./zh-CN/ttft-hybrid.md) | [zh-CN/ttft-troubleshooting.md](./zh-CN/ttft-troubleshooting.md) | [../src/tui-panel/README.zh-CN.md](../src/tui-panel/README.zh-CN.md) | [zh-CN/frontend-migration-plan.md](./zh-CN/frontend-migration-plan.md) |

Contributing: [CONTRIBUTING.md](../CONTRIBUTING.md).

> Dynamic pricing (time-of-day + context tiers): design notes in [zh-CN/dynamic-pricing-plan.md](./zh-CN/dynamic-pricing-plan.md).
14 changes: 14 additions & 0 deletions docs/en/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ flowchart TB
- Plugin: `createCostFormatter(loadPluginConfig().cost)`; default `costUnit: USD` → `currency: CNY`, `rate: 6.77`.
- Config file: `~/.config/opencode/cache-hit.json` (preferred) or `cache-hit.config.json` at plugin root (legacy). Defaults in `plugin-config.ts`.

## Dynamic pricing (`dynamicPricing`)

Two orthogonal price dimensions resolve to the effective per-1M rates shown in the sidebar:

- **Context tier**: reads the runtime cost tiers from `state.provider` (`tiers[]` / `experimentalOver200K`, normalized into the internal context tier with its own threshold); picks the tier by total context (`input + cacheRead`) vs the threshold (per-model config > runtime tier size > global > 200k). Zero-config for models like GPT-5.6.
- **Time-of-day tier**: `schedule` (default DeepSeek peak 09:00-12:00 / 14:00-18:00 Beijing, off-peak otherwise) + per-model `multipliers` (default DeepSeek off-peak 0.5×) or absolute `levels` (level miss falls back to static rates; `enabled: false` restores fully static pricing).

Lookup fallback chain (`src/dynamic-pricing/lookup.ts`): explicit `levels` → explicit `multipliers` → built-in DeepSeek default → static `state.provider` cost.

- `src/dynamic-pricing/schedule.ts`: timezone-aware window matching (`Intl.DateTimeFormat`), `nextBoundaryMs` drives a `setTimeout`-based refresh in `use-cache-hit-metrics.ts` — no polling.
- Session cost recompute (`src/dynamic-pricing/recompute.ts`): per message, `msg.time.created` selects the tier, total context (`input + cacheRead`) the context tier; `tokens.input` excludes cache, so cache reads are billed separately at `cacheReadRate`. Shown with `≈` when dynamic rules applied.
- Sub-agents: `session.list` entries carry `time.created` (`src/session-list.ts` → `child-session-sync.ts`), so each child's cost can be recomputed at its creation time (`recomputeSubAgentCost`).
- Timeline dashboard (`scripts/timeline-dashboard.ts`): offline recompute reads provider rates from `~/.config/opencode/opencode.json` (JSONC-aware) and injects `dynCost` per `LlmCallRecord` (shown with `≈`, counted in charts/totals).

## Runtime architecture

```mermaid
Expand Down
4 changes: 4 additions & 0 deletions docs/en/timeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type LlmCallRecord = {
scope: "main" | "child"
messageKey: string
modelId: string
providerId?: string // provider id at record time (older logs may lack it)
created: string
completedAt?: string
durationMs?: number
Expand Down Expand Up @@ -308,6 +309,9 @@ bun scripts/plot-hit-rate.ts "$LOG" -o /tmp/hit.svg
bun scripts/plot-hit-rate.ts "$LOG" --by-root -o /tmp/hit-multi.svg

# interactive HTML dashboard (filters, Chart.js); add --open to launch browser
# reads ~/.config/opencode/opencode.json (JSONC-aware) and recomputes costs with
# dynamic pricing (time-of-day / context tiers) — injected as `dynCost`, shown
# with ≈ and counted in charts/totals when it differs from the original cost
bun scripts/timeline-dashboard.ts --open
```

Expand Down
14 changes: 14 additions & 0 deletions docs/zh-CN/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ flowchart TB
- 插件:`createCostFormatter(loadPluginConfig().cost)`;默认 `costUnit: USD` → `currency: CNY`,`rate: 6.77`。
- 配置路径:优先 `~/.config/opencode/cache-hit.json`,兜底插件根目录 `cache-hit.config.json`。缺省见 `plugin-config.ts` 的 `DEFAULT_PLUGIN_CONFIG`。

## 动态计价(`dynamicPricing`)

两个正交的价格维度决定侧边栏显示的有效百万 token 单价:

- **上下文档位**:读取 `state.provider` 的运行时 cost 档位(`tiers[]` / `experimentalOver200K`,内部归一化为上下文档并携带自身阈值);按总上下文(`input + cacheRead`)与阈值选档(模型级配置 > 运行时档位阈值 > 全局 > 200k)。GPT-5.6 类模型零配置生效。
- **时段档**:`schedule`(默认 DeepSeek 高峰 09:00-12:00 / 14:00-18:00 北京时间,其余空闲)+ 单模型 `multipliers`(默认 DeepSeek 空闲 0.5×)或绝对价 `levels`(时段未命中回退静态价;`enabled: false` 恢复完全静态计价)。

查找回退链(`src/dynamic-pricing/lookup.ts`):显式 `levels` → 显式 `multipliers` → 内置 DeepSeek 默认 → `state.provider` 静态价。

- `src/dynamic-pricing/schedule.ts`:基于时区的窗口匹配(`Intl.DateTimeFormat`),`nextBoundaryMs` 驱动 `use-cache-hit-metrics.ts` 中 `setTimeout` 精确刷新——无需轮询。
- 会话成本重算(`src/dynamic-pricing/recompute.ts`):逐消息用 `msg.time.created` 选时段、总上下文(`input + cacheRead`)选上下文档;`tokens.input` 不含缓存,缓存命中部分按 `cacheReadRate` 单独计费。动态规则生效时展示标注 `≈`。
- 子 agent:`session.list` 条目携带 `time.created`(`src/session-list.ts` → `child-session-sync.ts`),可按子会话创建时刻重算成本(`recomputeSubAgentCost`)。
- 时间轴看板(`scripts/timeline-dashboard.ts`):离线重算读取 `~/.config/opencode/opencode.json`(JSONC 感知)的 provider 单价,逐条向 `LlmCallRecord` 注入 `dynCost`(`≈` 展示并计入图表/合计)。

## 运行时架构

```mermaid
Expand Down
Loading
Loading