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.
- ctxoptimizer/__init__.py +5 -0
- ctxoptimizer/__main__.py +14 -0
- ctxoptimizer/admin_security.py +130 -0
- ctxoptimizer/analysis/__init__.py +1 -0
- ctxoptimizer/analysis/baseline.py +69 -0
- ctxoptimizer/analysis/break_even.py +49 -0
- ctxoptimizer/analysis/drift.py +151 -0
- ctxoptimizer/analysis/edits.py +148 -0
- ctxoptimizer/analysis/markers.py +34 -0
- ctxoptimizer/analysis/recommend.py +114 -0
- ctxoptimizer/analysis/segments.py +57 -0
- ctxoptimizer/analysis/session.py +62 -0
- ctxoptimizer/analysis/state.py +239 -0
- ctxoptimizer/analysis/tokens.py +62 -0
- ctxoptimizer/analysis/waste.py +322 -0
- ctxoptimizer/cli.py +873 -0
- ctxoptimizer/config.py +512 -0
- ctxoptimizer/dashboard/__init__.py +1 -0
- ctxoptimizer/dashboard/api.py +537 -0
- ctxoptimizer/dashboard/service.py +157 -0
- ctxoptimizer/dashboard/static/.gitkeep +1 -0
- ctxoptimizer/dashboard/templates/.gitkeep +1 -0
- ctxoptimizer/dashboard/templates/doctor.html +46 -0
- ctxoptimizer/dashboard/templates/live.html +91 -0
- ctxoptimizer/dashboard/templates/overview.html +172 -0
- ctxoptimizer/dashboard/templates/pricing.html +115 -0
- ctxoptimizer/dashboard/templates/request.html +111 -0
- ctxoptimizer/dashboard/templates/session.html +67 -0
- ctxoptimizer/dashboard/templates/settings.html +79 -0
- ctxoptimizer/dashboard/templates/table.html +56 -0
- ctxoptimizer/dashboard/templates/transforms.html +118 -0
- ctxoptimizer/dashboard/templates/waste.html +120 -0
- ctxoptimizer/dashboard/views.py +429 -0
- ctxoptimizer/data/cache_limits.json +23 -0
- ctxoptimizer/data/default_config.toml +185 -0
- ctxoptimizer/data/pricing.json +421 -0
- ctxoptimizer/doctor.py +129 -0
- ctxoptimizer/guard.py +139 -0
- ctxoptimizer/ids.py +73 -0
- ctxoptimizer/log.py +73 -0
- ctxoptimizer/mcp.py +124 -0
- ctxoptimizer/models.py +145 -0
- ctxoptimizer/pipeline.py +126 -0
- ctxoptimizer/profiles.py +152 -0
- ctxoptimizer/providers/__init__.py +37 -0
- ctxoptimizer/providers/anthropic.py +105 -0
- ctxoptimizer/providers/base.py +191 -0
- ctxoptimizer/providers/gemini.py +143 -0
- ctxoptimizer/providers/ollama.py +133 -0
- ctxoptimizer/providers/openai_chat.py +135 -0
- ctxoptimizer/providers/openai_responses.py +121 -0
- ctxoptimizer/proxy.py +665 -0
- ctxoptimizer/routing.py +259 -0
- ctxoptimizer/server.py +319 -0
- ctxoptimizer/service.py +150 -0
- ctxoptimizer/telemetry/__init__.py +1 -0
- ctxoptimizer/telemetry/blobs.py +57 -0
- ctxoptimizer/telemetry/expand.py +62 -0
- ctxoptimizer/telemetry/pricing.py +168 -0
- ctxoptimizer/telemetry/redact.py +53 -0
- ctxoptimizer/telemetry/sse.py +145 -0
- ctxoptimizer/telemetry/store.py +829 -0
- ctxoptimizer/telemetry/usage.py +435 -0
- ctxoptimizer/transforms/__init__.py +87 -0
- ctxoptimizer/transforms/ansi.py +21 -0
- ctxoptimizer/transforms/anthropic_cache.py +100 -0
- ctxoptimizer/transforms/backref.py +74 -0
- ctxoptimizer/transforms/base.py +93 -0
- ctxoptimizer/transforms/cache_serve.py +146 -0
- ctxoptimizer/transforms/dedupe_lines.py +69 -0
- ctxoptimizer/transforms/eligibility.py +24 -0
- ctxoptimizer/transforms/openai_hints.py +84 -0
- ctxoptimizer/transforms/progress.py +52 -0
- ctxoptimizer/transforms/stale_result_elision.py +109 -0
- ctxoptimizer/transforms/truncate.py +100 -0
- ctxoptimizer/transforms/volatile.py +177 -0
- ctxoptimizer/transforms/whitespace.py +25 -0
- ctxoptimizer-0.4.1.dist-info/METADATA +196 -0
- ctxoptimizer-0.4.1.dist-info/RECORD +83 -0
- ctxoptimizer-0.4.1.dist-info/WHEEL +4 -0
- ctxoptimizer-0.4.1.dist-info/entry_points.txt +3 -0
- ctxoptimizer-0.4.1.dist-info/licenses/LICENSE +21 -0
- ctxoptimizer-0.4.1.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Incremental usage parsers for Anthropic, OpenAI Chat and OpenAI Responses.
|
|
2
|
+
|
|
3
|
+
The parsers are fed raw upstream bytes. ``feed`` never raises: a malformed stream
|
|
4
|
+
disables the parser and leaves whatever was already parsed (INV-3). Normalization
|
|
5
|
+
follows SPEC section 14.1 plus the corrections in ``docs/provider-notes.md``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from typing import Any, Protocol
|
|
12
|
+
|
|
13
|
+
import orjson
|
|
14
|
+
|
|
15
|
+
from ctxoptimizer.models import Endpoint, Provider, Usage
|
|
16
|
+
from ctxoptimizer.profiles import UpstreamProfile
|
|
17
|
+
from ctxoptimizer.telemetry.sse import SSEDecoder, SSEEvent
|
|
18
|
+
|
|
19
|
+
_CHAT_STYLES = frozenset({"openai", "deepseek", "openrouter", "gemini-openai", "ollama-openai"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UsageParser(Protocol):
|
|
23
|
+
"""Side-channel parser fed raw upstream bytes; never raises outward."""
|
|
24
|
+
|
|
25
|
+
def feed(self, chunk: bytes) -> None: ...
|
|
26
|
+
|
|
27
|
+
def disable(self) -> None: ...
|
|
28
|
+
|
|
29
|
+
def result(self) -> Usage: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NullUsageParser:
|
|
33
|
+
"""Records that usage is unavailable, without inspecting the stream."""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._disabled = False
|
|
37
|
+
|
|
38
|
+
def feed(self, chunk: bytes) -> None:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
def disable(self) -> None:
|
|
42
|
+
self._disabled = True
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def disabled(self) -> bool:
|
|
46
|
+
return self._disabled
|
|
47
|
+
|
|
48
|
+
def result(self) -> Usage:
|
|
49
|
+
return Usage(source="none")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _BaseParser:
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
self._decoder = SSEDecoder()
|
|
55
|
+
self._disabled = False
|
|
56
|
+
self._usage = Usage(source="none")
|
|
57
|
+
self._output_chars = 0
|
|
58
|
+
|
|
59
|
+
def feed(self, chunk: bytes) -> None:
|
|
60
|
+
if self._disabled:
|
|
61
|
+
return
|
|
62
|
+
try:
|
|
63
|
+
for event in self._decoder.feed(chunk):
|
|
64
|
+
self._handle(event)
|
|
65
|
+
except Exception:
|
|
66
|
+
self.disable()
|
|
67
|
+
|
|
68
|
+
def disable(self) -> None:
|
|
69
|
+
self._disabled = True
|
|
70
|
+
|
|
71
|
+
def result(self) -> Usage:
|
|
72
|
+
if self._usage.source != "provider" and self._output_chars:
|
|
73
|
+
self._usage.output = max(1, math.ceil(self._output_chars / 3.5))
|
|
74
|
+
self._usage.source = "estimated"
|
|
75
|
+
return self._usage
|
|
76
|
+
|
|
77
|
+
def _handle(self, event: SSEEvent) -> None:
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AnthropicUsageParser(_BaseParser):
|
|
82
|
+
"""Anthropic sends full usage at message_start and cumulative at message_delta."""
|
|
83
|
+
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
super().__init__()
|
|
86
|
+
self._fields: dict[str, Any] = {}
|
|
87
|
+
self._cache_creation: dict[str, Any] | None = None
|
|
88
|
+
|
|
89
|
+
def _handle(self, event: SSEEvent) -> None:
|
|
90
|
+
payload = _json(event.data)
|
|
91
|
+
if payload is None:
|
|
92
|
+
return
|
|
93
|
+
kind = payload.get("type") or event.event
|
|
94
|
+
if kind == "message_start":
|
|
95
|
+
message = _dict(payload.get("message"))
|
|
96
|
+
usage = message.get("usage")
|
|
97
|
+
if isinstance(usage, dict):
|
|
98
|
+
self._merge(usage)
|
|
99
|
+
elif kind == "message_delta":
|
|
100
|
+
usage = payload.get("usage")
|
|
101
|
+
if isinstance(usage, dict):
|
|
102
|
+
self._merge(usage)
|
|
103
|
+
elif kind == "content_block_delta":
|
|
104
|
+
delta = _dict(payload.get("delta"))
|
|
105
|
+
if delta.get("type") == "text_delta":
|
|
106
|
+
self._output_chars += len(_text(delta.get("text")))
|
|
107
|
+
elif delta.get("type") == "thinking_delta":
|
|
108
|
+
self._output_chars += len(_text(delta.get("thinking")))
|
|
109
|
+
|
|
110
|
+
def _merge(self, usage: dict[str, Any]) -> None:
|
|
111
|
+
for key in (
|
|
112
|
+
"input_tokens",
|
|
113
|
+
"cache_read_input_tokens",
|
|
114
|
+
"cache_creation_input_tokens",
|
|
115
|
+
"output_tokens",
|
|
116
|
+
):
|
|
117
|
+
value = usage.get(key)
|
|
118
|
+
if value is not None:
|
|
119
|
+
self._fields[key] = value
|
|
120
|
+
details = usage.get("output_tokens_details")
|
|
121
|
+
if isinstance(details, dict):
|
|
122
|
+
self._fields["output_tokens_details"] = details
|
|
123
|
+
cache_creation = usage.get("cache_creation")
|
|
124
|
+
if isinstance(cache_creation, dict):
|
|
125
|
+
self._cache_creation = cache_creation
|
|
126
|
+
self._usage = _anthropic_usage(self._fields, self._cache_creation)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class OpenAIChatUsageParser(_BaseParser):
|
|
130
|
+
"""Chat Completions usage arrives in the final chunk when requested."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, style: str = "openai") -> None:
|
|
133
|
+
super().__init__()
|
|
134
|
+
self._style = style
|
|
135
|
+
|
|
136
|
+
def _handle(self, event: SSEEvent) -> None:
|
|
137
|
+
data = event.data.strip()
|
|
138
|
+
if not data or data == "[DONE]":
|
|
139
|
+
return
|
|
140
|
+
payload = _json(data)
|
|
141
|
+
if payload is None:
|
|
142
|
+
return
|
|
143
|
+
usage = payload.get("usage")
|
|
144
|
+
if isinstance(usage, dict):
|
|
145
|
+
self._usage = _chat_usage(usage, self._style)
|
|
146
|
+
for choice in payload.get("choices") or []:
|
|
147
|
+
if not isinstance(choice, dict):
|
|
148
|
+
continue
|
|
149
|
+
delta = _dict(choice.get("delta"))
|
|
150
|
+
self._output_chars += len(_text(delta.get("content")))
|
|
151
|
+
self._output_chars += len(_text(delta.get("reasoning_content")))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class OpenAIResponsesUsageParser(_BaseParser):
|
|
155
|
+
"""Responses reports true usage on the final Response object."""
|
|
156
|
+
|
|
157
|
+
def _handle(self, event: SSEEvent) -> None:
|
|
158
|
+
payload = _json(event.data)
|
|
159
|
+
if payload is None:
|
|
160
|
+
return
|
|
161
|
+
kind = payload.get("type") or event.event
|
|
162
|
+
if kind in {"response.completed", "response.incomplete", "response.failed"}:
|
|
163
|
+
response = _dict(payload.get("response"))
|
|
164
|
+
usage = response.get("usage")
|
|
165
|
+
if isinstance(usage, dict):
|
|
166
|
+
self._usage = _responses_usage(usage)
|
|
167
|
+
elif kind == "response.output_text.delta":
|
|
168
|
+
self._output_chars += len(_text(payload.get("delta")))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class GeminiUsageParser(_BaseParser):
|
|
172
|
+
"""Native Gemini streams usage in ``usageMetadata`` (often on every chunk)."""
|
|
173
|
+
|
|
174
|
+
def _handle(self, event: SSEEvent) -> None:
|
|
175
|
+
payload = _json(event.data)
|
|
176
|
+
if payload is None:
|
|
177
|
+
return
|
|
178
|
+
raw = payload.get("usageMetadata")
|
|
179
|
+
if isinstance(raw, dict):
|
|
180
|
+
self._usage = _gemini_usage(raw)
|
|
181
|
+
for candidate in payload.get("candidates") or []:
|
|
182
|
+
if not isinstance(candidate, dict):
|
|
183
|
+
continue
|
|
184
|
+
content = _dict(candidate.get("content"))
|
|
185
|
+
for part in content.get("parts") or []:
|
|
186
|
+
if isinstance(part, dict):
|
|
187
|
+
self._output_chars += len(_text(part.get("text")))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class OllamaUsageParser:
|
|
191
|
+
"""Ollama speaks newline-delimited JSON, not SSE."""
|
|
192
|
+
|
|
193
|
+
def __init__(self) -> None:
|
|
194
|
+
self._buffer = bytearray()
|
|
195
|
+
self._disabled = False
|
|
196
|
+
self._usage = Usage(source="none")
|
|
197
|
+
self._output_chars = 0
|
|
198
|
+
|
|
199
|
+
def feed(self, chunk: bytes) -> None:
|
|
200
|
+
if self._disabled:
|
|
201
|
+
return
|
|
202
|
+
self._buffer.extend(chunk)
|
|
203
|
+
while True:
|
|
204
|
+
index = self._buffer.find(b"\n")
|
|
205
|
+
if index < 0:
|
|
206
|
+
break
|
|
207
|
+
line = bytes(self._buffer[:index])
|
|
208
|
+
del self._buffer[: index + 1]
|
|
209
|
+
self._handle_line(line)
|
|
210
|
+
|
|
211
|
+
def disable(self) -> None:
|
|
212
|
+
self._disabled = True
|
|
213
|
+
|
|
214
|
+
def result(self) -> Usage:
|
|
215
|
+
if self._buffer:
|
|
216
|
+
self._handle_line(bytes(self._buffer))
|
|
217
|
+
self._buffer.clear()
|
|
218
|
+
if self._usage.source != "provider" and self._output_chars:
|
|
219
|
+
self._usage.output = max(1, math.ceil(self._output_chars / 3.5))
|
|
220
|
+
self._usage.source = "estimated"
|
|
221
|
+
return self._usage
|
|
222
|
+
|
|
223
|
+
def _handle_line(self, line: bytes) -> None:
|
|
224
|
+
if not line.strip():
|
|
225
|
+
return
|
|
226
|
+
try:
|
|
227
|
+
payload = orjson.loads(line)
|
|
228
|
+
except (orjson.JSONDecodeError, TypeError):
|
|
229
|
+
self.disable()
|
|
230
|
+
return
|
|
231
|
+
if not isinstance(payload, dict):
|
|
232
|
+
return
|
|
233
|
+
message = _dict(payload.get("message"))
|
|
234
|
+
self._output_chars += len(_text(message.get("content")))
|
|
235
|
+
self._output_chars += len(_text(payload.get("response")))
|
|
236
|
+
if "prompt_eval_count" in payload or "eval_count" in payload:
|
|
237
|
+
self._usage = _ollama_usage(payload)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def new_usage_parser(
|
|
241
|
+
provider: Provider, endpoint: Endpoint, profile: UpstreamProfile
|
|
242
|
+
) -> UsageParser:
|
|
243
|
+
"""Select the parser for one request from its endpoint and profile."""
|
|
244
|
+
if endpoint is Endpoint.RESPONSES:
|
|
245
|
+
return OpenAIResponsesUsageParser()
|
|
246
|
+
if profile.usage == "anthropic":
|
|
247
|
+
return AnthropicUsageParser()
|
|
248
|
+
if profile.usage == "gemini":
|
|
249
|
+
return GeminiUsageParser()
|
|
250
|
+
if profile.usage == "ollama":
|
|
251
|
+
return OllamaUsageParser()
|
|
252
|
+
if profile.usage in _CHAT_STYLES:
|
|
253
|
+
return OpenAIChatUsageParser(profile.usage)
|
|
254
|
+
return NullUsageParser()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def parse_usage_body(
|
|
258
|
+
provider: Provider, endpoint: Endpoint, profile: UpstreamProfile, body: bytes
|
|
259
|
+
) -> Usage:
|
|
260
|
+
"""Parse a non-streaming response body."""
|
|
261
|
+
try:
|
|
262
|
+
payload = orjson.loads(body)
|
|
263
|
+
except (orjson.JSONDecodeError, TypeError):
|
|
264
|
+
return Usage(source="none")
|
|
265
|
+
if not isinstance(payload, dict):
|
|
266
|
+
return Usage(source="none")
|
|
267
|
+
if endpoint is Endpoint.RESPONSES:
|
|
268
|
+
usage = payload.get("usage")
|
|
269
|
+
return _responses_usage(usage) if isinstance(usage, dict) else Usage(source="none")
|
|
270
|
+
if profile.usage == "gemini":
|
|
271
|
+
usage = payload.get("usageMetadata")
|
|
272
|
+
return _gemini_usage(usage) if isinstance(usage, dict) else Usage(source="none")
|
|
273
|
+
if profile.usage == "ollama":
|
|
274
|
+
return _ollama_usage(payload)
|
|
275
|
+
if profile.usage == "anthropic":
|
|
276
|
+
usage = payload.get("usage")
|
|
277
|
+
if not isinstance(usage, dict):
|
|
278
|
+
return Usage(source="none")
|
|
279
|
+
cache_creation = usage.get("cache_creation")
|
|
280
|
+
return _anthropic_usage(usage, cache_creation if isinstance(cache_creation, dict) else None)
|
|
281
|
+
if profile.usage in _CHAT_STYLES:
|
|
282
|
+
usage = payload.get("usage")
|
|
283
|
+
return (
|
|
284
|
+
_chat_usage(usage, profile.usage) if isinstance(usage, dict) else Usage(source="none")
|
|
285
|
+
)
|
|
286
|
+
return Usage(source="none")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _anthropic_usage(raw: dict[str, Any], cache_creation: dict[str, Any] | None) -> Usage:
|
|
290
|
+
write_5m = _int(_dict(cache_creation).get("ephemeral_5m_input_tokens"))
|
|
291
|
+
write_1h = _int(_dict(cache_creation).get("ephemeral_1h_input_tokens"))
|
|
292
|
+
total_write = _int(raw.get("cache_creation_input_tokens"))
|
|
293
|
+
if write_5m is None and write_1h is None and total_write:
|
|
294
|
+
write_5m = total_write
|
|
295
|
+
details = _dict(raw.get("output_tokens_details"))
|
|
296
|
+
raw_out: dict[str, object] = dict(raw)
|
|
297
|
+
if cache_creation:
|
|
298
|
+
raw_out["cache_creation"] = cache_creation
|
|
299
|
+
return Usage(
|
|
300
|
+
fresh_input=_int(raw.get("input_tokens")),
|
|
301
|
+
cache_read=_int(raw.get("cache_read_input_tokens")),
|
|
302
|
+
cache_write_5m=write_5m,
|
|
303
|
+
cache_write_1h=write_1h,
|
|
304
|
+
output=_int(raw.get("output_tokens")),
|
|
305
|
+
reasoning=_int(details.get("thinking_tokens")),
|
|
306
|
+
source="provider",
|
|
307
|
+
raw=raw_out,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _gemini_usage(raw: dict[str, Any]) -> Usage:
|
|
312
|
+
prompt = _int(raw.get("promptTokenCount")) or 0
|
|
313
|
+
cached = _int(raw.get("cachedContentTokenCount")) or 0
|
|
314
|
+
return Usage(
|
|
315
|
+
fresh_input=max(prompt - cached, 0),
|
|
316
|
+
cache_read=cached,
|
|
317
|
+
output=_int(raw.get("candidatesTokenCount")) or 0,
|
|
318
|
+
reasoning=_int(raw.get("thoughtsTokenCount")),
|
|
319
|
+
source="provider",
|
|
320
|
+
raw=dict(raw),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _ollama_usage(raw: dict[str, Any]) -> Usage:
|
|
325
|
+
prompt = _int(raw.get("prompt_eval_count"))
|
|
326
|
+
output = _int(raw.get("eval_count"))
|
|
327
|
+
if prompt is None and output is None:
|
|
328
|
+
return Usage(source="none")
|
|
329
|
+
cached = _int(raw.get("prompt_eval_cached_count"))
|
|
330
|
+
cached_tokens = cached or 0
|
|
331
|
+
return Usage(
|
|
332
|
+
fresh_input=max((prompt or 0) - cached_tokens, 0),
|
|
333
|
+
cache_read=cached_tokens if cached is not None else None,
|
|
334
|
+
output=output,
|
|
335
|
+
source="provider",
|
|
336
|
+
raw=dict(raw),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _chat_usage(raw: dict[str, Any], style: str) -> Usage:
|
|
341
|
+
prompt = _int(raw.get("prompt_tokens")) or 0
|
|
342
|
+
details = _dict(raw.get("prompt_tokens_details"))
|
|
343
|
+
cached = _int(details.get("cached_tokens")) or 0
|
|
344
|
+
written = _int(details.get("cache_write_tokens")) or 0
|
|
345
|
+
hit = _int(raw.get("prompt_cache_hit_tokens"))
|
|
346
|
+
miss = _int(raw.get("prompt_cache_miss_tokens"))
|
|
347
|
+
if hit is not None or miss is not None:
|
|
348
|
+
cached = hit or 0
|
|
349
|
+
fresh = miss if miss is not None else max(prompt - cached - written, 0)
|
|
350
|
+
else:
|
|
351
|
+
fresh = max(prompt - cached - written, 0)
|
|
352
|
+
completion_details = _dict(raw.get("completion_tokens_details"))
|
|
353
|
+
return Usage(
|
|
354
|
+
fresh_input=fresh,
|
|
355
|
+
cache_read=cached,
|
|
356
|
+
cache_write_30m=written,
|
|
357
|
+
output=_int(raw.get("completion_tokens")) or 0,
|
|
358
|
+
reasoning=_int(completion_details.get("reasoning_tokens")),
|
|
359
|
+
provider_cost_usd=_float(raw.get("cost")),
|
|
360
|
+
source="provider",
|
|
361
|
+
raw=dict(raw),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _responses_usage(raw: dict[str, Any]) -> Usage:
|
|
366
|
+
input_tokens = _int(raw.get("input_tokens")) or 0
|
|
367
|
+
details = _dict(raw.get("input_tokens_details"))
|
|
368
|
+
cached = _int(details.get("cached_tokens")) or 0
|
|
369
|
+
written = _int(details.get("cache_write_tokens")) or 0
|
|
370
|
+
write_details = _dict(details.get("cache_write_tokens_details"))
|
|
371
|
+
write_30m = _int(write_details.get("ttl_30m_tokens"))
|
|
372
|
+
write_12h = _int(write_details.get("ttl_12h_tokens"))
|
|
373
|
+
if write_30m is None and write_12h is None:
|
|
374
|
+
write_30m = written
|
|
375
|
+
output_details = _dict(raw.get("output_tokens_details"))
|
|
376
|
+
return Usage(
|
|
377
|
+
fresh_input=max(input_tokens - cached - written, 0),
|
|
378
|
+
cache_read=cached,
|
|
379
|
+
cache_write_30m=write_30m,
|
|
380
|
+
cache_write_12h=write_12h,
|
|
381
|
+
output=_int(raw.get("output_tokens")) or 0,
|
|
382
|
+
reasoning=_int(output_details.get("reasoning_tokens")),
|
|
383
|
+
provider_cost_usd=_float(raw.get("cost")),
|
|
384
|
+
source="provider",
|
|
385
|
+
raw=dict(raw),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _json(data: str) -> dict[str, Any] | None:
|
|
390
|
+
if not data:
|
|
391
|
+
return None
|
|
392
|
+
try:
|
|
393
|
+
value = orjson.loads(data)
|
|
394
|
+
except (orjson.JSONDecodeError, TypeError):
|
|
395
|
+
return None
|
|
396
|
+
return value if isinstance(value, dict) else None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _dict(value: Any) -> dict[str, Any]:
|
|
400
|
+
return value if isinstance(value, dict) else {}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _int(value: Any) -> int | None:
|
|
404
|
+
if isinstance(value, bool):
|
|
405
|
+
return None
|
|
406
|
+
if isinstance(value, int):
|
|
407
|
+
return value
|
|
408
|
+
if isinstance(value, float):
|
|
409
|
+
return int(value)
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _float(value: Any) -> float | None:
|
|
414
|
+
if isinstance(value, bool):
|
|
415
|
+
return None
|
|
416
|
+
if isinstance(value, (int, float)):
|
|
417
|
+
return float(value)
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _text(value: Any) -> str:
|
|
422
|
+
return value if isinstance(value, str) else ""
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
__all__ = [
|
|
426
|
+
"AnthropicUsageParser",
|
|
427
|
+
"GeminiUsageParser",
|
|
428
|
+
"NullUsageParser",
|
|
429
|
+
"OllamaUsageParser",
|
|
430
|
+
"OpenAIChatUsageParser",
|
|
431
|
+
"OpenAIResponsesUsageParser",
|
|
432
|
+
"UsageParser",
|
|
433
|
+
"new_usage_parser",
|
|
434
|
+
"parse_usage_body",
|
|
435
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Deterministic payload transforms and the P3/P4 registry (SPEC section 12)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ctxoptimizer.config import Config
|
|
6
|
+
from ctxoptimizer.transforms import ansi, dedupe_lines, progress, truncate, whitespace
|
|
7
|
+
from ctxoptimizer.transforms.anthropic_cache import AnthropicCache
|
|
8
|
+
from ctxoptimizer.transforms.backref import DuplicateBackref
|
|
9
|
+
from ctxoptimizer.transforms.base import TextTransform, Transform
|
|
10
|
+
from ctxoptimizer.transforms.cache_serve import CacheServe, CacheServeState
|
|
11
|
+
from ctxoptimizer.transforms.eligibility import is_compactable, is_never_touch
|
|
12
|
+
from ctxoptimizer.transforms.openai_hints import OpenAIHints
|
|
13
|
+
from ctxoptimizer.transforms.stale_result_elision import StaleResultElision
|
|
14
|
+
from ctxoptimizer.transforms.volatile import VolatileNormalizer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_transforms(
|
|
18
|
+
config: Config,
|
|
19
|
+
*,
|
|
20
|
+
disabled: set[str] | None = None,
|
|
21
|
+
cache_serve_state: CacheServeState | None = None,
|
|
22
|
+
) -> list[Transform]:
|
|
23
|
+
"""Return enabled transforms in the fixed pipeline order."""
|
|
24
|
+
transforms = config.transforms
|
|
25
|
+
removed = disabled or set()
|
|
26
|
+
result: list[Transform] = []
|
|
27
|
+
|
|
28
|
+
def add(transform: Transform) -> None:
|
|
29
|
+
if transform.id not in removed:
|
|
30
|
+
result.append(transform)
|
|
31
|
+
|
|
32
|
+
if transforms.ansi_strip.enabled:
|
|
33
|
+
add(
|
|
34
|
+
TextTransform(
|
|
35
|
+
"ansi_strip",
|
|
36
|
+
ansi.clean,
|
|
37
|
+
eligible=lambda ref: not is_never_touch(ref, config),
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
if transforms.progress_collapse.enabled:
|
|
41
|
+
add(
|
|
42
|
+
TextTransform(
|
|
43
|
+
"progress_collapse",
|
|
44
|
+
progress.clean,
|
|
45
|
+
eligible=lambda ref: not is_never_touch(ref, config),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
if transforms.line_dedupe.enabled:
|
|
49
|
+
add(
|
|
50
|
+
TextTransform(
|
|
51
|
+
"line_dedupe",
|
|
52
|
+
lambda text: dedupe_lines.clean(text, transforms.line_dedupe.min_repeat),
|
|
53
|
+
eligible=lambda ref: is_compactable(ref, config),
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
if transforms.whitespace_trim.enabled:
|
|
57
|
+
add(
|
|
58
|
+
TextTransform(
|
|
59
|
+
"whitespace_trim",
|
|
60
|
+
lambda text: whitespace.clean(text, transforms.whitespace_trim.max_blank_run),
|
|
61
|
+
eligible=lambda ref: is_compactable(ref, config),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
if transforms.smart_truncate.enabled:
|
|
65
|
+
add(
|
|
66
|
+
TextTransform(
|
|
67
|
+
"smart_truncate",
|
|
68
|
+
lambda text: truncate.clean(text, transforms.smart_truncate),
|
|
69
|
+
eligible=lambda ref: is_compactable(ref, config),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if transforms.duplicate_backref.enabled:
|
|
73
|
+
add(DuplicateBackref())
|
|
74
|
+
if transforms.cache_serve.enabled and cache_serve_state is not None:
|
|
75
|
+
add(CacheServe(cache_serve_state))
|
|
76
|
+
if transforms.stale_result_elision.enabled:
|
|
77
|
+
add(StaleResultElision())
|
|
78
|
+
if transforms.volatile_normalizer.enabled:
|
|
79
|
+
add(VolatileNormalizer())
|
|
80
|
+
if transforms.anthropic_cache.enabled:
|
|
81
|
+
add(AnthropicCache())
|
|
82
|
+
if transforms.openai_hints.enabled:
|
|
83
|
+
add(OpenAIHints())
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = ["build_transforms"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""ANSI and control-character stripping (SPEC section 12.1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
CSI = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
8
|
+
OSC = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)")
|
|
9
|
+
ESC = re.compile(r"\x1b[@-Z\\-_]")
|
|
10
|
+
C0 = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def clean(text: str) -> str:
|
|
14
|
+
"""Remove ANSI CSI/OSC/ESC sequences and stray C0 controls."""
|
|
15
|
+
text = CSI.sub("", text)
|
|
16
|
+
text = OSC.sub("", text)
|
|
17
|
+
text = ESC.sub("", text)
|
|
18
|
+
return C0.sub("", text)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["clean"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Anthropic cache_control breakpoint injection (SPEC section 12.9)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ctxoptimizer.config import Config
|
|
9
|
+
from ctxoptimizer.models import Endpoint, Provider, RequestContext, TransformReport
|
|
10
|
+
from ctxoptimizer.providers.base import ProviderAdapter, blocks_of
|
|
11
|
+
|
|
12
|
+
_CACHEABLE = {"text", "tool_use", "tool_result", "image", "document"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AnthropicCache:
|
|
16
|
+
id = "anthropic_cache"
|
|
17
|
+
|
|
18
|
+
def applies(
|
|
19
|
+
self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
|
|
20
|
+
) -> bool:
|
|
21
|
+
return (
|
|
22
|
+
ctx.config.transforms.anthropic_cache.enabled
|
|
23
|
+
and ctx.provider is Provider.ANTHROPIC
|
|
24
|
+
and ctx.endpoint is Endpoint.MESSAGES
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def apply(
|
|
28
|
+
self,
|
|
29
|
+
ctx: RequestContext,
|
|
30
|
+
payload: dict[str, Any],
|
|
31
|
+
adapter: ProviderAdapter,
|
|
32
|
+
config: Config,
|
|
33
|
+
) -> TransformReport:
|
|
34
|
+
started = time.perf_counter()
|
|
35
|
+
settings = config.transforms.anthropic_cache
|
|
36
|
+
existing = _count_markers(payload)
|
|
37
|
+
remaining = settings.max_breakpoints - existing
|
|
38
|
+
added: list[str] = []
|
|
39
|
+
if remaining > 0:
|
|
40
|
+
system = payload.get("system")
|
|
41
|
+
if isinstance(system, str):
|
|
42
|
+
payload["system"] = [{"type": "text", "text": system}]
|
|
43
|
+
system = payload["system"]
|
|
44
|
+
if isinstance(system, list) and _mark_last(system, settings.ttl, added):
|
|
45
|
+
remaining -= 1
|
|
46
|
+
elif not system:
|
|
47
|
+
tools = payload.get("tools")
|
|
48
|
+
if isinstance(tools, list) and tools and remaining > 0:
|
|
49
|
+
if _mark_last(tools, settings.ttl, added):
|
|
50
|
+
remaining -= 1
|
|
51
|
+
messages = payload.get("messages")
|
|
52
|
+
if isinstance(messages, list) and remaining > 0 and len(messages) >= 3:
|
|
53
|
+
if _mark_last(blocks_of(messages[-3].get("content")), settings.ttl, added):
|
|
54
|
+
remaining -= 1
|
|
55
|
+
if isinstance(messages, list) and remaining > 0 and messages:
|
|
56
|
+
if _mark_last(blocks_of(messages[-1].get("content")), settings.ttl, added):
|
|
57
|
+
remaining -= 1
|
|
58
|
+
return TransformReport(
|
|
59
|
+
transform_id=self.id,
|
|
60
|
+
ran=True,
|
|
61
|
+
changed=bool(added),
|
|
62
|
+
items_touched=len(added),
|
|
63
|
+
duration_ms=(time.perf_counter() - started) * 1000,
|
|
64
|
+
notes={"added": added, "existing": existing},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _count_markers(payload: dict[str, Any]) -> int:
|
|
69
|
+
total = 0
|
|
70
|
+
for block in payload.get("tools") or []:
|
|
71
|
+
if isinstance(block, dict) and "cache_control" in block:
|
|
72
|
+
total += 1
|
|
73
|
+
system = payload.get("system")
|
|
74
|
+
if isinstance(system, list):
|
|
75
|
+
total += sum(1 for block in system if isinstance(block, dict) and "cache_control" in block)
|
|
76
|
+
for message in payload.get("messages") or []:
|
|
77
|
+
if not isinstance(message, dict):
|
|
78
|
+
continue
|
|
79
|
+
total += sum(1 for block in blocks_of(message.get("content")) if "cache_control" in block)
|
|
80
|
+
return total
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _mark_last(blocks: list[dict[str, Any]], ttl: str, added: list[str]) -> bool:
|
|
84
|
+
for block in reversed(blocks):
|
|
85
|
+
if block.get("type") not in _CACHEABLE:
|
|
86
|
+
continue
|
|
87
|
+
if block.get("type") == "text" and not block.get("text"):
|
|
88
|
+
continue
|
|
89
|
+
if "cache_control" in block:
|
|
90
|
+
return False
|
|
91
|
+
marker: dict[str, Any] = {"type": "ephemeral"}
|
|
92
|
+
if ttl == "1h":
|
|
93
|
+
marker["ttl"] = "1h"
|
|
94
|
+
block["cache_control"] = marker
|
|
95
|
+
added.append(str(block.get("type")))
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = ["AnthropicCache"]
|