cache-autopsy 0.1.0__tar.gz

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.
Files changed (53) hide show
  1. cache_autopsy-0.1.0/.github/ISSUE_TEMPLATE/bug.yml +22 -0
  2. cache_autopsy-0.1.0/.github/ISSUE_TEMPLATE/question.yml +11 -0
  3. cache_autopsy-0.1.0/.github/workflows/ci.yml +23 -0
  4. cache_autopsy-0.1.0/.gitignore +31 -0
  5. cache_autopsy-0.1.0/CHANGELOG.md +13 -0
  6. cache_autopsy-0.1.0/LICENSE +21 -0
  7. cache_autopsy-0.1.0/PKG-INFO +383 -0
  8. cache_autopsy-0.1.0/README.md +366 -0
  9. cache_autopsy-0.1.0/SECURITY.md +7 -0
  10. cache_autopsy-0.1.0/docs/causes.md +34 -0
  11. cache_autopsy-0.1.0/docs/images/01-demo.png +0 -0
  12. cache_autopsy-0.1.0/docs/images/02-analyze-broken.png +0 -0
  13. cache_autopsy-0.1.0/docs/images/03-diff.png +0 -0
  14. cache_autopsy-0.1.0/docs/images/04-lint.png +0 -0
  15. cache_autopsy-0.1.0/docs/images/05-analyze-fixed.png +0 -0
  16. cache_autopsy-0.1.0/examples/api_example.py +12 -0
  17. cache_autopsy-0.1.0/pyproject.toml +36 -0
  18. cache_autopsy-0.1.0/scripts/make_samples.py +195 -0
  19. cache_autopsy-0.1.0/src/cache_autopsy/__init__.py +21 -0
  20. cache_autopsy-0.1.0/src/cache_autopsy/__main__.py +5 -0
  21. cache_autopsy-0.1.0/src/cache_autopsy/cli.py +143 -0
  22. cache_autopsy-0.1.0/src/cache_autopsy/cost.py +96 -0
  23. cache_autopsy-0.1.0/src/cache_autopsy/demo.py +34 -0
  24. cache_autopsy-0.1.0/src/cache_autopsy/diff.py +138 -0
  25. cache_autopsy-0.1.0/src/cache_autopsy/lint.py +92 -0
  26. cache_autopsy-0.1.0/src/cache_autopsy/model.py +154 -0
  27. cache_autopsy-0.1.0/src/cache_autopsy/pricing.json +36 -0
  28. cache_autopsy-0.1.0/src/cache_autopsy/providers/__init__.py +53 -0
  29. cache_autopsy-0.1.0/src/cache_autopsy/providers/anthropic.py +103 -0
  30. cache_autopsy-0.1.0/src/cache_autopsy/providers/openai.py +102 -0
  31. cache_autopsy-0.1.0/src/cache_autopsy/py.typed +0 -0
  32. cache_autopsy-0.1.0/src/cache_autopsy/records.py +88 -0
  33. cache_autopsy-0.1.0/src/cache_autopsy/render.py +103 -0
  34. cache_autopsy-0.1.0/src/cache_autopsy/report.py +109 -0
  35. cache_autopsy-0.1.0/src/cache_autopsy/rules.py +115 -0
  36. cache_autopsy-0.1.0/src/cache_autopsy/samples/anthropic_broken.jsonl +8 -0
  37. cache_autopsy-0.1.0/src/cache_autopsy/samples/anthropic_fixed.jsonl +8 -0
  38. cache_autopsy-0.1.0/src/cache_autopsy/samples/openai_chat.jsonl +5 -0
  39. cache_autopsy-0.1.0/src/cache_autopsy/samples/openai_responses.jsonl +4 -0
  40. cache_autopsy-0.1.0/src/cache_autopsy/samples/requests/clean.json +51 -0
  41. cache_autopsy-0.1.0/src/cache_autopsy/samples/requests/tools_a.json +46 -0
  42. cache_autopsy-0.1.0/src/cache_autopsy/samples/requests/tools_b.json +46 -0
  43. cache_autopsy-0.1.0/src/cache_autopsy/samples/requests/volatile_system.json +51 -0
  44. cache_autopsy-0.1.0/src/cache_autopsy/verdict.py +160 -0
  45. cache_autopsy-0.1.0/tests/_helpers.py +13 -0
  46. cache_autopsy-0.1.0/tests/conftest.py +4 -0
  47. cache_autopsy-0.1.0/tests/test_cli.py +122 -0
  48. cache_autopsy-0.1.0/tests/test_diff.py +58 -0
  49. cache_autopsy-0.1.0/tests/test_lint.py +40 -0
  50. cache_autopsy-0.1.0/tests/test_openai.py +42 -0
  51. cache_autopsy-0.1.0/tests/test_usage.py +50 -0
  52. cache_autopsy-0.1.0/tests/test_verdict.py +66 -0
  53. cache_autopsy-0.1.0/uv.lock +79 -0
@@ -0,0 +1,22 @@
1
+ name: 버그 / Bug
2
+ description: README대로 했는데 다르게 동작해요 / Something does not work as the README says
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: what
7
+ attributes:
8
+ label: 무엇이 일어났나요? / What happened?
9
+ description: 실행한 명령과 나온 메시지를 그대로 붙여 주세요. / Paste the command and the output.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: expected
14
+ attributes:
15
+ label: 기대한 결과 / Expected
16
+ - type: input
17
+ id: env
18
+ attributes:
19
+ label: 환경 / Environment
20
+ placeholder: "macOS 15 (M3) · Ubuntu 24.04 · Windows 11 / uv 0.x / mock or real mode"
21
+ validations:
22
+ required: true
@@ -0,0 +1,11 @@
1
+ name: 질문 / Question
2
+ description: 이해가 안 되는 부분이나 더 알고 싶은 점 / Something unclear or worth explaining
3
+ labels: [question]
4
+ body:
5
+ - type: textarea
6
+ id: question
7
+ attributes:
8
+ label: 질문 / Question
9
+ description: README의 어느 부분(절 제목)에 대한 질문인지 알려 주세요. / Which README section is this about?
10
+ validations:
11
+ required: true
@@ -0,0 +1,23 @@
1
+ # Offline project: no API keys, no network at test time.
2
+ name: ci
3
+ on:
4
+ push:
5
+ pull_request:
6
+ schedule:
7
+ - cron: "17 3 * * 1" # weekly: catch breakage from runtime or action changes
8
+ workflow_dispatch:
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.12"] # versions actually tested locally: 3.12.4 and 3.15.2
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v6
18
+ - run: uv python install ${{ matrix.python-version }}
19
+ - run: uv sync --locked --python ${{ matrix.python-version }}
20
+ - run: uv run pytest -q
21
+ - run: uv run cache-autopsy demo
22
+ - name: Gate sanity check (the fixed sample must pass, exit code 0)
23
+ run: uv run cache-autopsy analyze src/cache_autopsy/samples/anthropic_fixed.jsonl --fail-under 0.6 --format md
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ *.egg-info/
9
+
10
+ # Node
11
+ node_modules/
12
+ dist/
13
+ .next/
14
+
15
+ # 실행 산출물·모델·데이터 캐시
16
+ runs/
17
+ outputs/
18
+ .cache/
19
+ *.safetensors
20
+ *.gguf
21
+ *.bin
22
+
23
+ # 환경·비밀
24
+ .env
25
+ .env.*
26
+ !.env.example
27
+
28
+ # OS·에디터
29
+ .DS_Store
30
+ .vscode/
31
+ .idea/
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
+
5
+ ## [0.1.0] - 2026-11-09
6
+
7
+ ### Added
8
+ - `diff`, `analyze`, `lint`, `causes` and `demo` commands.
9
+ - Anthropic Messages, OpenAI Chat Completions and OpenAI Responses support.
10
+ - Pair verdicts (`hit`, `explained_miss`, `unexplained_miss`) combining request structure and usage.
11
+ - Missed-token and missed-cost estimates with an overridable price table.
12
+ - `--fail-under` / `--fail-on` gates with exit codes 0 / 1 / 2.
13
+ - Bundled practice (mock) logs and `scripts/make_samples.py` to regenerate them.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LeeGyeongPil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,383 @@
1
+ Metadata-Version: 2.5
2
+ Name: cache-autopsy
3
+ Version: 0.1.0
4
+ Summary: Find out why your LLM prompt cache missed: offline diff, usage-aware hit rate, missed cost and CI gates for Anthropic and OpenAI request logs.
5
+ Project-URL: Homepage, https://github.com/LeeGyeongPil/cache-autopsy
6
+ Project-URL: Repository, https://github.com/LeeGyeongPil/cache-autopsy
7
+ Project-URL: Issues, https://github.com/LeeGyeongPil/cache-autopsy/issues
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: anthropic,ci,cli,llm,openai,prompt-caching
11
+ Classifier: Environment :: Console
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+
18
+ # cache-autopsy
19
+
20
+ > **English summary** — cache-autopsy reads your saved LLM request/response logs (JSONL) and tells you, offline, why the prompt cache missed: which request, which JSON path and byte offset diverged, what it cost, and how to fix it. It is for developers calling the Anthropic or OpenAI APIs directly who want to explain a bill or gate prompt changes in CI. Run `uv run cache-autopsy demo` after cloning; no API key, no network, zero dependencies.
21
+
22
+ 프롬프트 캐시가 왜 깨졌는지, 이미 쌓아 둔 요청·응답 기록만으로 알려 주는 오프라인 CLI입니다. 공급자 콘솔을 뒤지거나 두 요청 본문을 눈으로 비교하는 수고를 줄여 줍니다.
23
+
24
+ ```text
25
+ session calls hit rate missed tokens missed cost
26
+ broken 8 12.1% 21,110 $0.0728
27
+ fixed 8 87.2% 0 $0.0000
28
+ ```
29
+ *`cache-autopsy demo` 의 실제 출력. 같은 대화를 고치기 전(broken)과 후(fixed)로 비교한 연습용 목(mock) 데이터입니다.*
30
+
31
+ ![demo 명령 실행 화면](https://raw.githubusercontent.com/LeeGyeongPil/cache-autopsy/v0.1.0/docs/images/01-demo.png)
32
+ *그림 1. `cache-autopsy demo` 실행 화면.*
33
+
34
+ > 아래 모든 출력은 실제로 실행해 복사한 것입니다.
35
+
36
+ ## 이런 분께
37
+ - 오류는 없는데 입력 토큰 비용이 늘어서 원인을 찾아야 할 때
38
+ - 프롬프트·도구 정의를 바꾸는 PR에서 "캐시가 깨지지 않는지" CI로 막고 싶을 때
39
+ - "고정 프롬프트인데 `cached_tokens` 가 0" 같은 질문에 근거를 대야 할 때
40
+
41
+ ## 설치
42
+ 전제: Python 3.12 이상(3.12.4, 3.15.2 에서 테스트), [uv](https://docs.astral.sh/uv/), git. 의존 패키지는 없습니다.
43
+
44
+ ```bash
45
+ curl -LsSf https://astral.sh/uv/install.sh | sh # uv 가 없다면
46
+ git clone https://github.com/LeeGyeongPil/cache-autopsy.git
47
+ cd cache-autopsy
48
+ uv sync
49
+ uv run cache-autopsy --version
50
+ ```
51
+ ```text
52
+ cache-autopsy 0.1.0
53
+ ```
54
+ 최소 리눅스 컨테이너라면 `git`, `curl`, `ca-certificates` 가 필요합니다. PyPI 에는 아직 게시하지 않았습니다.
55
+
56
+ ## 빠르게 시작하기
57
+ ```bash
58
+ uv run cache-autopsy demo
59
+ ```
60
+ ```text
61
+ NOTE: practice mock data (연습용 목(mock) 데이터). These are NOT real API logs; usage numbers are invented to follow the providers' rules.
62
+ The analyzer really renders, diffs and classifies the fake requests below.
63
+
64
+ session calls hit rate missed tokens missed cost
65
+ broken 8 12.1% 21,110 $0.0728
66
+ fixed 8 87.2% 0 $0.0000
67
+
68
+ Why 'broken' misses (cause: calls, missed tokens):
69
+ ttl_expired_suspected 1 call(s) 3,560 tokens $0.0123
70
+ provider_side_suspected 1 call(s) 3,560 tokens $0.0123
71
+ setting_changed:tool_choice 1 call(s) 3,560 tokens $0.0123
72
+ json_key_order_changed 1 call(s) 3,520 tokens $0.0121
73
+ timestamp_in_prefix 1 call(s) 3,470 tokens $0.0120
74
+ tool_order_changed 1 call(s) 3,440 tokens $0.0119
75
+
76
+ Try: cache-autopsy analyze <your-log>.jsonl | cache-autopsy causes
77
+ ```
78
+ - **hit rate**: 입력 토큰 중 캐시에서 읽은 비율입니다.
79
+ - **missed tokens / cost**: 캐시가 맞았다면 싸게 읽었을 토큰과, 그 차액(예시 단가 기준)입니다.
80
+ - 아래 줄은 "깨진 원인별" 합계입니다. 숫자는 연습용 목 데이터라서 실제 서비스 값이 아닙니다.
81
+
82
+ ### 연습용 목(mock) 모드란
83
+ `demo` 와 `src/cache_autopsy/samples/` 의 로그는 `scripts/make_samples.py` 가 만든 **가짜 요청**입니다. 분석기는 이 요청 본문을 실제로 직렬화·비교·분류하므로 샘플 요청을 고치면 결과가 바뀝니다. 다만 응답의 `usage` 숫자는 공급자 규칙을 따르도록 지어낸 값입니다. 실제 로그를 넣으면 응답의 `usage` 를 그대로 읽습니다. 이 도구는 API 를 호출하지 않으므로 키가 필요 없고 느려질 일도 없습니다.
84
+
85
+ ## 캐시 규칙 한눈에
86
+ 확인일 2026-11-09. 모델·숫자는 자주 바뀌므로 아래 공식 문서가 기준입니다.
87
+
88
+ | | Anthropic | OpenAI |
89
+ |---|---|---|
90
+ | 접두부 순서 | tools → system → messages | 요청 앞부분이 같아야 함(암묵 캐시) |
91
+ | 최소 길이 | 모델별 512~4,096 토큰(미만이면 오류 없이 캐시 안 됨) | 1,024 토큰(모델별 차이 있음) |
92
+ | 유지 시간(TTL) | 5분(기본) 또는 1시간 | 모델별로 수십 분~24시간(옵션) |
93
+ | usage 필드 | `cache_read_input_tokens`, `cache_creation_input_tokens`, `input_tokens` | `input_tokens_details.cached_tokens` (Chat 은 `prompt_tokens_details`) |
94
+ | 적중률 분모 | read / (read + write + input) | cached / 전체 입력 토큰 |
95
+
96
+ Anthropic 의 `input_tokens` 는 마지막 캐시 지점 **뒤** 토큰만 세므로 `read / input_tokens` 로 계산하면 틀립니다. 출처: [Anthropic](https://platform.claude.com/docs/en/build-with-claude/prompt-caching), [OpenAI](https://developers.openai.com/api/docs/guides/prompt-caching).
97
+
98
+ ## 사용법
99
+ | 명령 | 하는 일 | 종료 코드 |
100
+ |---|---|---|
101
+ | `demo` | 내장 연습 데이터로 전체 흐름을 보여 줌 | 0 |
102
+ | `analyze LOG.jsonl` | 적중률, 놓친 토큰·비용, 호출 쌍마다 판정과 원인 | 0, 게이트 실패 1, 입력 오류 2 |
103
+ | `diff A.json B.json` | 두 요청 본문에서 처음 달라진 지점 | 0 |
104
+ | `lint REQUEST.json` | 요청 하나에서 캐시를 깰 만한 패턴 | 경고 있으면 1 |
105
+ | `causes` | 원인 코드와 고치는 법 목록 (`--format md` 가능) | 0 |
106
+
107
+ 주요 옵션: `--format text|json|md`, `--fail-under 0.6`(적중률 하한, **0~1 비율**이며 60 처럼 쓰면 종료 2), `--fail-on CODE,...`(해당 판정·원인이 있으면 실패, 모르는 값은 종료 2), `--session-key request.metadata.user_id`(대화 묶음 기준), `--pricing FILE`(단가 덮어쓰기), `--redact`(요청 텍스트를 출력하지 않음), `--provider`(자동 감지 대신 지정), `--min-tokens`(캐시 최소 토큰 수, 기본 1024).
108
+
109
+ ### 입력 형식
110
+ 한 줄에 JSON 객체 하나. `request`(필수), `response`(보통), `session_id`, `ts`(ISO-8601 또는 epoch 초)를 둡니다. 지원: Anthropic Messages, OpenAI Chat Completions, OpenAI Responses.
111
+ ```json
112
+ {"session_id": "s1", "provider": "anthropic", "ts": "2026-11-09T10:00:00+00:00",
113
+ "request": {"model": "...", "tools": [], "system": "...", "messages": []},
114
+ "response": {"usage": {"input_tokens": 12, "cache_creation_input_tokens": 3410, "cache_read_input_tokens": 0, "output_tokens": 20}}}
115
+ ```
116
+
117
+ ### 기록 남기는 법
118
+ 이 도구의 입력은 이미 쌓인 기록입니다. 호출할 때마다 **보낸 요청 인자**와 **응답 객체**를 한 줄씩 JSONL 에 덧붙이세요. 두 SDK 모두 응답 객체에 `model_dump()` 가 있어 dict 로 바꿀 수 있습니다.
119
+ ```python
120
+ import json, time
121
+
122
+ def append_record(path, session_id, request, response):
123
+ line = {"session_id": session_id, "ts": time.time(),
124
+ "request": request, "response": response.model_dump(mode="json")}
125
+ with open(path, "a", encoding="utf-8") as f:
126
+ f.write(json.dumps(line, ensure_ascii=False) + "\n")
127
+ ```
128
+ 사용 예(SDK 호출 형태는 공식 문서 기준이며, 이 레포는 SDK 를 설치하거나 호출해 보지 않았습니다):
129
+ ```python
130
+ # Anthropic: kwargs 를 변수로 두고 같은 것을 기록
131
+ kwargs = dict(model="...", max_tokens=256, system="...", messages=[...])
132
+ resp = anthropic.Anthropic().messages.create(**kwargs)
133
+ append_record("recorded.jsonl", "chat-1", kwargs, resp)
134
+
135
+ # OpenAI (Chat Completions)
136
+ kwargs = dict(model="...", messages=[...])
137
+ resp = openai.OpenAI().chat.completions.create(**kwargs)
138
+ append_record("recorded.jsonl", "chat-1", kwargs, resp)
139
+ ```
140
+ `response` 에 `usage` 가 들어 있어야 적중률을 계산할 수 있습니다. 아래는 위 `append_record` 로 가짜 `usage` 가 든 응답 객체를 두 줄 기록한 뒤 분석한 예시 출력입니다. 그 가짜 객체는 이 문서에 싣지 않았으므로 **모양을 보여 주는 연습용 숫자**이며, 직접 만든 기록에서는 숫자가 다르게 나옵니다. 같은 모양의 기록이 필요하면 `src/cache_autopsy/samples/` 의 샘플 파일을 보세요.
141
+ ```bash
142
+ uv run cache-autopsy analyze recorded.jsonl --fail-under 0.6 --format md
143
+ ```
144
+ ```text
145
+ FAIL: hit rate 0.00 < 0.60
146
+ ## cache-autopsy report
147
+
148
+ **Hit rate 0.0%** · missed 3,410 tokens · missed cost $0.0118
149
+
150
+ | session | # | verdict | reason / causes | path | byte | missed tokens | missed cost |
151
+ |---|---|---|---|---|---|---|---|
152
+ | s1 | 1 | unexplained_miss | unexplained/provider_side_suspected | messages[1] | 67 | 3,410 | $0.0118 |
153
+
154
+ | cause | calls | missed tokens | missed cost |
155
+ |---|---|---|---|
156
+ | provider_side_suspected | 1 | 3,410 | $0.0118 |
157
+ ```
158
+ 종료 코드 1. 두 번째 호출의 `usage` 를 캐시 읽기 0 으로 둔 가짜 값이라 미스로 나옵니다.
159
+
160
+ ### 시나리오 1 — 지난 기록에서 깨진 원인 찾기
161
+ ```bash
162
+ uv run cache-autopsy analyze src/cache_autopsy/samples/anthropic_broken.jsonl --fail-under 0.6
163
+ ```
164
+ ```text
165
+ FAIL: hit rate 0.12 < 0.60
166
+ cache-autopsy: 8 calls, 1 session(s), 7 comparisons
167
+
168
+ session broken: 8 calls, hit rate 12.1%, missed 21,110 tokens, $0.0728
169
+ # verdict reason / causes path byte missed tok missed usd
170
+ 1 hit messages_changed/append_only_ok messages[1] 13588 0 $0.0000
171
+ 2 explained_miss tools_changed/tool_order_changed tools[0].name 29 3,440 $0.0119
172
+ fix: Sort tools (for example by name) before sending, or register them in a fixed order.
173
+ old: ...search_orders","description":"Search ord
174
+ new: ...get_stock","description":"Get stock leve
175
+ 3 explained_miss system_changed/timestamp_in_prefix system 355 3,470 $0.0120
176
+ fix: Move the clock value to the end (last user message) or drop it.
177
+ old: ...You are a support assistant for a fictio
178
+ new: ...Current time: 2026-11-09T10:05:31Z\nYou
179
+ 4 explained_miss tools_changed/json_key_order_changed tools[0].input_schema.required 103 3,520 $0.0121
180
+ ...
181
+ 5 unexplained_miss unexplained/ttl_expired_suspected - - 3,560 $0.0123
182
+ note: structure is identical, so this is a suspicion, not a finding (gap 420s).
183
+ 6 unexplained_miss unexplained/provider_side_suspected - - 3,560 $0.0123
184
+ note: structure is identical, so this is a suspicion, not a finding (gap 30s).
185
+ 7 explained_miss messages_changed/setting_changed:tool_choice tool_choice - 3,560 $0.0123
186
+ ...
187
+ ```
188
+ 명령의 종료 코드는 1 입니다(위 블록은 프로그램 출력만 담았고 종료 코드 줄은 포함하지 않습니다). (위 출력은 가독성을 위해 일부 줄을 `...` 로 줄였습니다. 전체는 직접 실행해 보세요.)
189
+
190
+ 읽는 법:
191
+ 1. 한 행은 "바로 앞 호출 → 이 호출"의 비교입니다. `hit` 는 앞부분이 그대로 이어졌다는 뜻입니다(새 메시지만 덧붙임).
192
+ 2. `explained_miss` 는 요청 구조에서 원인을 찾았다는 뜻입니다. `path` 와 `byte` 가 처음 달라진 위치입니다.
193
+ 3. `unexplained_miss` 는 구조는 같은데 캐시가 안 맞은 경우입니다. 호출 간격이 TTL(캐시 유지 시간)보다 길면 `ttl_expired_suspected`, 아니면 `provider_side_suspected` 로 **추측**만 하며 단정하지 않습니다.
194
+ 4. 종료 코드 1 은 `--fail-under` 게이트 실패입니다. CI 가 이 코드로 멈춥니다. (stderr 의 `FAIL:` 줄이 먼저 보이는 것은 출력 버퍼 순서 때문입니다.)
195
+
196
+ ![깨진 기록 분석 화면](https://raw.githubusercontent.com/LeeGyeongPil/cache-autopsy/v0.1.0/docs/images/02-analyze-broken.png)
197
+ *그림 2. 깨진 기록 분석과 --fail-under 실패.*
198
+
199
+ 고친 뒤 같은 명령은 통과합니다.
200
+ ```bash
201
+ uv run cache-autopsy analyze src/cache_autopsy/samples/anthropic_fixed.jsonl --fail-under 0.6
202
+ ```
203
+ ```text
204
+ session fixed: 8 calls, hit rate 87.2%, missed 0 tokens, $0.0000
205
+ ...
206
+ hit rate 87.2% | missed 0 tokens | missed cost $0.0000
207
+ ```
208
+ 종료 코드 0.
209
+
210
+ ![고친 기록 분석 화면](https://raw.githubusercontent.com/LeeGyeongPil/cache-autopsy/v0.1.0/docs/images/05-analyze-fixed.png)
211
+ *그림 5. 고친 기록은 통과(종료 코드 0).*
212
+
213
+ ### 시나리오 2 — 두 요청을 비교하기
214
+ ```bash
215
+ uv run cache-autopsy diff src/cache_autopsy/samples/requests/tools_a.json src/cache_autopsy/samples/requests/tools_b.json
216
+ ```
217
+ ```text
218
+ reason : tools_changed
219
+ causes : tool_order_changed
220
+ where : path=tools[0].name byte=29 (local rendering, see README)
221
+ before : ..."claude-sonnet-4-5"[{"name":"
222
+ old : search_orders","description":"Search ord
223
+ new : get_stock","description":"Get stock leve
224
+ fix : Sort tools (for example by name) before sending, or register them in a fixed order.
225
+ ```
226
+ 도구 목록이 같은데 순서만 바뀌었습니다. 캐시는 도구 → 시스템 → 메시지 순으로 앞에서부터 이어 붙인 접두부(prefix)를 쓰므로, 맨 앞의 도구가 달라지면 뒤 전부가 무효가 됩니다.
227
+
228
+ ![두 요청 비교 화면](https://github.com/LeeGyeongPil/cache-autopsy/blob/v0.1.0/docs/images/03-diff.png)
229
+ *그림 3. 도구 순서 변경 비교.*
230
+
231
+ ### 시나리오 3 — 보내기 전에 점검(lint)
232
+ ```bash
233
+ uv run cache-autopsy lint src/cache_autopsy/samples/requests/volatile_system.json
234
+ ```
235
+ ```text
236
+ WARNING timestamp_in_prefix path=system[0].text
237
+ a value that changes between calls sits in the cached prefix (system[0].text).
238
+ fix: Move the clock value to the end (last user message) or drop it.
239
+ WARNING breakpoint_on_volatile_block path=system[0]
240
+ system[0] is cached, but the prefix up to it contains a changing value (system[0].text), so the cache key never repeats.
241
+ fix: Place cache_control on the last stable block, before anything volatile.
242
+ 2 warning(s), 0 info
243
+ ```
244
+ 경고가 있어 종료 코드는 1입니다. 문제없는 `requests/clean.json` 은 `no findings: ...` 와 종료 코드 0 입니다.
245
+
246
+ ![lint 경고 화면](https://raw.githubusercontent.com/LeeGyeongPil/cache-autopsy/v0.1.0/docs/images/04-lint.png)
247
+ *그림 4. 시간값이 접두부에 있다는 경고.*
248
+
249
+ ### 시나리오 4 — 파이썬에서 부르기
250
+ ```bash
251
+ uv run python examples/api_example.py
252
+ ```
253
+ ```text
254
+ 0.12101639576974944 21110
255
+ hit ['append_only_ok'] messages[1]
256
+ explained_miss ['tool_order_changed'] tools[0].name
257
+ explained_miss ['timestamp_in_prefix'] system
258
+ tools_changed ['tool_order_changed'] tools[0].name 29
259
+ ```
260
+ 공개 함수는 `analyze_records(records)`, `diff_requests(a, b)`, `lint_request(req)` 세 개입니다([examples/api_example.py](https://github.com/LeeGyeongPil/cache-autopsy/blob/v0.1.0/examples/api_example.py)).
261
+
262
+ ### CI에 넣기
263
+ 프롬프트·도구 정의를 바꾸는 PR 에서 캐시 회귀를 막는 예시입니다. 종료 코드: **0** 통과, **1** 게이트 실패(적중률 하한 미달, `--fail-on` 해당, lint 경고), **2** 입력·옵션 오류(캐시 회귀가 아니므로 기록·옵션부터 고치세요).
264
+ ```yaml
265
+ # .github/workflows/cache-gate.yml
266
+ name: cache-gate
267
+ on: pull_request
268
+ jobs:
269
+ cache-gate:
270
+ runs-on: ubuntu-latest
271
+ steps:
272
+ - uses: actions/checkout@v4
273
+ - uses: astral-sh/setup-uv@v6
274
+ - name: Lint the request template
275
+ run: uvx --from git+https://github.com/LeeGyeongPil/cache-autopsy cache-autopsy lint tests/fixtures/request.json
276
+ - name: Gate on recorded hit rate
277
+ run: uvx --from git+https://github.com/LeeGyeongPil/cache-autopsy cache-autopsy analyze tests/fixtures/recorded.jsonl --fail-under 0.6 --format md
278
+ ```
279
+ 이 레포의 CI([.github/workflows/ci.yml](https://github.com/LeeGyeongPil/cache-autopsy/blob/v0.1.0/.github/workflows/ci.yml))는 같은 게이트(`analyze ... --fail-under 0.6 --format md`)를 동봉 샘플에 실행합니다. 위 예시의 `uvx --from git+...` 줄은 레포가 공개되기 전이라 아직 실행해 보지 못했습니다. `analyze`·`lint` 명령 자체는 위 시나리오에서 실행 확인했습니다.
280
+
281
+ pre-commit 훅으로 쓰려면 로컬 훅으로 같은 명령을 겁니다.
282
+ ```yaml
283
+ # .pre-commit-config.yaml
284
+ repos:
285
+ - repo: local
286
+ hooks:
287
+ - id: cache-autopsy-lint
288
+ name: cache-autopsy lint
289
+ entry: uvx --from git+https://github.com/LeeGyeongPil/cache-autopsy cache-autopsy lint
290
+ language: system
291
+ files: ^prompts/.*\.json$
292
+ ```
293
+ 이 훅도 공개 전이라 실행 확인 전입니다. 전용 Action·훅 배포 형태는 아래 로드맵 M2 입니다.
294
+
295
+ ### 입력 오류
296
+ ```bash
297
+ uv run cache-autopsy analyze nope.jsonl
298
+ ```
299
+ ```text
300
+ error: file not found: nope.jsonl. Check the path.
301
+ ```
302
+ 종료 코드 2. JSON 이 깨진 줄은 줄 번호와 함께 보고됩니다. UTF-8 이 아닌 파일, 디렉터리, 잘못된 `--fail-under`·`--fail-on` 값도 traceback 없이 고칠 방법과 함께 종료 코드 2 로 끝납니다.
303
+ ```bash
304
+ uv run cache-autopsy analyze x.jsonl --fail-under 60
305
+ ```
306
+ ```text
307
+ error: --fail-under must be a ratio between 0 and 1 (got 60.0). Write 0.6 for 60%.
308
+ ```
309
+
310
+ ## 설정
311
+ - 환경변수·키·네트워크가 없습니다(`.env` 불필요).
312
+ - **단가**: `src/cache_autopsy/pricing.json` 에 모델별 예시 단가(USD / 1M 토큰)가 들어 있습니다. 파일 메타(`_meta`)에 `unverified_example_prices: true`(검증되지 않은 예시 단가), `carried_over: 2026-11-09`(이 날짜의 값을 옮겨 둠)와 출처([Anthropic](https://platform.claude.com/docs/en/about-claude/pricing), [OpenAI](https://developers.openai.com/api/docs/pricing))가 적혀 있지만, **이 값들은 개발 중 공식 가격 페이지와 다시 대조하지 못했습니다.** 그러니 "예시 단가"로 보고, 실제 청구와 맞춰 보려면 `--pricing my-prices.json`(같은 모양)으로 덮어쓰세요. 모르는 모델은 비용을 계산하지 않고 `unpriced_models` 로 알립니다.
313
+ - 응답에 `usage` 가 없으면 글자 수/4 로 토큰을 추정하고 결과에 추정임을 표시합니다.
314
+ - 바이트 위치와 JSON 경로는 문서에 적힌 섹션 순서를 따라 로컬에서 다시 그린 결과이며, 공급자 내부 직렬화와 같다는 보장은 없습니다.
315
+
316
+ ## 동작 원리
317
+ ```mermaid
318
+ flowchart LR
319
+ A[JSONL 기록] --> B[공급자 감지]
320
+ B --> C[접두부 렌더링<br/>tools, system, messages]
321
+ C --> D[앞뒤 호출 비교<br/>처음 달라진 위치]
322
+ A --> E[응답 usage 읽기]
323
+ D --> F[판정 + 원인]
324
+ E --> F
325
+ F --> G[놓친 토큰·비용]
326
+ G --> H[리포트 / CI 종료 코드]
327
+ ```
328
+ 1. 기록을 읽고 Anthropic·OpenAI 형식을 자동 감지합니다.
329
+ 2. 요청을 캐시가 보는 순서(도구, 시스템, 메시지)로 이어 붙여 접두부를 만듭니다.
330
+ 3. 같은 대화의 앞뒤 호출을 비교해 처음 달라지는 바이트와 JSON 경로를 찾고, 원인 규칙(도구 순서, 타임스탬프, 키 순서, 설정 변경 등)에 대 봅니다.
331
+ 4. 응답 `usage` 의 캐시 읽기·쓰기 토큰으로 실제 적중 여부를 확인해 `hit` / `explained_miss` / `unexplained_miss` 를 정합니다.
332
+ 5. 놓친 토큰에 단가 차액을 곱해 비용을 내고, 게이트 조건에 따라 종료 코드를 정합니다.
333
+
334
+ 원인 코드 전체: [docs/causes.md](https://github.com/LeeGyeongPil/cache-autopsy/blob/v0.1.0/docs/causes.md). 샘플 재생성: `uv run python scripts/make_samples.py`.
335
+
336
+ ## 다른 도구와 비교
337
+ | | cache-autopsy | 공급자 콘솔·사용량 대시보드 | 공급자 공식 진단 | 직접 diff |
338
+ |---|---|---|---|---|
339
+ | 오프라인·키 불필요 | 예 | 아니오(로그인 필요) | 아니오(요청 때 옵션을 켜야 함) | 예 |
340
+ | 이미 쌓인 기록 소급 분석 | 예 | 집계만 | 아니오(켠 뒤 요청부터) | 예 |
341
+ | 깨진 위치(경로·바이트) | 예 | 아니오 | 공급자가 알려 주는 사유 | 눈으로 찾아야 함 |
342
+ | 요청 구조 + usage 교차 확인 | 예 | usage 만 | 공급자 쪽 정보 | 아님 |
343
+ | CI 게이트 | 예 | 아니오 | 직접 구현 | 직접 구현 |
344
+ | 실제 청구액 | 단가 추정 | 정확함 | 아님 | 아님 |
345
+
346
+ **공식 진단과의 관계**: Anthropic 의 [cache diagnostics](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics), OpenAI 의 [prompt cache diagnostics](https://developers.openai.com/api/docs/guides/prompt-caching/diagnostics)를 대체하려는 것이 아니라, 이미 저장된 기록을 오프라인으로 분석하는 보완입니다. 기록 응답에 진단 필드가 들어 있으면 리포트에 `provider says: ... (agree=True|False)` 줄로 같이 보여 주어, 로컬 추정과 공급자 설명을 교차 확인할 수 있습니다.
347
+
348
+ **같은 주제의 다른 오픈소스 도구**: 캐시 적중 로그를 보는 작은 도구들이 있습니다(예: [prefixcash](https://pypi.org/project/prefixcash/), [cachelens](https://pypi.org/project/cachelens/), [prompt-cache-doctor](https://github.com/Ankitajainkuniya/prompt-cache-doctor)). 개발 중 조사한 바로는 공급자 하나만 지원하거나 도구 순서·설정 필드 변화·JSON 경로·CI 종료 코드까지는 다루지 않는 경우가 있었습니다(2026-11-09 확인, 이후 바뀌었을 수 있습니다). 각자 용도에 맞는 것을 고르세요.
349
+
350
+ 정확한 청구 금액은 공급자 콘솔이 낫습니다. 이 도구의 금액은 예시 단가로 계산한 추정치입니다. 이 표는 이 도구 개발 중 조사한 일반적 차이이며, 개별 서비스를 일일이 평가한 것은 아닙니다.
351
+
352
+ ## 자주 겪는 문제
353
+ | 증상 | 원인 | 해결 |
354
+ |---|---|---|
355
+ | `error: ... each record must be an object with a "request"` | 줄마다 `request` 키가 있어야 함 | 한 줄에 `{"request": {...}, "response": {...}}` 형태로 저장 |
356
+ | `cannot apply --fail-under: the log has no usage data.` (종료 2) | 응답에 usage 가 없음 | 응답 전체(또는 usage)를 같이 기록 |
357
+ | 구조는 같은데 `unexplained_miss` | TTL 만료, 첫 호출 동시 실행, 공급자 라우팅 등 | 출력의 `gap` 과 `fix` 를 보고 간격·`prompt_cache_key` 점검 |
358
+ | 비용이 `$0` 이거나 비어 있음 | 단가표에 없는 모델 | `--pricing` 으로 단가 추가 |
359
+ | 적중률이 낮은데 원인이 없음 | 앞 프롬프트가 캐시 최소 토큰 수보다 짧음 | `--min-tokens` 를 모델에 맞게 조정 |
360
+
361
+ ## 더 알아보기
362
+ 확인일 2026-11-09.
363
+ - Anthropic 프롬프트 캐싱: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
364
+ - Anthropic 캐시 진단: https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics
365
+ - OpenAI 프롬프트 캐싱 진단: https://developers.openai.com/api/docs/guides/prompt-caching/diagnostics
366
+
367
+ ## 로드맵
368
+ 현재는 M1(오프라인 분석기)입니다. 아래는 계획이며 약속이 아닙니다.
369
+ - **M2**: Gemini 입력(`usage.total_cached_tokens`), HAR·LiteLLM 로그 입력, 대화 자동 묶기, GitHub Action·pre-commit 훅 배포 형태, PyPI 게시 준비.
370
+ - **M3**: Python API 안정화와 테스트 도우미(`assert_prefix_stable` 같은 pytest 연동), 실제 키로 소량 녹화하는 선택 스크립트(`.env.example` 로 키 안내, 실제 모드는 공급자 상태에 따라 느릴 수 있음).
371
+
372
+ ## 개발·기여
373
+ ```bash
374
+ uv run pytest -q
375
+ uv run python -I -m unittest discover -s tests
376
+ ```
377
+ ```text
378
+ 41 passed in 0.29s
379
+ ```
380
+ 버그·제안은 이슈로 남겨 주세요. 변경 기록은 [CHANGELOG.md](https://github.com/LeeGyeongPil/cache-autopsy/blob/v0.1.0/CHANGELOG.md).
381
+
382
+ ## 라이선스
383
+ MIT. 샘플 데이터는 이 레포가 만든 가상의 서점 대화이며 실제 API 응답이 아닙니다.