flightdeck-sensor 0.7.1__tar.gz → 0.9.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.
Files changed (115) hide show
  1. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/CLAUDE.md +1 -1
  2. flightdeck_sensor-0.9.0/PKG-INFO +455 -0
  3. flightdeck_sensor-0.9.0/README.md +389 -0
  4. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/__init__.py +594 -113
  5. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/compat/__init__.py +2 -2
  6. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/compat/crewai_mcp.py +1 -1
  7. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/config.py +11 -0
  8. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/context.py +271 -45
  9. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/exceptions.py +8 -2
  10. flightdeck_sensor-0.9.0/flightdeck_sensor/core/import_hook.py +296 -0
  11. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/session.py +766 -55
  12. flightdeck_sensor-0.9.0/flightdeck_sensor/core/tool_capture.py +62 -0
  13. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/types.py +27 -0
  14. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/anthropic.py +21 -319
  15. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/base.py +186 -25
  16. flightdeck_sensor-0.9.0/flightdeck_sensor/interceptor/claude_agent_sdk.py +1075 -0
  17. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/crewai.py +5 -4
  18. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/langgraph.py +5 -4
  19. flightdeck_sensor-0.9.0/flightdeck_sensor/interceptor/litellm.py +823 -0
  20. flightdeck_sensor-0.9.0/flightdeck_sensor/interceptor/llama_index.py +554 -0
  21. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/mcp.py +4 -53
  22. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/openai.py +633 -60
  23. flightdeck_sensor-0.9.0/flightdeck_sensor/interceptor/openai_agents.py +847 -0
  24. flightdeck_sensor-0.9.0/flightdeck_sensor/interceptor/raw_views.py +510 -0
  25. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/provider.py +15 -0
  26. flightdeck_sensor-0.9.0/flightdeck_sensor/providers/_access.py +32 -0
  27. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/providers/anthropic.py +94 -7
  28. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/providers/litellm.py +445 -9
  29. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/providers/openai.py +240 -7
  30. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/providers/protocol.py +51 -12
  31. flightdeck_sensor-0.9.0/flightdeck_sensor/transport/body_limit.py +156 -0
  32. flightdeck_sensor-0.9.0/flightdeck_sensor/transport/client.py +1074 -0
  33. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/transport/retry.py +18 -10
  34. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/pyproject.toml +93 -9
  35. flightdeck_sensor-0.9.0/tests/unit/_child_process.py +244 -0
  36. flightdeck_sensor-0.9.0/tests/unit/_lazy_patch_child.py +284 -0
  37. flightdeck_sensor-0.9.0/tests/unit/_signal_child.py +150 -0
  38. flightdeck_sensor-0.9.0/tests/unit/test_agent_handoff.py +399 -0
  39. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_anthropic_resource_patch.py +16 -6
  40. flightdeck_sensor-0.9.0/tests/unit/test_body_limit.py +206 -0
  41. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_context.py +224 -20
  42. flightdeck_sensor-0.9.0/tests/unit/test_deferred_stream_unread.py +447 -0
  43. flightdeck_sensor-0.9.0/tests/unit/test_framework_attribution.py +412 -0
  44. flightdeck_sensor-0.9.0/tests/unit/test_identity_vectors.py +63 -0
  45. flightdeck_sensor-0.9.0/tests/unit/test_import_hook.py +269 -0
  46. flightdeck_sensor-0.9.0/tests/unit/test_init_defaults.py +463 -0
  47. flightdeck_sensor-0.9.0/tests/unit/test_interceptor_claude_agent_sdk.py +1094 -0
  48. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_interceptor_crewai.py +17 -0
  49. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_interceptor_langgraph.py +17 -0
  50. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_interceptor_litellm.py +15 -13
  51. flightdeck_sensor-0.9.0/tests/unit/test_interceptor_litellm_routes.py +1422 -0
  52. flightdeck_sensor-0.9.0/tests/unit/test_interceptor_llama_index.py +929 -0
  53. flightdeck_sensor-0.9.0/tests/unit/test_interceptor_openai_agents.py +1275 -0
  54. flightdeck_sensor-0.9.0/tests/unit/test_lazy_patch.py +549 -0
  55. flightdeck_sensor-0.9.0/tests/unit/test_openai_beta_chat.py +321 -0
  56. flightdeck_sensor-0.9.0/tests/unit/test_openai_constructed_resources.py +657 -0
  57. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_openai_parse_responses.py +18 -2
  58. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_provider_enum.py +34 -11
  59. flightdeck_sensor-0.9.0/tests/unit/test_raw_stream_capture.py +1374 -0
  60. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_session.py +7 -7
  61. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_session_lifecycle_enrichment.py +13 -5
  62. flightdeck_sensor-0.9.0/tests/unit/test_signal_shutdown.py +911 -0
  63. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_token_cost_fields.py +120 -0
  64. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_tool_capture_destination.py +6 -8
  65. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_transport.py +168 -2
  66. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/uv.lock +1069 -570
  67. flightdeck_sensor-0.7.1/PKG-INFO +0 -239
  68. flightdeck_sensor-0.7.1/README.md +0 -179
  69. flightdeck_sensor-0.7.1/flightdeck_sensor/interceptor/litellm.py +0 -544
  70. flightdeck_sensor-0.7.1/flightdeck_sensor/transport/client.py +0 -537
  71. flightdeck_sensor-0.7.1/tests/unit/test_framework_attribution.py +0 -211
  72. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/.gitignore +0 -0
  73. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/Makefile +0 -0
  74. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/__init__.py +0 -0
  75. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/agent_id.py +0 -0
  76. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/errors.py +0 -0
  77. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/mcp_policy.py +0 -0
  78. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/policy.py +0 -0
  79. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/core/schemas.py +0 -0
  80. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/__init__.py +0 -0
  81. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/mcp_identity.py +0 -0
  82. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/interceptor/platform.py +0 -0
  83. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/providers/__init__.py +0 -0
  84. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/py.typed +0 -0
  85. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/flightdeck_sensor/transport/__init__.py +0 -0
  86. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/__init__.py +0 -0
  87. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/conftest.py +0 -0
  88. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/__init__.py +0 -0
  89. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_agent_id.py +0 -0
  90. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_agent_type_validation.py +0 -0
  91. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_compat_crewai_mcp.py +0 -0
  92. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_cross_agent_messages.py +0 -0
  93. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_custom_directives.py +0 -0
  94. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_egress_url_validation.py +0 -0
  95. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_errors.py +0 -0
  96. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_interceptor.py +0 -0
  97. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_llm_event_enrichment.py +0 -0
  98. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_mcp_2x_paths.py +0 -0
  99. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_mcp_availability.py +0 -0
  100. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_mcp_identity.py +0 -0
  101. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_mcp_interceptor.py +0 -0
  102. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_mcp_policy_emission_enriched.py +0 -0
  103. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_mcp_policy_enforcement.py +0 -0
  104. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_mcp_policy_enforcement_all_paths.py +0 -0
  105. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_originating_event_id.py +0 -0
  106. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_patch.py +0 -0
  107. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_policy.py +0 -0
  108. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_policy_decision_summary.py +0 -0
  109. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_policy_directive_events.py +0 -0
  110. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_policy_emission_enriched.py +0 -0
  111. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_policy_events.py +0 -0
  112. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_prompt_capture.py +0 -0
  113. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_providers.py +0 -0
  114. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_stop_reason_errors.py +0 -0
  115. {flightdeck_sensor-0.7.1 → flightdeck_sensor-0.9.0}/tests/unit/test_streaming_usage.py +0 -0
@@ -16,7 +16,7 @@
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 Supervisor approval.
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.
@@ -0,0 +1,455 @@
1
+ Metadata-Version: 2.5
2
+ Name: flightdeck-sensor
3
+ Version: 0.9.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()`.