context-guardian 0.4.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LuminariSoftwares
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,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: context-guardian
3
+ Version: 0.4.0
4
+ Summary: A tiny proxy in front of any OpenAI-compatible LLM backend that compacts conversation history before the context window fills, so local-model coding CLIs don't hard-die from overflow.
5
+ Author: LuminariSoftwares
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/LuminariSoftwares/context-guardian
8
+ Project-URL: Repository, https://github.com/LuminariSoftwares/context-guardian
9
+ Project-URL: Changelog, https://github.com/LuminariSoftwares/context-guardian/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/LuminariSoftwares/context-guardian/issues
11
+ Keywords: ollama,local-llm,llm,openai-api,proxy,context-window,context-management,litellm,mcp,coding-assistant
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: fastapi>=0.110
26
+ Requires-Dist: uvicorn[standard]>=0.29
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: python-dotenv>=1.0
29
+ Dynamic: license-file
30
+
31
+ # Context Guardian
32
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
33
+ ![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)
34
+
35
+ A tiny proxy that sits in front of any OpenAI-compatible LLM backend (Ollama, LiteLLM, Headroom, vLLM, LM Studio, and similar) and forces a conversation-history compaction *before* the context window fills up — instead of letting requests grow until the backend hard-errors and the session dies.
36
+
37
+ ## Why this exists
38
+
39
+ Claude-Code-style coding CLIs (Claude Code itself, and OpenAI-compatible-backend tools like OpenClaude) ship with a built-in auto-compact feature. That feature depends on accurate, real-time token-usage accounting coming back from the API in the exact shape the CLI expects. Point one of these tools at a local model through an OpenAI-compatible bridge — Ollama's `/v1` endpoint, a LiteLLM proxy, a Headroom proxy — and that accounting is frequently missing, wrong, or shaped differently, so auto-compact silently never fires.
40
+
41
+ The visible symptom: the session just runs until the backend hard-rejects the request ("token limit reached"), you're forced to close and reopen, and there's no partial-compaction attempt in between — you just lose your place.
42
+
43
+ Context Guardian is a small, deliberately simple fallback for that specific gap. It estimates the running token count itself, and once a conversation crosses a configurable threshold, it asks the *same backend* to condense the older portion of the conversation into one summary message before forwarding the request onward. Recent messages are always kept verbatim. If the summarization call itself fails, Guardian fails **open** — it forwards the original, uncompacted request rather than risk silently dropping history.
44
+
45
+ ## Where it sits in your stack
46
+
47
+ This is a new link in an existing chain, not a replacement for anything you already have:
48
+
49
+ ```
50
+ Your CLI / agent (Claude Code, OpenClaude, etc.)
51
+ -> Context Guardian (this project)
52
+ -> your existing OpenAI-compatible backend
53
+ (Ollama directly, a LiteLLM proxy, Headroom, vLLM, ...)
54
+ ```
55
+
56
+ Point your CLI's `OPENAI_BASE_URL` at Context Guardian instead of directly at your backend, and set `GUARDIAN_UPSTREAM_URL` to wherever your backend actually lives. Guardian is a pure passthrough for everything except `POST /v1/chat/completions`, which gets the compaction check — every other route, including streaming responses, is forwarded byte-for-byte, untouched.
57
+
58
+ ## If you use MCP servers or an agentic CLI, read this
59
+
60
+ Guardian counts your `tools` array against the context budget. It did not
61
+ before 0.2.0, and that was a real bug — see the changelog.
62
+
63
+ Tool definitions are usually invisible in a way message history is not. You do
64
+ not type them, they do not scroll past, and your CLI's context display often
65
+ does not break them out. But they are in every single request. On the setup this
66
+ was developed against, seven MCP servers came to **28,689 tokens — 87.6% of a
67
+ 32,768-token window** — before the first user message.
68
+
69
+ **Guardian cannot compact them.** It summarizes conversation history; tool
70
+ definitions are a fixed floor underneath it. So there are two different problems
71
+ and only one of them is Guardian's:
72
+
73
+ | Problem | What fixes it |
74
+ |---|---|
75
+ | Conversation history grows until the window fills | Guardian |
76
+ | Two thirds of the window is gone before you type | Loading fewer tools |
77
+
78
+ Guardian will now tell you which one you have. It logs a `tool_budget` event the
79
+ first time it sees a given tool payload, and warns outright when the tool
80
+ definitions alone meet or exceed the whole window:
81
+
82
+ ```
83
+ [ContextGuardian] TOOL DEFINITIONS ALONE (2671) EXCEED THE ENTIRE CONTEXT
84
+ WINDOW (1000). Nothing this proxy does can fix that -- send fewer tools.
85
+ ```
86
+
87
+ If you see that, no proxy setting will help you. Most MCP-capable CLIs let you
88
+ scope which servers load per session — Claude Code and OpenClaude both accept
89
+ `--mcp-config <file>` together with `--strict-mcp-config`, which makes that file
90
+ the only source of MCP servers for the session.
91
+
92
+ One consequence worth expecting: **after upgrading, Guardian compacts sooner and
93
+ more often.** It is measuring the whole request now instead of a fraction of it.
94
+ If that feels aggressive, the honest reading is that your window was already
95
+ this full and you could not see it.
96
+
97
+ ## What this does *not* do
98
+
99
+ - **It doesn't replace or duplicate compression your backend already does** (e.g. Headroom, prompt caching). It forwards to your backend as-is once it's decided whether to compact first — the two are complementary, not competing.
100
+ - **It doesn't fix your CLI's own context-usage display.** Your CLI doesn't know this proxy exists, so its own token counter will drift from reality after a compaction happens. What matters is that the session keeps working instead of hard-stopping — a slightly-wrong displayed number afterward is an accepted tradeoff of doing this invisibly at the proxy layer, since the CLI itself usually isn't something you can modify.
101
+ - **It is not a tokenizer-accurate counter.** Token count is estimated from character length (~3.5 chars/token by default), not a real tokenizer, so it triggers a little early rather than late. Treat it as a safety-margin trigger, not a precise measurement.
102
+
103
+ ## Install
104
+
105
+ ```bash
106
+ git clone https://github.com/LuminariSoftwares/context-guardian.git
107
+ cd context-guardian
108
+ python -m venv .venv
109
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
110
+ pip install -r requirements.txt
111
+ ```
112
+
113
+ Then run `python configure.py` (see [Configure](#configure) below) before starting Guardian for the first time.
114
+
115
+ ## Configure
116
+
117
+ Run the interactive setup script instead of hand-editing a config file — it asks you a handful of questions about your specific hardware/backend (most importantly, your model's real context window) and writes the answers to `.env` for you:
118
+
119
+ ```bash
120
+ python configure.py
121
+ ```
122
+
123
+ Every question has a sensible default shown in `[brackets]` — press Enter to accept it. You can re-run `configure.py` any time to change your answers, or just edit `.env` directly afterward.
124
+
125
+ **The one setting that actually matters per-person is `GUARDIAN_NUM_CTX`.** This project was originally built and tested on a 16GB card (RTX 4070 Ti Super) running a model configured for a 32K context window — that number is specific to that hardware, not a universal default. Your correct value depends entirely on your own GPU/VRAM budget and which model you're running, so `configure.py` asks for it explicitly rather than silently assuming everyone's setup looks the same. If you're not sure what your real number is:
126
+
127
+ - **Ollama:** run `ollama ps` while your model is loaded — the `CONTEXT` column shows the live value actually in use (not necessarily the model's theoretical max).
128
+ - **LM Studio / vLLM / other servers:** check whatever context-length setting you configured when loading the model — Guardian has no way to auto-discover this, so it needs to match what you actually set.
129
+ - **If you're unsure or haven't set one explicitly:** start conservative (the `configure.py` default of 32768 is a reasonable, widely-safe starting point on a single consumer GPU) and raise it later once you've confirmed your backend can actually sustain it without running out of VRAM.
130
+
131
+ Setting this too high means Guardian won't compact soon enough and your backend can still hard-error before Guardian steps in. Setting it too low just means Guardian compacts a bit more often than strictly necessary — safe, just not optimal.
132
+
133
+ If you'd rather skip the wizard, copy `.env.example` to `.env` and edit it by hand:
134
+
135
+ ```bash
136
+ cp .env.example .env
137
+ ```
138
+
139
+ | Variable | Default | What it does |
140
+ |---|---|---|
141
+ | `GUARDIAN_PORT` | `8786` | Port Guardian itself listens on |
142
+ | `GUARDIAN_UPSTREAM_URL` | `http://localhost:11434/v1` | The OpenAI-compatible backend Guardian forwards to |
143
+ | `GUARDIAN_NUM_CTX` | `32768` | Your model's real context window, in tokens — keep this in sync with your actual backend/model config |
144
+ | `GUARDIAN_COMPACT_THRESHOLD` | `0.85` | Fraction of `GUARDIAN_NUM_CTX` at which compaction triggers |
145
+ | `GUARDIAN_KEEP_RECENT_MESSAGES` | `8` | Most-recent messages always kept verbatim, never summarized |
146
+ | `GUARDIAN_CHARS_PER_TOKEN` | `3.5` | Characters-per-token used for the estimate |
147
+ | `GUARDIAN_COUNT_TOOLS` | `1` | Count the `tools` array against the budget. Set `0` for pre-0.2.0 messages-only behaviour |
148
+ | `GUARDIAN_UPSTREAM_TIMEOUT` | `600` | Seconds to wait for the upstream backend to respond |
149
+ | `GUARDIAN_UPSTREAM_CONNECT_TIMEOUT` | `10` | Seconds to wait for the upstream connection itself |
150
+ | `GUARDIAN_LOG_PATH` | `<repo>/logs/context_guardian_log.json` | Where compaction events are logged (JSON lines) |
151
+ | `GUARDIAN_HOST` | `127.0.0.1` | Interface Guardian binds. **Leave this alone unless you know what you are doing** — Guardian fronts your backend with no authentication |
152
+ | `GUARDIAN_RESERVE_OUTPUT` | `8192` | Tokens held back for the model's *output*. The window has to hold the reply and (for reasoning models) the thinking too, so compaction triggers against what is LEFT. If this is ever ≥ `GUARDIAN_NUM_CTX` it is clamped to half the window and logged — fix the config |
153
+ | `GUARDIAN_SPAN_DIR` | `<repo>/logs/guardian_spans` | Where evicted messages are archived before folding. This is what makes compaction lossless on disk |
154
+ | `GUARDIAN_KEEP_SPANS` | `500` | How many span files to keep. `0` keeps none |
155
+ | `GUARDIAN_KEEP_SUMMARIES` | `1` | How many of Guardian's own previous summaries stay in the window. Retired ones are folded into the next span, not discarded |
156
+ | `GUARDIAN_MIN_SUMMARY_CHARS` | `40` | A summary shorter than this is treated as a FAILED summarisation and nothing is evicted. See 0.4.0 in the changelog for why this exists |
157
+ | `GUARDIAN_MIN_TRANSCRIPT_CHARS` | `80` | If the messages being evicted render to less than this, Guardian refuses to summarise rather than summarising nothing |
158
+ | `GUARDIAN_TOOL_ARG_CHARS` | `300` | How much of a tool call's arguments reaches the summariser. The full text is in the span |
159
+ | `GUARDIAN_SUMMARY_REASONING_EFFORT` | unset | Passed as `reasoning_effort` on the summarisation call only. Non-standard, so off by default; `low` roughly halved summarisation latency on gpt-oss |
160
+
161
+ **A note on timeouts:** local "thinking"/reasoning models can go silent for a long time before their first output token. If you see `500` errors appear only on real (non-trivial) requests after a long pause, raise `GUARDIAN_UPSTREAM_TIMEOUT` before assuming something is broken — the default 5-second timeout most HTTP clients ship with is sized for ordinary REST APIs, not local LLM inference, which is exactly the bug this project's own commit history caught during development.
162
+
163
+ ## Run
164
+
165
+ ```bash
166
+ python context_guardian.py
167
+ ```
168
+
169
+ Then point your CLI's `OPENAI_BASE_URL` at `http://localhost:8786/v1` (or whatever port you configured).
170
+
171
+ ## Testing before you trust it with a real session
172
+
173
+ 1. Start your real backend (Ollama, LiteLLM, Headroom, whatever you use) the way you normally would.
174
+ 2. Start Guardian: `python context_guardian.py`
175
+ 3. Send one manual request at it instead of your real CLI, to confirm plain passthrough works before testing compaction specifically:
176
+ ```bash
177
+ curl http://localhost:8786/v1/chat/completions \
178
+ -H "Content-Type: application/json" \
179
+ -d '{"model":"<your-model>","messages":[{"role":"user","content":"say hi"}]}'
180
+ ```
181
+ 4. Check `GET http://localhost:8786/guardian/stats` for the running token estimate and compaction count.
182
+ 5. Force a compaction test: temporarily set `GUARDIAN_NUM_CTX` and `GUARDIAN_COMPACT_THRESHOLD` low (e.g. `NUM_CTX=2000`, `THRESHOLD=0.5`), then send a conversation with several long messages. Confirm a compaction log entry appears at `GUARDIAN_LOG_PATH` and the request that actually reaches your backend is smaller than what was sent in.
183
+ 6. Only after that, point your CLI's `OPENAI_BASE_URL` at Guardian and test with a real session.
184
+
185
+ ## Running multiple models with different context windows
186
+
187
+ Guardian's `GUARDIAN_NUM_CTX` is fixed for the lifetime of one running instance. If you switch between models with meaningfully different context windows, either:
188
+
189
+ - run a second Guardian instance on a different `GUARDIAN_PORT` with its own `GUARDIAN_NUM_CTX`, or
190
+ - keep one instance and accept that its threshold is tuned to whichever model has the smaller/more-constrained window (safer than the alternative, since it just means Guardian compacts a bit earlier than strictly necessary for the larger-window model).
191
+
192
+ ## Development / running tests
193
+
194
+ ```bash
195
+ pip install -r requirements-dev.txt
196
+ pytest
197
+ ```
198
+
199
+ ## License
200
+
201
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,171 @@
1
+ # Context Guardian
2
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
3
+ ![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)
4
+
5
+ A tiny proxy that sits in front of any OpenAI-compatible LLM backend (Ollama, LiteLLM, Headroom, vLLM, LM Studio, and similar) and forces a conversation-history compaction *before* the context window fills up — instead of letting requests grow until the backend hard-errors and the session dies.
6
+
7
+ ## Why this exists
8
+
9
+ Claude-Code-style coding CLIs (Claude Code itself, and OpenAI-compatible-backend tools like OpenClaude) ship with a built-in auto-compact feature. That feature depends on accurate, real-time token-usage accounting coming back from the API in the exact shape the CLI expects. Point one of these tools at a local model through an OpenAI-compatible bridge — Ollama's `/v1` endpoint, a LiteLLM proxy, a Headroom proxy — and that accounting is frequently missing, wrong, or shaped differently, so auto-compact silently never fires.
10
+
11
+ The visible symptom: the session just runs until the backend hard-rejects the request ("token limit reached"), you're forced to close and reopen, and there's no partial-compaction attempt in between — you just lose your place.
12
+
13
+ Context Guardian is a small, deliberately simple fallback for that specific gap. It estimates the running token count itself, and once a conversation crosses a configurable threshold, it asks the *same backend* to condense the older portion of the conversation into one summary message before forwarding the request onward. Recent messages are always kept verbatim. If the summarization call itself fails, Guardian fails **open** — it forwards the original, uncompacted request rather than risk silently dropping history.
14
+
15
+ ## Where it sits in your stack
16
+
17
+ This is a new link in an existing chain, not a replacement for anything you already have:
18
+
19
+ ```
20
+ Your CLI / agent (Claude Code, OpenClaude, etc.)
21
+ -> Context Guardian (this project)
22
+ -> your existing OpenAI-compatible backend
23
+ (Ollama directly, a LiteLLM proxy, Headroom, vLLM, ...)
24
+ ```
25
+
26
+ Point your CLI's `OPENAI_BASE_URL` at Context Guardian instead of directly at your backend, and set `GUARDIAN_UPSTREAM_URL` to wherever your backend actually lives. Guardian is a pure passthrough for everything except `POST /v1/chat/completions`, which gets the compaction check — every other route, including streaming responses, is forwarded byte-for-byte, untouched.
27
+
28
+ ## If you use MCP servers or an agentic CLI, read this
29
+
30
+ Guardian counts your `tools` array against the context budget. It did not
31
+ before 0.2.0, and that was a real bug — see the changelog.
32
+
33
+ Tool definitions are usually invisible in a way message history is not. You do
34
+ not type them, they do not scroll past, and your CLI's context display often
35
+ does not break them out. But they are in every single request. On the setup this
36
+ was developed against, seven MCP servers came to **28,689 tokens — 87.6% of a
37
+ 32,768-token window** — before the first user message.
38
+
39
+ **Guardian cannot compact them.** It summarizes conversation history; tool
40
+ definitions are a fixed floor underneath it. So there are two different problems
41
+ and only one of them is Guardian's:
42
+
43
+ | Problem | What fixes it |
44
+ |---|---|
45
+ | Conversation history grows until the window fills | Guardian |
46
+ | Two thirds of the window is gone before you type | Loading fewer tools |
47
+
48
+ Guardian will now tell you which one you have. It logs a `tool_budget` event the
49
+ first time it sees a given tool payload, and warns outright when the tool
50
+ definitions alone meet or exceed the whole window:
51
+
52
+ ```
53
+ [ContextGuardian] TOOL DEFINITIONS ALONE (2671) EXCEED THE ENTIRE CONTEXT
54
+ WINDOW (1000). Nothing this proxy does can fix that -- send fewer tools.
55
+ ```
56
+
57
+ If you see that, no proxy setting will help you. Most MCP-capable CLIs let you
58
+ scope which servers load per session — Claude Code and OpenClaude both accept
59
+ `--mcp-config <file>` together with `--strict-mcp-config`, which makes that file
60
+ the only source of MCP servers for the session.
61
+
62
+ One consequence worth expecting: **after upgrading, Guardian compacts sooner and
63
+ more often.** It is measuring the whole request now instead of a fraction of it.
64
+ If that feels aggressive, the honest reading is that your window was already
65
+ this full and you could not see it.
66
+
67
+ ## What this does *not* do
68
+
69
+ - **It doesn't replace or duplicate compression your backend already does** (e.g. Headroom, prompt caching). It forwards to your backend as-is once it's decided whether to compact first — the two are complementary, not competing.
70
+ - **It doesn't fix your CLI's own context-usage display.** Your CLI doesn't know this proxy exists, so its own token counter will drift from reality after a compaction happens. What matters is that the session keeps working instead of hard-stopping — a slightly-wrong displayed number afterward is an accepted tradeoff of doing this invisibly at the proxy layer, since the CLI itself usually isn't something you can modify.
71
+ - **It is not a tokenizer-accurate counter.** Token count is estimated from character length (~3.5 chars/token by default), not a real tokenizer, so it triggers a little early rather than late. Treat it as a safety-margin trigger, not a precise measurement.
72
+
73
+ ## Install
74
+
75
+ ```bash
76
+ git clone https://github.com/LuminariSoftwares/context-guardian.git
77
+ cd context-guardian
78
+ python -m venv .venv
79
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
80
+ pip install -r requirements.txt
81
+ ```
82
+
83
+ Then run `python configure.py` (see [Configure](#configure) below) before starting Guardian for the first time.
84
+
85
+ ## Configure
86
+
87
+ Run the interactive setup script instead of hand-editing a config file — it asks you a handful of questions about your specific hardware/backend (most importantly, your model's real context window) and writes the answers to `.env` for you:
88
+
89
+ ```bash
90
+ python configure.py
91
+ ```
92
+
93
+ Every question has a sensible default shown in `[brackets]` — press Enter to accept it. You can re-run `configure.py` any time to change your answers, or just edit `.env` directly afterward.
94
+
95
+ **The one setting that actually matters per-person is `GUARDIAN_NUM_CTX`.** This project was originally built and tested on a 16GB card (RTX 4070 Ti Super) running a model configured for a 32K context window — that number is specific to that hardware, not a universal default. Your correct value depends entirely on your own GPU/VRAM budget and which model you're running, so `configure.py` asks for it explicitly rather than silently assuming everyone's setup looks the same. If you're not sure what your real number is:
96
+
97
+ - **Ollama:** run `ollama ps` while your model is loaded — the `CONTEXT` column shows the live value actually in use (not necessarily the model's theoretical max).
98
+ - **LM Studio / vLLM / other servers:** check whatever context-length setting you configured when loading the model — Guardian has no way to auto-discover this, so it needs to match what you actually set.
99
+ - **If you're unsure or haven't set one explicitly:** start conservative (the `configure.py` default of 32768 is a reasonable, widely-safe starting point on a single consumer GPU) and raise it later once you've confirmed your backend can actually sustain it without running out of VRAM.
100
+
101
+ Setting this too high means Guardian won't compact soon enough and your backend can still hard-error before Guardian steps in. Setting it too low just means Guardian compacts a bit more often than strictly necessary — safe, just not optimal.
102
+
103
+ If you'd rather skip the wizard, copy `.env.example` to `.env` and edit it by hand:
104
+
105
+ ```bash
106
+ cp .env.example .env
107
+ ```
108
+
109
+ | Variable | Default | What it does |
110
+ |---|---|---|
111
+ | `GUARDIAN_PORT` | `8786` | Port Guardian itself listens on |
112
+ | `GUARDIAN_UPSTREAM_URL` | `http://localhost:11434/v1` | The OpenAI-compatible backend Guardian forwards to |
113
+ | `GUARDIAN_NUM_CTX` | `32768` | Your model's real context window, in tokens — keep this in sync with your actual backend/model config |
114
+ | `GUARDIAN_COMPACT_THRESHOLD` | `0.85` | Fraction of `GUARDIAN_NUM_CTX` at which compaction triggers |
115
+ | `GUARDIAN_KEEP_RECENT_MESSAGES` | `8` | Most-recent messages always kept verbatim, never summarized |
116
+ | `GUARDIAN_CHARS_PER_TOKEN` | `3.5` | Characters-per-token used for the estimate |
117
+ | `GUARDIAN_COUNT_TOOLS` | `1` | Count the `tools` array against the budget. Set `0` for pre-0.2.0 messages-only behaviour |
118
+ | `GUARDIAN_UPSTREAM_TIMEOUT` | `600` | Seconds to wait for the upstream backend to respond |
119
+ | `GUARDIAN_UPSTREAM_CONNECT_TIMEOUT` | `10` | Seconds to wait for the upstream connection itself |
120
+ | `GUARDIAN_LOG_PATH` | `<repo>/logs/context_guardian_log.json` | Where compaction events are logged (JSON lines) |
121
+ | `GUARDIAN_HOST` | `127.0.0.1` | Interface Guardian binds. **Leave this alone unless you know what you are doing** — Guardian fronts your backend with no authentication |
122
+ | `GUARDIAN_RESERVE_OUTPUT` | `8192` | Tokens held back for the model's *output*. The window has to hold the reply and (for reasoning models) the thinking too, so compaction triggers against what is LEFT. If this is ever ≥ `GUARDIAN_NUM_CTX` it is clamped to half the window and logged — fix the config |
123
+ | `GUARDIAN_SPAN_DIR` | `<repo>/logs/guardian_spans` | Where evicted messages are archived before folding. This is what makes compaction lossless on disk |
124
+ | `GUARDIAN_KEEP_SPANS` | `500` | How many span files to keep. `0` keeps none |
125
+ | `GUARDIAN_KEEP_SUMMARIES` | `1` | How many of Guardian's own previous summaries stay in the window. Retired ones are folded into the next span, not discarded |
126
+ | `GUARDIAN_MIN_SUMMARY_CHARS` | `40` | A summary shorter than this is treated as a FAILED summarisation and nothing is evicted. See 0.4.0 in the changelog for why this exists |
127
+ | `GUARDIAN_MIN_TRANSCRIPT_CHARS` | `80` | If the messages being evicted render to less than this, Guardian refuses to summarise rather than summarising nothing |
128
+ | `GUARDIAN_TOOL_ARG_CHARS` | `300` | How much of a tool call's arguments reaches the summariser. The full text is in the span |
129
+ | `GUARDIAN_SUMMARY_REASONING_EFFORT` | unset | Passed as `reasoning_effort` on the summarisation call only. Non-standard, so off by default; `low` roughly halved summarisation latency on gpt-oss |
130
+
131
+ **A note on timeouts:** local "thinking"/reasoning models can go silent for a long time before their first output token. If you see `500` errors appear only on real (non-trivial) requests after a long pause, raise `GUARDIAN_UPSTREAM_TIMEOUT` before assuming something is broken — the default 5-second timeout most HTTP clients ship with is sized for ordinary REST APIs, not local LLM inference, which is exactly the bug this project's own commit history caught during development.
132
+
133
+ ## Run
134
+
135
+ ```bash
136
+ python context_guardian.py
137
+ ```
138
+
139
+ Then point your CLI's `OPENAI_BASE_URL` at `http://localhost:8786/v1` (or whatever port you configured).
140
+
141
+ ## Testing before you trust it with a real session
142
+
143
+ 1. Start your real backend (Ollama, LiteLLM, Headroom, whatever you use) the way you normally would.
144
+ 2. Start Guardian: `python context_guardian.py`
145
+ 3. Send one manual request at it instead of your real CLI, to confirm plain passthrough works before testing compaction specifically:
146
+ ```bash
147
+ curl http://localhost:8786/v1/chat/completions \
148
+ -H "Content-Type: application/json" \
149
+ -d '{"model":"<your-model>","messages":[{"role":"user","content":"say hi"}]}'
150
+ ```
151
+ 4. Check `GET http://localhost:8786/guardian/stats` for the running token estimate and compaction count.
152
+ 5. Force a compaction test: temporarily set `GUARDIAN_NUM_CTX` and `GUARDIAN_COMPACT_THRESHOLD` low (e.g. `NUM_CTX=2000`, `THRESHOLD=0.5`), then send a conversation with several long messages. Confirm a compaction log entry appears at `GUARDIAN_LOG_PATH` and the request that actually reaches your backend is smaller than what was sent in.
153
+ 6. Only after that, point your CLI's `OPENAI_BASE_URL` at Guardian and test with a real session.
154
+
155
+ ## Running multiple models with different context windows
156
+
157
+ Guardian's `GUARDIAN_NUM_CTX` is fixed for the lifetime of one running instance. If you switch between models with meaningfully different context windows, either:
158
+
159
+ - run a second Guardian instance on a different `GUARDIAN_PORT` with its own `GUARDIAN_NUM_CTX`, or
160
+ - keep one instance and accept that its threshold is tuned to whichever model has the smaller/more-constrained window (safer than the alternative, since it just means Guardian compacts a bit earlier than strictly necessary for the larger-window model).
161
+
162
+ ## Development / running tests
163
+
164
+ ```bash
165
+ pip install -r requirements-dev.txt
166
+ pytest
167
+ ```
168
+
169
+ ## License
170
+
171
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: context-guardian
3
+ Version: 0.4.0
4
+ Summary: A tiny proxy in front of any OpenAI-compatible LLM backend that compacts conversation history before the context window fills, so local-model coding CLIs don't hard-die from overflow.
5
+ Author: LuminariSoftwares
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/LuminariSoftwares/context-guardian
8
+ Project-URL: Repository, https://github.com/LuminariSoftwares/context-guardian
9
+ Project-URL: Changelog, https://github.com/LuminariSoftwares/context-guardian/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/LuminariSoftwares/context-guardian/issues
11
+ Keywords: ollama,local-llm,llm,openai-api,proxy,context-window,context-management,litellm,mcp,coding-assistant
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: fastapi>=0.110
26
+ Requires-Dist: uvicorn[standard]>=0.29
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: python-dotenv>=1.0
29
+ Dynamic: license-file
30
+
31
+ # Context Guardian
32
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
33
+ ![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)
34
+
35
+ A tiny proxy that sits in front of any OpenAI-compatible LLM backend (Ollama, LiteLLM, Headroom, vLLM, LM Studio, and similar) and forces a conversation-history compaction *before* the context window fills up — instead of letting requests grow until the backend hard-errors and the session dies.
36
+
37
+ ## Why this exists
38
+
39
+ Claude-Code-style coding CLIs (Claude Code itself, and OpenAI-compatible-backend tools like OpenClaude) ship with a built-in auto-compact feature. That feature depends on accurate, real-time token-usage accounting coming back from the API in the exact shape the CLI expects. Point one of these tools at a local model through an OpenAI-compatible bridge — Ollama's `/v1` endpoint, a LiteLLM proxy, a Headroom proxy — and that accounting is frequently missing, wrong, or shaped differently, so auto-compact silently never fires.
40
+
41
+ The visible symptom: the session just runs until the backend hard-rejects the request ("token limit reached"), you're forced to close and reopen, and there's no partial-compaction attempt in between — you just lose your place.
42
+
43
+ Context Guardian is a small, deliberately simple fallback for that specific gap. It estimates the running token count itself, and once a conversation crosses a configurable threshold, it asks the *same backend* to condense the older portion of the conversation into one summary message before forwarding the request onward. Recent messages are always kept verbatim. If the summarization call itself fails, Guardian fails **open** — it forwards the original, uncompacted request rather than risk silently dropping history.
44
+
45
+ ## Where it sits in your stack
46
+
47
+ This is a new link in an existing chain, not a replacement for anything you already have:
48
+
49
+ ```
50
+ Your CLI / agent (Claude Code, OpenClaude, etc.)
51
+ -> Context Guardian (this project)
52
+ -> your existing OpenAI-compatible backend
53
+ (Ollama directly, a LiteLLM proxy, Headroom, vLLM, ...)
54
+ ```
55
+
56
+ Point your CLI's `OPENAI_BASE_URL` at Context Guardian instead of directly at your backend, and set `GUARDIAN_UPSTREAM_URL` to wherever your backend actually lives. Guardian is a pure passthrough for everything except `POST /v1/chat/completions`, which gets the compaction check — every other route, including streaming responses, is forwarded byte-for-byte, untouched.
57
+
58
+ ## If you use MCP servers or an agentic CLI, read this
59
+
60
+ Guardian counts your `tools` array against the context budget. It did not
61
+ before 0.2.0, and that was a real bug — see the changelog.
62
+
63
+ Tool definitions are usually invisible in a way message history is not. You do
64
+ not type them, they do not scroll past, and your CLI's context display often
65
+ does not break them out. But they are in every single request. On the setup this
66
+ was developed against, seven MCP servers came to **28,689 tokens — 87.6% of a
67
+ 32,768-token window** — before the first user message.
68
+
69
+ **Guardian cannot compact them.** It summarizes conversation history; tool
70
+ definitions are a fixed floor underneath it. So there are two different problems
71
+ and only one of them is Guardian's:
72
+
73
+ | Problem | What fixes it |
74
+ |---|---|
75
+ | Conversation history grows until the window fills | Guardian |
76
+ | Two thirds of the window is gone before you type | Loading fewer tools |
77
+
78
+ Guardian will now tell you which one you have. It logs a `tool_budget` event the
79
+ first time it sees a given tool payload, and warns outright when the tool
80
+ definitions alone meet or exceed the whole window:
81
+
82
+ ```
83
+ [ContextGuardian] TOOL DEFINITIONS ALONE (2671) EXCEED THE ENTIRE CONTEXT
84
+ WINDOW (1000). Nothing this proxy does can fix that -- send fewer tools.
85
+ ```
86
+
87
+ If you see that, no proxy setting will help you. Most MCP-capable CLIs let you
88
+ scope which servers load per session — Claude Code and OpenClaude both accept
89
+ `--mcp-config <file>` together with `--strict-mcp-config`, which makes that file
90
+ the only source of MCP servers for the session.
91
+
92
+ One consequence worth expecting: **after upgrading, Guardian compacts sooner and
93
+ more often.** It is measuring the whole request now instead of a fraction of it.
94
+ If that feels aggressive, the honest reading is that your window was already
95
+ this full and you could not see it.
96
+
97
+ ## What this does *not* do
98
+
99
+ - **It doesn't replace or duplicate compression your backend already does** (e.g. Headroom, prompt caching). It forwards to your backend as-is once it's decided whether to compact first — the two are complementary, not competing.
100
+ - **It doesn't fix your CLI's own context-usage display.** Your CLI doesn't know this proxy exists, so its own token counter will drift from reality after a compaction happens. What matters is that the session keeps working instead of hard-stopping — a slightly-wrong displayed number afterward is an accepted tradeoff of doing this invisibly at the proxy layer, since the CLI itself usually isn't something you can modify.
101
+ - **It is not a tokenizer-accurate counter.** Token count is estimated from character length (~3.5 chars/token by default), not a real tokenizer, so it triggers a little early rather than late. Treat it as a safety-margin trigger, not a precise measurement.
102
+
103
+ ## Install
104
+
105
+ ```bash
106
+ git clone https://github.com/LuminariSoftwares/context-guardian.git
107
+ cd context-guardian
108
+ python -m venv .venv
109
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
110
+ pip install -r requirements.txt
111
+ ```
112
+
113
+ Then run `python configure.py` (see [Configure](#configure) below) before starting Guardian for the first time.
114
+
115
+ ## Configure
116
+
117
+ Run the interactive setup script instead of hand-editing a config file — it asks you a handful of questions about your specific hardware/backend (most importantly, your model's real context window) and writes the answers to `.env` for you:
118
+
119
+ ```bash
120
+ python configure.py
121
+ ```
122
+
123
+ Every question has a sensible default shown in `[brackets]` — press Enter to accept it. You can re-run `configure.py` any time to change your answers, or just edit `.env` directly afterward.
124
+
125
+ **The one setting that actually matters per-person is `GUARDIAN_NUM_CTX`.** This project was originally built and tested on a 16GB card (RTX 4070 Ti Super) running a model configured for a 32K context window — that number is specific to that hardware, not a universal default. Your correct value depends entirely on your own GPU/VRAM budget and which model you're running, so `configure.py` asks for it explicitly rather than silently assuming everyone's setup looks the same. If you're not sure what your real number is:
126
+
127
+ - **Ollama:** run `ollama ps` while your model is loaded — the `CONTEXT` column shows the live value actually in use (not necessarily the model's theoretical max).
128
+ - **LM Studio / vLLM / other servers:** check whatever context-length setting you configured when loading the model — Guardian has no way to auto-discover this, so it needs to match what you actually set.
129
+ - **If you're unsure or haven't set one explicitly:** start conservative (the `configure.py` default of 32768 is a reasonable, widely-safe starting point on a single consumer GPU) and raise it later once you've confirmed your backend can actually sustain it without running out of VRAM.
130
+
131
+ Setting this too high means Guardian won't compact soon enough and your backend can still hard-error before Guardian steps in. Setting it too low just means Guardian compacts a bit more often than strictly necessary — safe, just not optimal.
132
+
133
+ If you'd rather skip the wizard, copy `.env.example` to `.env` and edit it by hand:
134
+
135
+ ```bash
136
+ cp .env.example .env
137
+ ```
138
+
139
+ | Variable | Default | What it does |
140
+ |---|---|---|
141
+ | `GUARDIAN_PORT` | `8786` | Port Guardian itself listens on |
142
+ | `GUARDIAN_UPSTREAM_URL` | `http://localhost:11434/v1` | The OpenAI-compatible backend Guardian forwards to |
143
+ | `GUARDIAN_NUM_CTX` | `32768` | Your model's real context window, in tokens — keep this in sync with your actual backend/model config |
144
+ | `GUARDIAN_COMPACT_THRESHOLD` | `0.85` | Fraction of `GUARDIAN_NUM_CTX` at which compaction triggers |
145
+ | `GUARDIAN_KEEP_RECENT_MESSAGES` | `8` | Most-recent messages always kept verbatim, never summarized |
146
+ | `GUARDIAN_CHARS_PER_TOKEN` | `3.5` | Characters-per-token used for the estimate |
147
+ | `GUARDIAN_COUNT_TOOLS` | `1` | Count the `tools` array against the budget. Set `0` for pre-0.2.0 messages-only behaviour |
148
+ | `GUARDIAN_UPSTREAM_TIMEOUT` | `600` | Seconds to wait for the upstream backend to respond |
149
+ | `GUARDIAN_UPSTREAM_CONNECT_TIMEOUT` | `10` | Seconds to wait for the upstream connection itself |
150
+ | `GUARDIAN_LOG_PATH` | `<repo>/logs/context_guardian_log.json` | Where compaction events are logged (JSON lines) |
151
+ | `GUARDIAN_HOST` | `127.0.0.1` | Interface Guardian binds. **Leave this alone unless you know what you are doing** — Guardian fronts your backend with no authentication |
152
+ | `GUARDIAN_RESERVE_OUTPUT` | `8192` | Tokens held back for the model's *output*. The window has to hold the reply and (for reasoning models) the thinking too, so compaction triggers against what is LEFT. If this is ever ≥ `GUARDIAN_NUM_CTX` it is clamped to half the window and logged — fix the config |
153
+ | `GUARDIAN_SPAN_DIR` | `<repo>/logs/guardian_spans` | Where evicted messages are archived before folding. This is what makes compaction lossless on disk |
154
+ | `GUARDIAN_KEEP_SPANS` | `500` | How many span files to keep. `0` keeps none |
155
+ | `GUARDIAN_KEEP_SUMMARIES` | `1` | How many of Guardian's own previous summaries stay in the window. Retired ones are folded into the next span, not discarded |
156
+ | `GUARDIAN_MIN_SUMMARY_CHARS` | `40` | A summary shorter than this is treated as a FAILED summarisation and nothing is evicted. See 0.4.0 in the changelog for why this exists |
157
+ | `GUARDIAN_MIN_TRANSCRIPT_CHARS` | `80` | If the messages being evicted render to less than this, Guardian refuses to summarise rather than summarising nothing |
158
+ | `GUARDIAN_TOOL_ARG_CHARS` | `300` | How much of a tool call's arguments reaches the summariser. The full text is in the span |
159
+ | `GUARDIAN_SUMMARY_REASONING_EFFORT` | unset | Passed as `reasoning_effort` on the summarisation call only. Non-standard, so off by default; `low` roughly halved summarisation latency on gpt-oss |
160
+
161
+ **A note on timeouts:** local "thinking"/reasoning models can go silent for a long time before their first output token. If you see `500` errors appear only on real (non-trivial) requests after a long pause, raise `GUARDIAN_UPSTREAM_TIMEOUT` before assuming something is broken — the default 5-second timeout most HTTP clients ship with is sized for ordinary REST APIs, not local LLM inference, which is exactly the bug this project's own commit history caught during development.
162
+
163
+ ## Run
164
+
165
+ ```bash
166
+ python context_guardian.py
167
+ ```
168
+
169
+ Then point your CLI's `OPENAI_BASE_URL` at `http://localhost:8786/v1` (or whatever port you configured).
170
+
171
+ ## Testing before you trust it with a real session
172
+
173
+ 1. Start your real backend (Ollama, LiteLLM, Headroom, whatever you use) the way you normally would.
174
+ 2. Start Guardian: `python context_guardian.py`
175
+ 3. Send one manual request at it instead of your real CLI, to confirm plain passthrough works before testing compaction specifically:
176
+ ```bash
177
+ curl http://localhost:8786/v1/chat/completions \
178
+ -H "Content-Type: application/json" \
179
+ -d '{"model":"<your-model>","messages":[{"role":"user","content":"say hi"}]}'
180
+ ```
181
+ 4. Check `GET http://localhost:8786/guardian/stats` for the running token estimate and compaction count.
182
+ 5. Force a compaction test: temporarily set `GUARDIAN_NUM_CTX` and `GUARDIAN_COMPACT_THRESHOLD` low (e.g. `NUM_CTX=2000`, `THRESHOLD=0.5`), then send a conversation with several long messages. Confirm a compaction log entry appears at `GUARDIAN_LOG_PATH` and the request that actually reaches your backend is smaller than what was sent in.
183
+ 6. Only after that, point your CLI's `OPENAI_BASE_URL` at Guardian and test with a real session.
184
+
185
+ ## Running multiple models with different context windows
186
+
187
+ Guardian's `GUARDIAN_NUM_CTX` is fixed for the lifetime of one running instance. If you switch between models with meaningfully different context windows, either:
188
+
189
+ - run a second Guardian instance on a different `GUARDIAN_PORT` with its own `GUARDIAN_NUM_CTX`, or
190
+ - keep one instance and accept that its threshold is tuned to whichever model has the smaller/more-constrained window (safer than the alternative, since it just means Guardian compacts a bit earlier than strictly necessary for the larger-window model).
191
+
192
+ ## Development / running tests
193
+
194
+ ```bash
195
+ pip install -r requirements-dev.txt
196
+ pytest
197
+ ```
198
+
199
+ ## License
200
+
201
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ context_guardian.py
4
+ pyproject.toml
5
+ context_guardian.egg-info/PKG-INFO
6
+ context_guardian.egg-info/SOURCES.txt
7
+ context_guardian.egg-info/dependency_links.txt
8
+ context_guardian.egg-info/entry_points.txt
9
+ context_guardian.egg-info/requires.txt
10
+ context_guardian.egg-info/top_level.txt
11
+ tests/test_guardian.py
12
+ tests/test_spans.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ context-guardian = context_guardian:main
@@ -0,0 +1,4 @@
1
+ fastapi>=0.110
2
+ uvicorn[standard]>=0.29
3
+ httpx>=0.27
4
+ python-dotenv>=1.0
@@ -0,0 +1 @@
1
+ context_guardian