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
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,20 +209,23 @@ Shows how long the prompt cache has been alive. Color changes when exceeding 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).
Model rates are normally static per-million USD from OpenCode's provider registry. Some models price by **time of day** (DeepSeek V4: peak Mon–Fri 09:00-12:00 / 14:00-18:00 Beijing time, off-peak half price — weekends are off-peak) 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.
- Applies a built-in DeepSeek off-peak 0.5× multiplier when the schedule below matches (weekdays only for peak, weekends fall to the off-peak fallback).

```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"}] }
{ "level": "peak", "windows": [
{ "start": "09:00", "end": "12:00", "days": [1, 2, 3, 4, 5] },
{ "start": "14:00", "end": "18:00", "days": [1, 2, 3, 4, 5] }
] },
{ "level": "offpeak", "windows": [] }
],
"contextThreshold": 200000,
"providers": {
Expand All @@ -241,7 +244,7 @@ Without config, the plugin already:
|-------|---------|---------|
| `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 |
| `schedule` | DeepSeek peak/off-peak | `{level, windows:[{start,end,days?}]}` list; `HH:MM`, cross-midnight windows allowed. `days` is an optional ISO weekday list (1=Monday … 7=Sunday); omitted or `[]` = every day. A level with **empty `windows`** is the **catch-all fallback**: it applies whenever no windowed level matches (e.g. weekends with the default DeepSeek schedule) |
| `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 |

Expand All @@ -251,11 +254,14 @@ Per-model rules support two forms (explicit config wins over the built-in DeepSe
- `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).
Rates shown in the sidebar switch automatically at schedule boundaries (no polling). The `peak`/`off-peak` badge on the rate row appears **only when the model actually prices that level** (the level exists in its explicit `levels`/`multipliers`, or the built-in DeepSeek default applies); plain static models and unpriced levels (e.g. a peak-only rule at an off-peak moment) show no badge. 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).

> [!NOTE]
> **Migration (weekday-aware schedules).** Schedules now support an optional `days` field (ISO weekday, 1=Monday … 7=Sunday; omitted = every day) and a catch-all **fallback level** (a level with empty `windows`). DeepSeek's official peak is **Monday–Friday** 09:00-12:00 / 14:00-18:00 Beijing time; weekends are off-peak. The built-in default schedule is weekday-aware, and new configs written from the examples above are too. **Legacy configs without `days` keep the old behavior — weekends are still billed as peak.** To pick up the fix, add `"days": [1,2,3,4,5]` to your `peak` windows (or use the new default schedule with an `offpeak` fallback). If you resolve a DeepSeek model while your configured schedule has windowed levels but no `days`, the plugin logs a one-time hint to stderr.

**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).
**Refresh official DeepSeek rates**: `bun scripts/fetch-deepseek-pricing.ts` prints a ready-to-paste `dynamicPricing` snippet — a weekday-aware `schedule` (peak Mon–Fri, off-peak catch-all fallback) plus `providers` rates (CNY by default with `"currency": "CNY"`, `--usd --rate 6.77` for USD).

## Updating

Expand All @@ -273,7 +279,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.7.1"] }
{ "plugin": ["opencode-cache-hit@0.7.2"] }
```

## Compatibility
Expand Down
20 changes: 13 additions & 7 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,20 +209,23 @@ jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]

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

模型单价通常为 OpenCode provider 注册表中的静态 USD/百万 token。部分模型按**时段**计价(DeepSeek V4:高峰 09:00-12:00 / 14:00-18:00 北京时间,空闲半价),或按**上下文大小**分档(`context_over_200k`,如 GPT-5.6:超过 200k token 单价约翻倍)。
模型单价通常为 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× 倍率。
- 时段匹配时对 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"}] }
{ "level": "peak", "windows": [
{ "start": "09:00", "end": "12:00", "days": [1, 2, 3, 4, 5] },
{ "start": "14:00", "end": "18:00", "days": [1, 2, 3, 4, 5] }
] },
{ "level": "offpeak", "windows": [] }
],
"contextThreshold": 200000,
"providers": {
Expand All @@ -241,7 +244,7 @@ jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]
|------|--------|------|
| `enabled` | `true` | 总开关。设为 `false` 恢复完全静态计价 |
| `timezone` | `Asia/Shanghai` | 时段匹配所用 IANA 时区(DeepSeek 按北京时间计价) |
| `schedule` | DeepSeek 高峰/空闲 | `{level, windows:[{start,end}]}` 列表;`HH:MM` 格式,支持跨天窗口 |
| `schedule` | DeepSeek 高峰/空闲 | `{level, windows:[{start,end,days?}]}` 列表;`HH:MM` 格式,支持跨天窗口。`days` 为可选 ISO 星期列表(1=周一 … 7=周日);省略或 `[]` = 每天。**`windows` 为空的 level 是「回退档」**:任何窗口级未命中时兜底生效(如默认 schedule 的周末) |
| `contextThreshold` | `200000` | 上下文档位的 token 阈值;模型级 `contextThreshold` 优先于 `state.provider` 的运行时档位阈值 |
| `providers` | `{}` | 按 `providerID` → `modelID` 的规则 |

Expand All @@ -251,11 +254,14 @@ jq -r 'select(.rootSessionId=="YOUR_ROOT") | [.created,.scope,.hitPercent,.cost]
- `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 合计标注 `≈`)。
侧边栏单价会在时段边界自动切换(无需轮询)。单价行的 `peak`/`offpeak` 徽标**仅在该模型对当前时段档有定价时**显示(当前档存在于其显式 `levels`/`multipliers` 中,或内置 DeepSeek 默认生效);纯静态价模型、以及未定价的档(如只配 peak 的模型在 offpeak 时刻)不标注。会话成本在动态规则生效时按每条消息的请求时刻 + 上下文档位重算(标注 `≈`);否则使用 OpenCode 自身的 `msg.cost`。子 agent 行按其**会话创建时刻**(`session.list`)做时段计价(任一子会话被重算时 Agents 合计标注 `≈`)。

> [!NOTE]
> **迁移提示(星期感知 schedule)**。schedule 现支持可选 `days` 字段(ISO 星期,1=周一 … 7=周日;省略 = 每天)与「**回退档**」(`windows` 为空的 level)。DeepSeek 官方高峰为**周一至周五**北京时间 09:00-12:00 / 14:00-18:00,周末为空闲。内置默认 schedule 已是星期感知写法,上方示例亦然;**未写 `days` 的旧配置保持原语义——周末仍按高峰计费**。要修复,请为 `peak` 窗口补 `"days": [1,2,3,4,5]`(或改用含 `offpeak` 回退档的新默认 schedule)。若你解析 DeepSeek 模型时配置的 schedule 存在窗口级档但均未写 `days`,插件会向 stderr 输出一次性提示。

**时间轴看板**([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` 转美元)。
**刷新 DeepSeek 官方价**:`bun scripts/fetch-deepseek-pricing.ts` 输出可直接粘贴的 `dynamicPricing` 片段(星期感知 `schedule`:peak 仅周一~周五 + offpeak 回退档;`providers` 默认人民币并带 `"currency": "CNY"`,`--usd --rate 6.77` 转美元)。

## 更新

Expand Down
8 changes: 6 additions & 2 deletions cache-hit.config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@
"dynamicPricing": {
"enabled": true,
"timezone": "Asia/Shanghai",
"$comment-schedule": "Weekday-aware schedule: days = ISO weekday (1=Monday…7=Sunday); omitted = every day. A level with empty windows is the catch-all fallback. DeepSeek official: peak is Mon-Fri 09:00-12:00 / 14:00-18:00 Beijing, everything else (incl. weekends) is off-peak. Legacy configs without days keep peak on weekends — add days:[1,2,3,4,5] to match DeepSeek weekend idle pricing.",
"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"}] }
{ "level": "peak", "windows": [
{ "start": "09:00", "end": "12:00", "days": [1, 2, 3, 4, 5] },
{ "start": "14:00", "end": "18:00", "days": [1, 2, 3, 4, 5] }
] },
{ "level": "offpeak", "windows": [] }
],
"contextThreshold": 200000,
"providers": {
Expand Down
2 changes: 1 addition & 1 deletion docs/en/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ flowchart TB
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).
- **Time-of-day tier**: `schedule` (default DeepSeek peak Mon–Fri 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). Windows may carry an optional `days` list (ISO 1=Monday…7=Sunday; omitted = every day); a level with empty `windows` is the **catch-all fallback** (e.g. the default `offpeak`, which covers weekends). Cross-midnight windows anchor to their open day (evening part on `days` day, morning part on the next day).

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

Expand Down
4 changes: 2 additions & 2 deletions docs/zh-CN/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ flowchart TB
两个正交的价格维度决定侧边栏显示的有效百万 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` 恢复完全静态计价)。
- **时段档**:`schedule`(默认 DeepSeek 高峰为北京时间**周一至周五** 09:00-12:00 / 14:00-18:00,其余含周末为空闲)+ 单模型 `multipliers`(默认 DeepSeek 空闲 0.5×)或绝对价 `levels`(时段未命中回退静态价;`enabled: false` 恢复完全静态计价)。窗口可带可选 `days`(ISO 1=周一…7=周日;省略 = 每天);`windows` 为空的 level 即「**回退档**」(默认 `offpeak` 兜底周末等一切未覆盖时刻)。跨天窗口锚定开启日(`days` 日承载晚间段,次日承载早晨段)。

查找回退链(`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/schedule.ts`:基于时区的窗口匹配(`Intl.DateTimeFormat`,含 `tzWeekdayOf` 星期判定),`nextBoundaryMs` 星期感知地驱动 `use-cache-hit-metrics.ts` 中 `setTimeout` 精确刷新(周五 18:00 后直达周一 09:00,周末不轮询)——无需轮询。
- 会话成本重算(`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`(`≈` 展示并计入图表/合计)。
Expand Down
Loading
Loading