ดูว่าโดนชาร์จค่า Prompt Cache เท่าไหร่ คิดเป็นบาท พร้อม Hit Rate และคำแนะนำ

บอกว่า session นี้ใช้ Prompt Cache คุ้มไหม: token ที่อ่านจาก cache (ถูกกว่า input ปกติประมาณ 10 เท่า) กับที่เขียนเข้า cache, Hit Rate, ค่าใช้จ่ายเป็นบาท และเงินที่ประหยัดได้ แสดงไฟสี 🟢🟡🔴 ใน status line และเตือนเมื่อจะ /clear
| คำสั่ง | ทำอะไร |
|---|---|
/cache | รายงานของ session นี้ |
/cache all | ยอดสะสมทุก session |
/cache clear | ล้างสถิติ |
/plugin install cache-tax-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
cache_read_input_tokens และ cache_creation_input_tokens จาก usage/clear) แล้วปล่อยให้ทำงานต่อด้วย next(e)$.store$.ui.statusราคาเป็นค่าประมาณของ model ระดับ Sonnet แก้ได้ที่ PRICE
hooks/register.tsx 169 lines1/**
2 * ============================================================================
3 * MOD 05: Cache Tax TH - ดูค่า Prompt Cache เป็นบาท (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: https://github.com/karanb192/cache-tax
6 *
7 * ความรู้ที่หลายคนไม่รู้ (ราคาตัวอย่างของ model ระดับ Sonnet ต่อ 1 ล้าน token):
8 * - Input ปกติ ~$3
9 * - เขียนเข้า Cache ~$3.75 (แพงกว่าปกตินิดหน่อย)
10 * - อ่านจาก Cache ~$0.30 (ถูกกว่าปกติ 10 เท่า!)
11 * ถ้า /clear บ่อย cache จะหาย ต้องจ่ายเต็มราคาใหม่ Mod นี้ช่วยให้เห็นตัวเลข
12 *
13 * หน้าที่ของ Mod นี้:
14 * 1. อ่าน usage ของทุก turn (turn.complete) แล้วแยก cache read/write
15 * 2. คำนวณเงิน, Hit Rate และเงินที่ประหยัดได้ แสดงเป็นบาท
16 * 3. แสดง Hit Rate เป็นไฟสีใน status line
17 * 4. เตือนก่อน /clear ว่า cache จะหาย
18 */
19
20import type { EngineInterface, Register, TurnUsage } from 'claude-code'
21
22// ราคาโดยประมาณ (USD ต่อ 1 ล้าน token) - ตรวจราคาล่าสุดที่ anthropic.com/pricing แล้วแก้ได้เลย
23const PRICE = { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 }
24const THB_PER_USD = 36.5
25
26type CacheStats = {
27 turns: number
28 input: number
29 output: number
30 cacheRead: number
31 cacheWrite: number
32}
33
34const EMPTY: CacheStats = { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
35let session: CacheStats = { ...EMPTY } // ของ session นี้ (เริ่มใหม่เมื่อ reload)
36
37// ==========================================================================
38// บทที่ 1: คำนวณเงิน
39// ==========================================================================
40function costUSD(s: CacheStats): number {
41 return (
42 (s.input / 1e6) * PRICE.input +
43 (s.output / 1e6) * PRICE.output +
44 (s.cacheWrite / 1e6) * PRICE.cacheWrite +
45 (s.cacheRead / 1e6) * PRICE.cacheRead
46 )
47}
48
49// ถ้าไม่มี cache เลย token ที่อ่านจาก cache ต้องจ่ายราคา input เต็ม
50function savedUSD(s: CacheStats): number {
51 return (s.cacheRead / 1e6) * (PRICE.input - PRICE.cacheRead)
52}
53
54// ==========================================================================
55// บทที่ 2: Hit Rate = สัดส่วน input ที่อ่านจาก cache
56// ==========================================================================
57function hitRate(s: CacheStats): number {
58 const totalInput = s.input + s.cacheRead + s.cacheWrite
59 return totalInput === 0 ? 0 : (s.cacheRead / totalInput) * 100
60}
61
62function hitLight(rate: number): string {
63 if (rate >= 60) return '🟢'
64 if (rate >= 30) return '🟡'
65 return '🔴'
66}
67
68function add(a: CacheStats, u: TurnUsage): CacheStats {
69 return {
70 turns: a.turns + 1,
71 input: a.input + u.input_tokens,
72 output: a.output + u.output_tokens,
73 cacheRead: a.cacheRead + u.cache_read_input_tokens,
74 cacheWrite: a.cacheWrite + u.cache_creation_input_tokens,
75 }
76}
77
78const thb = (usd: number) => `${(usd * THB_PER_USD).toFixed(2)}฿`
79const n = (x: number) => x.toLocaleString('en-US')
80
81function buildReport(title: string, s: CacheStats): string {
82 const rate = hitRate(s)
83 const advice =
84 s.turns === 0
85 ? 'ยังไม่มีข้อมูล ลองคุยกับ Claude สักรอบก่อน'
86 : rate >= 70
87 ? '✅ Hit Rate สูงมาก ใช้ cache คุ้มแล้ว'
88 : rate >= 30
89 ? '💡 พอใช้ได้ ลองใช้ /compact แทน /clear เมื่อยังทำงานเรื่องเดิม'
90 : '⚠️ Hit Rate ต่ำ อาจ /clear บ่อยเกินไป หรือเว้นนานเกิน 5 นาทีจน cache หมดอายุ'
91
92 return [
93 `💰 ${title}`,
94 '----------------------------------------',
95 `📊 ${s.turns} turn | input ${n(s.input)} | output ${n(s.output)}`,
96 ` ├─ อ่านจาก Cache (ถูก): ${n(s.cacheRead)} tokens`,
97 ` ├─ เขียนเข้า Cache: ${n(s.cacheWrite)} tokens`,
98 ` └─ Hit Rate: ${hitLight(rate)} ${rate.toFixed(1)}%`,
99 '',
100 '💵 ค่าใช้จ่ายโดยประมาณ:',
101 ` ├─ รวมทั้งหมด: $${costUSD(s).toFixed(4)} (~${thb(costUSD(s))})`,
102 ` ├─ ค่าเขียน Cache: ~${thb((s.cacheWrite / 1e6) * PRICE.cacheWrite)}`,
103 ` └─ ประหยัดได้เพราะ cache: ~${thb(savedUSD(s))}`,
104 '',
105 `💡 ${advice}`,
106 ].join('\n')
107}
108
109// ยอดสะสมทุก session เก็บใน $.store (อยู่ข้าม session และข้ามการ reload)
110async function readLifetime($: EngineInterface): Promise<CacheStats> {
111 const saved = await $.store.get('lifetime')
112 return saved && typeof saved === 'object' ? { ...EMPTY, ...(saved as Partial<CacheStats>) } : { ...EMPTY }
113}
114
115export const register: Register = on => {
116 on('session.start', async ($, e, next) => {
117 session = { ...EMPTY }
118 await $.command.register({ name: 'cache', description: 'ดูรายงานค่า Prompt Cache เป็นบาท (/cache all, /cache clear)', argumentHint: '[all|clear]' })
119 return next(e)
120 })
121
122 // ==========================================================================
123 // บทที่ 3: turn.complete - ได้ usage ของทั้ง turn (รวมทุก request ใน turn)
124 // ==========================================================================
125 on('turn.complete', async ($, e, next) => {
126 const usage = e.usage
127 if (usage && !e.agentId) {
128 // agentId มีค่า = turn ของ subagent; ที่นี่นับเฉพาะ turn หลัก
129 session = add(session, usage)
130 await $.store.set('lifetime', add(await readLifetime($), usage))
131
132 const rate = hitRate(session)
133 $.ui.status(`${hitLight(rate)} Cache ${rate.toFixed(0)}% | ${thb(costUSD(session))}`)
134 }
135 return next(e)
136 })
137
138 // ==========================================================================
139 // บทที่ 4: เตือนก่อน /clear - ดักคำสั่ง built-in แล้วปล่อยให้ทำงานต่อ
140 // ==========================================================================
141 on('command.run', { command: 'clear' }, ($, e, next) => {
142 if (session.cacheRead + session.cacheWrite > 0) {
143 $.ui.toast('⚠️ /clear ทำให้ cache ของบทสนทนานี้ใช้ต่อไม่ได้ ถ้ายังทำเรื่องเดิมอยู่ /compact คุ้มกว่า')
144 }
145 return next(e) // ต้อง next ไม่งั้น /clear จะไม่ทำงาน
146 })
147
148 on('command.run', { command: 'cache' }, async ($, e) => {
149 const arg = e.args.trim()
150 if (arg === 'clear') {
151 session = { ...EMPTY }
152 await $.store.delete('lifetime')
153 $.ui.status(undefined)
154 return { text: '🧹 ล้างสถิติ cache แล้ว' }
155 }
156 if (arg === 'all') return { text: buildReport('Cache Tax Report - สะสมทุก session', await readLifetime($)) }
157 return { text: buildReport('Cache Tax Report - session นี้', session) }
158 })
159}
160
161/**
162 * ========================================================================
163 * การบ้าน / ต่อยอด
164 * ========================================================================
165 * 1. ตั้งงบรายวัน: ถ้าเกิน 500 บาท ให้ $.ui.notify("ใช้เกินงบแล้ว!") เด้งแจ้งเตือนของระบบ
166 * 2. ส่งยอดเข้าแชตทีม ด้วย $.http.fetch ไปยัง webhook ของ Slack/Discord/LINE Messaging API
167 * 3. แยกราคาตาม usage.model เพราะแต่ละ model ราคาไม่เท่ากัน
168 */
169