flightdeck-sensor 0.7.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/CLAUDE.md +3 -2
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/PKG-INFO +3 -3
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/README.md +2 -2
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/compat/__init__.py +2 -2
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/compat/crewai_mcp.py +1 -1
- flightdeck_sensor-0.8.0/flightdeck_sensor/transport/body_limit.py +154 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/transport/client.py +15 -4
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/pyproject.toml +1 -1
- flightdeck_sensor-0.8.0/tests/unit/test_body_limit.py +206 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_provider_enum.py +2 -2
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_transport.py +21 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/uv.lock +10 -10
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/.gitignore +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/Makefile +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/__init__.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/config.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/__init__.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/agent_id.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/context.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/errors.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/exceptions.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/mcp_policy.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/policy.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/schemas.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/session.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/core/types.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/__init__.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/anthropic.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/base.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/crewai.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/langgraph.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/litellm.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/mcp.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/mcp_identity.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/openai.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/platform.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/provider.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/providers/__init__.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/providers/anthropic.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/providers/litellm.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/providers/openai.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/providers/protocol.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/py.typed +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/transport/__init__.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/transport/retry.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/__init__.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/conftest.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/__init__.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_agent_id.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_agent_type_validation.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_anthropic_resource_patch.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_compat_crewai_mcp.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_context.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_cross_agent_messages.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_custom_directives.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_egress_url_validation.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_errors.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_framework_attribution.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_interceptor.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_interceptor_crewai.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_interceptor_langgraph.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_interceptor_litellm.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_llm_event_enrichment.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_2x_paths.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_availability.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_identity.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_interceptor.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_policy_emission_enriched.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_policy_enforcement.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_policy_enforcement_all_paths.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_openai_parse_responses.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_originating_event_id.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_patch.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_policy.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_policy_decision_summary.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_policy_directive_events.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_policy_emission_enriched.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_policy_events.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_prompt_capture.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_providers.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_session.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_session_lifecycle_enrichment.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_stop_reason_errors.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_streaming_usage.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_token_cost_fields.py +0 -0
- {flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_tool_capture_destination.py +0 -0
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
|
|
17
17
|
29. **Token counting carries over from tokencap.** Do not rewrite the counting logic.
|
|
18
18
|
Extend it. Pre-call estimation, post-call reconciliation, delta correction -- these
|
|
19
|
-
are proven and must not be changed without
|
|
19
|
+
are proven and must not be changed without the maintainer's approval.
|
|
20
20
|
|
|
21
21
|
30. **capture_prompts defaults to False.** The default init() call never captures
|
|
22
22
|
content. Always verify this default has not been accidentally changed.
|
|
23
23
|
|
|
24
24
|
31. **init() limit param fires WARN only.** Never upgrade a local limit to BLOCK
|
|
25
|
-
or DEGRADE regardless of what the server policy says
|
|
25
|
+
or DEGRADE regardless of what the server policy says: a local limit is a
|
|
26
|
+
developer safety net, and enforcement belongs to the server's policy.
|
|
26
27
|
|
|
27
28
|
32. **The sensor is a library wrapper, not an OS agent.** Never add background
|
|
28
29
|
threads, polling loops, or daemon threads to the sensor beyond the existing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flightdeck-sensor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: In-process agent observability sensor for Flightdeck
|
|
5
5
|
Project-URL: Repository, https://github.com/flightdeckhq/flightdeck
|
|
6
6
|
Author-email: Omri Arnon <omri.arnon@gmail.com>
|
|
@@ -222,8 +222,8 @@ the single-count assertion and the capture round-trip).
|
|
|
222
222
|
if framework reports surface calls that slip past the current
|
|
223
223
|
coverage, file a new issue.
|
|
224
224
|
- **`litellm.text_completion` / `atext_completion` and
|
|
225
|
-
`litellm.responses` / `aresponses`.** Neither is wrapped (
|
|
226
|
-
|
|
225
|
+
`litellm.responses` / `aresponses`.** Neither is wrapped (planned in
|
|
226
|
+
[#170](https://github.com/flightdeckhq/flightdeck/issues/170)), but some routes are still reported. The legacy
|
|
227
227
|
completion API calls `completion` through the name bound in
|
|
228
228
|
`litellm.main`, which the module-attribute swap does not reach; on
|
|
229
229
|
an OpenAI chat model the OpenAI interceptor reports the SDK call it
|
|
@@ -162,8 +162,8 @@ the single-count assertion and the capture round-trip).
|
|
|
162
162
|
if framework reports surface calls that slip past the current
|
|
163
163
|
coverage, file a new issue.
|
|
164
164
|
- **`litellm.text_completion` / `atext_completion` and
|
|
165
|
-
`litellm.responses` / `aresponses`.** Neither is wrapped (
|
|
166
|
-
|
|
165
|
+
`litellm.responses` / `aresponses`.** Neither is wrapped (planned in
|
|
166
|
+
[#170](https://github.com/flightdeckhq/flightdeck/issues/170)), but some routes are still reported. The legacy
|
|
167
167
|
completion API calls `completion` through the name bound in
|
|
168
168
|
`litellm.main`, which the module-attribute swap does not reach; on
|
|
169
169
|
an OpenAI chat model the OpenAI interceptor reports the SDK call it
|
|
@@ -11,6 +11,6 @@ Stability: these helpers exist as workarounds for upstream bugs.
|
|
|
11
11
|
Each module's docstring names the upstream package + version where
|
|
12
12
|
the bug was observed and points at the README "Known framework
|
|
13
13
|
constraints" section. The helpers ship behind their own modules so
|
|
14
|
-
removing one as upstream fixes land is a clean delete
|
|
15
|
-
|
|
14
|
+
removing one as upstream fixes land is a clean delete that closes
|
|
15
|
+
its roadmap issue, not a sweep through the sensor proper.
|
|
16
16
|
"""
|
|
@@ -30,7 +30,7 @@ path, raw ``model_json_schema()`` calls) sees the cleaned schema.
|
|
|
30
30
|
|
|
31
31
|
Remove this module once mcpadapt emits valid schemas. See README
|
|
32
32
|
"Known framework constraints" for the operator-facing explanation
|
|
33
|
-
and
|
|
33
|
+
and roadmap issue #193 for the removal.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
from __future__ import annotations
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Keep every event body the sensor POSTs within the server's size limit.
|
|
2
|
+
|
|
3
|
+
Ingestion reads at most 1 MiB of an event body, and NATS carries the
|
|
4
|
+
event on with the same 1 MiB ``max_payload``; a larger body is rejected
|
|
5
|
+
whole (413 from a proxy with nginx's default limit, 400 from ingestion
|
|
6
|
+
when the cut body fails to parse), and the call's token counts with it.
|
|
7
|
+
A captured prompt, a tool result or a sub-agent message can cross that.
|
|
8
|
+
An event over the limit has the strings of its captured ``content`` cut,
|
|
9
|
+
each to the same largest length that fits, ending in a marker; when no
|
|
10
|
+
cut fits, it is sent without content. The Claude Code plugin does the
|
|
11
|
+
same (``plugin/hooks/scripts/core/poster.mjs``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
_log = logging.getLogger("flightdeck_sensor.transport.body_limit")
|
|
21
|
+
|
|
22
|
+
#: Largest event body the sensor sends, as measured by
|
|
23
|
+
#: :func:`encoded_size`; the remaining quarter of the 1 MiB limit covers
|
|
24
|
+
#: the token id and name ingestion adds to a ``session_start``.
|
|
25
|
+
MAX_EVENT_BODY_BYTES = 768 * 1024
|
|
26
|
+
|
|
27
|
+
# Characters ingestion's Go encoder writes as six-byte unicode escapes
|
|
28
|
+
# when it re-encodes a session_start (one byte each here).
|
|
29
|
+
_GO_ESCAPED = (b"<", b">", b"&")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def encoded_size(body: bytes) -> int:
|
|
33
|
+
"""The size of a JSON body once ingestion has re-encoded it.
|
|
34
|
+
|
|
35
|
+
The sensor's ``json.dumps`` already escapes every non-ASCII
|
|
36
|
+
character, which Go writes as shorter UTF-8, so text grows only by
|
|
37
|
+
``<``, ``>`` and ``&``. Go also writes some floats longer than
|
|
38
|
+
Python does (``1e16`` as ``10000000000000000``), which this does not
|
|
39
|
+
count; a ``session_start``, the one event ingestion re-encodes,
|
|
40
|
+
carries text content. Every event is measured this way; for the
|
|
41
|
+
others it only over-counts.
|
|
42
|
+
"""
|
|
43
|
+
return len(body) + 5 * sum(body.count(ch) for ch in _GO_ESCAPED)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def truncation_marker(kept_bytes: int, total_bytes: int) -> str:
|
|
47
|
+
"""The text that replaces the cut end of a string in oversized content."""
|
|
48
|
+
return f"… [flightdeck: kept {kept_bytes} of {total_bytes} bytes to fit the event size limit]"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _cap_strings(value: Any, cap_bytes: int) -> Any:
|
|
52
|
+
"""Every string in ``value`` longer than ``cap_bytes`` of UTF-8, cut and marked."""
|
|
53
|
+
if isinstance(value, str):
|
|
54
|
+
# ``surrogatepass`` keeps a lone surrogate (from a
|
|
55
|
+
# ``surrogateescape`` decode) from raising; the cut drops it.
|
|
56
|
+
raw = value.encode("utf-8", "surrogatepass")
|
|
57
|
+
if len(raw) <= cap_bytes:
|
|
58
|
+
return value
|
|
59
|
+
# ``errors="ignore"`` drops a code point the cut split.
|
|
60
|
+
kept = raw[:cap_bytes].decode("utf-8", errors="ignore")
|
|
61
|
+
marked = kept + truncation_marker(len(kept.encode("utf-8", "surrogatepass")), len(raw))
|
|
62
|
+
# A string barely over the cap would grow by the marker; keep it.
|
|
63
|
+
return marked if len(marked.encode("utf-8", "surrogatepass")) < len(raw) else value
|
|
64
|
+
if isinstance(value, list):
|
|
65
|
+
return [_cap_strings(item, cap_bytes) for item in value]
|
|
66
|
+
if isinstance(value, dict):
|
|
67
|
+
return {key: _cap_strings(item, cap_bytes) for key, item in value.items()}
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _longest_string_bytes(value: Any) -> int:
|
|
72
|
+
if isinstance(value, str):
|
|
73
|
+
return len(value.encode("utf-8", "surrogatepass"))
|
|
74
|
+
items = value if isinstance(value, list) else value.values() if isinstance(value, dict) else ()
|
|
75
|
+
return max((_longest_string_bytes(item) for item in items), default=0)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _serialize_cut(payload: dict[str, Any], content: Any, cap: int) -> bytes:
|
|
79
|
+
return json.dumps({**payload, "content": _cap_strings(content, cap)}).encode()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Payload fields holding a sub-agent message stub that points at the
|
|
83
|
+
# event's content (``{has_content: true, content_bytes, captured_at}``).
|
|
84
|
+
_MESSAGE_STUB_FIELDS = ("incoming_message", "outgoing_message")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _without_content(payload: dict[str, Any]) -> dict[str, Any]:
|
|
88
|
+
"""``payload`` sent without its content: no stub still claims it."""
|
|
89
|
+
out = {**payload, "has_content": False, "content": None}
|
|
90
|
+
for field in _MESSAGE_STUB_FIELDS:
|
|
91
|
+
stub = out.get(field)
|
|
92
|
+
if isinstance(stub, dict) and stub.get("has_content"):
|
|
93
|
+
out[field] = {**stub, "has_content": False}
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def serialize_event(payload: dict[str, Any]) -> bytes:
|
|
98
|
+
"""The JSON body for one event, kept within :data:`MAX_EVENT_BODY_BYTES`.
|
|
99
|
+
|
|
100
|
+
An event without content, or one over the limit even without it, is
|
|
101
|
+
sent as it is.
|
|
102
|
+
"""
|
|
103
|
+
body = json.dumps(payload).encode()
|
|
104
|
+
size = encoded_size(body)
|
|
105
|
+
content = payload.get("content")
|
|
106
|
+
if size <= MAX_EVENT_BODY_BYTES or content is None:
|
|
107
|
+
return body
|
|
108
|
+
event_type = payload.get("event_type")
|
|
109
|
+
bare = json.dumps(_without_content(payload)).encode()
|
|
110
|
+
if encoded_size(bare) > MAX_EVENT_BODY_BYTES:
|
|
111
|
+
_log.warning(
|
|
112
|
+
"%s event is %d bytes and over the event size limit without its content; sent as it is",
|
|
113
|
+
event_type,
|
|
114
|
+
size,
|
|
115
|
+
)
|
|
116
|
+
return body
|
|
117
|
+
# The body only grows with the cap, so when even an empty cap does
|
|
118
|
+
# not fit, no cap does; that skips the search (each step serialises
|
|
119
|
+
# the whole event).
|
|
120
|
+
if encoded_size(_serialize_cut(payload, content, 0)) > MAX_EVENT_BODY_BYTES:
|
|
121
|
+
_log.warning(
|
|
122
|
+
"%s event is %d bytes and its content cannot be cut to fit the event size limit; "
|
|
123
|
+
"sent without it",
|
|
124
|
+
event_type,
|
|
125
|
+
size,
|
|
126
|
+
)
|
|
127
|
+
return bare
|
|
128
|
+
lo, hi = 0, min(_longest_string_bytes(content), MAX_EVENT_BODY_BYTES)
|
|
129
|
+
best: bytes | None = None
|
|
130
|
+
best_cap = 0
|
|
131
|
+
# Binary search for the largest per-string length that fits.
|
|
132
|
+
while lo <= hi:
|
|
133
|
+
cap = (lo + hi) // 2
|
|
134
|
+
candidate = _serialize_cut(payload, content, cap)
|
|
135
|
+
if encoded_size(candidate) <= MAX_EVENT_BODY_BYTES:
|
|
136
|
+
best, best_cap, lo = candidate, cap, cap + 1
|
|
137
|
+
else:
|
|
138
|
+
hi = cap - 1
|
|
139
|
+
if best is not None:
|
|
140
|
+
_log.warning(
|
|
141
|
+
"%s event is %d bytes; content strings over %d bytes were cut "
|
|
142
|
+
"to fit the event size limit",
|
|
143
|
+
event_type,
|
|
144
|
+
size,
|
|
145
|
+
best_cap,
|
|
146
|
+
)
|
|
147
|
+
return best
|
|
148
|
+
_log.warning(
|
|
149
|
+
"%s event is %d bytes and its content cannot be cut to fit the event size limit; "
|
|
150
|
+
"sent without it",
|
|
151
|
+
event_type,
|
|
152
|
+
size,
|
|
153
|
+
)
|
|
154
|
+
return bare
|
|
@@ -19,6 +19,7 @@ from pydantic import ValidationError
|
|
|
19
19
|
|
|
20
20
|
from flightdeck_sensor.core.exceptions import DirectiveError
|
|
21
21
|
from flightdeck_sensor.core.types import Directive, DirectiveAction
|
|
22
|
+
from flightdeck_sensor.transport.body_limit import serialize_event
|
|
22
23
|
from flightdeck_sensor.transport.retry import with_retry
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
@@ -78,7 +79,7 @@ class ControlPlaneClient:
|
|
|
78
79
|
field defaults to ``False`` for backward compatibility with
|
|
79
80
|
older ingestion deployments.
|
|
80
81
|
"""
|
|
81
|
-
return self._post("/v1/events", payload)
|
|
82
|
+
return self._post("/v1/events", payload, data=serialize_event(payload))
|
|
82
83
|
|
|
83
84
|
def sync_directives(
|
|
84
85
|
self,
|
|
@@ -150,10 +151,20 @@ class ControlPlaneClient:
|
|
|
150
151
|
# Internals
|
|
151
152
|
# ------------------------------------------------------------------
|
|
152
153
|
|
|
153
|
-
def _post(
|
|
154
|
-
|
|
154
|
+
def _post(
|
|
155
|
+
self,
|
|
156
|
+
path: str,
|
|
157
|
+
body: dict[str, Any],
|
|
158
|
+
data: bytes | None = None,
|
|
159
|
+
) -> tuple[Directive | None, bool]:
|
|
160
|
+
"""POST JSON and parse the response envelope for directive + attached.
|
|
161
|
+
|
|
162
|
+
``data`` is the already-encoded body when the caller has one
|
|
163
|
+
(an event, kept within the size limit by ``serialize_event``).
|
|
164
|
+
"""
|
|
155
165
|
url = f"{self._base_url}{path}"
|
|
156
|
-
data
|
|
166
|
+
if data is None:
|
|
167
|
+
data = json.dumps(body).encode()
|
|
157
168
|
req = urllib.request.Request(
|
|
158
169
|
url,
|
|
159
170
|
data=data,
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""transport/body_limit.py: every event body stays within the server's
|
|
2
|
+
1 MiB limit, less headroom, by cutting the strings of captured content."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from flightdeck_sensor.transport import body_limit
|
|
12
|
+
from flightdeck_sensor.transport.body_limit import (
|
|
13
|
+
MAX_EVENT_BODY_BYTES,
|
|
14
|
+
_cap_strings,
|
|
15
|
+
encoded_size,
|
|
16
|
+
serialize_event,
|
|
17
|
+
truncation_marker,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
_MARKER_RE = re.compile(r"… \[flightdeck: kept (\d+) of (\d+) bytes to fit the event size limit\]$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _post_call(content: Any) -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"event_type": "post_call",
|
|
29
|
+
"session_id": "s1",
|
|
30
|
+
"tokens_input": 42,
|
|
31
|
+
"has_content": True,
|
|
32
|
+
"content": content,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_an_event_within_the_limit_is_sent_unchanged(caplog: pytest.LogCaptureFixture) -> None:
|
|
37
|
+
payload = _post_call({"messages": [{"role": "user", "content": "hi"}]})
|
|
38
|
+
with caplog.at_level(logging.WARNING):
|
|
39
|
+
assert serialize_event(payload) == json.dumps(payload).encode()
|
|
40
|
+
assert caplog.records == []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_oversized_content_is_cut_and_the_tokens_kept(caplog: pytest.LogCaptureFixture) -> None:
|
|
44
|
+
big = "a" * (3 * 1024 * 1024)
|
|
45
|
+
payload = _post_call(
|
|
46
|
+
{
|
|
47
|
+
"provider": "anthropic",
|
|
48
|
+
"messages": [{"role": "user", "content": big}],
|
|
49
|
+
"response": {"content": [{"type": "text", "text": "short"}]},
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
with caplog.at_level(logging.WARNING):
|
|
53
|
+
body = serialize_event(payload)
|
|
54
|
+
assert encoded_size(body) <= MAX_EVENT_BODY_BYTES
|
|
55
|
+
sent = json.loads(body)
|
|
56
|
+
assert sent["tokens_input"] == 42
|
|
57
|
+
assert sent["has_content"] is True
|
|
58
|
+
assert sent["content"]["provider"] == "anthropic"
|
|
59
|
+
assert sent["content"]["response"] == payload["content"]["response"]
|
|
60
|
+
cut = sent["content"]["messages"][0]["content"]
|
|
61
|
+
match = _MARKER_RE.search(cut)
|
|
62
|
+
assert match, cut[-120:]
|
|
63
|
+
kept, total = int(match.group(1)), int(match.group(2))
|
|
64
|
+
assert total == len(big)
|
|
65
|
+
assert cut == "a" * kept + truncation_marker(kept, total)
|
|
66
|
+
assert kept > MAX_EVENT_BODY_BYTES * 0.9
|
|
67
|
+
assert len(caplog.records) == 1
|
|
68
|
+
assert "content strings over" in caplog.records[0].getMessage()
|
|
69
|
+
assert "aaaa" not in caplog.records[0].getMessage()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_every_long_string_gets_the_same_cap() -> None:
|
|
73
|
+
payload = _post_call(
|
|
74
|
+
{"system": "b" * 600_000, "messages": [{"role": "user", "content": "c" * 600_000}]}
|
|
75
|
+
)
|
|
76
|
+
sent = json.loads(serialize_event(payload))
|
|
77
|
+
|
|
78
|
+
def kept(s: str) -> int:
|
|
79
|
+
m = _MARKER_RE.search(s)
|
|
80
|
+
assert m
|
|
81
|
+
return int(m.group(1))
|
|
82
|
+
|
|
83
|
+
assert kept(sent["content"]["system"]) == kept(sent["content"]["messages"][0]["content"])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_a_multibyte_character_is_never_split() -> None:
|
|
87
|
+
payload = _post_call({"messages": [{"role": "user", "content": "\U0001f600" * 400_000}]})
|
|
88
|
+
body = serialize_event(payload)
|
|
89
|
+
assert encoded_size(body) <= MAX_EVENT_BODY_BYTES
|
|
90
|
+
text = json.loads(body)["content"]["messages"][0]["content"]
|
|
91
|
+
assert "�" not in text
|
|
92
|
+
assert re.match(r"^(\U0001f600)+… \[flightdeck: kept \d+ of 1600000 bytes", text)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_markup_heavy_content_fits_once_go_re_encodes_it() -> None:
|
|
96
|
+
# Ingestion's Go encoder writes <, > and & as six-byte escapes when
|
|
97
|
+
# it re-encodes a session_start.
|
|
98
|
+
payload = {
|
|
99
|
+
"event_type": "session_start",
|
|
100
|
+
"session_id": "s1",
|
|
101
|
+
"has_content": True,
|
|
102
|
+
"content": {"response": {"body": "<a>" * 240_000}},
|
|
103
|
+
}
|
|
104
|
+
body = serialize_event(payload)
|
|
105
|
+
assert encoded_size(body) <= MAX_EVENT_BODY_BYTES
|
|
106
|
+
assert len(body) + 5 * (body.count(b"<") + body.count(b">")) <= MAX_EVENT_BODY_BYTES
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_encoded_size_counts_go_escapes() -> None:
|
|
110
|
+
assert encoded_size(b'{"a":"x"}') == 9
|
|
111
|
+
assert encoded_size(b'{"a":"<&>"}') == 11 + 3 * 5
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_without_any_fitting_cut_the_event_goes_without_content(
|
|
115
|
+
caplog: pytest.LogCaptureFixture,
|
|
116
|
+
) -> None:
|
|
117
|
+
blocks = [{"type": "text", "text": ""} for _ in range(60_000)]
|
|
118
|
+
payload = _post_call({"messages": [], "response": {"content": blocks}})
|
|
119
|
+
with caplog.at_level(logging.WARNING):
|
|
120
|
+
sent = json.loads(serialize_event(payload))
|
|
121
|
+
assert sent["content"] is None
|
|
122
|
+
assert sent["has_content"] is False
|
|
123
|
+
assert sent["tokens_input"] == 42
|
|
124
|
+
assert "cannot be cut" in caplog.records[0].getMessage()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_an_event_over_the_limit_without_content_keeps_it(caplog: pytest.LogCaptureFixture) -> None:
|
|
128
|
+
payload = {
|
|
129
|
+
**_post_call({"messages": [{"role": "user", "content": "hi"}]}),
|
|
130
|
+
"note": "q" * MAX_EVENT_BODY_BYTES,
|
|
131
|
+
}
|
|
132
|
+
with caplog.at_level(logging.WARNING):
|
|
133
|
+
assert serialize_event(payload) == json.dumps(payload).encode()
|
|
134
|
+
assert "without its content" in caplog.records[0].getMessage()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_an_oversized_event_with_no_content_is_sent_as_it_is() -> None:
|
|
138
|
+
payload = {"event_type": "tool_call", "content": None, "note": "q" * MAX_EVENT_BODY_BYTES}
|
|
139
|
+
assert serialize_event(payload) == json.dumps(payload).encode()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_a_string_the_marker_would_lengthen_is_kept() -> None:
|
|
143
|
+
short = "s" * 70
|
|
144
|
+
assert _cap_strings(short, 10) == short
|
|
145
|
+
long = "l" * 200
|
|
146
|
+
assert _cap_strings(long, 10) == "l" * 10 + truncation_marker(10, 200)
|
|
147
|
+
assert _cap_strings({"a": [short, long], "n": 3, "b": None}, 10) == {
|
|
148
|
+
"a": [short, "l" * 10 + truncation_marker(10, 200)],
|
|
149
|
+
"n": 3,
|
|
150
|
+
"b": None,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_a_cut_never_splits_a_multibyte_character() -> None:
|
|
155
|
+
# Each emoji is four bytes; a six-byte cap lands inside the second.
|
|
156
|
+
cut = _cap_strings("\U0001f600" * 40, 6)
|
|
157
|
+
assert "\ufffd" not in cut
|
|
158
|
+
assert cut == "\U0001f600" + truncation_marker(4, 160)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_a_lone_surrogate_in_oversized_content_is_cut_not_raised() -> None:
|
|
162
|
+
# A tool result decoded with errors="surrogateescape" carries \udcXX.
|
|
163
|
+
payload = {
|
|
164
|
+
"event_type": "session_start",
|
|
165
|
+
"has_content": True,
|
|
166
|
+
"content": {"response": {"body": "\udc80" * 300_000}},
|
|
167
|
+
}
|
|
168
|
+
body = serialize_event(payload)
|
|
169
|
+
assert encoded_size(body) <= MAX_EVENT_BODY_BYTES
|
|
170
|
+
assert json.loads(body)["has_content"] is True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_dropping_the_content_clears_a_message_stub_that_points_at_it() -> None:
|
|
174
|
+
payload = {
|
|
175
|
+
"event_type": "session_start",
|
|
176
|
+
"has_content": True,
|
|
177
|
+
"incoming_message": {"has_content": True, "content_bytes": 3_000_000, "captured_at": "t"},
|
|
178
|
+
"content": {"response": {"body": [{"k": i} for i in range(200_000)]}},
|
|
179
|
+
}
|
|
180
|
+
sent = json.loads(serialize_event(payload))
|
|
181
|
+
assert sent["content"] is None
|
|
182
|
+
assert sent["has_content"] is False
|
|
183
|
+
assert sent["incoming_message"] == {
|
|
184
|
+
"has_content": False,
|
|
185
|
+
"content_bytes": 3_000_000,
|
|
186
|
+
"captured_at": "t",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_content_with_nothing_to_cut_skips_the_search(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
191
|
+
calls = []
|
|
192
|
+
real = body_limit._serialize_cut
|
|
193
|
+
|
|
194
|
+
def counting(payload: dict[str, Any], content: Any, cap: int) -> bytes:
|
|
195
|
+
calls.append(cap)
|
|
196
|
+
return real(payload, content, cap)
|
|
197
|
+
|
|
198
|
+
monkeypatch.setattr(body_limit, "_serialize_cut", counting)
|
|
199
|
+
vectors = [[0.123456789] * 3072 for _ in range(50)]
|
|
200
|
+
sent = json.loads(serialize_event(_post_call({"embedding_output": vectors})))
|
|
201
|
+
assert sent["content"] is None
|
|
202
|
+
assert calls == [0]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_the_budget_leaves_room_under_the_one_mib_limit() -> None:
|
|
206
|
+
assert MAX_EVENT_BODY_BYTES <= 0.8 * 1024 * 1024
|
|
@@ -66,8 +66,8 @@ def test_provider_has_exactly_six_members() -> None:
|
|
|
66
66
|
value-side drift; this one catches the count-side).
|
|
67
67
|
|
|
68
68
|
Sub-agent observability added two targets (CREWAI, LANGGRAPH). AutoGen
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
is archived upstream and not planned; its successor, Microsoft Agent
|
|
70
|
+
Framework, is roadmap issue #185.
|
|
71
71
|
"""
|
|
72
72
|
assert set(Provider) == {
|
|
73
73
|
Provider.ANTHROPIC,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
7
|
import threading
|
|
7
8
|
import time
|
|
@@ -12,6 +13,7 @@ import pytest
|
|
|
12
13
|
|
|
13
14
|
from flightdeck_sensor.core.exceptions import DirectiveError
|
|
14
15
|
from flightdeck_sensor.core.types import Directive, DirectiveAction
|
|
16
|
+
from flightdeck_sensor.transport.body_limit import MAX_EVENT_BODY_BYTES
|
|
15
17
|
from flightdeck_sensor.transport.client import (
|
|
16
18
|
_MAX_QUEUE_SIZE,
|
|
17
19
|
ControlPlaneClient,
|
|
@@ -64,6 +66,25 @@ def test_post_event_returns_attached_true_when_backend_attaches(
|
|
|
64
66
|
assert attached is True
|
|
65
67
|
|
|
66
68
|
|
|
69
|
+
def test_post_event_keeps_an_oversized_event_within_the_body_limit(
|
|
70
|
+
mock_control_plane: Any,
|
|
71
|
+
) -> None:
|
|
72
|
+
client = ControlPlaneClient(mock_control_plane["url"], "tok")
|
|
73
|
+
payload = {
|
|
74
|
+
"session_id": "123",
|
|
75
|
+
"event_type": "post_call",
|
|
76
|
+
"tokens_input": 900_000,
|
|
77
|
+
"has_content": True,
|
|
78
|
+
"content": {"messages": [{"role": "user", "content": "x" * (2 * 1024 * 1024)}]},
|
|
79
|
+
}
|
|
80
|
+
client.post_event(payload)
|
|
81
|
+
[request] = mock_control_plane["requests"]
|
|
82
|
+
sent = request["body"]
|
|
83
|
+
assert sent["tokens_input"] == 900_000
|
|
84
|
+
assert len(json.dumps(sent)) <= MAX_EVENT_BODY_BYTES
|
|
85
|
+
assert sent["content"]["messages"][0]["content"].endswith("bytes to fit the event size limit]")
|
|
86
|
+
|
|
87
|
+
|
|
67
88
|
def test_connectivity_failure_continue_returns_none() -> None:
|
|
68
89
|
client = ControlPlaneClient("http://127.0.0.1:1", "tok", unavailable_policy="continue")
|
|
69
90
|
directive, attached = client.post_event({"session_id": "x", "event_type": "post_call"})
|
|
@@ -1263,7 +1263,7 @@ wheels = [
|
|
|
1263
1263
|
|
|
1264
1264
|
[[package]]
|
|
1265
1265
|
name = "flightdeck-sensor"
|
|
1266
|
-
version = "0.
|
|
1266
|
+
version = "0.8.0"
|
|
1267
1267
|
source = { editable = "." }
|
|
1268
1268
|
dependencies = [
|
|
1269
1269
|
{ name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.14' and extra == 'extra-17-flightdeck-sensor-dev') or (extra == 'extra-17-flightdeck-sensor-dev' and extra == 'group-17-flightdeck-sensor-sdk-majors')" },
|
|
@@ -2446,7 +2446,7 @@ wheels = [
|
|
|
2446
2446
|
|
|
2447
2447
|
[[package]]
|
|
2448
2448
|
name = "langchain-anthropic"
|
|
2449
|
-
version = "1.7.
|
|
2449
|
+
version = "1.7.6"
|
|
2450
2450
|
source = { registry = "https://pypi.org/simple" }
|
|
2451
2451
|
dependencies = [
|
|
2452
2452
|
{ name = "anthropic", version = "1.17.0", source = { registry = "https://pypi.org/simple" } },
|
|
@@ -2454,14 +2454,14 @@ dependencies = [
|
|
|
2454
2454
|
{ name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.14' and extra == 'extra-17-flightdeck-sensor-dev') or (extra == 'extra-17-flightdeck-sensor-dev' and extra == 'group-17-flightdeck-sensor-sdk-majors')" },
|
|
2455
2455
|
{ name = "pydantic", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.14' and extra == 'extra-17-flightdeck-sensor-dev') or (extra == 'extra-17-flightdeck-sensor-dev' and extra == 'group-17-flightdeck-sensor-sdk-majors')" },
|
|
2456
2456
|
]
|
|
2457
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
2457
|
+
sdist = { url = "https://files.pythonhosted.org/packages/3c/45/b369666909be4a6f58877b18e692655ee792e06047c6c572d1218e09a31c/langchain_anthropic-1.7.6.tar.gz", hash = "sha256:e5b41959a39810e69bc608fd6622b7d5d017bde93f858b09332fbb6ac764c393", size = 765188, upload-time = "2026-10-12T14:39:58.537Z" }
|
|
2458
2458
|
wheels = [
|
|
2459
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
2459
|
+
{ url = "https://files.pythonhosted.org/packages/97/d6/da1ba42b7c26cd511c9be5473dbb178c51b07a337e94f09188d45f0306a2/langchain_anthropic-1.7.6-py3-none-any.whl", hash = "sha256:06bee97006d99e9a04abc4bd56599fc356c0c7f7f2a97629645cb603d9785c73", size = 65535, upload-time = "2026-10-12T14:39:57.386Z" },
|
|
2460
2460
|
]
|
|
2461
2461
|
|
|
2462
2462
|
[[package]]
|
|
2463
2463
|
name = "langchain-core"
|
|
2464
|
-
version = "1.6.
|
|
2464
|
+
version = "1.6.10"
|
|
2465
2465
|
source = { registry = "https://pypi.org/simple" }
|
|
2466
2466
|
dependencies = [
|
|
2467
2467
|
{ name = "httpx" },
|
|
@@ -2476,9 +2476,9 @@ dependencies = [
|
|
|
2476
2476
|
{ name = "typing-extensions" },
|
|
2477
2477
|
{ name = "uuid-utils" },
|
|
2478
2478
|
]
|
|
2479
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
2479
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f7/00/0a95f74a79908e7bc844a82fca35c1afc55689f55aaed086e95745946db8/langchain_core-1.6.10.tar.gz", hash = "sha256:3ad7a64eab150c1fea9f8a748b1c076aa1a960c5cf7c28d81a841a2f2dbffad1", size = 1010280, upload-time = "2026-10-12T14:13:51.184Z" }
|
|
2480
2480
|
wheels = [
|
|
2481
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
2481
|
+
{ url = "https://files.pythonhosted.org/packages/1e/2c/6ed698c6b451af0ed0efdbe94a703c18aea768d925347d8d1efd5645ae8c/langchain_core-1.6.10-py3-none-any.whl", hash = "sha256:14341bdd8b42d0dd9a53dbbcd8b0599ab47b0c718c7caa12e3eb5c50b32cffcb", size = 573253, upload-time = "2026-10-12T14:13:49.616Z" },
|
|
2482
2482
|
]
|
|
2483
2483
|
|
|
2484
2484
|
[[package]]
|
|
@@ -2497,7 +2497,7 @@ wheels = [
|
|
|
2497
2497
|
|
|
2498
2498
|
[[package]]
|
|
2499
2499
|
name = "langchain-openai"
|
|
2500
|
-
version = "1.7.
|
|
2500
|
+
version = "1.7.1"
|
|
2501
2501
|
source = { registry = "https://pypi.org/simple" }
|
|
2502
2502
|
dependencies = [
|
|
2503
2503
|
{ name = "certifi" },
|
|
@@ -2505,9 +2505,9 @@ dependencies = [
|
|
|
2505
2505
|
{ name = "openai", version = "2.54.0", source = { registry = "https://pypi.org/simple" } },
|
|
2506
2506
|
{ name = "tiktoken" },
|
|
2507
2507
|
]
|
|
2508
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
2508
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f3/20/2a19ea06055498711ee68c9675bc57125080916c001fae510646bc468ebb/langchain_openai-1.7.1.tar.gz", hash = "sha256:f9fa064c576911ff8d7c6566003bffb7eb5682c43d48407310d9de6b955f5496", size = 3405463, upload-time = "2026-10-12T14:32:52.655Z" }
|
|
2509
2509
|
wheels = [
|
|
2510
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
2510
|
+
{ url = "https://files.pythonhosted.org/packages/31/58/9a71963cb11bf9ddee997fda9d05ef6ee3645de39a65ac6fb46423a56dda/langchain_openai-1.7.1-py3-none-any.whl", hash = "sha256:e65f2da39199a8e155fb6ae732e0aea208a01da2710a5da701d5e1a116195fcf", size = 146099, upload-time = "2026-10-12T14:32:51.057Z" },
|
|
2511
2511
|
]
|
|
2512
2512
|
|
|
2513
2513
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/__init__.py
RENAMED
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/anthropic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/langgraph.py
RENAMED
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/litellm.py
RENAMED
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/mcp_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/interceptor/platform.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/flightdeck_sensor/providers/anthropic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_agent_type_validation.py
RENAMED
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_anthropic_resource_patch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_egress_url_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_framework_attribution.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_interceptor_langgraph.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_policy_emission_enriched.py
RENAMED
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_mcp_policy_enforcement.py
RENAMED
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_openai_parse_responses.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_policy_decision_summary.py
RENAMED
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_policy_directive_events.py
RENAMED
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_policy_emission_enriched.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_session_lifecycle_enrichment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flightdeck_sensor-0.7.0 → flightdeck_sensor-0.8.0}/tests/unit/test_tool_capture_destination.py
RENAMED
|
File without changes
|