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,135 @@
1
+ """OpenAI Chat Completions adapter (SPEC section 11.2)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import Any
7
+
8
+ from ctxoptimizer.analysis.segments import Segment, make_segment
9
+ from ctxoptimizer.models import Provider, ToolResultRef
10
+ from ctxoptimizer.providers.base import DictToolResultRef, blocks_of, text_of
11
+
12
+
13
+ class OpenAIChatAdapter:
14
+ provider = Provider.OPENAI
15
+
16
+ def model_of(self, payload: dict[str, Any]) -> str | None:
17
+ model = payload.get("model")
18
+ return model if isinstance(model, str) else None
19
+
20
+ def is_stream(self, payload: dict[str, Any]) -> bool:
21
+ return bool(payload.get("stream"))
22
+
23
+ def is_transformable(self, payload: dict[str, Any]) -> bool:
24
+ return True
25
+
26
+ def iter_tool_results(self, payload: dict[str, Any]) -> Iterator[ToolResultRef]:
27
+ names = self._tool_call_names(payload)
28
+ for message_index, message in enumerate(payload.get("messages") or []):
29
+ if not isinstance(message, dict) or message.get("role") != "tool":
30
+ continue
31
+ call_id = str(message.get("tool_call_id") or "")
32
+ yield DictToolResultRef(
33
+ message,
34
+ message_index=message_index,
35
+ block_index=None,
36
+ tool_use_id=call_id,
37
+ tool_name=names.get(call_id),
38
+ is_error=False,
39
+ key="content",
40
+ )
41
+
42
+ def segments(self, payload: dict[str, Any]) -> list[Segment]:
43
+ result: list[Segment] = []
44
+ for index, tool in enumerate(payload.get("tools") or []):
45
+ if isinstance(tool, dict):
46
+ result.append(make_segment("tool_def", tool, block_index=index))
47
+ for message_index, message in enumerate(payload.get("messages") or []):
48
+ if not isinstance(message, dict):
49
+ continue
50
+ role = str(message.get("role") or "")
51
+ content = message.get("content")
52
+ if isinstance(content, str):
53
+ result.append(
54
+ make_segment(
55
+ "message",
56
+ {"role": role, "content": content},
57
+ msg_index=message_index,
58
+ block_index=0,
59
+ )
60
+ )
61
+ continue
62
+ if content is None:
63
+ tool_calls = message.get("tool_calls")
64
+ if isinstance(tool_calls, list) and tool_calls:
65
+ result.append(
66
+ make_segment(
67
+ "message",
68
+ {"role": role, "tool_calls": tool_calls},
69
+ msg_index=message_index,
70
+ )
71
+ )
72
+ continue
73
+ for block_index, block in enumerate(blocks_of(content)):
74
+ result.append(
75
+ make_segment(
76
+ "message",
77
+ {"role": role, **block},
78
+ msg_index=message_index,
79
+ block_index=block_index,
80
+ )
81
+ )
82
+ return result
83
+
84
+ def system_text(self, payload: dict[str, Any]) -> str:
85
+ parts = [
86
+ text_of(message.get("content"))
87
+ for message in payload.get("messages") or []
88
+ if isinstance(message, dict) and message.get("role") in {"system", "developer"}
89
+ ]
90
+ return "\n".join(part for part in parts if part)
91
+
92
+ def first_user_text(self, payload: dict[str, Any]) -> str:
93
+ for message in payload.get("messages") or []:
94
+ if isinstance(message, dict) and message.get("role") == "user":
95
+ return text_of(message.get("content"))
96
+ return ""
97
+
98
+ def tool_names(self, payload: dict[str, Any]) -> list[str]:
99
+ names: list[str] = []
100
+ for tool in payload.get("tools") or []:
101
+ if not isinstance(tool, dict):
102
+ continue
103
+ function = tool.get("function")
104
+ if isinstance(function, dict) and isinstance(function.get("name"), str):
105
+ names.append(function["name"])
106
+ return names
107
+
108
+ def used_tool_names(self, payload: dict[str, Any]) -> list[str]:
109
+ names: list[str] = []
110
+ for message in payload.get("messages") or []:
111
+ if not isinstance(message, dict):
112
+ continue
113
+ for call in message.get("tool_calls") or []:
114
+ if not isinstance(call, dict):
115
+ continue
116
+ function = call.get("function")
117
+ if isinstance(function, dict) and isinstance(function.get("name"), str):
118
+ names.append(function["name"])
119
+ return names
120
+
121
+ def _tool_call_names(self, payload: dict[str, Any]) -> dict[str, str]:
122
+ names: dict[str, str] = {}
123
+ for message in payload.get("messages") or []:
124
+ if not isinstance(message, dict):
125
+ continue
126
+ for call in message.get("tool_calls") or []:
127
+ if not isinstance(call, dict):
128
+ continue
129
+ function = call.get("function")
130
+ if isinstance(call.get("id"), str) and isinstance(function, dict):
131
+ names[call["id"]] = str(function.get("name") or "")
132
+ return names
133
+
134
+
135
+ __all__ = ["OpenAIChatAdapter"]
@@ -0,0 +1,121 @@
1
+ """OpenAI Responses adapter (SPEC section 11.3)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import Any
7
+
8
+ from ctxoptimizer.analysis.segments import Segment, make_segment
9
+ from ctxoptimizer.models import Provider, ToolResultRef
10
+ from ctxoptimizer.providers.base import DictToolResultRef, blocks_of, text_of
11
+
12
+
13
+ class OpenAIResponsesAdapter:
14
+ provider = Provider.OPENAI
15
+
16
+ def model_of(self, payload: dict[str, Any]) -> str | None:
17
+ model = payload.get("model")
18
+ return model if isinstance(model, str) else None
19
+
20
+ def is_stream(self, payload: dict[str, Any]) -> bool:
21
+ return bool(payload.get("stream"))
22
+
23
+ def is_transformable(self, payload: dict[str, Any]) -> bool:
24
+ return not bool(payload.get("previous_response_id"))
25
+
26
+ def iter_tool_results(self, payload: dict[str, Any]) -> Iterator[ToolResultRef]:
27
+ names = self._call_names(payload)
28
+ for message_index, item in enumerate(self._input_items(payload)):
29
+ if not isinstance(item, dict) or item.get("type") != "function_call_output":
30
+ continue
31
+ call_id = str(item.get("call_id") or "")
32
+ yield DictToolResultRef(
33
+ item,
34
+ message_index=message_index,
35
+ block_index=None,
36
+ tool_use_id=call_id,
37
+ tool_name=names.get(call_id),
38
+ is_error=False,
39
+ key="output",
40
+ )
41
+
42
+ def segments(self, payload: dict[str, Any]) -> list[Segment]:
43
+ result: list[Segment] = []
44
+ instructions = payload.get("instructions")
45
+ if isinstance(instructions, str) and instructions:
46
+ result.append(
47
+ make_segment(
48
+ "system",
49
+ {"type": "text", "text": instructions},
50
+ block_index=0,
51
+ )
52
+ )
53
+ for index, tool in enumerate(payload.get("tools") or []):
54
+ if isinstance(tool, dict):
55
+ result.append(make_segment("tool_def", tool, block_index=index))
56
+ for message_index, item in enumerate(self._input_items(payload)):
57
+ if not isinstance(item, dict):
58
+ continue
59
+ if item.get("type") in {"message", None} and item.get("role"):
60
+ role = str(item["role"])
61
+ content = item.get("content")
62
+ if isinstance(content, str):
63
+ result.append(
64
+ make_segment(
65
+ "message",
66
+ {"role": role, "content": content},
67
+ msg_index=message_index,
68
+ block_index=0,
69
+ )
70
+ )
71
+ continue
72
+ for block_index, block in enumerate(blocks_of(content)):
73
+ result.append(
74
+ make_segment(
75
+ "message",
76
+ {"role": role, **block},
77
+ msg_index=message_index,
78
+ block_index=block_index,
79
+ )
80
+ )
81
+ else:
82
+ result.append(make_segment("message", item, msg_index=message_index))
83
+ return result
84
+
85
+ def system_text(self, payload: dict[str, Any]) -> str:
86
+ return payload["instructions"] if isinstance(payload.get("instructions"), str) else ""
87
+
88
+ def first_user_text(self, payload: dict[str, Any]) -> str:
89
+ for item in self._input_items(payload):
90
+ if isinstance(item, dict) and item.get("role") == "user":
91
+ return text_of(item.get("content"))
92
+ return ""
93
+
94
+ def tool_names(self, payload: dict[str, Any]) -> list[str]:
95
+ return [
96
+ str(tool["name"])
97
+ for tool in payload.get("tools") or []
98
+ if isinstance(tool, dict) and isinstance(tool.get("name"), str)
99
+ ]
100
+
101
+ def used_tool_names(self, payload: dict[str, Any]) -> list[str]:
102
+ return list(self._call_names(payload).values())
103
+
104
+ def _input_items(self, payload: dict[str, Any]) -> list[Any]:
105
+ value = payload.get("input")
106
+ if isinstance(value, str):
107
+ return [{"type": "message", "role": "user", "content": value}]
108
+ return value if isinstance(value, list) else []
109
+
110
+ def _call_names(self, payload: dict[str, Any]) -> dict[str, str]:
111
+ names: dict[str, str] = {}
112
+ for item in self._input_items(payload):
113
+ if isinstance(item, dict) and item.get("type") == "function_call":
114
+ call_id = item.get("call_id")
115
+ name = item.get("name")
116
+ if isinstance(call_id, str) and isinstance(name, str):
117
+ names[call_id] = name
118
+ return names
119
+
120
+
121
+ __all__ = ["OpenAIResponsesAdapter"]