SLOPSHOPPER

cache-tax-th

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

newcommandtoaststatus
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-tax-th
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /cache ⎿ cache-tax-th: 💰 Cache Tax Report - session นี้ ⎿ cache-tax-th: ---------------------------------------- ⎿ cache-tax-th: 📊 1 turn | input 2,100 | output 1,480 ⎿ cache-tax-th: ├─ อ่านจาก Cache (ถูก): 91,000 tokens ⎿ cache-tax-th: ├─ เขียนเข้า Cache: 4,300 tokens ⎿ cache-tax-th: └─ Hit Rate: 🟢 93.4% ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ cache-tax-th: 🟢 Cache 93% | 2.63฿
README

Mod 05: Cache Tax TH

บอกว่า 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. แกะ cache_read_input_tokens และ cache_creation_input_tokens จาก usage
  2. คำนวณ Hit Rate และเงินที่ประหยัด
  3. ดักคำสั่ง built-in (/clear) แล้วปล่อยให้ทำงานต่อด้วย next(e)
  4. เก็บยอดสะสมใน $.store
  5. แสดงผลใน status line ด้วย $.ui.status

หมายเหตุ

ราคาเป็นค่าประมาณของ model ระดับ Sonnet แก้ได้ที่ PRICE

Source 1 files
hooks/register.tsx 169 lines
1/**
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