opencode-cache-hit 0.1.0 → 0.2.1
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 +4 -2
- package/CONTRIBUTING.md +24 -8
- package/README.md +55 -22
- package/README.zh-CN.md +155 -92
- package/cache-hit.config.example.json +7 -2
- package/docs/assets/cache-hit-panel.v3.png +0 -0
- package/docs/en/design.md +30 -8
- package/docs/en/timeline-duplicate-writes.md +125 -0
- package/docs/en/timeline.md +26 -21
- package/docs/zh-CN/design.md +31 -9
- package/docs/zh-CN/timeline.md +28 -24
- package/package.json +1 -2
- package/scripts/README.md +64 -1
- package/scripts/plot-hit-rate.ts +4 -3
- package/scripts/timeline-dashboard.ts +728 -0
- package/src/agents-view.tsx +24 -10
- package/src/cache-ttl-view.tsx +128 -0
- package/src/format-cost.ts +83 -1
- package/src/format-model.ts +227 -0
- 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 +13 -7
- package/src/stats.ts +6 -15
- package/src/timeline/collector.ts +40 -87
- package/src/timeline/records.ts +18 -29
- package/src/timeline/types.ts +3 -3
- package/src/timeline/writer.ts +5 -4
- package/src/tui-panel/README.md +2 -2
- package/src/tui-panel/README.zh-CN.md +2 -2
- package/src/tui-panel/components.tsx +31 -4
- package/src/tui-panel/index.ts +6 -1
- package/src/tui-panel/palette.ts +5 -0
- package/src/types.ts +16 -0
- package/src/use-cache-hit-metrics.ts +8 -9
- package/src/version.ts +4 -1
- package/src/widget.tsx +11 -3
- package/docs/assets/cache-hit-panel.png +0 -0
package/src/timeline/writer.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { appendFile, mkdir, readdir, stat, unlink } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
2
3
|
import { dirname, join } from "node:path"
|
|
3
4
|
import type { TimelineConfig } from "../plugin-config.ts"
|
|
4
|
-
import { PLUGIN_ROOT } from "../load-config.ts"
|
|
5
5
|
import type { LlmCallRecord } from "./types.ts"
|
|
6
6
|
import { rotateFileBySize, trimFileToMaxLines } from "./rotation.ts"
|
|
7
7
|
|
|
8
|
-
export const DEFAULT_TIMELINE_DIR = join(
|
|
8
|
+
export const DEFAULT_TIMELINE_DIR = join(homedir(), ".local", "share", "opencode", "logs", "cache-hit")
|
|
9
9
|
export const TIMELINE_FILE_PREFIX = "timeline"
|
|
10
10
|
|
|
11
11
|
export type TimelineWriteOptions = Pick<
|
|
@@ -14,8 +14,9 @@ export type TimelineWriteOptions = Pick<
|
|
|
14
14
|
>
|
|
15
15
|
|
|
16
16
|
export function resolveTimelineDir(config: TimelineConfig): string {
|
|
17
|
-
const raw = config.dir
|
|
18
|
-
|
|
17
|
+
const raw = (config.dir ?? "").trim()
|
|
18
|
+
if (!raw) return DEFAULT_TIMELINE_DIR
|
|
19
|
+
return raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/** Local calendar day `YYYY-MM-DD` for daily log files. */
|
package/src/tui-panel/README.md
CHANGED
|
@@ -36,7 +36,7 @@ export function MySidebar(props: { theme: Record<string, unknown> }) {
|
|
|
36
36
|
open={open()}
|
|
37
37
|
onToggle={() => setOpen((o) => !o)}
|
|
38
38
|
title="My Panel"
|
|
39
|
-
version="0.
|
|
39
|
+
version="0.2.0"
|
|
40
40
|
/>
|
|
41
41
|
<Show when={open()}>
|
|
42
42
|
<Show
|
|
@@ -69,7 +69,7 @@ export function MySidebar(props: { theme: Record<string, unknown> }) {
|
|
|
69
69
|
| `TuiPanel` | Outer border + padding |
|
|
70
70
|
| `TuiPanelTitle` | Foldable title; optional `collapsed` summary |
|
|
71
71
|
| `TuiSection` | `▼` section header + separator fill |
|
|
72
|
-
| `TuiMetricRow` | Label left, value (+ unit) right |
|
|
72
|
+
| `TuiMetricRow` | Label left, value (+ unit) right; optional `labelFg` / `valueFg` for split colors |
|
|
73
73
|
| `TuiHitRow` | Hit bar + % + trend |
|
|
74
74
|
| `computeHitBarWidth` | Dynamic bar width |
|
|
75
75
|
|
|
@@ -36,7 +36,7 @@ export function MySidebar(props: { theme: Record<string, unknown> }) {
|
|
|
36
36
|
open={open()}
|
|
37
37
|
onToggle={() => setOpen((o) => !o)}
|
|
38
38
|
title="My Panel"
|
|
39
|
-
version="0.
|
|
39
|
+
version="0.2.0"
|
|
40
40
|
/>
|
|
41
41
|
<Show when={open()}>
|
|
42
42
|
<Show
|
|
@@ -69,7 +69,7 @@ export function MySidebar(props: { theme: Record<string, unknown> }) {
|
|
|
69
69
|
| `TuiPanel` | 外框 + padding |
|
|
70
70
|
| `TuiPanelTitle` | 可折叠标题;可选 `collapsed` 摘要 |
|
|
71
71
|
| `TuiSection` | `▼` 区块标题 + 分隔线填充 |
|
|
72
|
-
| `TuiMetricRow` | 左标签右数值(可选 unit
|
|
72
|
+
| `TuiMetricRow` | 左标签右数值(可选 unit);可选 `labelFg` / `valueFg` 分段上色 |
|
|
73
73
|
| `TuiHitRow` | Hit 条 + % + 趋势 |
|
|
74
74
|
| `computeHitBarWidth` | 动态进度条宽度 |
|
|
75
75
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { Show, type JSX } from "solid-js"
|
|
3
|
-
import { padBeforeTitleSummary, sepAfterPrefix, visualWidth } from "./layout.ts"
|
|
3
|
+
import { padBeforeTitleSummary, sepAfterPrefix, UNIT_GAP, visualWidth } from "./layout.ts"
|
|
4
4
|
import type { PanelLayout } from "./use-panel-layout.ts"
|
|
5
5
|
import type { PanelPalette } from "./palette.ts"
|
|
6
6
|
|
|
@@ -126,18 +126,45 @@ export function TuiSection(props: {
|
|
|
126
126
|
)
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
function metricRowGap(label: string, value: string, unit: string, gauge: number): number {
|
|
130
|
+
const used =
|
|
131
|
+
visualWidth(label) + visualWidth(value) + (unit ? visualWidth(unit) + UNIT_GAP : 0)
|
|
132
|
+
return Math.max(1, gauge - used)
|
|
133
|
+
}
|
|
134
|
+
|
|
129
135
|
export function TuiMetricRow(props: {
|
|
130
136
|
pal: PanelPalette
|
|
131
137
|
layout: PanelLayout
|
|
132
138
|
label: string
|
|
133
139
|
value: string
|
|
134
140
|
unit?: string
|
|
141
|
+
/** Whole line (label + value + unit). Ignored when `labelFg` / `valueFg` set. */
|
|
135
142
|
fg?: string
|
|
143
|
+
/** Label-only color (e.g. sub-agent model); value stays `valueFg` or muted. */
|
|
144
|
+
labelFg?: string
|
|
145
|
+
valueFg?: string
|
|
136
146
|
}) {
|
|
137
|
-
const
|
|
147
|
+
const unit = props.unit ?? ""
|
|
148
|
+
const unitSuffix = unit ? " " + unit : ""
|
|
149
|
+
const split = props.labelFg !== undefined || props.valueFg !== undefined
|
|
150
|
+
if (split) {
|
|
151
|
+
const gap = metricRowGap(props.label, props.value, unit, props.layout.gauge())
|
|
152
|
+
const labelColor = props.labelFg ?? props.fg ?? props.pal.muted
|
|
153
|
+
const valueColor = props.valueFg ?? props.fg ?? props.pal.muted
|
|
154
|
+
return (
|
|
155
|
+
<text>
|
|
156
|
+
<span style={{ fg: labelColor }}>{props.label}</span>
|
|
157
|
+
{" ".repeat(gap)}
|
|
158
|
+
<span style={{ fg: valueColor }}>
|
|
159
|
+
{props.value}
|
|
160
|
+
{unitSuffix}
|
|
161
|
+
</span>
|
|
162
|
+
</text>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
138
165
|
return (
|
|
139
|
-
<text fg={fg}>
|
|
140
|
-
{props.layout.row(props.label, props.value,
|
|
166
|
+
<text fg={props.fg ?? props.pal.muted}>
|
|
167
|
+
{props.layout.row(props.label, props.value, unit)}
|
|
141
168
|
</text>
|
|
142
169
|
)
|
|
143
170
|
}
|
package/src/tui-panel/index.ts
CHANGED
|
@@ -19,7 +19,12 @@ export {
|
|
|
19
19
|
padBeforeTitleSummary,
|
|
20
20
|
} from "./layout.ts"
|
|
21
21
|
|
|
22
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
buildPanelPalette,
|
|
24
|
+
themeColorToHex,
|
|
25
|
+
toneBrandHex,
|
|
26
|
+
type PanelPalette,
|
|
27
|
+
} from "./palette.ts"
|
|
23
28
|
|
|
24
29
|
export {
|
|
25
30
|
createPanelLayout,
|
package/src/tui-panel/palette.ts
CHANGED
|
@@ -79,6 +79,11 @@ export function themeColorToHex(raw: unknown, fallback: string): string {
|
|
|
79
79
|
return "#" + [c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Tone a vendor brand hex for dark TUI panels (same max saturation as theme colors). */
|
|
83
|
+
export function toneBrandHex(hex: string, fallback: string): string {
|
|
84
|
+
return desaturateTo(hex, MAX_SAT, fallback)
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
export function buildPanelPalette(theme: Record<string, unknown>): PanelPalette {
|
|
83
88
|
const sat = (k: string, fb: string) => desaturateTo(theme[k], MAX_SAT, fb)
|
|
84
89
|
return {
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type SessionSnapshot = {
|
|
2
2
|
model: string
|
|
3
|
+
providerID: string
|
|
3
4
|
input: number
|
|
4
5
|
output: number
|
|
5
6
|
reasoning: number
|
|
@@ -10,6 +11,8 @@ export type SessionSnapshot = {
|
|
|
10
11
|
|
|
11
12
|
export type SubAgentSummary = {
|
|
12
13
|
id: string
|
|
14
|
+
model: string
|
|
15
|
+
providerID: string
|
|
13
16
|
cost: number
|
|
14
17
|
input: number
|
|
15
18
|
output: number
|
|
@@ -23,6 +26,7 @@ export type AssistantMessage = {
|
|
|
23
26
|
id?: string
|
|
24
27
|
messageID?: string
|
|
25
28
|
modelID?: string
|
|
29
|
+
providerID?: string
|
|
26
30
|
cost?: number
|
|
27
31
|
/** OpenCode SDK: true = summary/compaction message, not a full LLM pricing turn */
|
|
28
32
|
summary?: boolean
|
|
@@ -38,9 +42,21 @@ export type AssistantMessage = {
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
export type ModelCost = {
|
|
46
|
+
input: number
|
|
47
|
+
output: number
|
|
48
|
+
cache: { read: number; write: number }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ProviderInfo = {
|
|
52
|
+
id: string
|
|
53
|
+
models: { [key: string]: { cost: ModelCost } }
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
export type OpenCodeTuiApi = {
|
|
42
57
|
state: {
|
|
43
58
|
path: { directory: string }
|
|
59
|
+
provider: ReadonlyArray<ProviderInfo>
|
|
44
60
|
session: {
|
|
45
61
|
messages: (id: string) => unknown[] | undefined
|
|
46
62
|
get: (id: string) => { parentID?: string } | undefined
|
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
import { computeHitBarWidth, visualWidth } from "./tui-panel/layout.ts"
|
|
11
11
|
import { buildPanelPalette, type PanelPalette } from "./tui-panel/palette.ts"
|
|
12
12
|
import type { PanelLayout } from "./tui-panel/use-panel-layout.ts"
|
|
13
|
-
import type { AssistantMessage, SessionSnapshot, SubAgentSummary } from "./types.ts"
|
|
13
|
+
import type { AssistantMessage, ProviderInfo, SessionSnapshot, SubAgentSummary } from "./types.ts"
|
|
14
14
|
import {
|
|
15
15
|
cacheHitRatio,
|
|
16
|
-
combinedCacheHitRatio,
|
|
17
16
|
computePerCallHitTrend,
|
|
18
17
|
mainSessionHasStats,
|
|
19
18
|
shortModelName,
|
|
20
19
|
} from "./stats.ts"
|
|
20
|
+
import { computePricing, type PricingInfo } from "./pricing.ts"
|
|
21
21
|
|
|
22
22
|
function activeLang(display: DisplayConfig) {
|
|
23
23
|
return display.lang === "auto" ? resolveLang("auto") : display.lang
|
|
@@ -35,6 +35,7 @@ export function useCacheHitMetrics(props: {
|
|
|
35
35
|
messages: Accessor<AssistantMessage[]>
|
|
36
36
|
main: Accessor<SessionSnapshot>
|
|
37
37
|
subAgents: Accessor<SubAgentSummary[]>
|
|
38
|
+
providers: Accessor<ReadonlyArray<ProviderInfo>>
|
|
38
39
|
layout: PanelLayout
|
|
39
40
|
}) {
|
|
40
41
|
const pal = createMemo(() => buildPanelPalette(props.theme()))
|
|
@@ -44,12 +45,10 @@ export function useCacheHitMetrics(props: {
|
|
|
44
45
|
const main = createMemo(() => props.main())
|
|
45
46
|
const perCall = createMemo(() => computePerCallHitTrend(props.messages()))
|
|
46
47
|
const sessionRatio = createMemo(() => cacheHitRatio(main().cacheRead, main().input))
|
|
47
|
-
const combinedRatio = createMemo(() => combinedCacheHitRatio(main(), subs()))
|
|
48
48
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
})
|
|
49
|
+
const pricing = createMemo<PricingInfo>(() =>
|
|
50
|
+
computePricing(props.providers(), main().providerID, main().model, main().cacheRead),
|
|
51
|
+
)
|
|
53
52
|
|
|
54
53
|
const mainHasStats = createMemo(() => mainSessionHasStats(main()))
|
|
55
54
|
const hasData = createMemo(() => mainHasStats() || subs().length > 0)
|
|
@@ -85,9 +84,9 @@ export function useCacheHitMetrics(props: {
|
|
|
85
84
|
main,
|
|
86
85
|
mainHasStats,
|
|
87
86
|
perCall,
|
|
87
|
+
pricing,
|
|
88
88
|
sessionPct: createMemo(() => formatRatioAsPercent(sessionRatio())),
|
|
89
|
-
|
|
90
|
-
showCombinedHit,
|
|
89
|
+
|
|
91
90
|
hasData,
|
|
92
91
|
trendLabel,
|
|
93
92
|
bar,
|
package/src/version.ts
CHANGED
package/src/widget.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { createMemo, createSignal, Show, type Accessor } from "solid-js"
|
|
3
|
-
import type { DisplayConfig } from "./plugin-config.ts"
|
|
4
|
-
import type { AssistantMessage, SessionSnapshot, SubAgentSummary } from "./types.ts"
|
|
3
|
+
import type { DisplayConfig, CacheTTLConfig } from "./plugin-config.ts"
|
|
4
|
+
import type { AssistantMessage, ProviderInfo, SessionSnapshot, SubAgentSummary } from "./types.ts"
|
|
5
5
|
import { PLUGIN_VERSION } from "./version.ts"
|
|
6
6
|
import { AgentsView } from "./agents-view.tsx"
|
|
7
7
|
import { MainSessionView } from "./main-session-view.tsx"
|
|
8
|
+
|
|
8
9
|
import { useCacheHitMetrics } from "./use-cache-hit-metrics.ts"
|
|
9
10
|
import {
|
|
10
11
|
createPanelLayout,
|
|
@@ -22,10 +23,13 @@ export function CacheHitSidebar(props: {
|
|
|
22
23
|
sessionId: Accessor<string>
|
|
23
24
|
theme: Record<string, unknown>
|
|
24
25
|
display: DisplayConfig
|
|
26
|
+
cacheTTL: CacheTTLConfig
|
|
25
27
|
messages: Accessor<AssistantMessage[]>
|
|
26
28
|
main: Accessor<SessionSnapshot>
|
|
27
29
|
subAgents: Accessor<SubAgentSummary[]>
|
|
30
|
+
providers: Accessor<ReadonlyArray<ProviderInfo>>
|
|
28
31
|
formatCost: (amount: number) => string
|
|
32
|
+
formatRate: (perMillion: number) => string
|
|
29
33
|
}) {
|
|
30
34
|
const [panelOpen, setPanelOpen] = createSignal(true)
|
|
31
35
|
const detail = createSectionFold(true)
|
|
@@ -41,6 +45,7 @@ export function CacheHitSidebar(props: {
|
|
|
41
45
|
messages: props.messages,
|
|
42
46
|
main: props.main,
|
|
43
47
|
subAgents: props.subAgents,
|
|
48
|
+
providers: props.providers,
|
|
44
49
|
layout,
|
|
45
50
|
})
|
|
46
51
|
|
|
@@ -96,6 +101,9 @@ export function CacheHitSidebar(props: {
|
|
|
96
101
|
detail={detail}
|
|
97
102
|
model={model}
|
|
98
103
|
formatCost={props.formatCost}
|
|
104
|
+
formatRate={props.formatRate}
|
|
105
|
+
cacheTTL={props.cacheTTL}
|
|
106
|
+
messages={props.messages}
|
|
99
107
|
/>
|
|
100
108
|
<Show when={m.subs().length > 0}>
|
|
101
109
|
<TuiSection
|
|
@@ -106,7 +114,7 @@ export function CacheHitSidebar(props: {
|
|
|
106
114
|
suffix={agentsSuffix()}
|
|
107
115
|
onToggle={agents.toggle}
|
|
108
116
|
>
|
|
109
|
-
<AgentsView m={m} layout={layout} formatCost={props.formatCost} />
|
|
117
|
+
<AgentsView m={m} layout={layout} providers={props.providers()} formatCost={props.formatCost} />
|
|
110
118
|
</TuiSection>
|
|
111
119
|
</Show>
|
|
112
120
|
</Show>
|
|
Binary file
|