flightdeck-sensor 0.5.0__tar.gz → 0.5.2__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.5.0 → flightdeck_sensor-0.5.2}/PKG-INFO +1 -1
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/__init__.py +2 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/agent_id.py +1 -4
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/context.py +10 -13
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/errors.py +1 -5
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/schemas.py +3 -1
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/session.py +7 -1
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/types.py +8 -1
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/anthropic.py +17 -5
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/base.py +26 -22
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/crewai.py +1 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/langgraph.py +1 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/litellm.py +38 -11
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/openai.py +13 -8
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/provider.py +1 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/anthropic.py +2 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/litellm.py +4 -4
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/openai.py +2 -3
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/transport/client.py +3 -8
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/pyproject.toml +8 -1
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/conftest.py +5 -4
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_agent_id.py +14 -37
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_agent_type_validation.py +7 -10
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_compat_crewai_mcp.py +6 -4
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_cross_agent_messages.py +8 -5
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_custom_directives.py +1 -5
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_errors.py +28 -9
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_framework_attribution.py +27 -10
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_interceptor.py +36 -20
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_interceptor_crewai.py +9 -6
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_interceptor_langgraph.py +4 -4
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_llm_event_enrichment.py +94 -38
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_interceptor.py +27 -11
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_patch.py +24 -27
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_policy.py +18 -8
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_policy_events.py +13 -31
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_prompt_capture.py +12 -4
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_provider_enum.py +4 -2
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_providers.py +17 -15
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_transport.py +18 -22
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/.gitignore +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/Makefile +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/README.md +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/compat/__init__.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/compat/crewai_mcp.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/config.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/__init__.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/exceptions.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/mcp_policy.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/core/policy.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/__init__.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/mcp.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/mcp_identity.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/__init__.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/protocol.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/py.typed +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/transport/__init__.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/transport/retry.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/__init__.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/__init__.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_context.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_d150_tool_capture_destination.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_identity.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_policy_emission_enriched.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_policy_enforcement.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_mcp_policy_enforcement_all_paths.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_originating_event_id.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_policy_decision_summary.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_policy_emission_enriched.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_session.py +0 -0
- {flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/tests/unit/test_session_lifecycle_enrichment.py +0 -0
|
@@ -118,6 +118,8 @@ def _resolve_user_name() -> str:
|
|
|
118
118
|
|
|
119
119
|
return pwd.getpwuid(os.getuid()).pw_name
|
|
120
120
|
except Exception:
|
|
121
|
+
# pwd missing or UID unmapped (distroless, scratch, Windows):
|
|
122
|
+
# fall through to the env-var probes below.
|
|
121
123
|
pass
|
|
122
124
|
for key in ("USER", "USERNAME", "LOGNAME"):
|
|
123
125
|
v = os.environ.get(key)
|
|
@@ -74,10 +74,7 @@ def derive_agent_id(
|
|
|
74
74
|
Returns a :class:`uuid.UUID`. Callers that need the canonical
|
|
75
75
|
string form use ``str(derive_agent_id(...))``.
|
|
76
76
|
"""
|
|
77
|
-
path =
|
|
78
|
-
f"flightdeck://{agent_type}/{user}@{hostname}"
|
|
79
|
-
f"/{client_type}/{agent_name}"
|
|
80
|
-
)
|
|
77
|
+
path = f"flightdeck://{agent_type}/{user}@{hostname}/{client_type}/{agent_name}"
|
|
81
78
|
if agent_role is not None:
|
|
82
79
|
trimmed = agent_role.strip()
|
|
83
80
|
if trimmed:
|
|
@@ -33,6 +33,7 @@ import re
|
|
|
33
33
|
import socket
|
|
34
34
|
import subprocess
|
|
35
35
|
import sys
|
|
36
|
+
from pathlib import Path
|
|
36
37
|
from typing import Any, Protocol
|
|
37
38
|
|
|
38
39
|
# Subprocess timeout for ``git`` invocations in GitCollector (M-5).
|
|
@@ -80,7 +81,7 @@ class ProcessCollector(BaseCollector):
|
|
|
80
81
|
result["pid"] = os.getpid()
|
|
81
82
|
argv0 = sys.argv[0] if sys.argv else ""
|
|
82
83
|
if argv0:
|
|
83
|
-
result["process_name"] =
|
|
84
|
+
result["process_name"] = Path(argv0).name
|
|
84
85
|
return result
|
|
85
86
|
|
|
86
87
|
|
|
@@ -96,11 +97,7 @@ class OSCollector(BaseCollector):
|
|
|
96
97
|
class UserCollector(BaseCollector):
|
|
97
98
|
def _gather(self) -> dict[str, Any]:
|
|
98
99
|
# USER on Unix, USERNAME on Windows, LOGNAME as final fallback.
|
|
99
|
-
user = (
|
|
100
|
-
os.environ.get("USER")
|
|
101
|
-
or os.environ.get("USERNAME")
|
|
102
|
-
or os.environ.get("LOGNAME")
|
|
103
|
-
)
|
|
100
|
+
user = os.environ.get("USER") or os.environ.get("USERNAME") or os.environ.get("LOGNAME")
|
|
104
101
|
return {"user": user} if user else {}
|
|
105
102
|
|
|
106
103
|
|
|
@@ -149,7 +146,7 @@ class GitCollector(BaseCollector):
|
|
|
149
146
|
if remote:
|
|
150
147
|
# Strip embedded credentials before extracting the repo name.
|
|
151
148
|
clean = re.sub(r"https?://[^@]+@", "https://", remote)
|
|
152
|
-
repo =
|
|
149
|
+
repo = Path(clean.rstrip("/")).name
|
|
153
150
|
if repo.endswith(".git"):
|
|
154
151
|
repo = repo[:-4]
|
|
155
152
|
if repo:
|
|
@@ -186,10 +183,7 @@ class KubernetesCollector(BaseCollector):
|
|
|
186
183
|
|
|
187
184
|
class DockerComposeCollector(BaseCollector):
|
|
188
185
|
def applies(self) -> bool:
|
|
189
|
-
return bool(
|
|
190
|
-
os.environ.get("COMPOSE_PROJECT_NAME")
|
|
191
|
-
or os.environ.get("COMPOSE_SERVICE")
|
|
192
|
-
)
|
|
186
|
+
return bool(os.environ.get("COMPOSE_PROJECT_NAME") or os.environ.get("COMPOSE_SERVICE"))
|
|
193
187
|
|
|
194
188
|
def _gather(self) -> dict[str, Any]:
|
|
195
189
|
result: dict[str, Any] = {"orchestration": "docker-compose"}
|
|
@@ -207,7 +201,7 @@ class DockerCollector(BaseCollector):
|
|
|
207
201
|
# /.dockerenv only exists inside Linux containers; on Windows
|
|
208
202
|
# or Mac hosts running Python natively this collector
|
|
209
203
|
# correctly returns nothing.
|
|
210
|
-
return
|
|
204
|
+
return Path("/.dockerenv").exists()
|
|
211
205
|
|
|
212
206
|
def _gather(self) -> dict[str, Any]:
|
|
213
207
|
return {"orchestration": "docker"}
|
|
@@ -224,7 +218,7 @@ class AWSECSCollector(BaseCollector):
|
|
|
224
218
|
result: dict[str, Any] = {"orchestration": "aws-ecs"}
|
|
225
219
|
task = os.environ.get("ECS_TASK_DEFINITION")
|
|
226
220
|
if task:
|
|
227
|
-
result["ecs_task"] =
|
|
221
|
+
result["ecs_task"] = Path(task).name
|
|
228
222
|
return result
|
|
229
223
|
|
|
230
224
|
|
|
@@ -397,6 +391,7 @@ def collect() -> dict[str, Any]:
|
|
|
397
391
|
if collector.applies():
|
|
398
392
|
ctx.update(collector.collect())
|
|
399
393
|
except Exception:
|
|
394
|
+
# best-effort: a single broken collector must never crash init().
|
|
400
395
|
pass
|
|
401
396
|
|
|
402
397
|
for collector in ORCHESTRATION_COLLECTORS:
|
|
@@ -405,6 +400,7 @@ def collect() -> dict[str, Any]:
|
|
|
405
400
|
ctx.update(collector.collect())
|
|
406
401
|
break
|
|
407
402
|
except Exception:
|
|
403
|
+
# best-effort: a single broken collector must never crash init().
|
|
408
404
|
pass
|
|
409
405
|
|
|
410
406
|
for collector in OTHER_COLLECTORS:
|
|
@@ -412,6 +408,7 @@ def collect() -> dict[str, Any]:
|
|
|
412
408
|
if collector.applies():
|
|
413
409
|
ctx.update(collector.collect())
|
|
414
410
|
except Exception:
|
|
411
|
+
# best-effort: a single broken collector must never crash init().
|
|
415
412
|
pass
|
|
416
413
|
|
|
417
414
|
return ctx
|
|
@@ -166,11 +166,7 @@ def _override_from_status_and_code(
|
|
|
166
166
|
# OpenAI sometimes returns 503 "engine overloaded"; distinguish on
|
|
167
167
|
# the message hint when present. The combined condition keeps ruff's
|
|
168
168
|
# SIM102 happy without losing the 503-vs-500 branch clarity.
|
|
169
|
-
if (
|
|
170
|
-
http_status == 503
|
|
171
|
-
and provider_error_code
|
|
172
|
-
and "overload" in provider_error_code.lower()
|
|
173
|
-
):
|
|
169
|
+
if http_status == 503 and provider_error_code and "overload" in provider_error_code.lower():
|
|
174
170
|
return ErrorType.OVERLOADED
|
|
175
171
|
if http_status == 400 and provider_error_code:
|
|
176
172
|
code = provider_error_code.lower()
|
|
@@ -11,6 +11,8 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel
|
|
13
13
|
|
|
14
|
+
from flightdeck_sensor.core.types import _DEFAULT_GRACE_PERIOD_MS
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class DirectivePayloadSchema(BaseModel):
|
|
16
18
|
"""Payload for action='custom' directives."""
|
|
@@ -48,7 +50,7 @@ class DirectiveResponseSchema(BaseModel):
|
|
|
48
50
|
|
|
49
51
|
action: str
|
|
50
52
|
reason: str | None = None
|
|
51
|
-
grace_period_ms: int =
|
|
53
|
+
grace_period_ms: int = _DEFAULT_GRACE_PERIOD_MS
|
|
52
54
|
degrade_to: str | None = None
|
|
53
55
|
payload: dict[str, Any] | None = None
|
|
54
56
|
|
|
@@ -148,6 +148,12 @@ _PREFLIGHT_TIMEOUT_SECS = 1
|
|
|
148
148
|
# directive queue but NOT event throughput.
|
|
149
149
|
_CUSTOM_DIRECTIVE_HANDLER_TIMEOUT_SECS = 5
|
|
150
150
|
|
|
151
|
+
# Per-(provider, request_id) retry-counter LRU bound. Eviction-on-overflow
|
|
152
|
+
# keeps memory flat for long-lived sessions making millions of calls; the
|
|
153
|
+
# counter only answers a short-window question ("did this retry chain give
|
|
154
|
+
# up"), so dropping the oldest entries is safe.
|
|
155
|
+
_RETRY_COUNTER_LRU_SIZE = 256
|
|
156
|
+
|
|
151
157
|
# D126 § 6 sub-agent message routing thresholds.
|
|
152
158
|
#
|
|
153
159
|
# Bodies up to ``SUBAGENT_INLINE_THRESHOLD_BYTES`` ride inline on the
|
|
@@ -404,7 +410,7 @@ class Session:
|
|
|
404
410
|
current = self._retry_counters.get(key, 0) + 1
|
|
405
411
|
self._retry_counters[key] = current
|
|
406
412
|
self._retry_counters.move_to_end(key)
|
|
407
|
-
while len(self._retry_counters) >
|
|
413
|
+
while len(self._retry_counters) > _RETRY_COUNTER_LRU_SIZE:
|
|
408
414
|
self._retry_counters.popitem(last=False)
|
|
409
415
|
return current
|
|
410
416
|
|
|
@@ -235,13 +235,20 @@ class SensorConfig:
|
|
|
235
235
|
self.api_url = self.server
|
|
236
236
|
|
|
237
237
|
|
|
238
|
+
# Default directive grace period (ms): the window an agent gets to wind down
|
|
239
|
+
# the current call before an enforcement action (degrade / block / shutdown)
|
|
240
|
+
# takes effect. Mirrored as the default in DirectiveResponseSchema so a
|
|
241
|
+
# wire-absent value resolves identically.
|
|
242
|
+
_DEFAULT_GRACE_PERIOD_MS = 5000
|
|
243
|
+
|
|
244
|
+
|
|
238
245
|
@dataclass(frozen=True)
|
|
239
246
|
class Directive:
|
|
240
247
|
"""A control-plane directive received in the event response envelope."""
|
|
241
248
|
|
|
242
249
|
action: DirectiveAction
|
|
243
250
|
reason: str
|
|
244
|
-
grace_period_ms: int =
|
|
251
|
+
grace_period_ms: int = _DEFAULT_GRACE_PERIOD_MS
|
|
245
252
|
payload: dict[str, Any] = field(default_factory=dict)
|
|
246
253
|
|
|
247
254
|
|
{flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/anthropic.py
RENAMED
|
@@ -94,6 +94,7 @@ try:
|
|
|
94
94
|
from anthropic.resources.beta.beta import (
|
|
95
95
|
Beta as _OrigBetaImported,
|
|
96
96
|
)
|
|
97
|
+
|
|
97
98
|
_OrigAnthropic: type | None = _anthropic_module.Anthropic
|
|
98
99
|
_OrigAsyncAnthropic: type | None = _anthropic_module.AsyncAnthropic
|
|
99
100
|
# The Beta / AsyncBeta classes are imported at module load so the
|
|
@@ -162,7 +163,10 @@ class SensorMessages:
|
|
|
162
163
|
"""
|
|
163
164
|
if self._is_async:
|
|
164
165
|
return base.call_stream_async(
|
|
165
|
-
self._real.stream,
|
|
166
|
+
self._real.stream,
|
|
167
|
+
kwargs,
|
|
168
|
+
self._session,
|
|
169
|
+
self._provider,
|
|
166
170
|
)
|
|
167
171
|
return base.call_stream(self._real.stream, kwargs, self._session, self._provider)
|
|
168
172
|
|
|
@@ -425,19 +429,27 @@ def patch_anthropic_classes(quiet: bool = False) -> None:
|
|
|
425
429
|
return
|
|
426
430
|
|
|
427
431
|
_patch_one_class(
|
|
428
|
-
_OrigAnthropic,
|
|
432
|
+
_OrigAnthropic,
|
|
433
|
+
is_async=False,
|
|
434
|
+
quiet=quiet,
|
|
429
435
|
descriptor_cls=_AnthropicMessagesDescriptor,
|
|
430
436
|
)
|
|
431
437
|
_patch_one_class(
|
|
432
|
-
_OrigAsyncAnthropic,
|
|
438
|
+
_OrigAsyncAnthropic,
|
|
439
|
+
is_async=True,
|
|
440
|
+
quiet=quiet,
|
|
433
441
|
descriptor_cls=_AnthropicMessagesDescriptor,
|
|
434
442
|
)
|
|
435
443
|
_patch_one_class(
|
|
436
|
-
_OrigBeta,
|
|
444
|
+
_OrigBeta,
|
|
445
|
+
is_async=False,
|
|
446
|
+
quiet=quiet,
|
|
437
447
|
descriptor_cls=_AnthropicBetaMessagesDescriptor,
|
|
438
448
|
)
|
|
439
449
|
_patch_one_class(
|
|
440
|
-
_OrigAsyncBeta,
|
|
450
|
+
_OrigAsyncBeta,
|
|
451
|
+
is_async=True,
|
|
452
|
+
quiet=quiet,
|
|
441
453
|
descriptor_cls=_AnthropicBetaMessagesDescriptor,
|
|
442
454
|
)
|
|
443
455
|
|
|
@@ -37,6 +37,7 @@ def _safe_pct(used: int, limit: int | None) -> int:
|
|
|
37
37
|
return 0
|
|
38
38
|
return (used * 100) // limit
|
|
39
39
|
|
|
40
|
+
|
|
40
41
|
if TYPE_CHECKING:
|
|
41
42
|
from collections.abc import Iterator
|
|
42
43
|
from types import TracebackType
|
|
@@ -159,7 +160,12 @@ def call_stream(
|
|
|
159
160
|
estimated, estimated_via = provider.estimate_tokens(kwargs)
|
|
160
161
|
call_kwargs = _pre_call(session, provider, kwargs, estimated, estimated_via)
|
|
161
162
|
return GuardedStream(
|
|
162
|
-
real_fn,
|
|
163
|
+
real_fn,
|
|
164
|
+
call_kwargs,
|
|
165
|
+
session,
|
|
166
|
+
provider,
|
|
167
|
+
estimated,
|
|
168
|
+
estimated_via,
|
|
163
169
|
)
|
|
164
170
|
|
|
165
171
|
|
|
@@ -180,7 +186,12 @@ def call_stream_async(
|
|
|
180
186
|
estimated, estimated_via = provider.estimate_tokens(kwargs)
|
|
181
187
|
call_kwargs = _pre_call(session, provider, kwargs, estimated, estimated_via)
|
|
182
188
|
return GuardedAsyncStream(
|
|
183
|
-
real_fn,
|
|
189
|
+
real_fn,
|
|
190
|
+
call_kwargs,
|
|
191
|
+
session,
|
|
192
|
+
provider,
|
|
193
|
+
estimated,
|
|
194
|
+
estimated_via,
|
|
184
195
|
)
|
|
185
196
|
|
|
186
197
|
|
|
@@ -301,9 +312,7 @@ class GuardedStream:
|
|
|
301
312
|
# already propagating.
|
|
302
313
|
try:
|
|
303
314
|
is_stream_error = self._first_chunk_t is not None
|
|
304
|
-
abort_reason =
|
|
305
|
-
"error_mid_stream" if is_stream_error else "error_before_stream"
|
|
306
|
-
)
|
|
315
|
+
abort_reason = "error_mid_stream" if is_stream_error else "error_before_stream"
|
|
307
316
|
# Re-examine the exception class: APITimeoutError and
|
|
308
317
|
# plain TimeoutError both map to ``timeout`` per
|
|
309
318
|
# taxonomy, but abort_reason stays more descriptive.
|
|
@@ -345,7 +354,8 @@ class GuardedStream:
|
|
|
345
354
|
)
|
|
346
355
|
except Exception:
|
|
347
356
|
_log.warning(
|
|
348
|
-
"Failed to post stream reconciliation event",
|
|
357
|
+
"Failed to post stream reconciliation event",
|
|
358
|
+
exc_info=True,
|
|
349
359
|
)
|
|
350
360
|
|
|
351
361
|
# Iterator protocol. Self-iterating so we can timestamp every chunk
|
|
@@ -521,6 +531,7 @@ class GuardedAsyncStream:
|
|
|
521
531
|
# ``AttributeError: coroutine object has no
|
|
522
532
|
# attribute '__aenter__'`` (caught by Rule 40d smoke).
|
|
523
533
|
import inspect
|
|
534
|
+
|
|
524
535
|
if inspect.iscoroutine(self._ctx):
|
|
525
536
|
self._ctx = await self._ctx
|
|
526
537
|
self._stream = await self._ctx.__aenter__()
|
|
@@ -564,9 +575,7 @@ class GuardedAsyncStream:
|
|
|
564
575
|
if exc_val is not None:
|
|
565
576
|
try:
|
|
566
577
|
is_stream_error = self._first_chunk_t is not None
|
|
567
|
-
abort_reason =
|
|
568
|
-
"error_mid_stream" if is_stream_error else "error_before_stream"
|
|
569
|
-
)
|
|
578
|
+
abort_reason = "error_mid_stream" if is_stream_error else "error_before_stream"
|
|
570
579
|
if type(exc_val).__name__ in ("APITimeoutError", "TimeoutError"):
|
|
571
580
|
abort_reason = "timeout"
|
|
572
581
|
_emit_error(
|
|
@@ -602,7 +611,8 @@ class GuardedAsyncStream:
|
|
|
602
611
|
)
|
|
603
612
|
except Exception:
|
|
604
613
|
_log.warning(
|
|
605
|
-
"Failed to post async stream reconciliation event",
|
|
614
|
+
"Failed to post async stream reconciliation event",
|
|
615
|
+
exc_info=True,
|
|
606
616
|
)
|
|
607
617
|
|
|
608
618
|
def __aiter__(self) -> Any:
|
|
@@ -723,8 +733,7 @@ def _pre_call(
|
|
|
723
733
|
warn_token_limit = session.policy.token_limit
|
|
724
734
|
pre_call_decision = PolicyDecisionSummary(
|
|
725
735
|
policy_id=result.policy_id or ("local" if local_warn else ""),
|
|
726
|
-
scope=result.matched_policy_scope
|
|
727
|
-
or ("local_failsafe" if local_warn else ""),
|
|
736
|
+
scope=result.matched_policy_scope or ("local_failsafe" if local_warn else ""),
|
|
728
737
|
decision="warn",
|
|
729
738
|
reason=(
|
|
730
739
|
f"Pre-call check: {session.tokens_used + estimated}/"
|
|
@@ -846,8 +855,7 @@ def _pre_call(
|
|
|
846
855
|
local_warn = warn_source == "local"
|
|
847
856
|
warn_decision = PolicyDecisionSummary(
|
|
848
857
|
policy_id=result.policy_id or ("local" if local_warn else ""),
|
|
849
|
-
scope=result.matched_policy_scope
|
|
850
|
-
or ("local_failsafe" if local_warn else ""),
|
|
858
|
+
scope=result.matched_policy_scope or ("local_failsafe" if local_warn else ""),
|
|
851
859
|
decision="warn",
|
|
852
860
|
reason=(
|
|
853
861
|
f"Token usage {session.tokens_used}/{warn_token_limit} "
|
|
@@ -1013,15 +1021,12 @@ def _post_call(
|
|
|
1013
1021
|
try:
|
|
1014
1022
|
post_result = session.policy.check(session_total, 0)
|
|
1015
1023
|
if post_result.decision in (PolicyDecision.WARN, PolicyDecision.BLOCK):
|
|
1016
|
-
decision_str =
|
|
1017
|
-
"warn" if post_result.decision == PolicyDecision.WARN else "block"
|
|
1018
|
-
)
|
|
1024
|
+
decision_str = "warn" if post_result.decision == PolicyDecision.WARN else "block"
|
|
1019
1025
|
post_source = post_result.source or "server"
|
|
1020
1026
|
local = post_source == "local"
|
|
1021
1027
|
post_decision = PolicyDecisionSummary(
|
|
1022
1028
|
policy_id=post_result.policy_id or ("local" if local else ""),
|
|
1023
|
-
scope=post_result.matched_policy_scope
|
|
1024
|
-
or ("local_failsafe" if local else ""),
|
|
1029
|
+
scope=post_result.matched_policy_scope or ("local_failsafe" if local else ""),
|
|
1025
1030
|
decision=decision_str,
|
|
1026
1031
|
reason=(
|
|
1027
1032
|
f"Post-call cumulative usage crossed {decision_str} "
|
|
@@ -1084,6 +1089,7 @@ def _post_call(
|
|
|
1084
1089
|
from flightdeck_sensor.interceptor.mcp import (
|
|
1085
1090
|
_build_tool_capture_content,
|
|
1086
1091
|
)
|
|
1092
|
+
|
|
1087
1093
|
capture_content = _build_tool_capture_content(
|
|
1088
1094
|
tool_input=inv.tool_input,
|
|
1089
1095
|
tool_output=None,
|
|
@@ -1161,9 +1167,7 @@ def _emit_error(
|
|
|
1161
1167
|
# str at runtime. cast(str, ...) tells mypy the result is str
|
|
1162
1168
|
# without manufacturing a third "" fallback that would conflate
|
|
1163
1169
|
# missing-key, present-but-None, and present-but-empty cases.
|
|
1164
|
-
provider_name = cast(
|
|
1165
|
-
"str", error_payload.get("provider") or getattr(provider, "name", "")
|
|
1166
|
-
)
|
|
1170
|
+
provider_name = cast("str", error_payload.get("provider") or getattr(provider, "name", ""))
|
|
1167
1171
|
# Trust-boundary check: error_payload is sensor-internal but
|
|
1168
1172
|
# composed from provider-supplied error attributes (request_id
|
|
1169
1173
|
# is whatever the SDK exposed). isinstance enforces the
|
{flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/interceptor/litellm.py
RENAMED
|
@@ -68,6 +68,7 @@ _STREAMING_NOT_SUPPORTED_MSG = (
|
|
|
68
68
|
|
|
69
69
|
try:
|
|
70
70
|
import litellm as _litellm_module
|
|
71
|
+
|
|
71
72
|
_OrigCompletion: Any = _litellm_module.completion
|
|
72
73
|
_OrigAcompletion: Any = _litellm_module.acompletion
|
|
73
74
|
# Phase 4: embedding / aembedding are the two entry points for
|
|
@@ -131,8 +132,7 @@ class SensorLitellm:
|
|
|
131
132
|
) -> None:
|
|
132
133
|
if not _LITELLM_AVAILABLE:
|
|
133
134
|
raise ImportError(
|
|
134
|
-
"litellm is not installed. "
|
|
135
|
-
"Install with `pip install flightdeck-sensor[litellm]`."
|
|
135
|
+
"litellm is not installed. Install with `pip install flightdeck-sensor[litellm]`."
|
|
136
136
|
)
|
|
137
137
|
self._session = session
|
|
138
138
|
self._provider = provider or LitellmProvider(
|
|
@@ -158,7 +158,10 @@ class SensorLitellm:
|
|
|
158
158
|
if kwargs.get("stream"):
|
|
159
159
|
raise NotImplementedError(_STREAMING_NOT_SUPPORTED_MSG)
|
|
160
160
|
return await base.call_async(
|
|
161
|
-
_OrigAcompletion,
|
|
161
|
+
_OrigAcompletion,
|
|
162
|
+
kwargs,
|
|
163
|
+
self._session,
|
|
164
|
+
self._provider,
|
|
162
165
|
)
|
|
163
166
|
|
|
164
167
|
# Phase 4: embeddings parallel to completion. Same delegation pattern;
|
|
@@ -171,26 +174,34 @@ class SensorLitellm:
|
|
|
171
174
|
chat completions.
|
|
172
175
|
"""
|
|
173
176
|
from flightdeck_sensor.core.types import EventType
|
|
177
|
+
|
|
174
178
|
if _OrigEmbedding is None:
|
|
175
179
|
raise RuntimeError(
|
|
176
180
|
"litellm.embedding is not available on the installed version; "
|
|
177
181
|
"upgrade litellm or use a different embedding client."
|
|
178
182
|
)
|
|
179
183
|
return base.call(
|
|
180
|
-
_OrigEmbedding,
|
|
184
|
+
_OrigEmbedding,
|
|
185
|
+
kwargs,
|
|
186
|
+
self._session,
|
|
187
|
+
self._provider,
|
|
181
188
|
event_type=EventType.EMBEDDINGS,
|
|
182
189
|
)
|
|
183
190
|
|
|
184
191
|
async def aembedding(self, **kwargs: Any) -> Any:
|
|
185
192
|
"""Intercept ``litellm.aembedding(**kwargs)``."""
|
|
186
193
|
from flightdeck_sensor.core.types import EventType
|
|
194
|
+
|
|
187
195
|
if _OrigAembedding is None:
|
|
188
196
|
raise RuntimeError(
|
|
189
197
|
"litellm.aembedding is not available on the installed version; "
|
|
190
198
|
"upgrade litellm or use a different embedding client."
|
|
191
199
|
)
|
|
192
200
|
return await base.call_async(
|
|
193
|
-
_OrigAembedding,
|
|
201
|
+
_OrigAembedding,
|
|
202
|
+
kwargs,
|
|
203
|
+
self._session,
|
|
204
|
+
self._provider,
|
|
194
205
|
event_type=EventType.EMBEDDINGS,
|
|
195
206
|
)
|
|
196
207
|
|
|
@@ -273,11 +284,15 @@ def patch_litellm_functions(quiet: bool = False) -> None:
|
|
|
273
284
|
if session is None or orig_embedding is None:
|
|
274
285
|
return orig_embedding(**kwargs) if orig_embedding else None
|
|
275
286
|
from flightdeck_sensor.core.types import EventType
|
|
287
|
+
|
|
276
288
|
provider = LitellmProvider(
|
|
277
289
|
capture_prompts=session.config.capture_prompts,
|
|
278
290
|
)
|
|
279
291
|
return base.call(
|
|
280
|
-
orig_embedding,
|
|
292
|
+
orig_embedding,
|
|
293
|
+
kwargs,
|
|
294
|
+
session,
|
|
295
|
+
provider,
|
|
281
296
|
event_type=EventType.EMBEDDINGS,
|
|
282
297
|
)
|
|
283
298
|
|
|
@@ -286,11 +301,15 @@ def patch_litellm_functions(quiet: bool = False) -> None:
|
|
|
286
301
|
if session is None or orig_aembedding is None:
|
|
287
302
|
return await orig_aembedding(**kwargs) if orig_aembedding else None
|
|
288
303
|
from flightdeck_sensor.core.types import EventType
|
|
304
|
+
|
|
289
305
|
provider = LitellmProvider(
|
|
290
306
|
capture_prompts=session.config.capture_prompts,
|
|
291
307
|
)
|
|
292
308
|
return await base.call_async(
|
|
293
|
-
orig_aembedding,
|
|
309
|
+
orig_aembedding,
|
|
310
|
+
kwargs,
|
|
311
|
+
session,
|
|
312
|
+
provider,
|
|
294
313
|
event_type=EventType.EMBEDDINGS,
|
|
295
314
|
)
|
|
296
315
|
|
|
@@ -331,16 +350,24 @@ def unpatch_litellm_functions(quiet: bool = False) -> None:
|
|
|
331
350
|
return
|
|
332
351
|
|
|
333
352
|
orig_completion = getattr(
|
|
334
|
-
_litellm_module,
|
|
353
|
+
_litellm_module,
|
|
354
|
+
"_flightdeck_orig_completion",
|
|
355
|
+
None,
|
|
335
356
|
)
|
|
336
357
|
orig_acompletion = getattr(
|
|
337
|
-
_litellm_module,
|
|
358
|
+
_litellm_module,
|
|
359
|
+
"_flightdeck_orig_acompletion",
|
|
360
|
+
None,
|
|
338
361
|
)
|
|
339
362
|
orig_embedding = getattr(
|
|
340
|
-
_litellm_module,
|
|
363
|
+
_litellm_module,
|
|
364
|
+
"_flightdeck_orig_embedding",
|
|
365
|
+
None,
|
|
341
366
|
)
|
|
342
367
|
orig_aembedding = getattr(
|
|
343
|
-
_litellm_module,
|
|
368
|
+
_litellm_module,
|
|
369
|
+
"_flightdeck_orig_aembedding",
|
|
370
|
+
None,
|
|
344
371
|
)
|
|
345
372
|
if orig_completion is not None:
|
|
346
373
|
_litellm_module.completion = orig_completion
|
|
@@ -81,6 +81,7 @@ _log = logging.getLogger("flightdeck_sensor.interceptor.openai")
|
|
|
81
81
|
|
|
82
82
|
try:
|
|
83
83
|
import openai as _openai_module
|
|
84
|
+
|
|
84
85
|
_OrigOpenAI: type | None = _openai_module.OpenAI
|
|
85
86
|
_OrigAsyncOpenAI: type | None = _openai_module.AsyncOpenAI
|
|
86
87
|
_OPENAI_AVAILABLE = True
|
|
@@ -144,6 +145,7 @@ class SensorCompletions:
|
|
|
144
145
|
self._session,
|
|
145
146
|
self._provider,
|
|
146
147
|
)
|
|
148
|
+
|
|
147
149
|
return _async_stream_create()
|
|
148
150
|
return base.call_stream(
|
|
149
151
|
self._real.create,
|
|
@@ -226,9 +228,7 @@ class _SensorCompletionsRawResponseWrapper:
|
|
|
226
228
|
"""Intercept the raw-response create closure with the sensor pipeline."""
|
|
227
229
|
real_fn = self._real.create
|
|
228
230
|
if self._is_async:
|
|
229
|
-
return base.call_async(
|
|
230
|
-
real_fn, kwargs, self._session, self._provider
|
|
231
|
-
)
|
|
231
|
+
return base.call_async(real_fn, kwargs, self._session, self._provider)
|
|
232
232
|
return base.call(real_fn, kwargs, self._session, self._provider)
|
|
233
233
|
|
|
234
234
|
def __getattr__(self, name: str) -> Any:
|
|
@@ -300,9 +300,7 @@ class SensorResponses:
|
|
|
300
300
|
"""Intercept responses.create() -- sync or async dispatch."""
|
|
301
301
|
real_fn = self._real.create
|
|
302
302
|
if self._is_async:
|
|
303
|
-
return base.call_async(
|
|
304
|
-
real_fn, kwargs, self._session, self._provider
|
|
305
|
-
)
|
|
303
|
+
return base.call_async(real_fn, kwargs, self._session, self._provider)
|
|
306
304
|
return base.call(real_fn, kwargs, self._session, self._provider)
|
|
307
305
|
|
|
308
306
|
def __getattr__(self, name: str) -> Any:
|
|
@@ -352,13 +350,20 @@ class SensorEmbeddings:
|
|
|
352
350
|
"""
|
|
353
351
|
real_fn = self._real.create
|
|
354
352
|
from flightdeck_sensor.core.types import EventType
|
|
353
|
+
|
|
355
354
|
if self._is_async:
|
|
356
355
|
return base.call_async(
|
|
357
|
-
real_fn,
|
|
356
|
+
real_fn,
|
|
357
|
+
kwargs,
|
|
358
|
+
self._session,
|
|
359
|
+
self._provider,
|
|
358
360
|
event_type=EventType.EMBEDDINGS,
|
|
359
361
|
)
|
|
360
362
|
return base.call(
|
|
361
|
-
real_fn,
|
|
363
|
+
real_fn,
|
|
364
|
+
kwargs,
|
|
365
|
+
self._session,
|
|
366
|
+
self._provider,
|
|
362
367
|
event_type=EventType.EMBEDDINGS,
|
|
363
368
|
)
|
|
364
369
|
|
{flightdeck_sensor-0.5.0 → flightdeck_sensor-0.5.2}/flightdeck_sensor/providers/anthropic.py
RENAMED
|
@@ -194,6 +194,7 @@ class AnthropicProvider:
|
|
|
194
194
|
# MessageStream returns a real Message which
|
|
195
195
|
# ``model_dump`` handles cleanly.
|
|
196
196
|
import inspect
|
|
197
|
+
|
|
197
198
|
if not inspect.iscoroutine(candidate):
|
|
198
199
|
response = candidate
|
|
199
200
|
else:
|
|
@@ -209,6 +210,7 @@ class AnthropicProvider:
|
|
|
209
210
|
# are not JSON-serialisable so the wire payload stays
|
|
210
211
|
# valid even on exotic response shapes.
|
|
211
212
|
import json
|
|
213
|
+
|
|
212
214
|
resp_dict = {}
|
|
213
215
|
for k, v in dict(response.__dict__).items():
|
|
214
216
|
try:
|
|
@@ -78,7 +78,9 @@ class LitellmProvider:
|
|
|
78
78
|
what is reachable on the ModelResponse object."""
|
|
79
79
|
try:
|
|
80
80
|
request_id = getattr(response, "id", None) or getattr(
|
|
81
|
-
response,
|
|
81
|
+
response,
|
|
82
|
+
"_request_id",
|
|
83
|
+
None,
|
|
82
84
|
)
|
|
83
85
|
model = getattr(response, "model", None)
|
|
84
86
|
out: dict[str, Any] = {}
|
|
@@ -172,9 +174,7 @@ class LitellmProvider:
|
|
|
172
174
|
vectors = []
|
|
173
175
|
for entry in data:
|
|
174
176
|
emb = getattr(entry, "embedding", None) or (
|
|
175
|
-
entry.get("embedding")
|
|
176
|
-
if isinstance(entry, dict)
|
|
177
|
-
else None
|
|
177
|
+
entry.get("embedding") if isinstance(entry, dict) else None
|
|
178
178
|
)
|
|
179
179
|
if emb is not None:
|
|
180
180
|
vectors.append(list(emb))
|
|
@@ -258,9 +258,7 @@ class OpenAIProvider:
|
|
|
258
258
|
vectors = []
|
|
259
259
|
for entry in data:
|
|
260
260
|
emb = getattr(entry, "embedding", None) or (
|
|
261
|
-
entry.get("embedding")
|
|
262
|
-
if isinstance(entry, dict)
|
|
263
|
-
else None
|
|
261
|
+
entry.get("embedding") if isinstance(entry, dict) else None
|
|
264
262
|
)
|
|
265
263
|
if emb is not None:
|
|
266
264
|
vectors.append(list(emb))
|
|
@@ -294,6 +292,7 @@ class OpenAIProvider:
|
|
|
294
292
|
# ``Object of type AsyncStream is not JSON
|
|
295
293
|
# serializable`` (caught by Rule 40d smoke).
|
|
296
294
|
import json as _json
|
|
295
|
+
|
|
297
296
|
resp_dict = {}
|
|
298
297
|
for k, v in dict(response.__dict__).items():
|
|
299
298
|
try:
|