flightdeck-sensor 0.8.0__tar.gz → 0.10.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.10.0/PKG-INFO +455 -0
- flightdeck_sensor-0.10.0/README.md +389 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/__init__.py +594 -113
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/config.py +11 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/context.py +271 -45
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/exceptions.py +8 -2
- flightdeck_sensor-0.10.0/flightdeck_sensor/core/import_hook.py +296 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/session.py +766 -55
- flightdeck_sensor-0.10.0/flightdeck_sensor/core/tool_capture.py +62 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/types.py +27 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/anthropic.py +21 -319
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/base.py +186 -25
- flightdeck_sensor-0.10.0/flightdeck_sensor/interceptor/claude_agent_sdk.py +1075 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/crewai.py +5 -4
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/langgraph.py +5 -4
- flightdeck_sensor-0.10.0/flightdeck_sensor/interceptor/litellm.py +823 -0
- flightdeck_sensor-0.10.0/flightdeck_sensor/interceptor/llama_index.py +554 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/mcp.py +4 -53
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/openai.py +633 -60
- flightdeck_sensor-0.10.0/flightdeck_sensor/interceptor/openai_agents.py +847 -0
- flightdeck_sensor-0.10.0/flightdeck_sensor/interceptor/raw_views.py +510 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/provider.py +15 -0
- flightdeck_sensor-0.10.0/flightdeck_sensor/providers/_access.py +32 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/providers/anthropic.py +94 -7
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/providers/litellm.py +445 -9
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/providers/openai.py +240 -7
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/providers/protocol.py +51 -12
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/transport/body_limit.py +13 -11
- flightdeck_sensor-0.10.0/flightdeck_sensor/transport/client.py +1074 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/transport/retry.py +18 -10
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/pyproject.toml +93 -9
- flightdeck_sensor-0.10.0/tests/unit/_child_process.py +244 -0
- flightdeck_sensor-0.10.0/tests/unit/_lazy_patch_child.py +284 -0
- flightdeck_sensor-0.10.0/tests/unit/_signal_child.py +150 -0
- flightdeck_sensor-0.10.0/tests/unit/test_agent_handoff.py +399 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_anthropic_resource_patch.py +16 -6
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_context.py +224 -20
- flightdeck_sensor-0.10.0/tests/unit/test_deferred_stream_unread.py +447 -0
- flightdeck_sensor-0.10.0/tests/unit/test_framework_attribution.py +412 -0
- flightdeck_sensor-0.10.0/tests/unit/test_identity_vectors.py +63 -0
- flightdeck_sensor-0.10.0/tests/unit/test_import_hook.py +269 -0
- flightdeck_sensor-0.10.0/tests/unit/test_init_defaults.py +463 -0
- flightdeck_sensor-0.10.0/tests/unit/test_interceptor_claude_agent_sdk.py +1094 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_interceptor_crewai.py +17 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_interceptor_langgraph.py +17 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_interceptor_litellm.py +15 -13
- flightdeck_sensor-0.10.0/tests/unit/test_interceptor_litellm_routes.py +1422 -0
- flightdeck_sensor-0.10.0/tests/unit/test_interceptor_llama_index.py +929 -0
- flightdeck_sensor-0.10.0/tests/unit/test_interceptor_openai_agents.py +1275 -0
- flightdeck_sensor-0.10.0/tests/unit/test_lazy_patch.py +549 -0
- flightdeck_sensor-0.10.0/tests/unit/test_openai_beta_chat.py +321 -0
- flightdeck_sensor-0.10.0/tests/unit/test_openai_constructed_resources.py +657 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_openai_parse_responses.py +18 -2
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_provider_enum.py +34 -11
- flightdeck_sensor-0.10.0/tests/unit/test_raw_stream_capture.py +1374 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_session.py +7 -7
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_session_lifecycle_enrichment.py +13 -5
- flightdeck_sensor-0.10.0/tests/unit/test_signal_shutdown.py +911 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_token_cost_fields.py +120 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_tool_capture_destination.py +6 -8
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_transport.py +147 -2
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/uv.lock +1069 -570
- flightdeck_sensor-0.8.0/PKG-INFO +0 -239
- flightdeck_sensor-0.8.0/README.md +0 -179
- flightdeck_sensor-0.8.0/flightdeck_sensor/interceptor/litellm.py +0 -544
- flightdeck_sensor-0.8.0/flightdeck_sensor/transport/client.py +0 -548
- flightdeck_sensor-0.8.0/tests/unit/test_framework_attribution.py +0 -211
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/.gitignore +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/CLAUDE.md +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/Makefile +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/compat/__init__.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/compat/crewai_mcp.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/__init__.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/agent_id.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/errors.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/mcp_policy.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/policy.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/core/schemas.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/__init__.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/mcp_identity.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/interceptor/platform.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/providers/__init__.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/py.typed +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/flightdeck_sensor/transport/__init__.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/__init__.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/conftest.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/__init__.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_agent_id.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_agent_type_validation.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_body_limit.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_compat_crewai_mcp.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_cross_agent_messages.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_custom_directives.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_egress_url_validation.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_errors.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_interceptor.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_llm_event_enrichment.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_mcp_2x_paths.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_mcp_availability.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_mcp_identity.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_mcp_interceptor.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_mcp_policy_emission_enriched.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_mcp_policy_enforcement.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_mcp_policy_enforcement_all_paths.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_originating_event_id.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_patch.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_policy.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_policy_decision_summary.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_policy_directive_events.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_policy_emission_enriched.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_policy_events.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_prompt_capture.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_providers.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_stop_reason_errors.py +0 -0
- {flightdeck_sensor-0.8.0 → flightdeck_sensor-0.10.0}/tests/unit/test_streaming_usage.py +0 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flightdeck-sensor
|
|
3
|
+
Version: 0.10.0
|
|
4
|
+
Summary: In-process agent observability sensor for Flightdeck
|
|
5
|
+
Project-URL: Repository, https://github.com/flightdeckhq/flightdeck
|
|
6
|
+
Author-email: Omri Arnon <omri.arnon@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: <3.15,>=3.11
|
|
18
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: anthropic>=0.86; extra == 'anthropic'
|
|
21
|
+
Provides-Extra: claude-agent-sdk
|
|
22
|
+
Requires-Dist: claude-agent-sdk<0.3,>=0.2.168; extra == 'claude-agent-sdk'
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: anthropic>=0.86; extra == 'dev'
|
|
25
|
+
Requires-Dist: claude-agent-sdk<0.3,>=0.2.168; extra == 'dev'
|
|
26
|
+
Requires-Dist: crewai>=1.14; (python_version < '3.14') and extra == 'dev'
|
|
27
|
+
Requires-Dist: httpx>=0.25; extra == 'dev'
|
|
28
|
+
Requires-Dist: langchain-anthropic<2,>=1; extra == 'dev'
|
|
29
|
+
Requires-Dist: langchain-mcp-adapters<0.4,>=0.3; extra == 'dev'
|
|
30
|
+
Requires-Dist: langchain-openai<2,>=1; extra == 'dev'
|
|
31
|
+
Requires-Dist: langchain<2,>=1; extra == 'dev'
|
|
32
|
+
Requires-Dist: langgraph<2,>=1; extra == 'dev'
|
|
33
|
+
Requires-Dist: litellm>=1.100; extra == 'dev'
|
|
34
|
+
Requires-Dist: llama-index-llms-anthropic>=0.1; extra == 'dev'
|
|
35
|
+
Requires-Dist: llama-index-llms-openai>=0.1; extra == 'dev'
|
|
36
|
+
Requires-Dist: llama-index-tools-mcp>=0.1; extra == 'dev'
|
|
37
|
+
Requires-Dist: mcp<2,>=1.28; extra == 'dev'
|
|
38
|
+
Requires-Dist: mcpadapt>=0.0.6; extra == 'dev'
|
|
39
|
+
Requires-Dist: mypy>=2.4; extra == 'dev'
|
|
40
|
+
Requires-Dist: openai-agents<0.21,>=0.20; extra == 'dev'
|
|
41
|
+
Requires-Dist: openai>=1.26; extra == 'dev'
|
|
42
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: pytest-cov>=7.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: pytest-xdist<4,>=3.5; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest>=9; extra == 'dev'
|
|
46
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
47
|
+
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
48
|
+
Provides-Extra: litellm
|
|
49
|
+
Requires-Dist: litellm>=1.83.0; extra == 'litellm'
|
|
50
|
+
Provides-Extra: mcp
|
|
51
|
+
Requires-Dist: mcp<3,>=1.28; extra == 'mcp'
|
|
52
|
+
Provides-Extra: openai
|
|
53
|
+
Requires-Dist: openai>=1.26; extra == 'openai'
|
|
54
|
+
Requires-Dist: tiktoken>=0.5; extra == 'openai'
|
|
55
|
+
Provides-Extra: openai-agents
|
|
56
|
+
Requires-Dist: openai-agents>=0.6; extra == 'openai-agents'
|
|
57
|
+
Provides-Extra: test
|
|
58
|
+
Requires-Dist: mypy>=2.4; extra == 'test'
|
|
59
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'test'
|
|
60
|
+
Requires-Dist: pytest-xdist<4,>=3.5; extra == 'test'
|
|
61
|
+
Requires-Dist: pytest>=9; extra == 'test'
|
|
62
|
+
Requires-Dist: respx>=0.20; extra == 'test'
|
|
63
|
+
Requires-Dist: ruff>=0.3; extra == 'test'
|
|
64
|
+
Requires-Dist: tiktoken>=0.5; extra == 'test'
|
|
65
|
+
Description-Content-Type: text/markdown
|
|
66
|
+
|
|
67
|
+
# flightdeck-sensor
|
|
68
|
+
|
|
69
|
+
In-process agent observability sensor for [Flightdeck](https://github.com/flightdeckhq/flightdeck).
|
|
70
|
+
|
|
71
|
+
## Quickstart
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install flightdeck-sensor
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import flightdeck_sensor
|
|
79
|
+
|
|
80
|
+
flightdeck_sensor.init()
|
|
81
|
+
flightdeck_sensor.patch()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`init()` resolves `server` and `token` each as: the `FLIGHTDECK_SERVER`
|
|
85
|
+
/ `FLIGHTDECK_TOKEN` environment variable when it is set, else the
|
|
86
|
+
kwarg, else the default. The server default is
|
|
87
|
+
`http://localhost:4000/ingest`, where the `flightdeck` binary and the
|
|
88
|
+
dev compose stack listen. The token default is the dev seed `tok_dev`,
|
|
89
|
+
used only when the server was defaulted as well and its host is
|
|
90
|
+
loopback; `init()` logs one WARNING when it is in use. A server
|
|
91
|
+
supplied without a token raises `ConfigurationError` (a `ValueError`)
|
|
92
|
+
naming both variables, whatever its host: the sensor never sends
|
|
93
|
+
`tok_dev` to a server you chose. [Environment
|
|
94
|
+
variables](#environment-variables) gives the precedence of every other
|
|
95
|
+
`init()` argument that has a variable.
|
|
96
|
+
|
|
97
|
+
## Identity
|
|
98
|
+
|
|
99
|
+
Every session carries a persistent **flavor** and an ephemeral **session ID**. Set the flavor via environment variable, typically from your orchestrator manifest:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
env:
|
|
103
|
+
- name: AGENT_FLAVOR
|
|
104
|
+
value: "research-agent"
|
|
105
|
+
- name: FLIGHTDECK_SERVER
|
|
106
|
+
value: "https://flightdeck.svc.cluster.local"
|
|
107
|
+
- name: FLIGHTDECK_TOKEN
|
|
108
|
+
valueFrom:
|
|
109
|
+
secretKeyRef:
|
|
110
|
+
name: flightdeck-token
|
|
111
|
+
key: token
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Agents without `AGENT_FLAVOR` appear as `{user}@{hostname}`. That is how agents deployed outside your blessed config become visible.
|
|
115
|
+
|
|
116
|
+
Every session also carries a **client type**, the kind of client that produced it (the sensor sends `flightdeck_sensor`, the Claude Code plugin `claude_code`). The dashboard shows it as a pill on each agent and offers it as a filter. A stable `agent_id`, derived from the agent type, user, host, client type and agent name, ties a client's sessions to one agent.
|
|
117
|
+
|
|
118
|
+
For one continuous session across the re-runs of a Temporal workflow or an Airflow DAG, pass a stable `session_id` at `init()`: see [Optional `session_id` hint](#optional-session_id-hint).
|
|
119
|
+
|
|
120
|
+
## Sensor reference
|
|
121
|
+
|
|
122
|
+
### `patch()` vs `wrap()`
|
|
123
|
+
|
|
124
|
+
`patch()` is the right answer in almost every case. For Anthropic it patches `create`, `stream` and `parse` on the SDK's `Messages` resource classes, which every `anthropic` client class hands out, so every client — first-party, Bedrock, Vertex, Google Cloud, Foundry, AWS, Mantle, and the ones frameworks build internally — is intercepted, including a client built before `patch()` (from its next call on; only a `with_raw_response` / `with_streaming_response` view first touched before `patch()` stays unmetered). For OpenAI it installs a class-level descriptor on `openai.OpenAI` and `openai.AsyncOpenAI` so every instance, including ones frameworks build internally, is intercepted.
|
|
125
|
+
|
|
126
|
+
`wrap()` instruments a single client instance:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
client = flightdeck_sensor.wrap(anthropic.Anthropic())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use `wrap()` only when you deliberately do not call `patch()`. Clients built by frameworks will be invisible unless `patch()` is active.
|
|
133
|
+
|
|
134
|
+
Call `patch()` before any framework or user code constructs a client. In practice this means calling `init()` + `patch()` at the top of your entrypoint.
|
|
135
|
+
|
|
136
|
+
### Environment variables
|
|
137
|
+
|
|
138
|
+
| Variable | Purpose |
|
|
139
|
+
|---------------------------------|-----------------------------------------------------------------|
|
|
140
|
+
| `FLIGHTDECK_SERVER` | The server's address: its base URL (`https://flightdeck.example.com`) or its ingestion URL (`https://flightdeck.example.com/ingest`, what the start-up block prints). Wins over the `server=` kwarg. Default `http://localhost:4000/ingest`. The Claude Code plugin reads the same variable and takes the same two forms. |
|
|
141
|
+
| `FLIGHTDECK_TOKEN` | Access token. Wins over the `token=` kwarg. Defaults to `tok_dev` only when the server is defaulted too. |
|
|
142
|
+
| `FLIGHTDECK_API_URL` | Control-plane base URL. Wins over the `api_url=` kwarg; derived from the server when neither is set. |
|
|
143
|
+
| `FLIGHTDECK_SESSION_ID` | Stable session UUID for orchestrator re-runs. Wins over the `session_id=` kwarg. |
|
|
144
|
+
| `FLIGHTDECK_CAPTURE_PROMPTS` | `true` to enable full payload capture, `false` to turn it off. Wins over the `capture_prompts=` kwarg. |
|
|
145
|
+
| `FLIGHTDECK_BILLING_MODE` | `metered`, `subscription` or `local`. Wins over the `billing_mode=` kwarg. |
|
|
146
|
+
| `FLIGHTDECK_ALLOW_INSECURE_TRANSPORT` | `true` to allow plain `http://` to a control plane that is not local. Wins over the `allow_insecure_transport=` kwarg. |
|
|
147
|
+
| `FLIGHTDECK_UNAVAILABLE_POLICY` | `continue` (default) or `halt` when the control plane is down. |
|
|
148
|
+
| `AGENT_FLAVOR` / `FLIGHTDECK_AGENT_NAME` | Persistent agent label. Default: `{user}@{hostname}`. The `agent_name=` kwarg wins over both, and `FLIGHTDECK_AGENT_NAME` over `AGENT_FLAVOR`. `AGENT_FLAVOR`, when set, is also the session's `flavor`, whatever the label. |
|
|
149
|
+
| `AGENT_TYPE` / `FLIGHTDECK_AGENT_TYPE` | `coding` or `production`. Default: `production`. The `agent_type=` kwarg wins over both, and `FLIGHTDECK_AGENT_TYPE` over `AGENT_TYPE`. Any other value raises `ConfigurationError`. |
|
|
150
|
+
| `FLIGHTDECK_HOSTNAME` | Override `socket.gethostname()` (useful for k8s pod grouping). |
|
|
151
|
+
|
|
152
|
+
A variable that wins over its kwarg does so even when the kwarg is passed, so an exported value redirects an agent that hard-codes its configuration. `init()` logs a WARNING naming both values when `FLIGHTDECK_SERVER` or `FLIGHTDECK_API_URL` overrides a kwarg with a different value (compared as written, so a server's base URL and its ingestion URL count as different); the other overrides are silent. A blank `FLIGHTDECK_SERVER` or `FLIGHTDECK_TOKEN` is an error, not an absent value. A blank `FLIGHTDECK_API_URL`, `FLIGHTDECK_SESSION_ID` or `FLIGHTDECK_BILLING_MODE` falls through to the kwarg, and `FLIGHTDECK_CAPTURE_PROMPTS` and `FLIGHTDECK_ALLOW_INSECURE_TRANSPORT` take effect only as `true`, `1`, `yes`, `false`, `0` or `no`, in any case.
|
|
153
|
+
|
|
154
|
+
### Threading model
|
|
155
|
+
|
|
156
|
+
| Pattern | Description | Status |
|
|
157
|
+
|---------------------------------|------------------------------------------------------------|-----------------------|
|
|
158
|
+
| Single-threaded agent | One `init()`, one thread, sequential LLM calls | Supported |
|
|
159
|
+
| Multithreaded agent | One `init()`, many threads sharing patched clients | Supported |
|
|
160
|
+
| Multi-agent in one process | Multiple `init()` calls, one per logical agent | One `init()` per process; a second `init()` is a no-op with a warning |
|
|
161
|
+
|
|
162
|
+
The sensor reports over HTTP on background daemon threads. Control plane downtime is handled by the configured policy; it does not block agent code.
|
|
163
|
+
|
|
164
|
+
### Known limitations
|
|
165
|
+
|
|
166
|
+
- `patch()` must run before OpenAI clients are constructed. On openai 1.78 and later, instances that already accessed `.chat`, `.responses`, or `.embeddings` before `patch()` keep the raw resource cached and are not intercepted. Anthropic clients are intercepted from their next call on, whenever they were built, except that a `with_raw_response` / `with_streaming_response` view first touched before `patch()` binds the unpatched method and is not metered.
|
|
167
|
+
- The OpenAI patch needs `openai>=1.26` (the first release whose chat `create` takes `stream_options`, which the sensor adds to streamed calls); an older SDK is reported with a warning at import and its calls are not metered. On openai before 1.78 a client built before `patch()` is metered from its next call. Its client-level `with_raw_response` / `with_streaming_response` view is the exception: the SDK builds it with the client, so on a client built before `patch()` or with no session active it is not metered, and on a client built while a session was active it reports to that session.
|
|
168
|
+
- The Anthropic patch needs `anthropic>=0.86` (the lowest release with the resource layout it imports); an older SDK is reported with a warning at import and its calls are not metered.
|
|
169
|
+
- A `with_raw_response.create(stream=True)` response (Anthropic or OpenAI) that is never parsed has no close hook to settle on: it is reported on the pre-call estimate (as an aborted stream with no chunks when its body was never read), at the next LLM call the sensor intercepts after the response is collected, or when the session ends. The streaming view (`with_streaming_response`) settles when its `with` block exits, whether or not its stream was parsed or read.
|
|
170
|
+
- Anthropic Message Batches (`messages.batches.*`) and `count_tokens` are not patched: batch spend runs outside the kill switch and the token budget.
|
|
171
|
+
- With `capture_prompts=True`, prompts sent through the Bedrock, Vertex, Foundry, AWS and Mantle clients — platforms often chosen for data residency — reach the Flightdeck control plane like any other captured prompt. Keep capture off for those sessions, or keep the control plane inside the same residency boundary.
|
|
172
|
+
- llama-index-llms-anthropic (through 0.12.3) always sends `temperature`, which anthropic 1.x's `Messages.create` rejects, so LlamaIndex's Anthropic LLM fails on anthropic 1.x with or without the sensor; the playgrounds skip it there.
|
|
173
|
+
- On mcp 2.x, `mcp.Client` answers `list_tools`, `list_resources`, `list_resource_templates`, `list_prompts` and `read_resource` from its response cache when it can; a cached answer makes no MCP request, so no `mcp_*` event is emitted for it (pass `cache=None` to the `Client` to observe every request). Tool calls are not cached and are always observed and policed.
|
|
174
|
+
- Per-thread Session isolation is not yet supported. Multi-agent frameworks (CrewAI, LangGraph, etc.) work fine under a single `init()` and shared `AGENT_FLAVOR`.
|
|
175
|
+
- Custom directive handler input validation is yours. The `parameters` schema drives the dashboard form and the directive fingerprint; it is not enforced at execution time.
|
|
176
|
+
- While a litellm call is in flight, an SDK call made from inside it (by a litellm callback, guardrail or custom provider, or by a task created inside the call and run before it ends) is neither policed nor metered: the litellm wrapper has already metered the call it serves, and the mark that tells the SDK interceptors so covers the whole call. A litellm call made from inside it is reported as a call of its own, except litellm's own re-issues of the same call (its responses/completion bridge and its `fallbacks` loop, which report once, with the model that answered). A chat call through litellm's MCP gateway is reported twice, and a sync call with `fallbacks` made while an event loop is running in its thread (a notebook) has each attempt reported as a call of its own beside it (a failed primary and one fallback: three `pre_call`s, two `post_call`s, one `llm_error`). Under a DEGRADE policy a litellm call's `fallbacks` are dropped, so it runs only the degrade target.
|
|
177
|
+
|
|
178
|
+
## Local development venv
|
|
179
|
+
|
|
180
|
+
Flightdeck pins **Python 3.12** for local development. The sensor
|
|
181
|
+
supports `3.11 ≤ x < 3.15` (pyproject `requires-python`); `sensor/.venv`
|
|
182
|
+
is the canonical interpreter for every Make target that runs Python.
|
|
183
|
+
|
|
184
|
+
Recreate from a fresh clone or after a `rm -rf sensor/.venv`:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python3.12 -m venv sensor/.venv
|
|
188
|
+
./sensor/.venv/bin/python -m pip install -e ".[dev,anthropic,openai]"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`make -C sensor install` does the same once the venv exists. To get
|
|
192
|
+
the exact versions CI tests with, install from the lockfile instead:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
cd sensor && uv sync --locked --python 3.12 --extra dev --extra anthropic --extra openai
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
CI installs from `sensor/uv.lock` into the `actions/setup-python`
|
|
199
|
+
interpreter and overrides via env (`PYTHON=python make -C sensor
|
|
200
|
+
test`). The lock covers Python 3.11 to 3.14 and every CI lane: the
|
|
201
|
+
framework lane (`dev` extra), the newest-SDK lane (`sdk-majors`
|
|
202
|
+
dependency group), the oldest-openai lane (`openai-floor` dependency
|
|
203
|
+
group, openai 1.26.0, which runs `tests/unit/test_openai_constructed_resources.py`)
|
|
204
|
+
and the SDK-floors lane (`sdk-floors` dependency group: anthropic
|
|
205
|
+
0.86.0, openai 2.8.0, litellm 1.83.0 and mcp 1.28.0 on Python 3.11,
|
|
206
|
+
which runs the unit suite). pydantic 2.10 is the declared core floor;
|
|
207
|
+
the mcp extra lifts it to 2.11 (mcp 1.28 requires it), and the
|
|
208
|
+
SDK-floors lane resolves a newer pydantic, so 2.10 itself is not tested
|
|
209
|
+
in CI.
|
|
210
|
+
`uv lock` resolves only releases at least 14 days
|
|
211
|
+
old, anthropic a day old (`[tool.uv]` in `pyproject.toml`).
|
|
212
|
+
|
|
213
|
+
## Optional `session_id` hint
|
|
214
|
+
|
|
215
|
+
By default `init()` auto-generates a fresh UUID every time the process
|
|
216
|
+
starts. Orchestrators that re-run the same logical workflow (Temporal,
|
|
217
|
+
Airflow, cron) can instead pass a stable identifier; if the backend
|
|
218
|
+
already has a row for that session, the new execution is attached to it
|
|
219
|
+
and appears as a continuation of the prior run in the fleet view.
|
|
220
|
+
|
|
221
|
+
Supply the hint via either the `session_id=` kwarg or the
|
|
222
|
+
`FLIGHTDECK_SESSION_ID` environment variable. The env var takes
|
|
223
|
+
precedence.
|
|
224
|
+
|
|
225
|
+
The value MUST parse as a canonical UUID (any version) -- the
|
|
226
|
+
sessions table column is UUID-typed. If you pass a non-UUID the
|
|
227
|
+
sensor logs a warning and falls back to auto-generating one.
|
|
228
|
+
Orchestrators that use string identifiers (Temporal workflow_id,
|
|
229
|
+
Airflow dag_run_id) should hash the identifier into a deterministic
|
|
230
|
+
UUID with `uuid.uuid5`.
|
|
231
|
+
|
|
232
|
+
### Temporal workflow example
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
import uuid
|
|
236
|
+
import flightdeck_sensor as fd
|
|
237
|
+
from temporalio import workflow
|
|
238
|
+
|
|
239
|
+
# Pick any fixed namespace UUID for your deployment. The same
|
|
240
|
+
# workflow_id + namespace always produces the same session UUID,
|
|
241
|
+
# so re-runs of the same workflow all map to the same sessions row.
|
|
242
|
+
FLIGHTDECK_NS = uuid.UUID("00000000-0000-0000-0000-000000000001")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@workflow.defn
|
|
246
|
+
class MyWorkflow:
|
|
247
|
+
@workflow.run
|
|
248
|
+
async def run(self, input):
|
|
249
|
+
ctx = workflow.info()
|
|
250
|
+
fd.init(
|
|
251
|
+
server="http://flightdeck.internal/ingest",
|
|
252
|
+
token="ftd_...",
|
|
253
|
+
session_id=str(uuid.uuid5(FLIGHTDECK_NS, ctx.workflow_id)),
|
|
254
|
+
)
|
|
255
|
+
# If this workflow_id has run before, the backend attaches
|
|
256
|
+
# this execution to the existing session automatically; the
|
|
257
|
+
# sensor logs INFO on the first response that confirms it.
|
|
258
|
+
...
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The sensor logs a single WARNING at `init()` time whenever a custom
|
|
262
|
+
`session_id` is in play so the behaviour is visible in operational
|
|
263
|
+
logs, and an INFO line on the first response where the backend
|
|
264
|
+
confirms attachment. See ARCHITECTURE.md ("Session attachment flow")
|
|
265
|
+
for the full protocol.
|
|
266
|
+
|
|
267
|
+
## Shutdown and signals
|
|
268
|
+
|
|
269
|
+
The session ends, sending `session_end`, when the process exits normally
|
|
270
|
+
(an `atexit` handler), when you call `flightdeck_sensor.teardown()`, or
|
|
271
|
+
on SIGTERM or SIGINT. For a signal, the sensor's handler does not end the
|
|
272
|
+
session on the thread the signal interrupted, which may be holding one of
|
|
273
|
+
the sensor's locks: the event queue's background thread sends the queued
|
|
274
|
+
events and `session_end`, and the handler waits up to 2 seconds for it.
|
|
275
|
+
It then passes the signal on to the handler it replaced, so the process
|
|
276
|
+
ends as it would have without the sensor: SIGTERM with no handler of your
|
|
277
|
+
own ends the process, SIGINT raises `KeyboardInterrupt`, and your own
|
|
278
|
+
handler is called. If `session_end` is not out yet and the signal's
|
|
279
|
+
action is to end the process, the sensor lets the interrupted code go on
|
|
280
|
+
and delivers the signal again once `session_end` is sent. That shutdown
|
|
281
|
+
takes 8 seconds at most, or about 10.5 when an event's POST was under way
|
|
282
|
+
as the signal arrived (it runs to its 10-second timeout first, without a
|
|
283
|
+
retry); both are well inside Kubernetes' 30-second grace period. Each
|
|
284
|
+
event gets one attempt, so a control plane that is down or slow delays
|
|
285
|
+
the exit by that much at most; a slow DNS lookup for the control plane
|
|
286
|
+
is not covered. Python acts on the signal delivered again when the main
|
|
287
|
+
thread next runs Python code, so a long call into native code there
|
|
288
|
+
delays the exit until it returns. A second signal is passed on at once
|
|
289
|
+
and forces the exit. A signal your application ignores stays ignored, and
|
|
290
|
+
the session carries on.
|
|
291
|
+
|
|
292
|
+
## Framework support
|
|
293
|
+
|
|
294
|
+
`flightdeck_sensor.patch()` installs seven interceptors process-wide:
|
|
295
|
+
three LLM-call interceptors (class-level or module-level), one MCP
|
|
296
|
+
interceptor, two sub-agent interceptors and one Claude Agent SDK
|
|
297
|
+
interceptor. Every LLM call that flows through a patched entry point
|
|
298
|
+
emits a `pre_call` / `post_call` event pair without any
|
|
299
|
+
framework-specific wiring; the other interceptors emit their own event
|
|
300
|
+
types, described in the table.
|
|
301
|
+
|
|
302
|
+
`patch()` never imports an SDK. An SDK your application has already
|
|
303
|
+
imported is patched at once; one it imports later is patched as it is
|
|
304
|
+
imported, before the `import` statement returns, so the order of
|
|
305
|
+
`import openai` and `flightdeck_sensor.patch()` does not matter, and an
|
|
306
|
+
SDK the application never imports is never loaded. `import
|
|
307
|
+
flightdeck_sensor`, `init()` and `patch()` therefore load none of
|
|
308
|
+
anthropic, openai, litellm, mcp, crewai, langgraph, claude-agent-sdk or openai-agents,
|
|
309
|
+
and litellm downloads its price list from GitHub (which it does when it
|
|
310
|
+
is imported) only if your application imports it. `unpatch()` (and
|
|
311
|
+
`teardown()`) stops patching SDKs as they are imported.
|
|
312
|
+
|
|
313
|
+
| Framework | Interceptor | Entry points |
|
|
314
|
+
| --- | --- | --- |
|
|
315
|
+
| Anthropic SDK — every client class (`Anthropic`, `AnthropicBedrock`, `AnthropicVertex`, `AnthropicFoundry`, `AnthropicAWS`, `AnthropicBedrockMantle`, `AnthropicGoogleCloud` and their async forms), direct or via a framework that constructs one internally (LangChain, LlamaIndex, CrewAI native) | `patch_anthropic_classes` | `messages.create` (including `stream=True`), `.stream`, `.parse`, async + sync, beta resources, the raw view (`with_raw_response`) and the streaming view (`with_streaming_response`, metered at `parse()`); streamed calls are metered from the in-stream usage events, and a raw stream (`create(stream=True)`) is assembled with the SDK's own accumulator, so it stores its response under `capture_prompts` and emits its `tool_call` events like a non-streamed call; every `post_call` carries `provider_metadata.platform` (`anthropic`, `bedrock`, `bedrock-mantle`, `aws`, `vertex`, `google-cloud`, `foundry`, or a gateway's host) |
|
|
316
|
+
| OpenAI SDK (direct or via a framework that constructs `OpenAI()` internally — LangChain, LlamaIndex, CrewAI native) | `patch_openai_classes` | `OpenAI.chat.completions.create` / `.parse` / `.stream` (and `OpenAI.beta.chat.completions.parse` / `.stream`), `.responses.create` / `.parse` / `.stream`, `.embeddings.create`, async + sync, and the raw and streaming views of each (`with_raw_response` / `with_streaming_response`, on the resource or the client; a streamed view call is metered when its response is parsed, and one whose stream is never read is reported as an aborted stream with no chunks); streamed calls are metered from the chat usage chunk and `response.completed` and assembled with the SDK's own stream state, so they store their response under `capture_prompts` and emit their `tool_call` events; a Responses call stores its `instructions` and `input`, and an `mcp_call` names its server as `server_name` |
|
|
317
|
+
| litellm (router that aggregates many providers behind one function surface) | `patch_litellm_functions` | `litellm.completion`, `litellm.acompletion`, `litellm.text_completion`, `litellm.atext_completion`, `litellm.responses`, `litellm.aresponses` (each with `stream=True`), `litellm.embedding`, `litellm.aembedding` |
|
|
318
|
+
| MCP (official Python `mcp` SDK, reached transitively by `langchain-mcp-adapters`, `llama-index-tools-mcp` and `mcpadapt`) | `patch_mcp_classes` | `mcp.client.session.ClientSession.initialize` on mcp 1.x / `.adopt` on mcp 2.x (server fingerprint capture, every 2.x handshake path), `.list_tools`, `.call_tool`, `.list_resources`, `.read_resource`, `.list_prompts`, `.get_prompt` (one `mcp_*` event type each), plus the stdio / SSE / streamable-HTTP / websocket transport client factories and mcp 2.x's `Client._build_session` for transport attribution. `call_tool` also enforces the MCP Protection Policy. Supported SDK range: `mcp>=1.28,<3`. |
|
|
319
|
+
| CrewAI (sub-agent sessions) | `patch_crewai_classes` | `crewai.Agent.execute_task` / `.aexecute_task` — each Crew member's turn lands as a child session (`session_start` / `session_end`) with `parent_session_id`, `agent_role` taken from `Agent.role`, and incoming / outgoing message capture gated on `capture_prompts` |
|
|
320
|
+
| LangGraph (sub-agent sessions) | `patch_langgraph_classes` | `langgraph.graph.StateGraph.add_node` — the registered node callable is wrapped so each node run lands as a child session with `agent_role` taken from the node name; every node is treated as agent-bearing unless narrowed with `init(langgraph_agent_node_pattern=...)`. Only a node whose action is a plain callable is wrapped: Runnable, `ToolNode` and compiled-subgraph nodes are not |
|
|
321
|
+
| LlamaIndex agent workflows (sub-agent sessions, `llama-index-core>=0.14`) | `patch_llama_index` | A span handler on LlamaIndex's root instrumentation dispatcher (no LlamaIndex method is replaced): each agent of a `FunctionAgent`, `ReActAgent`, `CodeActAgent` or `AgentWorkflow` run lands as a child session with `agent_role` the agent's name (the agent's model, tool and MCP calls stay on the sensor's session, as for CrewAI and LangGraph); an `AgentWorkflow` hand-off ends one child, records an `agent_handoff` signal on it and opens the next; a tool call the model wrote as text (ReAct, CodeAct) is reported once as a `tool_call`, its arguments and output under `capture_prompts` |
|
|
322
|
+
| Claude Agent SDK (`claude-agent-sdk>=0.2.168,<0.3`) | `patch_claude_agent_sdk` | `query()` (however it was imported) and `ClaudeSDKClient` — each run lands as its own `claude_agent_sdk` session keyed by the Claude Code session id, with a `post_call` per assistant message (tokens, cache split, billing mode) and a `tool_call` per tool use, read from the SDK's message stream; a Claude Code plugin loaded into the run stays silent. `PreToolUse` and `UserPromptSubmit` callbacks added after the caller's hooks apply the run's directives (a stop denies the next tool call and blocks prompts; budget warnings reach the run) and the MCP Protection Policy. Install with `pip install flightdeck-sensor[claude-agent-sdk]`. |
|
|
323
|
+
| OpenAI Agents SDK (`openai-agents>=0.6`, sub-agent sessions) | `patch_openai_agents` | `AgentRunner.run` / `.run_streamed` (so `Runner.run`, `run_sync` and `run_streamed`) and the `run` of the four guardrail classes — each agent that runs lands as a child session with `agent_role` taken from `Agent.name`, an agent used as a tool nests under the agent that called it, a hand-off posts `agent_handoff` on the agent handing off, a guardrail that fires posts `guardrail_tripwire` (names only), and the run's conversation id lands in the child's context as `gen_ai_conversation_id`. Model, tool and MCP calls stay on the sensor's session (the OpenAI and MCP interceptors meter them); a shutdown also stops the run when the next agent starts or hands off. Patched only when `agents` comes from the `openai-agents` distribution. Install with `pip install flightdeck-sensor[openai-agents]`. |
|
|
324
|
+
|
|
325
|
+
### Coverage beyond these interceptors
|
|
326
|
+
|
|
327
|
+
Any framework that calls a model through the Anthropic or OpenAI SDK or
|
|
328
|
+
litellm is metered and enforced through the interceptors above, with no
|
|
329
|
+
wrap of its own: Deep Agents, Microsoft Agent Framework,
|
|
330
|
+
Google ADK (on `LiteLlm`, Anthropic or OpenAI models), Pydantic AI and
|
|
331
|
+
Strands Agents (on their Anthropic, OpenAI and LiteLLM models), Agno,
|
|
332
|
+
Haystack, smolagents, Semantic Kernel, AutoGen, AG2 and DSPy on its
|
|
333
|
+
litellm engine. Each event names the framework in
|
|
334
|
+
`framework` (see "Framework attribution" in ARCHITECTURE.md).
|
|
335
|
+
|
|
336
|
+
- **Sub-agent sessions planned**
|
|
337
|
+
([#185](https://github.com/flightdeckhq/flightdeck/issues/185)): Google
|
|
338
|
+
ADK, Microsoft Agent Framework and Deep Agents (each agent and each
|
|
339
|
+
`task` sub-agent).
|
|
340
|
+
- **LLM calls planned**
|
|
341
|
+
([#185](https://github.com/flightdeckhq/flightdeck/issues/185)): a DSPy
|
|
342
|
+
callback adapter for its default engine, which has its own HTTP
|
|
343
|
+
transport and bypasses the SDKs and litellm.
|
|
344
|
+
- **No sub-agent sessions from `create_agent`**: the LangGraph
|
|
345
|
+
interceptor wraps only nodes whose action is a plain callable.
|
|
346
|
+
LangChain's `create_agent`, and Deep Agents built on it, register
|
|
347
|
+
Runnable and `ToolNode` nodes, so their runs are one session (their LLM
|
|
348
|
+
calls are still metered).
|
|
349
|
+
- **Structure through OpenTelemetry instead**: Pydantic AI and Strands
|
|
350
|
+
Agents export GenAI spans that Flightdeck's OTLP receiver maps into
|
|
351
|
+
observe-only child sessions. The sensor does not wrap them. Report a
|
|
352
|
+
run one way: a run the sensor and an exporter both report counts twice
|
|
353
|
+
([#279](https://github.com/flightdeckhq/flightdeck/issues/279)).
|
|
354
|
+
- **Labelled, structure on the roadmap**: Agno, Haystack and smolagents
|
|
355
|
+
([#278](https://github.com/flightdeckhq/flightdeck/issues/278)).
|
|
356
|
+
- **Not supported**: Gemini through `google-genai`, Google ADK's default
|
|
357
|
+
model included ([#276](https://github.com/flightdeckhq/flightdeck/issues/276));
|
|
358
|
+
boto3 `bedrock-runtime` Converse, Strands' default model included
|
|
359
|
+
([#277](https://github.com/flightdeckhq/flightdeck/issues/277)); BAML,
|
|
360
|
+
Letta, MetaGPT and CAMEL.
|
|
361
|
+
|
|
362
|
+
ARCHITECTURE.md ("Framework support") has the full matrix.
|
|
363
|
+
|
|
364
|
+
### litellm example
|
|
365
|
+
|
|
366
|
+
litellm's Anthropic route uses raw httpx instead of the Anthropic
|
|
367
|
+
SDK, so it bypasses the SDK-class patches. The litellm interceptor
|
|
368
|
+
patches `litellm.completion` / `litellm.acompletion` directly to
|
|
369
|
+
close this gap. An Anthropic model string via litellm exercises the
|
|
370
|
+
bypass case:
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
import flightdeck_sensor
|
|
374
|
+
import litellm
|
|
375
|
+
|
|
376
|
+
flightdeck_sensor.init(
|
|
377
|
+
server="http://flightdeck.internal/ingest",
|
|
378
|
+
token="ftd_...",
|
|
379
|
+
)
|
|
380
|
+
flightdeck_sensor.patch()
|
|
381
|
+
|
|
382
|
+
# After patch(), this call routes through the sensor's pre/post-call
|
|
383
|
+
# plumbing regardless of which underlying provider litellm picks for
|
|
384
|
+
# the model string.
|
|
385
|
+
response = litellm.completion(
|
|
386
|
+
model="claude-haiku-4-5-20251001",
|
|
387
|
+
messages=[{"role": "user", "content": "hi"}],
|
|
388
|
+
max_tokens=5,
|
|
389
|
+
)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Install the optional dependency: `pip install flightdeck-sensor[litellm]`
|
|
393
|
+
(the extra requires `litellm>=1.83.0`; 1.82.7 and 1.82.8 were compromised
|
|
394
|
+
builds).
|
|
395
|
+
|
|
396
|
+
### What the litellm interceptor catches, and what it doesn't
|
|
397
|
+
|
|
398
|
+
**Catches.** Direct callers of `litellm.completion` and
|
|
399
|
+
`litellm.acompletion` — the vast majority of litellm
|
|
400
|
+
integrations, including the default chat-completion surface exposed
|
|
401
|
+
by user code, Router, and frameworks that route through the public
|
|
402
|
+
API (CrewAI non-native flavors, langchain-community's litellm
|
|
403
|
+
adapter, etc.) — streaming included: `stream=True` on either returns
|
|
404
|
+
the sensor's stream guard around litellm's `CustomStreamWrapper`,
|
|
405
|
+
iterated the same way (`for` / `async for`), metered from the token
|
|
406
|
+
counts litellm reports in its trailing usage chunk; that chunk is
|
|
407
|
+
kept from a caller who did not ask for `stream_options.include_usage`.
|
|
408
|
+
`litellm.text_completion` / `atext_completion` and
|
|
409
|
+
`litellm.responses` / `aresponses` are wrapped the same way, streaming
|
|
410
|
+
included (a Responses stream is metered from its terminal event), on
|
|
411
|
+
every route: one served through the OpenAI SDK, litellm's own httpx
|
|
412
|
+
routes (Anthropic, the native Responses API), and a Responses call
|
|
413
|
+
litellm bridges through `completion` for a provider without a native
|
|
414
|
+
Responses API. Each litellm call is counted once, by the wrapper of
|
|
415
|
+
the entry point the caller used, also where litellm calls the OpenAI
|
|
416
|
+
SDK or its own `completion` underneath. Embedding calls through
|
|
417
|
+
`litellm.embedding` and `litellm.aembedding` are wrapped the same way,
|
|
418
|
+
so embedding-model token usage lands too. Arguments may be passed
|
|
419
|
+
positionally or by name, as litellm takes them (`12_litellm.py` in the
|
|
420
|
+
playground exercises sync and async streaming, the single-count
|
|
421
|
+
assertion and the capture round-trip; `30_parse_and_litellm_routes.py`
|
|
422
|
+
covers `text_completion` and `responses` on each route kind).
|
|
423
|
+
|
|
424
|
+
**Does NOT catch.**
|
|
425
|
+
- **Lower-level litellm entry points.** Some integrations reach
|
|
426
|
+
past `completion` into `litellm.llms.custom_httpx.http_handler`
|
|
427
|
+
or other internal helpers directly. Those calls bypass the
|
|
428
|
+
module-level wrapper. A broader httpx-level interceptor was
|
|
429
|
+
considered when the litellm interceptor was scoped and deferred —
|
|
430
|
+
if framework reports surface calls that slip past the current
|
|
431
|
+
coverage, file a new issue.
|
|
432
|
+
- **SDK calls made from inside a litellm call.** The litellm wrapper
|
|
433
|
+
marks the call it serves so the SDK interceptors pass it through; the
|
|
434
|
+
mark covers the whole call, so an SDK call made while it is in flight
|
|
435
|
+
by a litellm callback, guardrail or custom provider, or by a task
|
|
436
|
+
created inside the call, is neither policed nor metered. A litellm
|
|
437
|
+
call made from inside it is a call of its own and is reported; only
|
|
438
|
+
litellm's own re-issues of the same call pass through: its
|
|
439
|
+
responses/completion bridge and its `fallbacks` loop (a call a fallback
|
|
440
|
+
answered is reported once, with the model that answered; a failed
|
|
441
|
+
attempt before it is not reported on its own, and neither are the
|
|
442
|
+
tokens of a non-streamed attempt the provider billed before it failed).
|
|
443
|
+
Under a DEGRADE policy a call's `fallbacks` and
|
|
444
|
+
`context_window_fallback_dict` are dropped, so it runs only the degrade
|
|
445
|
+
target.
|
|
446
|
+
- **Reported more than once.** A chat call routed through litellm's MCP gateway
|
|
447
|
+
(tools that name it), whose re-issue cannot be told from its billed
|
|
448
|
+
follow-ups; a sync call with `fallbacks` made while an event loop is
|
|
449
|
+
running in its thread (a notebook), whose attempts run in a worker
|
|
450
|
+
thread without the call's context, so each attempt is reported as a
|
|
451
|
+
call of its own beside the call (a failed primary and one fallback:
|
|
452
|
+
three `pre_call`s, two `post_call`s, one `llm_error`); and re-issued
|
|
453
|
+
calls when another
|
|
454
|
+
instrumentation library replaces a litellm entry point after
|
|
455
|
+
`flightdeck_sensor.patch()`.
|