ctxoptimizer 0.4.1__py3-none-any.whl

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 (83) hide show
  1. ctxoptimizer/__init__.py +5 -0
  2. ctxoptimizer/__main__.py +14 -0
  3. ctxoptimizer/admin_security.py +130 -0
  4. ctxoptimizer/analysis/__init__.py +1 -0
  5. ctxoptimizer/analysis/baseline.py +69 -0
  6. ctxoptimizer/analysis/break_even.py +49 -0
  7. ctxoptimizer/analysis/drift.py +151 -0
  8. ctxoptimizer/analysis/edits.py +148 -0
  9. ctxoptimizer/analysis/markers.py +34 -0
  10. ctxoptimizer/analysis/recommend.py +114 -0
  11. ctxoptimizer/analysis/segments.py +57 -0
  12. ctxoptimizer/analysis/session.py +62 -0
  13. ctxoptimizer/analysis/state.py +239 -0
  14. ctxoptimizer/analysis/tokens.py +62 -0
  15. ctxoptimizer/analysis/waste.py +322 -0
  16. ctxoptimizer/cli.py +873 -0
  17. ctxoptimizer/config.py +512 -0
  18. ctxoptimizer/dashboard/__init__.py +1 -0
  19. ctxoptimizer/dashboard/api.py +537 -0
  20. ctxoptimizer/dashboard/service.py +157 -0
  21. ctxoptimizer/dashboard/static/.gitkeep +1 -0
  22. ctxoptimizer/dashboard/templates/.gitkeep +1 -0
  23. ctxoptimizer/dashboard/templates/doctor.html +46 -0
  24. ctxoptimizer/dashboard/templates/live.html +91 -0
  25. ctxoptimizer/dashboard/templates/overview.html +172 -0
  26. ctxoptimizer/dashboard/templates/pricing.html +115 -0
  27. ctxoptimizer/dashboard/templates/request.html +111 -0
  28. ctxoptimizer/dashboard/templates/session.html +67 -0
  29. ctxoptimizer/dashboard/templates/settings.html +79 -0
  30. ctxoptimizer/dashboard/templates/table.html +56 -0
  31. ctxoptimizer/dashboard/templates/transforms.html +118 -0
  32. ctxoptimizer/dashboard/templates/waste.html +120 -0
  33. ctxoptimizer/dashboard/views.py +429 -0
  34. ctxoptimizer/data/cache_limits.json +23 -0
  35. ctxoptimizer/data/default_config.toml +185 -0
  36. ctxoptimizer/data/pricing.json +421 -0
  37. ctxoptimizer/doctor.py +129 -0
  38. ctxoptimizer/guard.py +139 -0
  39. ctxoptimizer/ids.py +73 -0
  40. ctxoptimizer/log.py +73 -0
  41. ctxoptimizer/mcp.py +124 -0
  42. ctxoptimizer/models.py +145 -0
  43. ctxoptimizer/pipeline.py +126 -0
  44. ctxoptimizer/profiles.py +152 -0
  45. ctxoptimizer/providers/__init__.py +37 -0
  46. ctxoptimizer/providers/anthropic.py +105 -0
  47. ctxoptimizer/providers/base.py +191 -0
  48. ctxoptimizer/providers/gemini.py +143 -0
  49. ctxoptimizer/providers/ollama.py +133 -0
  50. ctxoptimizer/providers/openai_chat.py +135 -0
  51. ctxoptimizer/providers/openai_responses.py +121 -0
  52. ctxoptimizer/proxy.py +665 -0
  53. ctxoptimizer/routing.py +259 -0
  54. ctxoptimizer/server.py +319 -0
  55. ctxoptimizer/service.py +150 -0
  56. ctxoptimizer/telemetry/__init__.py +1 -0
  57. ctxoptimizer/telemetry/blobs.py +57 -0
  58. ctxoptimizer/telemetry/expand.py +62 -0
  59. ctxoptimizer/telemetry/pricing.py +168 -0
  60. ctxoptimizer/telemetry/redact.py +53 -0
  61. ctxoptimizer/telemetry/sse.py +145 -0
  62. ctxoptimizer/telemetry/store.py +829 -0
  63. ctxoptimizer/telemetry/usage.py +435 -0
  64. ctxoptimizer/transforms/__init__.py +87 -0
  65. ctxoptimizer/transforms/ansi.py +21 -0
  66. ctxoptimizer/transforms/anthropic_cache.py +100 -0
  67. ctxoptimizer/transforms/backref.py +74 -0
  68. ctxoptimizer/transforms/base.py +93 -0
  69. ctxoptimizer/transforms/cache_serve.py +146 -0
  70. ctxoptimizer/transforms/dedupe_lines.py +69 -0
  71. ctxoptimizer/transforms/eligibility.py +24 -0
  72. ctxoptimizer/transforms/openai_hints.py +84 -0
  73. ctxoptimizer/transforms/progress.py +52 -0
  74. ctxoptimizer/transforms/stale_result_elision.py +109 -0
  75. ctxoptimizer/transforms/truncate.py +100 -0
  76. ctxoptimizer/transforms/volatile.py +177 -0
  77. ctxoptimizer/transforms/whitespace.py +25 -0
  78. ctxoptimizer-0.4.1.dist-info/METADATA +196 -0
  79. ctxoptimizer-0.4.1.dist-info/RECORD +83 -0
  80. ctxoptimizer-0.4.1.dist-info/WHEEL +4 -0
  81. ctxoptimizer-0.4.1.dist-info/entry_points.txt +3 -0
  82. ctxoptimizer-0.4.1.dist-info/licenses/LICENSE +21 -0
  83. ctxoptimizer-0.4.1.dist-info/licenses/NOTICE +13 -0
@@ -0,0 +1,100 @@
1
+ """Error-aware truncation with signal windows (SPEC section 12.5)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from hashlib import sha256
7
+
8
+ from ctxoptimizer.config import SmartTruncateConfig
9
+
10
+ SIGNAL = re.compile(
11
+ r"error|fail(?:ed|ure)?|exception|traceback|panic|assert(?:ion)?|fatal|"
12
+ r"segfault|cannot|can't|undefined|not found|denied|timeout|"
13
+ r"exit (?:code|status)|npm ERR!|^\s*E\s{3}\S|^FAILED|^ERROR|"
14
+ r"error\[E\d+\]|^\s*File \".*\", line \d+|\S+\.\w+:\d+(?::\d+)?:",
15
+ re.IGNORECASE | re.MULTILINE,
16
+ )
17
+ SUMMARY = re.compile(
18
+ r"\d+ (?:passed|failed|errors?|warnings?)|Tests?:|Test Suites:|"
19
+ r"Ran \d+ tests|BUILD (?:SUCCESS|FAILURE)|Finished|Compiling .* in",
20
+ re.IGNORECASE,
21
+ )
22
+ BASE64 = re.compile(r"[A-Za-z0-9+/=]{1000,}")
23
+
24
+
25
+ def clean(
26
+ text: str,
27
+ cfg: SmartTruncateConfig,
28
+ *,
29
+ is_error: bool = False,
30
+ ) -> str:
31
+ """Keep head, tail, signal windows and summaries; marker every gap."""
32
+ if "\x00" in text:
33
+ return text
34
+ if len(text.encode("utf-8")) <= cfg.trigger_bytes:
35
+ return text
36
+ ref = sha256(text.encode("utf-8")).hexdigest()[:8]
37
+ lines = [_prepare_line(line, cfg) for line in text.split("\n")]
38
+ head = min(cfg.head_lines, len(lines))
39
+ tail_start = max(0, len(lines) - cfg.tail_lines)
40
+ keep: set[int] = set(range(head)) | set(range(tail_start, len(lines)))
41
+
42
+ signal_limit = cfg.max_signal_lines * (2 if is_error else 1)
43
+ signal_seen = 0
44
+ for index, line in enumerate(lines):
45
+ if not (SIGNAL.search(line) or SUMMARY.search(line)):
46
+ continue
47
+ if signal_seen >= signal_limit:
48
+ break
49
+ signal_seen += 1
50
+ for neighbour in range(
51
+ max(0, index - cfg.context_lines),
52
+ min(len(lines), index + cfg.context_lines + 1),
53
+ ):
54
+ keep.add(neighbour)
55
+
56
+ output = _render(lines, keep, ref)
57
+ if len(output.encode("utf-8")) > cfg.target_bytes * 1.5:
58
+ keep = {index for index in keep if index < head or index >= tail_start}
59
+ output = _render(lines, keep, ref)
60
+ if len(output.encode("utf-8")) > cfg.target_bytes * 1.5:
61
+ keep = {
62
+ index
63
+ for index in keep
64
+ if index < max(1, head // 2)
65
+ or index >= min(len(lines), len(lines) - max(1, cfg.tail_lines // 2))
66
+ }
67
+ output = _render(lines, keep, ref)
68
+ if len(output.encode("utf-8")) > cfg.target_bytes * 1.5:
69
+ output = output.encode("utf-8")[: int(cfg.target_bytes * 1.5)].decode("utf-8", "ignore")
70
+ return output
71
+
72
+
73
+ def _prepare_line(line: str, cfg: SmartTruncateConfig) -> str:
74
+ if len(line) > cfg.max_line_chars:
75
+ line = line[: cfg.max_line_chars] + f" [ctxoptimizer: line truncated, {len(line)} chars]"
76
+ return BASE64.sub(
77
+ lambda match: f"[ctxoptimizer: base64 blob, {len(match.group(0))} chars]", line
78
+ )
79
+
80
+
81
+ def _render(lines: list[str], keep: set[int], ref: str) -> str:
82
+ result: list[str] = []
83
+ index = 0
84
+ total = len(lines)
85
+ while index < total:
86
+ if index in keep:
87
+ result.append(lines[index])
88
+ index += 1
89
+ continue
90
+ end = index
91
+ while end < total and end not in keep:
92
+ end += 1
93
+ omitted = end - index
94
+ size_kb = sum(len(lines[i].encode("utf-8")) for i in range(index, end)) / 1024
95
+ result.append(f"[ctxoptimizer: {omitted} lines ({size_kb:.1f} KB) omitted; ref={ref}]")
96
+ index = end
97
+ return "\n".join(result)
98
+
99
+
100
+ __all__ = ["clean"]
@@ -0,0 +1,177 @@
1
+ """Experimental volatile-token normalization (SPEC section 12.11).
2
+
3
+ Opt-in only; it deliberately mutates system text (INV-9). Time values are
4
+ reduced to day granularity, UUIDs are replaced, and epoch seconds are removed
5
+ only inside ``<env>`` blocks. The function is deterministic and idempotent.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import time
12
+ from typing import Any
13
+
14
+ from ctxoptimizer.analysis.tokens import estimate_tokens
15
+ from ctxoptimizer.config import Config
16
+ from ctxoptimizer.models import Provider, RequestContext, TransformReport
17
+ from ctxoptimizer.providers.base import ProviderAdapter
18
+
19
+ _ISO_DAY = re.compile(
20
+ r"\b(\d{4}-\d{2}-\d{2})[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?"
21
+ r"(?:Z|[+-]\d{2}:?\d{2})?\b"
22
+ )
23
+ _UUID4 = re.compile(
24
+ r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-"
25
+ r"[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b"
26
+ )
27
+ _TIME_ONLY = re.compile(r"(?i)\b(Current time|Time)\s*:\s*\d{1,2}:\d{2}(?::\d{2})?")
28
+ _ENV_EPOCH = re.compile(r"(?<!\d)\d{10}(?!\d)")
29
+ _ENV_BLOCK = re.compile(r"(?is)<env>.*?</env>")
30
+
31
+
32
+ def normalize_system(text: str) -> str:
33
+ text = _ISO_DAY.sub(r"\1", text)
34
+ text = _UUID4.sub("[uuid]", text)
35
+ text = _TIME_ONLY.sub(r"\1: [time omitted]", text)
36
+ return text
37
+
38
+
39
+ def normalize_user_env(text: str) -> str:
40
+ def repl(match: re.Match[str]) -> str:
41
+ block = normalize_system(match.group(0))
42
+ return _ENV_EPOCH.sub("[epoch]", block)
43
+
44
+ return _ENV_BLOCK.sub(repl, text)
45
+
46
+
47
+ class VolatileNormalizer:
48
+ id = "volatile_normalizer"
49
+
50
+ def applies(
51
+ self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
52
+ ) -> bool:
53
+ return ctx.config.transforms.volatile_normalizer.enabled
54
+
55
+ def apply(
56
+ self,
57
+ ctx: RequestContext,
58
+ payload: dict[str, Any],
59
+ adapter: ProviderAdapter,
60
+ config: Config,
61
+ ) -> TransformReport:
62
+ started = time.perf_counter()
63
+ items = 0
64
+ bytes_before = 0
65
+ bytes_after = 0
66
+ tokens_before = 0
67
+ tokens_after = 0
68
+ for target, key, user_text in _text_targets(payload, ctx.provider):
69
+ original = target.get(key)
70
+ if not isinstance(original, str):
71
+ continue
72
+ updated = normalize_user_env(original) if user_text else normalize_system(original)
73
+ if updated == original:
74
+ continue
75
+ target[key] = updated
76
+ items += 1
77
+ bytes_before += len(original.encode("utf-8"))
78
+ bytes_after += len(updated.encode("utf-8"))
79
+ tokens_before += estimate_tokens(original)
80
+ tokens_after += estimate_tokens(updated)
81
+ return TransformReport(
82
+ transform_id=self.id,
83
+ ran=True,
84
+ changed=items > 0,
85
+ items_touched=items,
86
+ bytes_before=bytes_before,
87
+ bytes_after=bytes_after,
88
+ est_tokens_before=tokens_before,
89
+ est_tokens_after=tokens_after,
90
+ duration_ms=(time.perf_counter() - started) * 1000,
91
+ )
92
+
93
+
94
+ def _text_targets(
95
+ payload: dict[str, Any], provider: Provider
96
+ ) -> list[tuple[dict[str, Any], str, bool]]:
97
+ targets: list[tuple[dict[str, Any], str, bool]] = []
98
+ if provider is Provider.ANTHROPIC:
99
+ system = payload.get("system")
100
+ if isinstance(system, str):
101
+ targets.append((payload, "system", False))
102
+ elif isinstance(system, list):
103
+ targets.extend(
104
+ (block, "text", False)
105
+ for block in system
106
+ if isinstance(block, dict) and isinstance(block.get("text"), str)
107
+ )
108
+ elif provider is Provider.GEMINI:
109
+ system = payload.get("systemInstruction")
110
+ if isinstance(system, dict):
111
+ targets.extend(
112
+ (part, "text", False)
113
+ for part in system.get("parts") or []
114
+ if isinstance(part, dict) and isinstance(part.get("text"), str)
115
+ )
116
+ elif provider is Provider.OLLAMA:
117
+ if isinstance(payload.get("system"), str):
118
+ targets.append((payload, "system", False))
119
+
120
+ if provider is Provider.OPENAI and isinstance(payload.get("instructions"), str):
121
+ targets.append((payload, "instructions", False))
122
+
123
+ messages = payload.get("messages")
124
+ if isinstance(messages, list):
125
+ for message in messages:
126
+ if not isinstance(message, dict):
127
+ continue
128
+ role = message.get("role")
129
+ content = message.get("content")
130
+ if role in {"system", "developer"}:
131
+ _add_content_targets(targets, message, "content", user_text=False)
132
+ elif role == "user":
133
+ _add_content_targets(targets, message, "content", user_text=True)
134
+
135
+ input_items = payload.get("input")
136
+ if isinstance(input_items, list):
137
+ for item in input_items:
138
+ if not isinstance(item, dict):
139
+ continue
140
+ if item.get("role") in {"system", "developer"}:
141
+ _add_content_targets(targets, item, "content", user_text=False)
142
+ elif item.get("role") == "user":
143
+ _add_content_targets(targets, item, "content", user_text=True)
144
+
145
+ contents = payload.get("contents")
146
+ if isinstance(contents, list):
147
+ for content in contents:
148
+ if not isinstance(content, dict):
149
+ continue
150
+ if content.get("role") == "user":
151
+ targets.extend(
152
+ (part, "text", True)
153
+ for part in content.get("parts") or []
154
+ if isinstance(part, dict) and isinstance(part.get("text"), str)
155
+ )
156
+ return targets
157
+
158
+
159
+ def _add_content_targets(
160
+ targets: list[tuple[dict[str, Any], str, bool]],
161
+ message: dict[str, Any],
162
+ key: str,
163
+ *,
164
+ user_text: bool,
165
+ ) -> None:
166
+ content = message.get(key)
167
+ if isinstance(content, str):
168
+ targets.append((message, key, user_text))
169
+ elif isinstance(content, list):
170
+ targets.extend(
171
+ (block, "text", user_text)
172
+ for block in content
173
+ if isinstance(block, dict) and isinstance(block.get("text"), str)
174
+ )
175
+
176
+
177
+ __all__ = ["VolatileNormalizer", "normalize_system", "normalize_user_env"]
@@ -0,0 +1,25 @@
1
+ """Trailing whitespace and blank-run trimming (SPEC section 12.4)."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def clean(text: str, max_blank_run: int = 2) -> str:
7
+ lines = [line.rstrip() for line in text.split("\n")]
8
+ result: list[str] = []
9
+ blanks = 0
10
+ for line in lines:
11
+ if line == "":
12
+ blanks += 1
13
+ if blanks <= max_blank_run:
14
+ result.append(line)
15
+ else:
16
+ blanks = 0
17
+ result.append(line)
18
+ while result and result[-1] == "":
19
+ result.pop()
20
+ if not result:
21
+ return ""
22
+ return "\n".join(result) + "\n"
23
+
24
+
25
+ __all__ = ["clean"]
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctxoptimizer
3
+ Version: 0.4.1
4
+ Summary: Local, deterministic cost optimizer for AI coding agents
5
+ Author: ahmed
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ License-File: NOTICE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Dist: httpx[http2]>=0.27
11
+ Requires-Dist: jinja2>=3.1
12
+ Requires-Dist: orjson>=3.10
13
+ Requires-Dist: platformdirs>=4.2
14
+ Requires-Dist: pydantic>=2.7
15
+ Requires-Dist: starlette>=0.37
16
+ Requires-Dist: typer>=0.12
17
+ Requires-Dist: uvicorn>=0.30
18
+ Requires-Dist: coverage>=7 ; extra == 'dev'
19
+ Requires-Dist: hypothesis>=6.100 ; extra == 'dev'
20
+ Requires-Dist: mypy>=1.10 ; extra == 'dev'
21
+ Requires-Dist: pip-audit ; extra == 'dev'
22
+ Requires-Dist: pytest>=8 ; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
24
+ Requires-Dist: pytest-benchmark>=4 ; extra == 'dev'
25
+ Requires-Dist: pytest-cov>=5 ; extra == 'dev'
26
+ Requires-Dist: respx>=0.21 ; extra == 'dev'
27
+ Requires-Dist: ruff>=0.5 ; extra == 'dev'
28
+ Requires-Dist: google-re2>=1.1 ; extra == 're2'
29
+ Requires-Dist: tiktoken>=0.7 ; extra == 'tokens'
30
+ Requires-Python: >=3.11
31
+ Project-URL: Homepage, https://github.com/ahmed-dev25/ctxoptimizer
32
+ Project-URL: Repository, https://github.com/ahmed-dev25/ctxoptimizer
33
+ Project-URL: Issues, https://github.com/ahmed-dev25/ctxoptimizer/issues
34
+ Project-URL: Changelog, https://github.com/ahmed-dev25/ctxoptimizer/blob/main/CHANGELOG.md
35
+ Provides-Extra: dev
36
+ Provides-Extra: re2
37
+ Provides-Extra: tokens
38
+ Description-Content-Type: text/markdown
39
+
40
+ # ctxoptimizer
41
+
42
+ A local, deterministic cost optimizer for AI coding agents. It sits between your
43
+ agent and the LLM provider, measures real token usage and spend, explains why
44
+ prompt caching missed, and applies cache-safe payload reductions.
45
+
46
+ - **Local only** — binds to loopback; no telemetry leaves the machine.
47
+ - **Deterministic** — no LLM calls inside the proxy; every decision is a hash,
48
+ regex, counter or threshold.
49
+ - **Honest numbers** — provider-reported usage is ground truth; everything else
50
+ is labelled `est.` or `sim.`.
51
+ - **Fail-open** — invalid transforms retry with the original bytes; the proxy
52
+ never blocks traffic.
53
+
54
+ ## Install
55
+
56
+ ```powershell
57
+ uv tool install ctxoptimizer
58
+ # or: pipx install ctxoptimizer
59
+ ctxoptimizer run
60
+ ```
61
+
62
+ If PyPI does not have the release yet, install from GitHub:
63
+
64
+ ```powershell
65
+ uv tool install "git+https://github.com/ahmed-dev25/ctxoptimizer.git"
66
+ # or: pipx install "git+https://github.com/ahmed-dev25/ctxoptimizer.git"
67
+ ctxoptimizer run
68
+ ```
69
+
70
+ From a checkout:
71
+
72
+ ```powershell
73
+ uv sync --extra dev
74
+ uv run ctxoptimizer run
75
+ ```
76
+
77
+ The startup banner prints the dashboard URL with a per-run admin token:
78
+
79
+ ```text
80
+ dashboard: http://127.0.0.1:8787/_ctxoptimizer/?token=...
81
+ ```
82
+
83
+ Open that URL once; the token is stored as an `HttpOnly; SameSite=Strict`
84
+ cookie. The dashboard is also protected by a Host allowlist and CSRF checks.
85
+
86
+ ## Quickstart
87
+
88
+ ```powershell
89
+ ctxoptimizer env --shell powershell # bash | zsh | fish | powershell
90
+ ctxoptimizer status # health + counters
91
+ ctxoptimizer stats # recorded usage and cost
92
+ ```
93
+
94
+ Point a client at the printed base URL. See
95
+ [`docs/client-setup.md`](docs/client-setup.md) for Claude Code, OpenCode,
96
+ DeepSeek, OpenRouter, Gemini, Ollama, Codex and the other supported clients.
97
+
98
+ Default mode is `shadow`: everything is measured and simulated, while the
99
+ client's bytes are forwarded unchanged. Switch to `active` from the dashboard
100
+ (Settings), with `ctxoptimizer run --mode active`, or with
101
+ `x-ctxoptimizer-mode: active` per request.
102
+
103
+ ## What it does
104
+
105
+ - Measures provider usage and cost for Anthropic, OpenAI Chat/Responses,
106
+ native Gemini, native Ollama and named OpenAI-compatible upstreams
107
+ (DeepSeek, OpenRouter, Gemini/Ollama compatibility, and custom gateways).
108
+ - Detects drift (tools/system/history/TTL/config/model) and waste W1–W8:
109
+ redundant re-reads, identical outputs, unbatched fan-out, runaway sessions,
110
+ recurring/param drift, edit churn and unused tool schemas.
111
+ - Applies deterministic transforms: ANSI/progress cleanup, line dedupe,
112
+ whitespace trim, signal-aware truncation, duplicate back-references,
113
+ byte-true `cache_serve`, opt-in stale-result elision, Anthropic
114
+ `cache_control` hints and OpenAI `prompt_cache_key`.
115
+ - Auto-quarantines a transform per session when fallback/drift guard counters
116
+ trip; the dashboard shows the history and lets you re-enable it.
117
+ - Provides `ctxoptimizer_expand` over MCP so an agent can retrieve any omitted
118
+ original from the local blob store.
119
+ - Serves a local dashboard: Overview, Live (SSE), Sessions, Requests + diff
120
+ viewer, Waste Opportunities with status controls, Transforms with hot
121
+ toggles/stats, Pricing editor, Settings and Doctor.
122
+
123
+ ## Privacy
124
+
125
+ Payload capture defaults to `changed_blocks`; set it to `off` to store nothing.
126
+ Captured text stays in the per-user data directory, is redacted, and is bounded
127
+ by retention. `ctxoptimizer purge --all` wipes telemetry, blobs and findings. See
128
+ [`docs/privacy.md`](docs/privacy.md).
129
+
130
+ ## Service
131
+
132
+ ```powershell
133
+ ctxoptimizer service install # launchd / systemd --user / Windows scheduled task
134
+ ctxoptimizer service install --dry-run
135
+ ```
136
+
137
+ ## Contributing
138
+
139
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup and gates. Report
140
+ vulnerabilities privately via [`SECURITY.md`](SECURITY.md); do not open a public
141
+ issue for a security problem.
142
+
143
+ ## License
144
+
145
+ ctxoptimizer is free and open-source software licensed under the **MIT License**.
146
+ See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).
147
+
148
+ ## Updating
149
+
150
+ Use the package manager that installed ctxoptimizer:
151
+
152
+ ```powershell
153
+ uv tool upgrade ctxoptimizer
154
+ # or
155
+ pipx upgrade ctxoptimizer
156
+ ```
157
+
158
+ There is no built-in self-update command. PyPI releases are published with
159
+ Trusted Publishing and PEP 740 attestations. Source installs follow the GitHub
160
+ repository; downloaded wheels must be replaced manually. See
161
+ [`docs/updating.md`](docs/updating.md).
162
+
163
+ ## Documentation
164
+
165
+ - [`SPEC.md`](SPEC.md) — authoritative specification
166
+ - [`docs/client-setup.md`](docs/client-setup.md) — per-client recipes
167
+ - [`docs/how-to-use.md`](docs/how-to-use.md) — complete install/run/update guide
168
+ - [`docs/provider-notes.md`](docs/provider-notes.md) — dated provider behavior
169
+ - [`docs/architecture.md`](docs/architecture.md) — component overview
170
+ - [`docs/privacy.md`](docs/privacy.md) — data handling and purge
171
+ - [`docs/faq.md`](docs/faq.md) — cache, savings, troubleshooting
172
+ - [`docs/test-report.md`](docs/test-report.md) — gates and test coverage summary
173
+ - [`docs/updating.md`](docs/updating.md) — package-manager upgrades and release artifacts
174
+ - [`docs/pypi-release.md`](docs/pypi-release.md) — PyPI Trusted Publishing setup
175
+ - [`docs/release-checklist.md`](docs/release-checklist.md) — release checklist
176
+ - [`docs/progress.md`](docs/progress.md) — build status and handoff notes
177
+ - [`CHANGELOG.md`](CHANGELOG.md)
178
+ - [`CONTRIBUTING.md`](CONTRIBUTING.md)
179
+ - [`SECURITY.md`](SECURITY.md)
180
+ - [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md)
181
+ - [`docs/decisions/`](docs/decisions/) — architecture decision records
182
+ - [`AGENTS.md`](AGENTS.md) — coding and git workflow rules
183
+
184
+ ## Development
185
+
186
+ ```powershell
187
+ uv run ruff check .
188
+ uv run ruff format --check .
189
+ uv run mypy --strict src
190
+ uv run pytest -q
191
+ uv build --wheel
192
+ ```
193
+
194
+ Supported Python: 3.11+. The package is built with `uv_build`; the test suite
195
+ uses a real fake upstream and recorded/documented fixtures. Live provider tests
196
+ are opt-in (`pytest -m live`) and require keys.
@@ -0,0 +1,83 @@
1
+ ctxoptimizer/__init__.py,sha256=m9kqXRWCFOIGILYBMgfg3kOyEm5Q95aLeO8qXUljhXw,139
2
+ ctxoptimizer/__main__.py,sha256=oILSSwAoyrQXYyX9Py1HaUrpGdSYCJvXInXtq-0nfz8,241
3
+ ctxoptimizer/admin_security.py,sha256=fFU2BkiERsFlDWaKfBjzZu-28x6Ak0lyxgVfAsIE_pY,4239
4
+ ctxoptimizer/analysis/__init__.py,sha256=3xDJhZkmEpEZKaEomHnKXYL2FpYPjFxVF41AM3sulh8,85
5
+ ctxoptimizer/analysis/baseline.py,sha256=6a6zMAL9aUg95zEdLzmCXZCbrZThxwZHhJ0fVsQN1Fk,2222
6
+ ctxoptimizer/analysis/break_even.py,sha256=u5iJZVzN8-v-jBKhIP_ja236OzJ6AfIfTJ0Thq1iTCc,1539
7
+ ctxoptimizer/analysis/drift.py,sha256=ZQx9lKDKjnvKv_fzHh-22ntvU4VVzgQE-7jWRdym_AE,4684
8
+ ctxoptimizer/analysis/edits.py,sha256=HHuSnImbDGigFOPg_1TgT_dr8WgB2H8YZNFme0f7w0U,4901
9
+ ctxoptimizer/analysis/markers.py,sha256=lC8DqakrjHd69rXkWVWf9cbKXNGwcEIAU948SFsxXxw,958
10
+ ctxoptimizer/analysis/recommend.py,sha256=OX4Ljc93HtcULJOt4Plf3MpSt7i2_xcLYsPsejrp_QY,4291
11
+ ctxoptimizer/analysis/segments.py,sha256=GBTkhin12rhTMyJKByG9a3Q9uChZjm1g6WbH5yV0PJQ,1549
12
+ ctxoptimizer/analysis/session.py,sha256=-xZakAi_rulEEj7p2GEl7zm0iWO-BMKNR_jW-LQDRIw,2030
13
+ ctxoptimizer/analysis/state.py,sha256=B_on-eqk91xW7kZcoFE-OSd-j3ckSbM9b9qrf216Smo,8016
14
+ ctxoptimizer/analysis/tokens.py,sha256=fdY25I0L6XWxykM1EYQOb6KYiPHcolSPpqBRXbZrE5w,1814
15
+ ctxoptimizer/analysis/waste.py,sha256=1-EXfZEi-I2NY_27u3Ybyzmism5mIzsuGqejHdCrAEs,9418
16
+ ctxoptimizer/cli.py,sha256=7ETbw-7hApVl1WghB_5AGGmlsyq7cvhJBWViIAFQ1Wo,31687
17
+ ctxoptimizer/config.py,sha256=-J3sa47l-zevsHYbG2IVia5tvPnJYJJTtot95DJjd-s,17010
18
+ ctxoptimizer/dashboard/__init__.py,sha256=AOiOaOTyRBFBw6L2Z0IfECWH4hw9hy3cN8vxECp5u08,65
19
+ ctxoptimizer/dashboard/api.py,sha256=gId20pChmxndQQI2ds_l8uDPwHnrmt-wEbTDae7uXpY,19226
20
+ ctxoptimizer/dashboard/service.py,sha256=ipfvoELR-hTj8paPjn-UDAEEff6l271gQeJa6XX6B-E,5115
21
+ ctxoptimizer/dashboard/static/.gitkeep,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
22
+ ctxoptimizer/dashboard/templates/.gitkeep,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
+ ctxoptimizer/dashboard/templates/doctor.html,sha256=AvqIYvfBqpvlKb0oS2Or7lcRhwKqic4p7Zwo8QiST9M,2056
24
+ ctxoptimizer/dashboard/templates/live.html,sha256=qT35RBiSXKn2WsmehU43I8e7Zq4zy6q_PZQLvOLZeTE,4812
25
+ ctxoptimizer/dashboard/templates/overview.html,sha256=ZamPqCMdB1HO4KGvLq9WbVprzaMZWI557ex7O5gB3C4,8189
26
+ ctxoptimizer/dashboard/templates/pricing.html,sha256=VzpZKhCHOs6riaI_7d3thCAEW8n-NNXxknn-wCyvWdg,5248
27
+ ctxoptimizer/dashboard/templates/request.html,sha256=pUFSHZ8d8OoDHICIA8RQCRcRe9GAsxjeMVsegYxBuYQ,4717
28
+ ctxoptimizer/dashboard/templates/session.html,sha256=vI-K1033_-vAcLrVawNXRLR2d6wRuhH16sopQ2xv0N4,3244
29
+ ctxoptimizer/dashboard/templates/settings.html,sha256=iL_URUcEMveoXC72rsYnJe3llHpO7Cbrem0WoCT_yzs,3409
30
+ ctxoptimizer/dashboard/templates/table.html,sha256=0_wtFttPRzmYSwGu--c_5CbonZXtEGDFqYBffLcMyU0,2083
31
+ ctxoptimizer/dashboard/templates/transforms.html,sha256=adU--n1LIb1lrKnjDBwiJvKIb9ILtsTSRMEA_cPlIJo,5479
32
+ ctxoptimizer/dashboard/templates/waste.html,sha256=dGAeCFhwJCa4uN7m4MtY47E_NgrGvNaW8bky27m21tg,5363
33
+ ctxoptimizer/dashboard/views.py,sha256=bW-7OoSOMfiX9pmzN47kprbfx1uQFx8RYNHS_mjWq2M,14090
34
+ ctxoptimizer/data/cache_limits.json,sha256=EgJWBd07ZZMoUwg9zPbw19Z0BGJN68Pk3y9eDBVAym8,1068
35
+ ctxoptimizer/data/default_config.toml,sha256=-FEkyoIOIO1tLDLVMdTqewJPAkICvcjZf-5-S3CqoBc,3515
36
+ ctxoptimizer/data/pricing.json,sha256=8_ivliNzDKClYzkhi2I_AlAco7K46J6-emuRv7-RsRE,11735
37
+ ctxoptimizer/doctor.py,sha256=ZXQhcGm6H-h1Pd6nce-RWmFIMyRtpTUhCmHC1YxnlkM,4041
38
+ ctxoptimizer/guard.py,sha256=AVO_PEzoMc0jKyzXkiIhIrwdhfiAweJwB-c6kTdhMnQ,5913
39
+ ctxoptimizer/ids.py,sha256=YZa2SZD1EkRF4DmB2JsvZHTi8kmtuMGRVBsSUvEoGlM,2175
40
+ ctxoptimizer/log.py,sha256=9weVpT_ct8P_0NX2O5hfI3t4tHPO0cIFYeOgW3wHwgI,2259
41
+ ctxoptimizer/mcp.py,sha256=_A9iPy9drdbV-g6viU00oFRt2FFzmyqbuDxY6CnCzgw,4307
42
+ ctxoptimizer/models.py,sha256=ByJe_uSNJYyKZZ_T5iIYkMedxTOHWjFZNcr53LvQbgA,3582
43
+ ctxoptimizer/pipeline.py,sha256=WS4oeZu_T_zzuSRPvAApKOva77oao7Y1QQOonvi41w4,4471
44
+ ctxoptimizer/profiles.py,sha256=Agw0NGcYNUca6OP4ps0KJHA51tXnaHL1GfgQVI8-TF0,4602
45
+ ctxoptimizer/providers/__init__.py,sha256=SOrlG2pwaAERvooqPntgHgzj84pwcSkkB6JoxhDOFgA,1216
46
+ ctxoptimizer/providers/anthropic.py,sha256=n6myDvBSSqIrVTR2FH6YWrp2s5xa7LmkeL5bRlwMtoU,4115
47
+ ctxoptimizer/providers/base.py,sha256=Q5848ZWZXH3TwUK9LNJ2A1p2So3eClOlxiwYyacvj8U,5300
48
+ ctxoptimizer/providers/gemini.py,sha256=54d40_ELl6ECK1RYC9D6uOZp2YsxmShRuC-T1OsHo0o,5493
49
+ ctxoptimizer/providers/ollama.py,sha256=qHzU3YhjiV8MI0qX0K0-OqzW28xGwh2UDb2SVsvmqi8,5348
50
+ ctxoptimizer/providers/openai_chat.py,sha256=TeI7Ort82kPlrafYf2pzMTHKXqm4RtJIRJh8W-48uqo,5371
51
+ ctxoptimizer/providers/openai_responses.py,sha256=qTqRxERFnYILmSTGWl3MNOUPAcmJs7v8_IUfeBOskbA,4837
52
+ ctxoptimizer/proxy.py,sha256=r6VSerD2pzbMbs2T6gEsZMZ19mkZITAQ0BfUgwHaA20,24494
53
+ ctxoptimizer/routing.py,sha256=HY6M2udNJ8RgzgeZ-W5AcphIAxP2B99eOBsaF4MKcC8,8600
54
+ ctxoptimizer/server.py,sha256=90hMJu0XusjWClHMiib0JWUub2dqW4iGWrWbgKtlWCc,12624
55
+ ctxoptimizer/service.py,sha256=BV3MJRWDYm0SUyhTfkX5-KdP6C55SPrihWkvfbX_Nis,4438
56
+ ctxoptimizer/telemetry/__init__.py,sha256=xezH3zIzrIur0ueJpAvv5LvYYEueig0Nl4BUdYTfhKQ,82
57
+ ctxoptimizer/telemetry/blobs.py,sha256=421ybXUsYPSJ-x4OWMhdhSICFA1vvyrpTtz_Lii9IOQ,1609
58
+ ctxoptimizer/telemetry/expand.py,sha256=N3DgU2wWa138kCBkfQ3BKGMJBkNfVzzQFi2eKrgSwCc,1768
59
+ ctxoptimizer/telemetry/pricing.py,sha256=n9r2GrvvG8rmNihkoFoHQ3GPgWTwsSoEffQ8jeETA_s,5352
60
+ ctxoptimizer/telemetry/redact.py,sha256=dN6tB3uKC47qJD5281bbP17sQtdg8Ni0_uhgvva1NbY,1646
61
+ ctxoptimizer/telemetry/sse.py,sha256=jIXkoIMPq18n-Q_GWEGsAlL36PB7lI7uyCbvljd72vo,4569
62
+ ctxoptimizer/telemetry/store.py,sha256=9-f_kof9rfHCAoeQGX5whs5weKnTggefkJ_iFttR2LI,32669
63
+ ctxoptimizer/telemetry/usage.py,sha256=Bv0qSXbwxXQ-0GPDikhX-yQO3cLuWWyF-jydde56dpU,14874
64
+ ctxoptimizer/transforms/__init__.py,sha256=6JT4EJ10DCdEXE3dV_VG0mBM54YkLOKN3LLZEweZUG8,3148
65
+ ctxoptimizer/transforms/ansi.py,sha256=fhcR97gp4Z218MKWcvyaV_SWAodpxKuXCvgUQqTeWBo,531
66
+ ctxoptimizer/transforms/anthropic_cache.py,sha256=xUn0AIy6FbPOj6DpLx9EosUhz91K92vXsKbxodhgntk,3694
67
+ ctxoptimizer/transforms/backref.py,sha256=mQSwfYqtZuwrTobbQR3DjjXMLEd70n7USWLMTxsDZDk,2648
68
+ ctxoptimizer/transforms/base.py,sha256=mbeaLkHwBSLy7y-txOHssR3PtW_nH3gMAwL2fubKD1M,2736
69
+ ctxoptimizer/transforms/cache_serve.py,sha256=w1LXICyHVVo0f77M6YGijemYodSQSyQ_1NUhujkO898,5167
70
+ ctxoptimizer/transforms/dedupe_lines.py,sha256=Kz3fF1rP8hjlQDAYCdvmSAFU9m8rRt7peAlRU1_hFDI,2076
71
+ ctxoptimizer/transforms/eligibility.py,sha256=bPVMIeAHVBoJLzyf-zLNjTBCqTOwTfND-tmd7saHILY,748
72
+ ctxoptimizer/transforms/openai_hints.py,sha256=WnI09LMAJnVkBnrtcmm49H_at3Xmqfr8mjWKdSAEE6g,2909
73
+ ctxoptimizer/transforms/progress.py,sha256=kKyL5xphZcR9HMZ59WI3LoPJSNm1iT_8jnNP1xx0PKU,1569
74
+ ctxoptimizer/transforms/stale_result_elision.py,sha256=wRm3A8ZULt6X6ChilEG7QeHm_i932JeG78xCW1t__Jg,3432
75
+ ctxoptimizer/transforms/truncate.py,sha256=MzUQtXFr4g_u27mbXovK0m94W0e8GL2a0bKgXd3T0wg,3488
76
+ ctxoptimizer/transforms/volatile.py,sha256=dPYLHt17vl52hHeeRmqdou4HBIKrp3n89_kX2cqJTFo,6246
77
+ ctxoptimizer/transforms/whitespace.py,sha256=G3K_ZhGAOAGnP-zIYkA0jTVhfgaT0m4WoHWS5i0i05M,630
78
+ ctxoptimizer-0.4.1.dist-info/licenses/LICENSE,sha256=mT3OIEepsAfZheCsFU_-faNVDCJadYSSvjDz5hPDheI,1062
79
+ ctxoptimizer-0.4.1.dist-info/licenses/NOTICE,sha256=wy8RRAkVzCIyl1DvAAtpy2n65AsqfIvwTOC9_1dIA9o,560
80
+ ctxoptimizer-0.4.1.dist-info/WHEEL,sha256=si2p8vW03RnhyBnhWl4tj-ObJa78SpE-c9HsEtblGR0,80
81
+ ctxoptimizer-0.4.1.dist-info/entry_points.txt,sha256=lV3sTBBTZ6tNUf4FJYXBLIg9QpqxmFblBBQAhBd29-I,55
82
+ ctxoptimizer-0.4.1.dist-info/METADATA,sha256=5SH7DENaYe7CaXzUIO42WblJU9H5SrLuVEfUbxFrars,7478
83
+ ctxoptimizer-0.4.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.5
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ ctxoptimizer = ctxoptimizer.cli:app
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmed
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,13 @@
1
+ ctxoptimizer
2
+ Copyright (c) 2026 Ahmed
3
+
4
+ ctxoptimizer is free and open-source software licensed under the MIT License.
5
+ See LICENSE.
6
+
7
+ Provider names (Anthropic, OpenAI, Google Gemini, Ollama, DeepSeek, OpenRouter,
8
+ OpenCode, Claude Code, Codex, Cursor, Aider, Cline and others) are trademarks of
9
+ their respective owners and are used only for descriptive compatibility. This
10
+ project is not affiliated with or endorsed by any provider.
11
+
12
+ Third-party components are distributed under their own licenses. See
13
+ THIRD_PARTY_LICENSES.md and the installed package metadata.