synapse-cli-agent 0.1.13__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.
- synapse/__init__.py +13 -0
- synapse/__main__.py +6 -0
- synapse/app/__init__.py +1 -0
- synapse/app/agent.py +492 -0
- synapse/app/agent_md.py +107 -0
- synapse/cli.py +750 -0
- synapse/commands/__init__.py +1 -0
- synapse/commands/compression.py +573 -0
- synapse/commands/helpers.py +22 -0
- synapse/commands/mcp.py +406 -0
- synapse/commands/model.py +173 -0
- synapse/commands/result.py +34 -0
- synapse/commands/sessions.py +443 -0
- synapse/commands/slash_cmds.py +521 -0
- synapse/commands/slash_complete.py +816 -0
- synapse/commands/theme.py +99 -0
- synapse/config.py +27 -0
- synapse/content/__init__.py +1 -0
- synapse/content/input_history.py +122 -0
- synapse/content/multimodal.py +733 -0
- synapse/content/prompts.py +249 -0
- synapse/content/skills_catalog.py +128 -0
- synapse/integrations/__init__.py +1 -0
- synapse/integrations/checkpoint_seed.py +281 -0
- synapse/integrations/codex_history.py +375 -0
- synapse/integrations/codex_import.py +393 -0
- synapse/integrations/codex_sessions.py +629 -0
- synapse/integrations/describe_image.py +370 -0
- synapse/integrations/http_clients.py +199 -0
- synapse/integrations/llm_openai_compat.py +90 -0
- synapse/integrations/llm_openai_websocket.py +187 -0
- synapse/integrations/mcp_client.py +646 -0
- synapse/integrations/vision_middleware.py +62 -0
- synapse/models/__init__.py +5 -0
- synapse/models/config.py +240 -0
- synapse/models/helpers.py +206 -0
- synapse/models/profile.py +59 -0
- synapse/models/registry.py +722 -0
- synapse/models_registry.py +7 -0
- synapse/observability/__init__.py +1 -0
- synapse/observability/startup_trace.py +127 -0
- synapse/runtime/__init__.py +1 -0
- synapse/runtime/async_runtime.py +176 -0
- synapse/runtime/backends.py +458 -0
- synapse/runtime/context_compact.py +249 -0
- synapse/runtime/execute_capture.py +48 -0
- synapse/runtime/fs_permissions.py +79 -0
- synapse/runtime/harness.py +57 -0
- synapse/runtime/hitl.py +197 -0
- synapse/runtime/interaction_ledger.py +82 -0
- synapse/runtime/middleware.py +802 -0
- synapse/runtime/model_request_compression_middleware.py +745 -0
- synapse/runtime/pathing.py +146 -0
- synapse/runtime/safety.py +184 -0
- synapse/runtime/steer.py +240 -0
- synapse/runtime/subagents.py +207 -0
- synapse/runtime/tool_ignore.py +221 -0
- synapse/runtime/tool_output_eval.py +118 -0
- synapse/runtime/tool_output_middleware.py +585 -0
- synapse/runtime/tool_output_usage_middleware.py +60 -0
- synapse/sessions/__init__.py +31 -0
- synapse/sessions/cancel_repair.py +208 -0
- synapse/sessions/session_recap.py +174 -0
- synapse/sessions/store.py +695 -0
- synapse/sessions/transcript.py +754 -0
- synapse/settings/__init__.py +5 -0
- synapse/settings/config_paths.py +184 -0
- synapse/settings/schema.py +464 -0
- synapse/tool_output/__init__.py +59 -0
- synapse/tool_output/detection.py +170 -0
- synapse/tool_output/metrics.py +32 -0
- synapse/tool_output/models.py +173 -0
- synapse/tool_output/pipeline.py +330 -0
- synapse/tool_output/repository.py +721 -0
- synapse/tool_output/transformers.py +648 -0
- synapse/tools/__init__.py +5 -0
- synapse/tools/session_tools.py +204 -0
- synapse/ui/__init__.py +10 -0
- synapse/ui/bottombar/__init__.py +73 -0
- synapse/ui/bottombar/components/__init__.py +143 -0
- synapse/ui/bottombar/components/key_hints.py +30 -0
- synapse/ui/bottombar/components/mcp.py +64 -0
- synapse/ui/bottombar/components/mode.py +24 -0
- synapse/ui/bottombar/components/model.py +28 -0
- synapse/ui/bottombar/components/thread.py +29 -0
- synapse/ui/bottombar/context.py +36 -0
- synapse/ui/bottombar/core.py +74 -0
- synapse/ui/dialogs/__init__.py +25 -0
- synapse/ui/dialogs/base.py +362 -0
- synapse/ui/dialogs/codex_session_list.py +84 -0
- synapse/ui/dialogs/compression_diagnostics.py +210 -0
- synapse/ui/dialogs/git_explore.py +702 -0
- synapse/ui/dialogs/mcp_panel.py +407 -0
- synapse/ui/dialogs/model_picker.py +128 -0
- synapse/ui/dialogs/safety_panel.py +63 -0
- synapse/ui/dialogs/session_list.py +98 -0
- synapse/ui/dialogs/theme_designer.py +863 -0
- synapse/ui/dialogs/theme_picker.py +113 -0
- synapse/ui/git_explore/__init__.py +31 -0
- synapse/ui/git_explore/engine.py +82 -0
- synapse/ui/git_explore/provider.py +242 -0
- synapse/ui/git_explore/unified.py +85 -0
- synapse/ui/rendering.py +350 -0
- synapse/ui/sink.py +70 -0
- synapse/ui/steer_widget.py +367 -0
- synapse/ui/stream.py +1207 -0
- synapse/ui/stream_events.py +421 -0
- synapse/ui/stream_runtime.py +252 -0
- synapse/ui/theme.py +1154 -0
- synapse/ui/timeline.py +621 -0
- synapse/ui/topbar/__init__.py +97 -0
- synapse/ui/topbar/components/__init__.py +150 -0
- synapse/ui/topbar/components/branch.py +41 -0
- synapse/ui/topbar/components/title.py +24 -0
- synapse/ui/topbar/components/tool_output.py +24 -0
- synapse/ui/topbar/components/usage.py +24 -0
- synapse/ui/topbar/components/workspace.py +32 -0
- synapse/ui/topbar/context.py +32 -0
- synapse/ui/topbar/core.py +979 -0
- synapse/ui/topbar/git_changes_popover.py +178 -0
- synapse/ui/topbar/git_chrome.py +475 -0
- synapse/ui/topbar/tool_output_popover.py +84 -0
- synapse/ui/topbar/widget.py +474 -0
- synapse/ui/tui.py +5717 -0
- synapse/ui/turn_rail.py +71 -0
- synapse/ui/user_turn.py +83 -0
- synapse/ui/welcome.py +261 -0
- synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
- synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
- synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
- synapse_cli_agent-0.1.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
"""Request-level compression accounting and provider mutation-safety diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from langchain.agents.middleware import AgentMiddleware, AgentState
|
|
12
|
+
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
13
|
+
from langchain_core.messages.utils import count_tokens_approximately
|
|
14
|
+
|
|
15
|
+
from synapse.runtime.interaction_ledger import begin_model_call
|
|
16
|
+
from synapse.tool_output.models import ModelRequestCompressionEvent
|
|
17
|
+
from synapse.tool_output.repository import ToolOutputRepository
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _runtime_config(request: Any) -> dict[str, Any]:
|
|
21
|
+
"""Return the active Runnable config for a model middleware request."""
|
|
22
|
+
runtime = getattr(request, "runtime", None)
|
|
23
|
+
config = getattr(runtime, "config", None)
|
|
24
|
+
try:
|
|
25
|
+
from langgraph.config import get_config
|
|
26
|
+
|
|
27
|
+
active = get_config()
|
|
28
|
+
if active:
|
|
29
|
+
config = active
|
|
30
|
+
except (ImportError, RuntimeError):
|
|
31
|
+
pass
|
|
32
|
+
return dict(config) if isinstance(config, dict) else {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _thread_id(request: Any) -> str:
|
|
36
|
+
configurable = _runtime_config(request).get("configurable") or {}
|
|
37
|
+
return str((configurable or {}).get("thread_id") or "")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _messages(request: Any) -> list[Any]:
|
|
41
|
+
messages = list(getattr(request, "messages", None) or [])
|
|
42
|
+
system = getattr(request, "system_message", None)
|
|
43
|
+
return [system, *messages] if system is not None else messages
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _state_messages(request: Any) -> list[Any]:
|
|
47
|
+
state = getattr(request, "state", None) or {}
|
|
48
|
+
return list(state.get("messages") or []) if isinstance(state, dict) else []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _count(messages: list[Any], tools: list[Any] | None = None) -> int:
|
|
52
|
+
try:
|
|
53
|
+
return max(0, int(count_tokens_approximately(messages, tools=tools or [])))
|
|
54
|
+
except Exception: # noqa: BLE001
|
|
55
|
+
return max(0, sum((len(str(getattr(msg, "content", msg))) + 3) // 4 for msg in messages))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _message_content_tokens(message: Any) -> int:
|
|
59
|
+
"""Approximate one message's content tokens without role/schema overhead."""
|
|
60
|
+
content = getattr(message, "content", "")
|
|
61
|
+
try:
|
|
62
|
+
if isinstance(content, str):
|
|
63
|
+
return max(0, (len(content) + 3) // 4)
|
|
64
|
+
return max(0, (len(str(content)) + 3) // 4)
|
|
65
|
+
except Exception: # noqa: BLE001
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _reasoning_tokens(message: Any) -> int:
|
|
70
|
+
total = 0
|
|
71
|
+
additional = getattr(message, "additional_kwargs", None) or {}
|
|
72
|
+
metadata = getattr(message, "response_metadata", None) or {}
|
|
73
|
+
for source in (additional, metadata):
|
|
74
|
+
if not isinstance(source, dict):
|
|
75
|
+
continue
|
|
76
|
+
for key in ("reasoning_content", "encrypted_content", "thinking"):
|
|
77
|
+
value = source.get(key)
|
|
78
|
+
if value:
|
|
79
|
+
total += max(1, (len(str(value)) + 3) // 4)
|
|
80
|
+
return total
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _tool_call_argument_tokens(message: Any) -> int:
|
|
84
|
+
calls = getattr(message, "tool_calls", None) or []
|
|
85
|
+
total = 0
|
|
86
|
+
for call in calls:
|
|
87
|
+
args = call.get("args") if isinstance(call, dict) else getattr(call, "args", None)
|
|
88
|
+
if args is not None:
|
|
89
|
+
total += max(0, (len(str(args)) + 3) // 4)
|
|
90
|
+
return total
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _tool_schema_tokens(tools: list[Any]) -> int:
|
|
94
|
+
if not tools:
|
|
95
|
+
return 0
|
|
96
|
+
baseline = _count([], tools)
|
|
97
|
+
return max(0, baseline - _count([]))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _content_breakdown(request: Any) -> dict[str, int]:
|
|
101
|
+
"""Classify the final model request into actionable token-source buckets."""
|
|
102
|
+
messages = list(getattr(request, "messages", None) or [])
|
|
103
|
+
system = getattr(request, "system_message", None)
|
|
104
|
+
tools = list(getattr(request, "tools", None) or [])
|
|
105
|
+
breakdown = {
|
|
106
|
+
"system": _count([system]) if system is not None else 0,
|
|
107
|
+
"tool_schemas": _tool_schema_tokens(tools),
|
|
108
|
+
"historical_user": 0,
|
|
109
|
+
"current_user": 0,
|
|
110
|
+
"assistant_content": 0,
|
|
111
|
+
"reasoning": 0,
|
|
112
|
+
"tool_call_arguments": 0,
|
|
113
|
+
"tool_output_visible": 0,
|
|
114
|
+
"tool_output_original": 0,
|
|
115
|
+
"unknown": 0,
|
|
116
|
+
}
|
|
117
|
+
human_indexes = [index for index, msg in enumerate(messages) if isinstance(msg, HumanMessage)]
|
|
118
|
+
latest_human = human_indexes[-1] if human_indexes else -1
|
|
119
|
+
for index, message in enumerate(messages):
|
|
120
|
+
content_tokens = _message_content_tokens(message)
|
|
121
|
+
if isinstance(message, ToolMessage):
|
|
122
|
+
breakdown["tool_output_visible"] += _count([message])
|
|
123
|
+
artifact = getattr(message, "artifact", None) or {}
|
|
124
|
+
transform = (
|
|
125
|
+
artifact.get("tool_output_transform")
|
|
126
|
+
if isinstance(artifact, dict)
|
|
127
|
+
else None
|
|
128
|
+
)
|
|
129
|
+
visible_tokens = _count([message])
|
|
130
|
+
if isinstance(transform, dict):
|
|
131
|
+
explicit_original = int(transform.get("estimated_original_tokens", 0) or 0)
|
|
132
|
+
saved_tokens = int(transform.get("estimated_saved_tokens", 0) or 0)
|
|
133
|
+
breakdown["tool_output_original"] += max(
|
|
134
|
+
explicit_original, visible_tokens + saved_tokens
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
breakdown["tool_output_original"] += visible_tokens
|
|
138
|
+
continue
|
|
139
|
+
if isinstance(message, HumanMessage):
|
|
140
|
+
key = "current_user" if index == latest_human else "historical_user"
|
|
141
|
+
breakdown[key] += content_tokens
|
|
142
|
+
continue
|
|
143
|
+
if isinstance(message, AIMessage):
|
|
144
|
+
reasoning = _reasoning_tokens(message)
|
|
145
|
+
args = _tool_call_argument_tokens(message)
|
|
146
|
+
breakdown["reasoning"] += reasoning
|
|
147
|
+
breakdown["tool_call_arguments"] += args
|
|
148
|
+
breakdown["assistant_content"] += content_tokens
|
|
149
|
+
continue
|
|
150
|
+
breakdown["unknown"] += content_tokens
|
|
151
|
+
classified = sum(
|
|
152
|
+
breakdown[key]
|
|
153
|
+
for key in (
|
|
154
|
+
"system",
|
|
155
|
+
"tool_schemas",
|
|
156
|
+
"historical_user",
|
|
157
|
+
"current_user",
|
|
158
|
+
"assistant_content",
|
|
159
|
+
"reasoning",
|
|
160
|
+
"tool_call_arguments",
|
|
161
|
+
"tool_output_visible",
|
|
162
|
+
"unknown",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
total = _count(([system] if system is not None else []) + messages, tools)
|
|
166
|
+
breakdown["unknown"] += max(0, total - classified)
|
|
167
|
+
return breakdown
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _opportunities(breakdown: dict[str, int]) -> dict[str, int]:
|
|
171
|
+
"""Rank unoptimized token sources without prescribing an implementation."""
|
|
172
|
+
opportunities: dict[str, int] = {}
|
|
173
|
+
mappings = {
|
|
174
|
+
"tool_schemas": "tool_schema_fixed_overhead",
|
|
175
|
+
"historical_user": "historical_user_context",
|
|
176
|
+
"current_user": "current_user_not_in_pipeline",
|
|
177
|
+
"assistant_content": "assistant_history_not_in_pipeline",
|
|
178
|
+
"reasoning": "reasoning_not_in_pipeline",
|
|
179
|
+
"tool_call_arguments": "tool_call_arguments_not_in_pipeline",
|
|
180
|
+
}
|
|
181
|
+
for source, reason in mappings.items():
|
|
182
|
+
tokens = max(0, int(breakdown.get(source, 0) or 0))
|
|
183
|
+
if tokens:
|
|
184
|
+
opportunities[reason] = tokens
|
|
185
|
+
original = max(0, int(breakdown.get("tool_output_original", 0) or 0))
|
|
186
|
+
visible = max(0, int(breakdown.get("tool_output_visible", 0) or 0))
|
|
187
|
+
if visible and original <= visible:
|
|
188
|
+
opportunities["uncompressed_tool_outputs"] = visible
|
|
189
|
+
if breakdown.get("unknown", 0):
|
|
190
|
+
opportunities["unknown_request_overhead"] = int(breakdown["unknown"])
|
|
191
|
+
return opportunities
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _tool_output_savings(messages: list[Any]) -> tuple[int, int, int]:
|
|
195
|
+
saved = 0
|
|
196
|
+
candidates = 0
|
|
197
|
+
transformed = 0
|
|
198
|
+
for message in messages:
|
|
199
|
+
artifact = getattr(message, "artifact", None)
|
|
200
|
+
if not isinstance(artifact, dict):
|
|
201
|
+
continue
|
|
202
|
+
transform = artifact.get("tool_output_transform")
|
|
203
|
+
if not isinstance(transform, dict):
|
|
204
|
+
continue
|
|
205
|
+
candidates += 1
|
|
206
|
+
if str(transform.get("decision") or "transformed") == "transformed":
|
|
207
|
+
transformed += 1
|
|
208
|
+
saved += max(0, int(transform.get("estimated_saved_tokens", 0) or 0))
|
|
209
|
+
return saved, candidates, transformed
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _model_identity(request: Any) -> tuple[str, str, str, str]:
|
|
213
|
+
model = getattr(request, "model", None)
|
|
214
|
+
model_name = str(
|
|
215
|
+
getattr(model, "model_name", None)
|
|
216
|
+
or getattr(model, "model", None)
|
|
217
|
+
or getattr(model, "model_id", None)
|
|
218
|
+
or model.__class__.__name__
|
|
219
|
+
)
|
|
220
|
+
class_name = model.__class__.__name__.casefold()
|
|
221
|
+
base_url = str(
|
|
222
|
+
getattr(model, "openai_api_base", None)
|
|
223
|
+
or getattr(model, "base_url", None)
|
|
224
|
+
or ""
|
|
225
|
+
).casefold()
|
|
226
|
+
if "anthropic" in class_name or "claude" in model_name.casefold():
|
|
227
|
+
return "anthropic", "messages", "payg", model_name
|
|
228
|
+
if "chatgpt.com/backend-api/codex" in base_url or "codex" in model_name.casefold():
|
|
229
|
+
return "openai", "responses", "subscription", model_name
|
|
230
|
+
if "openai" in class_name or "openai" in base_url:
|
|
231
|
+
use_responses = bool(
|
|
232
|
+
getattr(model, "use_responses_api", False)
|
|
233
|
+
or getattr(model, "_use_responses_api", False)
|
|
234
|
+
)
|
|
235
|
+
return "openai", "responses" if use_responses else "chat-completions", "payg", model_name
|
|
236
|
+
return "unknown", "langchain", "unknown", model_name
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _content_has_cache_control(message: Any) -> bool:
|
|
240
|
+
content = getattr(message, "content", None)
|
|
241
|
+
if isinstance(content, list):
|
|
242
|
+
return any(isinstance(block, dict) and block.get("cache_control") for block in content)
|
|
243
|
+
additional = getattr(message, "additional_kwargs", None) or {}
|
|
244
|
+
return bool(isinstance(additional, dict) and additional.get("cache_control"))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _provider_protected_tokens(request: Any, provider: str, api_style: str) -> dict[str, int]:
|
|
248
|
+
messages = list(getattr(request, "messages", None) or [])
|
|
249
|
+
protected: dict[str, int] = {}
|
|
250
|
+
if provider == "anthropic":
|
|
251
|
+
marker = -1
|
|
252
|
+
for index, message in enumerate(messages):
|
|
253
|
+
if _content_has_cache_control(message):
|
|
254
|
+
marker = index
|
|
255
|
+
if marker >= 0:
|
|
256
|
+
protected["anthropic_before_cache_control"] = _count(messages[: marker + 1])
|
|
257
|
+
return protected
|
|
258
|
+
|
|
259
|
+
tool_indexes = [index for index, msg in enumerate(messages) if isinstance(msg, ToolMessage)]
|
|
260
|
+
if tool_indexes:
|
|
261
|
+
older = [messages[index] for index in tool_indexes[:-1]]
|
|
262
|
+
if older:
|
|
263
|
+
reason = (
|
|
264
|
+
"codex_historical_output"
|
|
265
|
+
if api_style == "responses"
|
|
266
|
+
else "openai_historical_message"
|
|
267
|
+
)
|
|
268
|
+
protected[reason] = _count(older)
|
|
269
|
+
if api_style == "responses":
|
|
270
|
+
reasoning_tokens = 0
|
|
271
|
+
for message in messages:
|
|
272
|
+
additional = getattr(message, "additional_kwargs", None) or {}
|
|
273
|
+
response = getattr(message, "response_metadata", None) or {}
|
|
274
|
+
if not isinstance(additional, dict) or not isinstance(response, dict):
|
|
275
|
+
continue
|
|
276
|
+
reasoning = additional.get("reasoning_content") or response.get("reasoning_content")
|
|
277
|
+
encrypted = additional.get("encrypted_content") or response.get("encrypted_content")
|
|
278
|
+
if reasoning or encrypted:
|
|
279
|
+
reasoning_tokens += max(1, (len(str(reasoning or encrypted)) + 3) // 4)
|
|
280
|
+
if reasoning_tokens:
|
|
281
|
+
protected["codex_reasoning_protected"] = reasoning_tokens
|
|
282
|
+
return protected
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _response_messages(response: Any) -> list[Any]:
|
|
286
|
+
model_response = getattr(response, "model_response", response)
|
|
287
|
+
return list(getattr(model_response, "result", None) or [])
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _usage(response: Any) -> dict[str, int]:
|
|
291
|
+
values = {
|
|
292
|
+
"input_tokens": 0,
|
|
293
|
+
"output_tokens": 0,
|
|
294
|
+
"cache_read_tokens": 0,
|
|
295
|
+
"cache_write_tokens": 0,
|
|
296
|
+
"uncached_input_tokens": 0,
|
|
297
|
+
}
|
|
298
|
+
for message in _response_messages(response):
|
|
299
|
+
usage = getattr(message, "usage_metadata", None) or {}
|
|
300
|
+
metadata = getattr(message, "response_metadata", None) or {}
|
|
301
|
+
token_usage = metadata.get("token_usage") if isinstance(metadata, dict) else {}
|
|
302
|
+
if not isinstance(usage, dict):
|
|
303
|
+
usage = {
|
|
304
|
+
"input_tokens": getattr(usage, "input_tokens", 0),
|
|
305
|
+
"output_tokens": getattr(usage, "output_tokens", 0),
|
|
306
|
+
"input_token_details": getattr(usage, "input_token_details", None),
|
|
307
|
+
}
|
|
308
|
+
values["input_tokens"] += int(usage.get("input_tokens", 0) or 0)
|
|
309
|
+
values["output_tokens"] += int(usage.get("output_tokens", 0) or 0)
|
|
310
|
+
details = usage.get("input_token_details") or {}
|
|
311
|
+
if not isinstance(details, dict):
|
|
312
|
+
details = vars(details) if hasattr(details, "__dict__") else {}
|
|
313
|
+
token_usage = token_usage if isinstance(token_usage, dict) else {}
|
|
314
|
+
for key in ("cache_read", "cache_read_tokens", "cached_tokens"):
|
|
315
|
+
if details.get(key) is not None:
|
|
316
|
+
values["cache_read_tokens"] += int(details.get(key) or 0)
|
|
317
|
+
break
|
|
318
|
+
else:
|
|
319
|
+
values["cache_read_tokens"] += int(
|
|
320
|
+
token_usage.get("cache_read_input_tokens", 0)
|
|
321
|
+
or token_usage.get("cached_tokens", 0)
|
|
322
|
+
or 0
|
|
323
|
+
)
|
|
324
|
+
values["cache_write_tokens"] += int(
|
|
325
|
+
details.get("cache_creation", 0)
|
|
326
|
+
or details.get("cache_write", 0)
|
|
327
|
+
or token_usage.get("cache_creation_input_tokens", 0)
|
|
328
|
+
or 0
|
|
329
|
+
)
|
|
330
|
+
values["uncached_input_tokens"] = max(
|
|
331
|
+
0,
|
|
332
|
+
values["input_tokens"] - values["cache_read_tokens"] - values["cache_write_tokens"],
|
|
333
|
+
)
|
|
334
|
+
return values
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _stable_json(value: Any) -> str:
|
|
338
|
+
try:
|
|
339
|
+
if hasattr(value, "model_dump"):
|
|
340
|
+
value = value.model_dump()
|
|
341
|
+
elif not isinstance(value, dict | list | tuple | str | int | float | bool | type(None)):
|
|
342
|
+
value = vars(value) if hasattr(value, "__dict__") else str(value)
|
|
343
|
+
return json.dumps(
|
|
344
|
+
value,
|
|
345
|
+
ensure_ascii=False,
|
|
346
|
+
sort_keys=True,
|
|
347
|
+
default=str,
|
|
348
|
+
separators=(",", ":"),
|
|
349
|
+
)
|
|
350
|
+
except Exception: # noqa: BLE001
|
|
351
|
+
return str(value)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _fingerprint(value: Any) -> str:
|
|
355
|
+
return hashlib.sha256(_stable_json(value).encode("utf-8")).hexdigest()[:16]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _current_user_content(messages: list[Any]) -> Any:
|
|
359
|
+
for message in reversed(messages):
|
|
360
|
+
if isinstance(message, HumanMessage):
|
|
361
|
+
return getattr(message, "content", "")
|
|
362
|
+
return ""
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _live_zone_plan(request: Any, provider: str, api_style: str) -> list[dict[str, Any]]:
|
|
366
|
+
messages = list(getattr(request, "messages", None) or [])
|
|
367
|
+
latest_user = max(
|
|
368
|
+
(index for index, message in enumerate(messages) if isinstance(message, HumanMessage)),
|
|
369
|
+
default=-1,
|
|
370
|
+
)
|
|
371
|
+
latest_tool = max(
|
|
372
|
+
(index for index, message in enumerate(messages) if isinstance(message, ToolMessage)),
|
|
373
|
+
default=-1,
|
|
374
|
+
)
|
|
375
|
+
plan: list[dict[str, Any]] = []
|
|
376
|
+
for index, message in enumerate(messages):
|
|
377
|
+
tokens = _count([message])
|
|
378
|
+
zone = "frozen"
|
|
379
|
+
reason = "historical_message"
|
|
380
|
+
if isinstance(message, AIMessage) and _reasoning_tokens(message):
|
|
381
|
+
zone, reason = "protected", "reasoning_protected"
|
|
382
|
+
elif provider == "anthropic":
|
|
383
|
+
additional = getattr(message, "additional_kwargs", None) or {}
|
|
384
|
+
if additional.get("cache_control"):
|
|
385
|
+
zone, reason = "frozen", "anthropic_cache_boundary"
|
|
386
|
+
elif index >= latest_user:
|
|
387
|
+
zone, reason = "live", "anthropic_after_latest_user"
|
|
388
|
+
else:
|
|
389
|
+
reason = "anthropic_before_live_zone"
|
|
390
|
+
elif api_style == "responses":
|
|
391
|
+
if isinstance(message, HumanMessage) and index == latest_user:
|
|
392
|
+
zone, reason = "live", "responses_latest_user"
|
|
393
|
+
elif isinstance(message, ToolMessage) and index == latest_tool and index >= latest_user:
|
|
394
|
+
zone, reason = "live", "responses_latest_tool_output"
|
|
395
|
+
else:
|
|
396
|
+
reason = "responses_historical_output"
|
|
397
|
+
else:
|
|
398
|
+
if isinstance(message, HumanMessage) and index == latest_user:
|
|
399
|
+
zone, reason = "live", "openai_latest_user"
|
|
400
|
+
elif isinstance(message, ToolMessage) and index == latest_tool and index >= latest_user:
|
|
401
|
+
zone, reason = "live", "openai_latest_tool_output"
|
|
402
|
+
else:
|
|
403
|
+
reason = "openai_historical_message"
|
|
404
|
+
plan.append(
|
|
405
|
+
{
|
|
406
|
+
"message_index": index,
|
|
407
|
+
"message_type": getattr(message, "type", message.__class__.__name__),
|
|
408
|
+
"zone": zone,
|
|
409
|
+
"reason": reason,
|
|
410
|
+
"estimated_tokens": tokens,
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
return plan
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _tool_schema_profiles(tools: list[Any]) -> list[dict[str, Any]]:
|
|
417
|
+
profiles: list[dict[str, Any]] = []
|
|
418
|
+
for index, tool in enumerate(tools):
|
|
419
|
+
raw = _stable_json(tool)
|
|
420
|
+
data = tool.model_dump() if hasattr(tool, "model_dump") else tool
|
|
421
|
+
data = data if isinstance(data, dict) else {}
|
|
422
|
+
function = data.get("function") if isinstance(data.get("function"), dict) else data
|
|
423
|
+
name = str(function.get("name") or getattr(tool, "name", None) or f"tool-{index}")
|
|
424
|
+
description = str(function.get("description") or getattr(tool, "description", None) or "")
|
|
425
|
+
parameters = function.get("parameters") or function.get("args_schema") or {}
|
|
426
|
+
profiles.append(
|
|
427
|
+
{
|
|
428
|
+
"index": index,
|
|
429
|
+
"tool_name": name,
|
|
430
|
+
"schema_bytes": len(raw.encode("utf-8")),
|
|
431
|
+
"estimated_tokens": _count([], [tool]),
|
|
432
|
+
"description_bytes": len(description.encode("utf-8")),
|
|
433
|
+
"parameters_bytes": len(_stable_json(parameters).encode("utf-8")),
|
|
434
|
+
"schema_hash": _fingerprint(tool),
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
return profiles
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _wire_fingerprints(request: Any) -> dict[str, Any]:
|
|
441
|
+
system = getattr(request, "system_message", None)
|
|
442
|
+
messages = list(getattr(request, "messages", None) or [])
|
|
443
|
+
tools = list(getattr(request, "tools", None) or [])
|
|
444
|
+
return {
|
|
445
|
+
"system_hash": _fingerprint(system) if system is not None else "",
|
|
446
|
+
"tools_hash": _fingerprint(tools),
|
|
447
|
+
"message_hashes": [_fingerprint(message) for message in messages],
|
|
448
|
+
"message_count": len(messages),
|
|
449
|
+
"tool_count": len(tools),
|
|
450
|
+
"request_prefix_hash": _fingerprint([system, tools, messages]),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _cache_diagnostics(
|
|
455
|
+
previous: dict[str, Any] | None,
|
|
456
|
+
current: dict[str, Any],
|
|
457
|
+
usage: dict[str, int],
|
|
458
|
+
) -> dict[str, Any]:
|
|
459
|
+
previous = previous or {}
|
|
460
|
+
previous_hashes = list(previous.get("message_hashes") or [])
|
|
461
|
+
current_hashes = list(current.get("message_hashes") or [])
|
|
462
|
+
first_change = next(
|
|
463
|
+
(
|
|
464
|
+
index
|
|
465
|
+
for index, (before, after) in enumerate(
|
|
466
|
+
zip(previous_hashes, current_hashes, strict=False)
|
|
467
|
+
)
|
|
468
|
+
if before != after
|
|
469
|
+
),
|
|
470
|
+
min(len(previous_hashes), len(current_hashes)),
|
|
471
|
+
)
|
|
472
|
+
input_tokens = max(0, int(usage.get("input_tokens", 0) or 0))
|
|
473
|
+
cache_read = max(0, int(usage.get("cache_read_tokens", 0) or 0))
|
|
474
|
+
return {
|
|
475
|
+
"previous_request_available": bool(previous),
|
|
476
|
+
"system_changed": bool(
|
|
477
|
+
previous and previous.get("system_hash") != current.get("system_hash")
|
|
478
|
+
),
|
|
479
|
+
"tools_changed": bool(
|
|
480
|
+
previous and previous.get("tools_hash") != current.get("tools_hash")
|
|
481
|
+
),
|
|
482
|
+
"first_changed_message_index": first_change if previous else None,
|
|
483
|
+
"cache_hit_ratio": round(cache_read / input_tokens, 4) if input_tokens else 0.0,
|
|
484
|
+
"cache_bust_suspected": bool(
|
|
485
|
+
previous and input_tokens and cache_read / input_tokens < 0.5
|
|
486
|
+
),
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _read_lifecycle(messages: list[Any], plan: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
491
|
+
tool_calls: dict[str, dict[str, Any]] = {}
|
|
492
|
+
for index, message in enumerate(messages):
|
|
493
|
+
if not isinstance(message, AIMessage):
|
|
494
|
+
continue
|
|
495
|
+
for call in getattr(message, "tool_calls", None) or []:
|
|
496
|
+
if not isinstance(call, dict):
|
|
497
|
+
continue
|
|
498
|
+
args = call.get("args") if isinstance(call.get("args"), dict) else {}
|
|
499
|
+
tool_calls[str(call.get("id") or "")] = {
|
|
500
|
+
"message_index": index,
|
|
501
|
+
"tool_name": str(call.get("name") or ""),
|
|
502
|
+
"file_path": str(args.get("file_path") or args.get("path") or ""),
|
|
503
|
+
"offset": args.get("offset"),
|
|
504
|
+
"limit": args.get("limit"),
|
|
505
|
+
}
|
|
506
|
+
operations: list[dict[str, Any]] = []
|
|
507
|
+
for index, message in enumerate(messages):
|
|
508
|
+
if not isinstance(message, ToolMessage):
|
|
509
|
+
continue
|
|
510
|
+
call_id = str(getattr(message, "tool_call_id", None) or "")
|
|
511
|
+
call = tool_calls.get(call_id) or {}
|
|
512
|
+
name = str(call.get("tool_name") or getattr(message, "name", None) or "")
|
|
513
|
+
if name not in {"read_file", "edit_file", "write_file"}:
|
|
514
|
+
continue
|
|
515
|
+
operations.append({"message_index": index, "tool_call_id": call_id, **call})
|
|
516
|
+
result: list[dict[str, Any]] = []
|
|
517
|
+
for operation in operations:
|
|
518
|
+
if operation.get("tool_name") != "read_file" or not operation.get("file_path"):
|
|
519
|
+
continue
|
|
520
|
+
later = [
|
|
521
|
+
item
|
|
522
|
+
for item in operations
|
|
523
|
+
if item["message_index"] > operation["message_index"]
|
|
524
|
+
and item.get("file_path") == operation.get("file_path")
|
|
525
|
+
]
|
|
526
|
+
state = "fresh"
|
|
527
|
+
if any(item.get("tool_name") in {"edit_file", "write_file"} for item in later):
|
|
528
|
+
state = "stale"
|
|
529
|
+
elif any(item.get("tool_name") == "read_file" for item in later):
|
|
530
|
+
state = "superseded"
|
|
531
|
+
zone = next(
|
|
532
|
+
(
|
|
533
|
+
item["zone"]
|
|
534
|
+
for item in plan
|
|
535
|
+
if item["message_index"] == operation["message_index"]
|
|
536
|
+
),
|
|
537
|
+
"frozen",
|
|
538
|
+
)
|
|
539
|
+
result.append(
|
|
540
|
+
{
|
|
541
|
+
**operation,
|
|
542
|
+
"state": state,
|
|
543
|
+
"zone": zone,
|
|
544
|
+
"replaceable": state in {"stale", "superseded"} and zone == "live",
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
return result
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _apply_read_lifecycle(
|
|
551
|
+
request: Any,
|
|
552
|
+
lifecycle: list[dict[str, Any]],
|
|
553
|
+
repository: ToolOutputRepository,
|
|
554
|
+
thread_id: str,
|
|
555
|
+
) -> tuple[Any, list[dict[str, Any]]]:
|
|
556
|
+
replaceable = {
|
|
557
|
+
int(item["message_index"]): item for item in lifecycle if item.get("replaceable")
|
|
558
|
+
}
|
|
559
|
+
if not replaceable:
|
|
560
|
+
return request, lifecycle
|
|
561
|
+
messages = list(getattr(request, "messages", None) or [])
|
|
562
|
+
updated = list(messages)
|
|
563
|
+
for index, item in replaceable.items():
|
|
564
|
+
if index >= len(messages) or not isinstance(messages[index], ToolMessage):
|
|
565
|
+
continue
|
|
566
|
+
message = messages[index]
|
|
567
|
+
original = str(getattr(message, "content", "") or "")
|
|
568
|
+
record = repository.put(
|
|
569
|
+
thread_id=thread_id,
|
|
570
|
+
checkpoint_ns="read-lifecycle",
|
|
571
|
+
tool_call_id=str(item.get("tool_call_id") or ""),
|
|
572
|
+
tool_name="read_file",
|
|
573
|
+
status="success",
|
|
574
|
+
content=original,
|
|
575
|
+
)
|
|
576
|
+
state = str(item.get("state") or "stale")
|
|
577
|
+
path = str(item.get("file_path") or "unknown")
|
|
578
|
+
marker = (
|
|
579
|
+
f"[read_file {state}: {path}; re-read for current content if needed. "
|
|
580
|
+
f"Original: {record.ref}]"
|
|
581
|
+
)
|
|
582
|
+
if hasattr(message, "model_copy"):
|
|
583
|
+
updated[index] = message.model_copy(update={"content": marker})
|
|
584
|
+
else:
|
|
585
|
+
updated[index] = ToolMessage(
|
|
586
|
+
content=marker,
|
|
587
|
+
tool_call_id=str(getattr(message, "tool_call_id", None) or ""),
|
|
588
|
+
name=getattr(message, "name", None),
|
|
589
|
+
)
|
|
590
|
+
item["replacement_ref"] = record.ref
|
|
591
|
+
item["replacement_bytes_before"] = len(original.encode("utf-8"))
|
|
592
|
+
item["replacement_bytes_after"] = len(marker.encode("utf-8"))
|
|
593
|
+
if updated == messages or not hasattr(request, "override"):
|
|
594
|
+
return request, lifecycle
|
|
595
|
+
return request.override(messages=updated), lifecycle
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def build_model_request_compression_middleware(repository: ToolOutputRepository) -> Any:
|
|
599
|
+
"""Record final model-visible request size, reconstructed baseline, and usage."""
|
|
600
|
+
|
|
601
|
+
class _ModelRequestCompressionMiddleware(AgentMiddleware):
|
|
602
|
+
state_schema = AgentState
|
|
603
|
+
tools: list[Any] = []
|
|
604
|
+
|
|
605
|
+
def __init__(self) -> None:
|
|
606
|
+
self._previous_wire: dict[str, dict[str, Any]] = {}
|
|
607
|
+
|
|
608
|
+
def _prepare(self, request: Any) -> dict[str, Any]:
|
|
609
|
+
started = time.perf_counter()
|
|
610
|
+
request_messages = _messages(request)
|
|
611
|
+
tools = list(getattr(request, "tools", None) or [])
|
|
612
|
+
input_after = _count(request_messages, tools)
|
|
613
|
+
state_messages = _state_messages(request)
|
|
614
|
+
state_count = _count(state_messages)
|
|
615
|
+
tool_saved, candidates, transformed = _tool_output_savings(request_messages)
|
|
616
|
+
active_messages = list(getattr(request, "messages", None) or [])
|
|
617
|
+
summarization_saved = max(0, state_count - _count(active_messages))
|
|
618
|
+
provider, api_style, auth_mode, model = _model_identity(request)
|
|
619
|
+
thread_id = _thread_id(request)
|
|
620
|
+
turn_index_hint = sum(isinstance(message, HumanMessage) for message in active_messages)
|
|
621
|
+
previous_turn, previous_call = repository.latest_request_position(thread_id=thread_id)
|
|
622
|
+
position = begin_model_call(
|
|
623
|
+
thread_id,
|
|
624
|
+
_current_user_content(active_messages),
|
|
625
|
+
turn_index_hint=turn_index_hint,
|
|
626
|
+
model_call_index_hint=previous_call if previous_turn == turn_index_hint else 0,
|
|
627
|
+
)
|
|
628
|
+
live_zone_plan = _live_zone_plan(request, provider, api_style)
|
|
629
|
+
wire_fingerprints = _wire_fingerprints(request)
|
|
630
|
+
schema_profiles = _tool_schema_profiles(tools)
|
|
631
|
+
read_lifecycle = _read_lifecycle(active_messages, live_zone_plan)
|
|
632
|
+
request, read_lifecycle = _apply_read_lifecycle(
|
|
633
|
+
request, read_lifecycle, repository, thread_id
|
|
634
|
+
)
|
|
635
|
+
request_messages = _messages(request)
|
|
636
|
+
tools = list(getattr(request, "tools", None) or [])
|
|
637
|
+
input_after = _count(request_messages, tools)
|
|
638
|
+
tool_saved, candidates, transformed = _tool_output_savings(request_messages)
|
|
639
|
+
active_messages = list(getattr(request, "messages", None) or [])
|
|
640
|
+
breakdown = _content_breakdown(request)
|
|
641
|
+
wire_fingerprints = _wire_fingerprints(request)
|
|
642
|
+
schema_profiles = _tool_schema_profiles(tools)
|
|
643
|
+
live_zone_tokens: dict[str, int] = {}
|
|
644
|
+
for item in live_zone_plan:
|
|
645
|
+
zone = str(item["zone"])
|
|
646
|
+
live_zone_tokens[zone] = live_zone_tokens.get(zone, 0) + int(
|
|
647
|
+
item["estimated_tokens"] or 0
|
|
648
|
+
)
|
|
649
|
+
protected = _provider_protected_tokens(request, provider, api_style)
|
|
650
|
+
breakdown = _content_breakdown(request)
|
|
651
|
+
from synapse.runtime.middleware import current_prompt_cleanup_saved_tokens
|
|
652
|
+
|
|
653
|
+
prompt_saved = current_prompt_cleanup_saved_tokens()
|
|
654
|
+
return {
|
|
655
|
+
"prepared_request": request,
|
|
656
|
+
"started": started,
|
|
657
|
+
"request_id": uuid.uuid4().hex,
|
|
658
|
+
"thread_id": thread_id,
|
|
659
|
+
"turn_id": position.turn_id,
|
|
660
|
+
"turn_index": position.turn_index,
|
|
661
|
+
"model_call_index": position.model_call_index,
|
|
662
|
+
"provider": provider,
|
|
663
|
+
"api_style": api_style,
|
|
664
|
+
"auth_mode": auth_mode,
|
|
665
|
+
"model": model,
|
|
666
|
+
"input_after": input_after,
|
|
667
|
+
"tool_saved": tool_saved,
|
|
668
|
+
"prompt_saved": prompt_saved,
|
|
669
|
+
"summarization_saved": summarization_saved,
|
|
670
|
+
"candidate_blocks": candidates,
|
|
671
|
+
"transformed_blocks": transformed,
|
|
672
|
+
"protected": protected,
|
|
673
|
+
"breakdown": breakdown,
|
|
674
|
+
"opportunities": _opportunities(breakdown),
|
|
675
|
+
"live_zone_plan": live_zone_plan,
|
|
676
|
+
"live_zone_tokens": live_zone_tokens,
|
|
677
|
+
"wire_fingerprints": wire_fingerprints,
|
|
678
|
+
"schema_profiles": schema_profiles,
|
|
679
|
+
"read_lifecycle": read_lifecycle,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
def _finish(self, data: dict[str, Any], response: Any) -> None:
|
|
683
|
+
thread_id = str(data["thread_id"] or "")
|
|
684
|
+
if not thread_id:
|
|
685
|
+
return
|
|
686
|
+
usage = _usage(response)
|
|
687
|
+
tool_saved = int(data["tool_saved"] or 0)
|
|
688
|
+
prompt_saved = int(data["prompt_saved"] or 0)
|
|
689
|
+
summarization_saved = int(data["summarization_saved"] or 0)
|
|
690
|
+
total_saved = tool_saved + prompt_saved + summarization_saved
|
|
691
|
+
wire = dict(data["wire_fingerprints"] or {})
|
|
692
|
+
cache_diagnostics = _cache_diagnostics(
|
|
693
|
+
self._previous_wire.get(thread_id), wire, usage
|
|
694
|
+
)
|
|
695
|
+
cache_diagnostics["read_lifecycle"] = list(data["read_lifecycle"] or [])
|
|
696
|
+
self._previous_wire[thread_id] = wire
|
|
697
|
+
repository.record_model_request(
|
|
698
|
+
thread_id=thread_id,
|
|
699
|
+
event=ModelRequestCompressionEvent(
|
|
700
|
+
request_id=str(data["request_id"]),
|
|
701
|
+
provider=str(data["provider"]),
|
|
702
|
+
api_style=str(data["api_style"]),
|
|
703
|
+
auth_mode=str(data["auth_mode"]),
|
|
704
|
+
model=str(data["model"]),
|
|
705
|
+
input_tokens_before=int(data["input_after"] or 0) + total_saved,
|
|
706
|
+
input_tokens_after=int(data["input_after"] or 0),
|
|
707
|
+
provider_input_tokens=usage["input_tokens"],
|
|
708
|
+
cache_read_tokens=usage["cache_read_tokens"],
|
|
709
|
+
cache_write_tokens=usage["cache_write_tokens"],
|
|
710
|
+
uncached_input_tokens=usage["uncached_input_tokens"],
|
|
711
|
+
output_tokens=usage["output_tokens"],
|
|
712
|
+
tool_output_saved_tokens=tool_saved,
|
|
713
|
+
prompt_saved_tokens=prompt_saved,
|
|
714
|
+
summarization_saved_tokens=summarization_saved,
|
|
715
|
+
total_saved_tokens=total_saved,
|
|
716
|
+
candidate_blocks=int(data["candidate_blocks"] or 0),
|
|
717
|
+
transformed_blocks=int(data["transformed_blocks"] or 0),
|
|
718
|
+
protected_tokens_by_reason=dict(data["protected"] or {}),
|
|
719
|
+
content_breakdown=dict(data["breakdown"] or {}),
|
|
720
|
+
opportunity_tokens_by_reason=dict(data["opportunities"] or {}),
|
|
721
|
+
turn_id=str(data["turn_id"] or ""),
|
|
722
|
+
turn_index=int(data["turn_index"] or 0),
|
|
723
|
+
model_call_index=int(data["model_call_index"] or 0),
|
|
724
|
+
live_zone_plan=list(data["live_zone_plan"] or []),
|
|
725
|
+
live_zone_tokens=dict(data["live_zone_tokens"] or {}),
|
|
726
|
+
wire_fingerprints=wire,
|
|
727
|
+
cache_diagnostics=cache_diagnostics,
|
|
728
|
+
tool_schema_profiles=list(data["schema_profiles"] or []),
|
|
729
|
+
duration_ms=(time.perf_counter() - float(data["started"])) * 1000,
|
|
730
|
+
),
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def wrap_model_call(self, request: Any, handler: Any) -> Any:
|
|
734
|
+
data = self._prepare(request)
|
|
735
|
+
response = handler(data["prepared_request"])
|
|
736
|
+
self._finish(data, response)
|
|
737
|
+
return response
|
|
738
|
+
|
|
739
|
+
async def awrap_model_call(self, request: Any, handler: Any) -> Any:
|
|
740
|
+
data = self._prepare(request)
|
|
741
|
+
response = await handler(data["prepared_request"])
|
|
742
|
+
self._finish(data, response)
|
|
743
|
+
return response
|
|
744
|
+
|
|
745
|
+
return _ModelRequestCompressionMiddleware()
|