cavisson-pythonagent 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
  2. cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
  3. cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
  4. cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
  5. cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
  6. pythonagent/__init__.py +22 -0
  7. pythonagent/agent/__init__.py +219 -0
  8. pythonagent/agent/internal/__init__.py +1 -0
  9. pythonagent/agent/internal/agent.py +1651 -0
  10. pythonagent/agent/internal/framesinfo.py +152 -0
  11. pythonagent/agent/internal/heap_dump.py +39 -0
  12. pythonagent/agent/internal/intercept_module.py +70 -0
  13. pythonagent/agent/internal/logs.py +122 -0
  14. pythonagent/agent/internal/metadata/__init__.py +0 -0
  15. pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
  16. pythonagent/agent/internal/proc_compat.py +75 -0
  17. pythonagent/agent/internal/profile.py +47 -0
  18. pythonagent/agent/internal/provider.py +83 -0
  19. pythonagent/agent/internal/thread_dump.py +85 -0
  20. pythonagent/agent/internal/udp.py +245 -0
  21. pythonagent/agent/internal/udp_message.py +800 -0
  22. pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
  23. pythonagent/agent/probes/Instrumentation/find.py +243 -0
  24. pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
  25. pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
  26. pythonagent/agent/probes/Instrumentation/parser.py +129 -0
  27. pythonagent/agent/probes/__init__.py +219 -0
  28. pythonagent/agent/probes/base.py +303 -0
  29. pythonagent/agent/probes/cache/__init__.py +51 -0
  30. pythonagent/agent/probes/cache/redis.py +119 -0
  31. pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
  32. pythonagent/agent/probes/coroutines/__init__.py +1 -0
  33. pythonagent/agent/probes/coroutines/asyncio.py +63 -0
  34. pythonagent/agent/probes/elasticdb/__init__.py +7 -0
  35. pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
  36. pythonagent/agent/probes/frameworks/__init__.py +27 -0
  37. pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
  38. pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
  39. pythonagent/agent/probes/frameworks/aisess.py +151 -0
  40. pythonagent/agent/probes/frameworks/asgi.py +340 -0
  41. pythonagent/agent/probes/frameworks/bottle.py +27 -0
  42. pythonagent/agent/probes/frameworks/cherry.py +25 -0
  43. pythonagent/agent/probes/frameworks/django.py +128 -0
  44. pythonagent/agent/probes/frameworks/falcon.py +21 -0
  45. pythonagent/agent/probes/frameworks/fastapi.py +35 -0
  46. pythonagent/agent/probes/frameworks/flask.py +30 -0
  47. pythonagent/agent/probes/frameworks/pyramid.py +56 -0
  48. pythonagent/agent/probes/frameworks/test.py +108 -0
  49. pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
  50. pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
  51. pythonagent/agent/probes/frameworks/wsgi.py +353 -0
  52. pythonagent/agent/probes/grpc/__init__.py +76 -0
  53. pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
  54. pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
  55. pythonagent/agent/probes/havoc/__init__.py +0 -0
  56. pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
  57. pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
  58. pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
  59. pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
  60. pythonagent/agent/probes/http/__init__.py +49 -0
  61. pythonagent/agent/probes/http/aiohttp_client.py +59 -0
  62. pythonagent/agent/probes/http/boto.py +12 -0
  63. pythonagent/agent/probes/http/httplib.py +110 -0
  64. pythonagent/agent/probes/http/httpx_client.py +116 -0
  65. pythonagent/agent/probes/http/requests.py +15 -0
  66. pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
  67. pythonagent/agent/probes/http/urllib3.py +16 -0
  68. pythonagent/agent/probes/langchain/__init__.py +21 -0
  69. pythonagent/agent/probes/langchain/base_tool.py +95 -0
  70. pythonagent/agent/probes/langchain/langchain_community.py +136 -0
  71. pythonagent/agent/probes/langchain/langchain_core.py +32 -0
  72. pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
  73. pythonagent/agent/probes/logging/__init__.py +106 -0
  74. pythonagent/agent/probes/message_brokers/__init__.py +4 -0
  75. pythonagent/agent/probes/message_brokers/pika.py +126 -0
  76. pythonagent/agent/probes/mongodb/__init__.py +6 -0
  77. pythonagent/agent/probes/mongodb/pymongo.py +286 -0
  78. pythonagent/agent/probes/openai/__init__.py +3 -0
  79. pythonagent/agent/probes/openai/openai.py +797 -0
  80. pythonagent/agent/probes/span.py +101 -0
  81. pythonagent/agent/probes/sql/__init__.py +13 -0
  82. pythonagent/agent/probes/sql/botocores3.py +51 -0
  83. pythonagent/agent/probes/sql/dbapi.py +285 -0
  84. pythonagent/agent/probes/sql/dynamodb.py +90 -0
  85. pythonagent/agent/probes/sql/mysql_connector.py +24 -0
  86. pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
  87. pythonagent/agent/probes/sql/mysqldb.py +43 -0
  88. pythonagent/agent/probes/sql/psycopg2.py +174 -0
  89. pythonagent/agent/probes/sql/pymysql.py +25 -0
  90. pythonagent/bootstrap/__init__.py +0 -0
  91. pythonagent/bootstrap/cav_gunicorn.py +26 -0
  92. pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
  93. pythonagent/bootstrap/run.py +47 -0
  94. pythonagent/bootstrap/sitecustomize.py +287 -0
  95. pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
  96. pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
  97. pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
  98. pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
  99. pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
  100. pythonagent/config.py +279 -0
  101. pythonagent/find.py +72 -0
  102. pythonagent/find_mod_cls_name.py +54 -0
  103. pythonagent/lang.py +131 -0
  104. pythonagent/lib.py +91 -0
  105. pythonagent/main/__init__.py +0 -0
  106. pythonagent/main/pytrace/__init__.py +79 -0
  107. pythonagent/main/pytrace/commands/__init__.py +0 -0
  108. pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
  109. pythonagent/main/pytrace/commands/run.py +401 -0
  110. pythonagent/main/pytrace/pytrace.py +133 -0
  111. pythonagent/main/wsgi.py +6 -0
  112. pythonagent/main.py +27 -0
  113. pythonagent/run.py +46 -0
  114. pythonagent/sqins.py +8 -0
  115. pythonagent/test.py +73 -0
  116. pythonagent/utils.py +168 -0
  117. pythonagent/vendor/__init__.py +0 -0
  118. pythonagent/vendor/pympler/__init__.py +1 -0
  119. pythonagent/vendor/pympler/asizeof.py +2810 -0
  120. pythonagent/vendor/pympler/charts.py +62 -0
  121. pythonagent/vendor/pympler/classtracker.py +590 -0
  122. pythonagent/vendor/pympler/classtracker_stats.py +780 -0
  123. pythonagent/vendor/pympler/garbagegraph.py +80 -0
  124. pythonagent/vendor/pympler/mprofile.py +97 -0
  125. pythonagent/vendor/pympler/muppy.py +275 -0
  126. pythonagent/vendor/pympler/panels.py +115 -0
  127. pythonagent/vendor/pympler/process.py +238 -0
  128. pythonagent/vendor/pympler/py.typed +0 -0
  129. pythonagent/vendor/pympler/refbrowser.py +451 -0
  130. pythonagent/vendor/pympler/refgraph.py +350 -0
  131. pythonagent/vendor/pympler/summary.py +321 -0
  132. pythonagent/vendor/pympler/tracker.py +267 -0
  133. pythonagent/vendor/pympler/util/__init__.py +0 -0
  134. pythonagent/vendor/pympler/util/bottle.py +3809 -0
  135. pythonagent/vendor/pympler/util/compat.py +23 -0
  136. pythonagent/vendor/pympler/util/stringutils.py +77 -0
  137. pythonagent/vendor/pympler/web.py +346 -0
  138. pythonagent/vendor/werkzeug/__init__.py +20 -0
  139. pythonagent/vendor/werkzeug/_compat.py +228 -0
  140. pythonagent/vendor/werkzeug/_internal.py +473 -0
  141. pythonagent/vendor/werkzeug/_reloader.py +341 -0
  142. pythonagent/vendor/werkzeug/datastructures.py +3120 -0
  143. pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
  144. pythonagent/vendor/werkzeug/debug/console.py +218 -0
  145. pythonagent/vendor/werkzeug/debug/repr.py +297 -0
  146. pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
  147. pythonagent/vendor/werkzeug/exceptions.py +829 -0
  148. pythonagent/vendor/werkzeug/filesystem.py +64 -0
  149. pythonagent/vendor/werkzeug/formparser.py +584 -0
  150. pythonagent/vendor/werkzeug/http.py +1307 -0
  151. pythonagent/vendor/werkzeug/local.py +420 -0
  152. pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
  153. pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
  154. pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
  155. pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
  156. pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
  157. pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
  158. pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
  159. pythonagent/vendor/werkzeug/posixemulation.py +117 -0
  160. pythonagent/vendor/werkzeug/routing.py +2210 -0
  161. pythonagent/vendor/werkzeug/security.py +249 -0
  162. pythonagent/vendor/werkzeug/serving.py +1117 -0
  163. pythonagent/vendor/werkzeug/test.py +1123 -0
  164. pythonagent/vendor/werkzeug/testapp.py +241 -0
  165. pythonagent/vendor/werkzeug/urls.py +1138 -0
  166. pythonagent/vendor/werkzeug/useragents.py +202 -0
  167. pythonagent/vendor/werkzeug/utils.py +778 -0
  168. pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
  169. pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
  170. pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
  171. pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
  172. pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
  173. pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
  174. pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
  175. pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
  176. pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
  177. pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
  178. pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
  179. pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
  180. pythonagent/vendor/werkzeug/wsgi.py +1000 -0
@@ -0,0 +1,797 @@
1
+ from ..base import BaseInterceptor, ExitCallInterceptor
2
+ from ..span import LLMSpan, EmbeddingSpan
3
+
4
+ from pythonagent.utils import get_validated_duration, get_current_timestamp_in_us, build_host_name_str, split_fp_topo
5
+ import logging
6
+
7
+ """
8
+ Instrumentation wrappers for the OpenAI Python SDK (v1.x).
9
+
10
+ Covers:
11
+ - Sync chat completions (non-streaming + streaming)
12
+ - Async chat completions (non-streaming + streaming)
13
+ - Legacy (non-chat) completions, sync + async
14
+ - Embeddings, sync + async
15
+ """
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Shared helpers
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def _safe_usage_metrics(usage):
23
+ """Build a metrics dict from a usage object, guarding every field for None."""
24
+ if usage is None:
25
+ return {
26
+ "prompt_tokens": None,
27
+ "completion_tokens": None,
28
+ "total_tokens": None,
29
+ "prompt_tokens_details": {"prompt_audio_tokens": None, "prompt_cached_tokens": None},
30
+ "completion_tokens_details": {
31
+ "completion_accepted_prediction_tokens": None,
32
+ "completion_rejected_prediction_tokens": None,
33
+ "completion_audio_tokens": None,
34
+ "completion_reasoning_tokens": None,
35
+ },
36
+ }
37
+ ptd = getattr(usage, "prompt_tokens_details", None)
38
+ ctd = getattr(usage, "completion_tokens_details", None)
39
+ return {
40
+ "prompt_tokens": getattr(usage, "prompt_tokens", None),
41
+ "completion_tokens": getattr(usage, "completion_tokens", None),
42
+ "total_tokens": getattr(usage, "total_tokens", None),
43
+ "prompt_tokens_details": {
44
+ "prompt_audio_tokens": getattr(ptd, "audio_tokens", None) if ptd else None,
45
+ "prompt_cached_tokens": getattr(ptd, "cached_tokens", None) if ptd else None,
46
+ },
47
+ "completion_tokens_details": {
48
+ "completion_accepted_prediction_tokens": getattr(ctd, "accepted_prediction_tokens", None) if ctd else None,
49
+ "completion_rejected_prediction_tokens": getattr(ctd, "rejected_prediction_tokens", None) if ctd else None,
50
+ "completion_audio_tokens": getattr(ctd, "audio_tokens", None) if ctd else None,
51
+ "completion_reasoning_tokens": getattr(ctd, "reasoning_tokens", None) if ctd else None,
52
+ },
53
+ }
54
+
55
+
56
+ def _get_messages_input(args, kwargs):
57
+ """Return messages list, handling both positional and keyword argument (Gap 5)."""
58
+ return kwargs.get("messages") or (args[0] if args else [])
59
+
60
+
61
+ def _get_model_name(args, kwargs):
62
+ """Best-effort extraction of the requested model name before the call is made
63
+ (ported from the old single-entry-point file's _get_model_name)."""
64
+ model_name = kwargs.get("model")
65
+ if not model_name and args:
66
+ model_name = getattr(args[0], "model", None) or getattr(args[0], "model_name", None)
67
+ return model_name or "default"
68
+
69
+
70
+ def _build_chat_output(result):
71
+ """
72
+ Extract output from all choices of a ChatCompletion response.
73
+ Returns a single dict for n=1, a list for n>1 (Gap 8).
74
+ """
75
+ choices = getattr(result, "choices", None) or []
76
+ outputs = []
77
+ for choice in choices:
78
+ msg = getattr(choice, "message", None)
79
+ if msg is None:
80
+ continue
81
+ outputs.append({
82
+ "content": getattr(msg, "content", None),
83
+ "role": getattr(msg, "role", None),
84
+ "tool_calls": str(getattr(msg, "tool_calls", None)),
85
+ "completion_id": getattr(result, "id", None),
86
+ "finish_reason": getattr(choice, "finish_reason", None),
87
+ "refusal": getattr(msg, "refusal", None),
88
+ "annotations": getattr(msg, "annotations", None),
89
+ "audio": getattr(msg, "audio", None),
90
+ "function_call": str(getattr(msg, "function_call", None)),
91
+ })
92
+ if len(outputs) == 1:
93
+ return outputs[0]
94
+ return outputs if outputs else None
95
+
96
+
97
+ def _accumulate_stream_chunks(chunks):
98
+ """
99
+ Merge ChatCompletionChunk list into aggregated output fields.
100
+ Returns (content_by_index, model_name, completion_id, finish_reason, usage).
101
+ """
102
+ content_by_index = {}
103
+ model_name = "default"
104
+ completion_id = None
105
+ finish_reason = None
106
+ usage = None
107
+
108
+ for chunk in chunks:
109
+ model_name = getattr(chunk, "model", None) or model_name
110
+ completion_id = getattr(chunk, "id", None) or completion_id
111
+ # usage arrives on the last chunk when stream_options={"include_usage": True}
112
+ chunk_usage = getattr(chunk, "usage", None)
113
+ if chunk_usage is not None:
114
+ usage = chunk_usage
115
+ for choice in (getattr(chunk, "choices", None) or []):
116
+ idx = getattr(choice, "index", 0)
117
+ delta = getattr(choice, "delta", None)
118
+ if delta:
119
+ text = getattr(delta, "content", None)
120
+ if text:
121
+ content_by_index[idx] = content_by_index.get(idx, "") + text
122
+ fr = getattr(choice, "finish_reason", None)
123
+ if fr:
124
+ finish_reason = fr
125
+
126
+ return content_by_index, model_name, completion_id, finish_reason, usage
127
+
128
+
129
+ def _build_stream_output(content_by_index, completion_id, finish_reason):
130
+ """Build output dict(s) from accumulated streaming content."""
131
+ indices = sorted(content_by_index.keys())
132
+ if not indices:
133
+ return {"content": None, "completion_id": completion_id, "finish_reason": finish_reason,
134
+ "role": "assistant"}
135
+ outputs = [
136
+ {
137
+ "content": content_by_index.get(i),
138
+ "completion_id": completion_id,
139
+ "finish_reason": finish_reason,
140
+ "role": "assistant",
141
+ }
142
+ for i in indices
143
+ ]
144
+ return outputs[0] if len(outputs) == 1 else outputs
145
+
146
+
147
+ def _split_fp(context):
148
+ """Safely split fp_topo from context; returns 7-tuple of Nones on failure."""
149
+ try:
150
+ if context:
151
+ return split_fp_topo(context.fp_topo)
152
+ except Exception:
153
+ pass
154
+ return None, None, None, None, None, None, None
155
+
156
+
157
+ def _record_llm_span(interceptor, bt, fqm, start_time, duration, context,
158
+ args, kwargs, model_name, output, usage):
159
+ """Create and send an LLMSpan if nf_enabled."""
160
+ if not interceptor.agent.nf_enabled:
161
+ return
162
+ metrics = _safe_usage_metrics(usage)
163
+ test_run, fpi, entry_fpi, nd_session_id, nv_session_id, page_id, full_fp = _split_fp(context)
164
+ meta = {
165
+ "span_kind": "llm",
166
+ "input": _get_messages_input(args, kwargs),
167
+ "output": output,
168
+ "model_name": model_name,
169
+ "model_provider": "OpenAI",
170
+ "metadata": kwargs.get("metadata", None),
171
+ }
172
+ llm_span = LLMSpan(
173
+ fqm, start_time, duration, "ok", meta, metrics,
174
+ test_run, fpi, entry_fpi, nd_session_id, nv_session_id, page_id, full_fp
175
+ )
176
+ if interceptor.agent.logger.isEnabledFor(logging.INFO):
177
+ interceptor.agent.logger.info("openai llm sending llm span output={}".format(output))
178
+ interceptor.agent.dump_log_message(bt, llm_span.create_span_json())
179
+
180
+
181
+ def _record_call_success(interceptor, bt, exit_call, start_time, fqm, context,
182
+ username, args, kwargs, model_name, output, usage, label="OpenAI LLM"):
183
+ """Compute duration, call db_call_end, and send LLMSpan."""
184
+ end_time = get_current_timestamp_in_us()
185
+ duration = get_validated_duration(start_time, end_time, label)
186
+ host_name_str = build_host_name_str("NA", "NA", "NA", model_name, username)
187
+ if context:
188
+ context.host_str = host_name_str
189
+ context.status_code = 200
190
+ context.db_callout_time = 0
191
+ if interceptor.agent.logger.isEnabledFor(logging.INFO):
192
+ interceptor.agent.logger.info(
193
+ "openai llm db call end bt={} exit_call={} duration={} fqm={} model={}".format(
194
+ bt, exit_call, duration, fqm, model_name
195
+ )
196
+ )
197
+ interceptor.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
198
+ _record_llm_span(interceptor, bt, fqm, start_time, duration, context,
199
+ args, kwargs, model_name, output, usage)
200
+
201
+
202
+ def _record_call_error(interceptor, bt, exit_call, start_time, fqm, context, username,
203
+ label="OpenAI LLM"):
204
+ """Call db_call_end on the exception path with status 500."""
205
+ end_time = get_current_timestamp_in_us()
206
+ duration = get_validated_duration(start_time, end_time, label)
207
+ host_name_str = build_host_name_str("NA", "NA", "LLM", "OpenAI_default", username)
208
+ if context:
209
+ context.host_str = host_name_str
210
+ context.status_code = 500
211
+ context.db_callout_time = 0
212
+ if interceptor.agent.logger.isEnabledFor(logging.INFO):
213
+ interceptor.agent.logger.info(
214
+ "openai llm db call end (error) bt={} exit_call={} fqm={}".format(bt, exit_call, fqm)
215
+ )
216
+ interceptor.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Stream wrappers
221
+ # ---------------------------------------------------------------------------
222
+
223
+ class SyncStreamWrapper:
224
+ """
225
+ Transparent wrapper around openai.Stream[ChatCompletionChunk].
226
+ Accumulates chunks and fires on_finish after the last chunk is consumed
227
+ or the iterator is closed early (via finally). (Gap 2)
228
+ """
229
+ def __init__(self, stream, on_finish):
230
+ self._stream = stream
231
+ self._on_finish = on_finish
232
+ self._chunks = []
233
+ self._finished = False
234
+
235
+ def _fire_finish(self):
236
+ if not self._finished:
237
+ self._finished = True
238
+ self._on_finish(self._chunks)
239
+
240
+ def __iter__(self):
241
+ try:
242
+ for chunk in self._stream:
243
+ self._chunks.append(chunk)
244
+ yield chunk
245
+ finally:
246
+ self._fire_finish()
247
+
248
+ def __enter__(self):
249
+ return self
250
+
251
+ def __exit__(self, *args):
252
+ self._fire_finish()
253
+ aexit = getattr(self._stream, "__exit__", None)
254
+ if aexit:
255
+ return aexit(*args)
256
+
257
+ def __getattr__(self, name):
258
+ return getattr(self._stream, name)
259
+
260
+
261
+ class AsyncStreamWrapper:
262
+ """
263
+ Transparent wrapper around openai.AsyncStream[ChatCompletionChunk].
264
+ Fires on_finish (synchronous SDK call) after the last chunk or early close. (Gap 3)
265
+ """
266
+ def __init__(self, stream, on_finish):
267
+ self._stream = stream
268
+ self._on_finish = on_finish
269
+ self._chunks = []
270
+ self._finished = False
271
+ self._iterator = None
272
+
273
+ def _fire_finish(self):
274
+ if not self._finished:
275
+ self._finished = True
276
+ self._on_finish(self._chunks)
277
+
278
+ def __aiter__(self):
279
+ self._iterator = self._stream.__aiter__()
280
+ return self
281
+
282
+ async def __anext__(self):
283
+ try:
284
+ chunk = await self._iterator.__anext__()
285
+ self._chunks.append(chunk)
286
+ return chunk
287
+ except StopAsyncIteration:
288
+ self._fire_finish()
289
+ raise
290
+
291
+ async def __aenter__(self):
292
+ return self
293
+
294
+ async def __aexit__(self, *args):
295
+ self._fire_finish()
296
+ aexit = getattr(self._stream, "__aexit__", None)
297
+ if aexit:
298
+ await aexit(*args)
299
+
300
+ def __getattr__(self, name):
301
+ return getattr(self._stream, name)
302
+
303
+
304
+ # ---------------------------------------------------------------------------
305
+ # Internal: build streaming on_finish closure
306
+ # ---------------------------------------------------------------------------
307
+
308
+ def _make_stream_on_finish(interceptor, bt, exit_call, start_time, fqm,
309
+ context, username, args, kwargs):
310
+ """Return a closure suitable for SyncStreamWrapper / AsyncStreamWrapper."""
311
+ def on_finish(chunks):
312
+ try:
313
+ content_by_idx, model_name, completion_id, finish_reason, usage = \
314
+ _accumulate_stream_chunks(chunks)
315
+ output = _build_stream_output(content_by_idx, completion_id, finish_reason)
316
+ _record_call_success(
317
+ interceptor, bt, exit_call, start_time, fqm, context,
318
+ username, args, kwargs, model_name, output, usage
319
+ )
320
+ except Exception as e:
321
+ interceptor.agent.logger.exception(
322
+ "openai stream on_finish error: {}".format(e)
323
+ )
324
+ return on_finish
325
+
326
+
327
+ # ---------------------------------------------------------------------------
328
+ # Chat Completions — Sync (existing, with all gap fixes applied)
329
+ # ---------------------------------------------------------------------------
330
+
331
+ class OpenAIInterceptor(ExitCallInterceptor):
332
+ def wrap_create(self, create, *args, **kwargs):
333
+ start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
334
+ try:
335
+ bt = self.bt
336
+ username = "NA"
337
+ host, port, query, query_params = "NA", "NA", "NA", None
338
+ model_name = _get_model_name(args, kwargs)
339
+ host_name_str = build_host_name_str(host, port, "LLM", "OpenAI_" + model_name, username)
340
+ fqm = "{}.{}".format(create.__module__, create.__qualname__)
341
+ context = self.agent.get_transaction_context()
342
+ if context:
343
+ context.entry_point_fqm = fqm
344
+ context.host_str = host_name_str
345
+ start_time = get_current_timestamp_in_us()
346
+ if self.agent.logger.isEnabledFor(logging.INFO):
347
+ self.agent.logger.info(
348
+ "openai llm db call begin bt={} start_time={}".format(bt, start_time)
349
+ )
350
+ exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
351
+ except Exception as e:
352
+ from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
353
+ if isinstance(e, NDHavocException):
354
+ raise e
355
+ self.agent.logger.exception("openai llm db call begin: {}".format(e))
356
+
357
+ try:
358
+ result = create(*args, **kwargs)
359
+ except Exception as e:
360
+ self.agent.logger.exception("openai llm create error: {}".format(e))
361
+ try:
362
+ _record_call_error(self, bt, exit_call, start_time, fqm, context, username)
363
+ except Exception as inner:
364
+ self.agent.logger.exception("openai llm db call end (error): {}".format(inner))
365
+ raise
366
+
367
+ # Streaming path — wrap stream, defer telemetry until consumed (Gap 2)
368
+ if kwargs.get("stream", False):
369
+ return SyncStreamWrapper(
370
+ result,
371
+ _make_stream_on_finish(self, bt, exit_call, start_time, fqm,
372
+ context, username, args, kwargs)
373
+ )
374
+
375
+ # Non-streaming path
376
+ try:
377
+ model_name = getattr(result, "model", None) or getattr(result, "model_name", None) or "default"
378
+ output = _build_chat_output(result)
379
+ usage = getattr(result, "usage", None)
380
+ _record_call_success(
381
+ self, bt, exit_call, start_time, fqm, context,
382
+ username, args, kwargs, model_name, output, usage
383
+ )
384
+ except Exception as e:
385
+ self.agent.logger.exception("openai llm db call end: {}".format(e))
386
+
387
+ return result
388
+
389
+
390
+ # ---------------------------------------------------------------------------
391
+ # Chat Completions — Async (Gap 1 + Gap 3)
392
+ # ---------------------------------------------------------------------------
393
+
394
+ class AsyncOpenAIInterceptor(ExitCallInterceptor):
395
+ async def wrap_create(self, create, *args, **kwargs):
396
+ start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
397
+ try:
398
+ bt = self.bt
399
+ username = "NA"
400
+ host, port, query, query_params = "NA", "NA", "NA", None
401
+ model_name = _get_model_name(args, kwargs)
402
+ host_name_str = build_host_name_str(host, port, "LLM", "OpenAI_" + model_name, username)
403
+ fqm = "{}.{}".format(create.__module__, create.__qualname__)
404
+ context = self.agent.get_transaction_context()
405
+ if context:
406
+ context.entry_point_fqm = fqm
407
+ context.host_str = host_name_str
408
+ start_time = get_current_timestamp_in_us()
409
+ if self.agent.logger.isEnabledFor(logging.INFO):
410
+ self.agent.logger.info(
411
+ "openai async llm db call begin bt={} start_time={}".format(bt, start_time)
412
+ )
413
+ exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
414
+ except Exception as e:
415
+ from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
416
+ if isinstance(e, NDHavocException):
417
+ raise e
418
+ self.agent.logger.exception("openai async llm db call begin: {}".format(e))
419
+
420
+ try:
421
+ result = await create(*args, **kwargs)
422
+ except Exception as e:
423
+ self.agent.logger.exception("openai async llm create error: {}".format(e))
424
+ try:
425
+ _record_call_error(self, bt, exit_call, start_time, fqm, context, username)
426
+ except Exception as inner:
427
+ self.agent.logger.exception("openai async llm db call end (error): {}".format(inner))
428
+ raise
429
+
430
+ # Async streaming path (Gap 3)
431
+ if kwargs.get("stream", False):
432
+ return AsyncStreamWrapper(
433
+ result,
434
+ _make_stream_on_finish(self, bt, exit_call, start_time, fqm,
435
+ context, username, args, kwargs)
436
+ )
437
+
438
+ # Non-streaming path
439
+ try:
440
+ model_name = getattr(result, "model", None) or getattr(result, "model_name", None) or "default"
441
+ output = _build_chat_output(result)
442
+ usage = getattr(result, "usage", None)
443
+ _record_call_success(
444
+ self, bt, exit_call, start_time, fqm, context,
445
+ username, args, kwargs, model_name, output, usage
446
+ )
447
+ except Exception as e:
448
+ self.agent.logger.exception("openai async llm db call end: {}".format(e))
449
+
450
+ return result
451
+
452
+
453
+ # ---------------------------------------------------------------------------
454
+ # Legacy (non-chat) Completions — Sync (Gap 6)
455
+ # ---------------------------------------------------------------------------
456
+
457
+ def _build_legacy_output(result):
458
+ """Extract output from legacy Completion choices (result.choices[n].text)."""
459
+ choices = getattr(result, "choices", None) or []
460
+ outputs = [
461
+ {
462
+ "text": getattr(c, "text", None),
463
+ "finish_reason": getattr(c, "finish_reason", None),
464
+ "completion_id": getattr(result, "id", None),
465
+ }
466
+ for c in choices
467
+ ]
468
+ if len(outputs) == 1:
469
+ return outputs[0]
470
+ return outputs if outputs else None
471
+
472
+
473
+ class OpenAILegacyCompletionsInterceptor(ExitCallInterceptor):
474
+ def wrap_create(self, create, *args, **kwargs):
475
+ start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
476
+ try:
477
+ bt = self.bt
478
+ username = "NA"
479
+ host, port, query, query_params = "NA", "NA", "NA", None
480
+ model_name = _get_model_name(args, kwargs)
481
+ host_name_str = build_host_name_str(host, port, "LLM", "OpenAI_" + model_name, username)
482
+ fqm = "{}.{}".format(create.__module__, create.__qualname__)
483
+ context = self.agent.get_transaction_context()
484
+ if context:
485
+ context.entry_point_fqm = fqm
486
+ context.host_str = host_name_str
487
+ start_time = get_current_timestamp_in_us()
488
+ exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
489
+ except Exception as e:
490
+ from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
491
+ if isinstance(e, NDHavocException):
492
+ raise e
493
+ self.agent.logger.exception("openai legacy completions db call begin: {}".format(e))
494
+
495
+ try:
496
+ result = create(*args, **kwargs)
497
+ except Exception as e:
498
+ self.agent.logger.exception("openai legacy completions create error: {}".format(e))
499
+ try:
500
+ _record_call_error(self, bt, exit_call, start_time, fqm, context, username,
501
+ label="OpenAI Legacy Completions")
502
+ except Exception as inner:
503
+ self.agent.logger.exception("openai legacy completions db call end (error): {}".format(inner))
504
+ raise
505
+
506
+ if kwargs.get("stream", False):
507
+ return SyncStreamWrapper(
508
+ result,
509
+ _make_stream_on_finish(self, bt, exit_call, start_time, fqm,
510
+ context, username, args, kwargs)
511
+ )
512
+
513
+ try:
514
+ model_name = getattr(result, "model", None) or "default"
515
+ output = _build_legacy_output(result)
516
+ usage = getattr(result, "usage", None)
517
+ _record_call_success(
518
+ self, bt, exit_call, start_time, fqm, context,
519
+ username, args, kwargs, model_name, output, usage,
520
+ label="OpenAI Legacy Completions"
521
+ )
522
+ except Exception as e:
523
+ self.agent.logger.exception("openai legacy completions db call end: {}".format(e))
524
+
525
+ return result
526
+
527
+
528
+ # ---------------------------------------------------------------------------
529
+ # Legacy Completions — Async (Gap 6)
530
+ # ---------------------------------------------------------------------------
531
+
532
+ class AsyncOpenAILegacyCompletionsInterceptor(ExitCallInterceptor):
533
+ async def wrap_create(self, create, *args, **kwargs):
534
+ start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
535
+ try:
536
+ bt = self.bt
537
+ username = "NA"
538
+ host, port, query, query_params = "NA", "NA", "NA", None
539
+ model_name = _get_model_name(args, kwargs)
540
+ host_name_str = build_host_name_str(host, port, "LLM", "OpenAI_" + model_name, username)
541
+ fqm = "{}.{}".format(create.__module__, create.__qualname__)
542
+ context = self.agent.get_transaction_context()
543
+ if context:
544
+ context.entry_point_fqm = fqm
545
+ context.host_str = host_name_str
546
+ start_time = get_current_timestamp_in_us()
547
+ exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
548
+ except Exception as e:
549
+ from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
550
+ if isinstance(e, NDHavocException):
551
+ raise e
552
+ self.agent.logger.exception("openai async legacy completions db call begin: {}".format(e))
553
+
554
+ try:
555
+ result = await create(*args, **kwargs)
556
+ except Exception as e:
557
+ self.agent.logger.exception("openai async legacy completions create error: {}".format(e))
558
+ try:
559
+ _record_call_error(self, bt, exit_call, start_time, fqm, context, username,
560
+ label="OpenAI Legacy Completions")
561
+ except Exception as inner:
562
+ self.agent.logger.exception("openai async legacy completions db call end (error): {}".format(inner))
563
+ raise
564
+
565
+ if kwargs.get("stream", False):
566
+ return AsyncStreamWrapper(
567
+ result,
568
+ _make_stream_on_finish(self, bt, exit_call, start_time, fqm,
569
+ context, username, args, kwargs)
570
+ )
571
+
572
+ try:
573
+ model_name = getattr(result, "model", None) or "default"
574
+ output = _build_legacy_output(result)
575
+ usage = getattr(result, "usage", None)
576
+ _record_call_success(
577
+ self, bt, exit_call, start_time, fqm, context,
578
+ username, args, kwargs, model_name, output, usage,
579
+ label="OpenAI Legacy Completions"
580
+ )
581
+ except Exception as e:
582
+ self.agent.logger.exception("openai async legacy completions db call end: {}".format(e))
583
+
584
+ return result
585
+
586
+
587
+ # ---------------------------------------------------------------------------
588
+ # Embeddings — Sync + Async (Gap 7)
589
+ # ---------------------------------------------------------------------------
590
+
591
+ def _build_embedding_output(result):
592
+ """Build output list from EmbeddingResponse.data."""
593
+ data = getattr(result, "data", None) or []
594
+ vectors = [
595
+ {
596
+ "index": getattr(d, "index", i),
597
+ "embedding_length": len(getattr(d, "embedding", None) or []),
598
+ "embedding_sample": (getattr(d, "embedding", None) or [])[:5],
599
+ }
600
+ for i, d in enumerate(data)
601
+ ]
602
+ return vectors[0] if len(vectors) == 1 else (vectors or None)
603
+
604
+
605
+ def _record_embedding_span(interceptor, bt, exit_call, start_time, fqm,
606
+ context, username, kwargs, model_name, output, usage):
607
+ """db_call_end + EmbeddingSpan."""
608
+ end_time = get_current_timestamp_in_us()
609
+ duration = get_validated_duration(start_time, end_time, "OpenAI Embeddings")
610
+ host_name_str = build_host_name_str("NA", "NA", "NA", model_name, username)
611
+ if context:
612
+ context.host_str = host_name_str
613
+ context.status_code = 200
614
+ context.db_callout_time = 0
615
+ interceptor.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
616
+
617
+ if not interceptor.agent.nf_enabled:
618
+ return
619
+ test_run, fpi, entry_fpi, nd_session_id, nv_session_id, page_id, full_fp = _split_fp(context)
620
+ meta = {
621
+ "span_kind": "embedding",
622
+ "input": kwargs.get("input") or kwargs.get("inputs"),
623
+ "output": output,
624
+ "model_name": model_name,
625
+ "model_provider": "OpenAI",
626
+ "metadata": kwargs.get("metadata", None),
627
+ }
628
+ embedding_span = EmbeddingSpan(
629
+ fqm, start_time, duration, "ok", meta,
630
+ test_run, fpi, entry_fpi, nd_session_id, nv_session_id, page_id, full_fp
631
+ )
632
+ # Attach token metrics (EmbeddingSpan inherits self.metrics = {} from BaseSpan)
633
+ embedding_span.metrics = _safe_usage_metrics(usage)
634
+ if interceptor.agent.logger.isEnabledFor(logging.INFO):
635
+ interceptor.agent.logger.info(
636
+ "openai embeddings sending span output={}".format(output)
637
+ )
638
+ interceptor.agent.dump_log_message(bt, embedding_span.create_span_json())
639
+
640
+
641
+ class OpenAIEmbeddingsInterceptor(ExitCallInterceptor):
642
+ def wrap_create(self, create, *args, **kwargs):
643
+ start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
644
+ try:
645
+ bt = self.bt
646
+ username = "NA"
647
+ host, port, query, query_params = "NA", "NA", "NA", None
648
+ model_name = _get_model_name(args, kwargs)
649
+ host_name_str = build_host_name_str(host, port, "Embedding", "OpenAI_" + model_name, username)
650
+ fqm = "{}.{}".format(create.__module__, create.__qualname__)
651
+ context = self.agent.get_transaction_context()
652
+ if context:
653
+ context.entry_point_fqm = fqm
654
+ context.host_str = host_name_str
655
+ start_time = get_current_timestamp_in_us()
656
+ exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
657
+ except Exception as e:
658
+ from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
659
+ if isinstance(e, NDHavocException):
660
+ raise e
661
+ self.agent.logger.exception("openai embeddings db call begin: {}".format(e))
662
+
663
+ try:
664
+ result = create(*args, **kwargs)
665
+ except Exception as e:
666
+ self.agent.logger.exception("openai embeddings create error: {}".format(e))
667
+ try:
668
+ end_time = get_current_timestamp_in_us()
669
+ duration = get_validated_duration(start_time, end_time, "OpenAI Embeddings")
670
+ host_name_str = build_host_name_str("NA", "NA", "Embedding", "OpenAI_default", username)
671
+ if context:
672
+ context.host_str = host_name_str
673
+ context.status_code = 500
674
+ self.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
675
+ except Exception as inner:
676
+ self.agent.logger.exception("openai embeddings db call end (error): {}".format(inner))
677
+ raise
678
+
679
+ try:
680
+ model_name = getattr(result, "model", None) or "default"
681
+ output = _build_embedding_output(result)
682
+ usage = getattr(result, "usage", None)
683
+ _record_embedding_span(
684
+ self, bt, exit_call, start_time, fqm, context,
685
+ username, kwargs, model_name, output, usage
686
+ )
687
+ except Exception as e:
688
+ self.agent.logger.exception("openai embeddings db call end: {}".format(e))
689
+
690
+ return result
691
+
692
+
693
+ class AsyncOpenAIEmbeddingsInterceptor(ExitCallInterceptor):
694
+ async def wrap_create(self, create, *args, **kwargs):
695
+ start_time, exit_call, bt, fqm, context, username = 0, 0, None, None, None, None
696
+ try:
697
+ bt = self.bt
698
+ username = "NA"
699
+ host, port, query, query_params = "NA", "NA", "NA", None
700
+ model_name = _get_model_name(args, kwargs)
701
+ host_name_str = build_host_name_str(host, port, "Embedding", "OpenAI_" + model_name, username)
702
+ fqm = "{}.{}".format(create.__module__, create.__qualname__)
703
+ context = self.agent.get_transaction_context()
704
+ if context:
705
+ context.entry_point_fqm = fqm
706
+ context.host_str = host_name_str
707
+ start_time = get_current_timestamp_in_us()
708
+ exit_call = self.db_call_begin(bt, host, query, query_params, start_time, fqm, host_name_str)
709
+ except Exception as e:
710
+ from pythonagent.agent.probes.havoc.havoc_manager import NDHavocException
711
+ if isinstance(e, NDHavocException):
712
+ raise e
713
+ self.agent.logger.exception("openai async embeddings db call begin: {}".format(e))
714
+
715
+ try:
716
+ result = await create(*args, **kwargs)
717
+ except Exception as e:
718
+ self.agent.logger.exception("openai async embeddings create error: {}".format(e))
719
+ try:
720
+ end_time = get_current_timestamp_in_us()
721
+ duration = get_validated_duration(start_time, end_time, "OpenAI Embeddings")
722
+ host_name_str = build_host_name_str("NA", "NA", "Embedding", "OpenAI_default", username)
723
+ if context:
724
+ context.host_str = host_name_str
725
+ context.status_code = 500
726
+ self.db_call_end(bt, exit_call, duration, start_time, fqm, host_name_str)
727
+ except Exception as inner:
728
+ self.agent.logger.exception("openai async embeddings db call end (error): {}".format(inner))
729
+ raise
730
+
731
+ try:
732
+ model_name = getattr(result, "model", None) or "default"
733
+ output = _build_embedding_output(result)
734
+ usage = getattr(result, "usage", None)
735
+ _record_embedding_span(
736
+ self, bt, exit_call, start_time, fqm, context,
737
+ username, kwargs, model_name, output, usage
738
+ )
739
+ except Exception as e:
740
+ self.agent.logger.exception("openai async embeddings db call end: {}".format(e))
741
+
742
+ return result
743
+
744
+
745
+ # ---------------------------------------------------------------------------
746
+ # Registration
747
+ # ---------------------------------------------------------------------------
748
+
749
+ def intercept_openai(agent, mod):
750
+ """
751
+ Patch all relevant OpenAI SDK entry points.
752
+ Each registration is wrapped independently so a missing class on an older
753
+ SDK version does not prevent the others from being instrumented.
754
+ """
755
+ agent.logger.warning("Instrument module: openai{}".format(", mod: {}".format(mod) if mod else ""))
756
+ # 1. Sync chat completions (fixed: null-safety, positional messages, streaming, all choices)
757
+ try:
758
+ OpenAIInterceptor(agent, mod.resources.chat.completions.Completions) \
759
+ .attach('create', patched_method_name='wrap_create')
760
+ except Exception as e:
761
+ agent.logger.exception("openai sync chat completions intercept failed: {}".format(e))
762
+
763
+ # 2. Async chat completions (Gap 1 + Gap 3)
764
+ try:
765
+ AsyncOpenAIInterceptor(agent, mod.resources.chat.completions.AsyncCompletions) \
766
+ .attach('create', patched_method_name='wrap_create')
767
+ except Exception as e:
768
+ agent.logger.exception("openai async chat completions intercept failed: {}".format(e))
769
+
770
+ # 3. Legacy completions — sync (Gap 6)
771
+ try:
772
+ OpenAILegacyCompletionsInterceptor(agent, mod.resources.completions.Completions) \
773
+ .attach('create', patched_method_name='wrap_create')
774
+ except Exception as e:
775
+ agent.logger.exception("openai legacy sync completions intercept failed: {}".format(e))
776
+
777
+ # 4. Legacy completions — async (Gap 6)
778
+ try:
779
+ AsyncOpenAILegacyCompletionsInterceptor(agent, mod.resources.completions.AsyncCompletions) \
780
+ .attach('create', patched_method_name='wrap_create')
781
+ except Exception as e:
782
+ agent.logger.exception("openai legacy async completions intercept failed: {}".format(e))
783
+
784
+ # 5. Embeddings — sync (Gap 7)
785
+ try:
786
+ OpenAIEmbeddingsInterceptor(agent, mod.resources.embeddings.Embeddings) \
787
+ .attach('create', patched_method_name='wrap_create')
788
+ except Exception as e:
789
+ agent.logger.exception("openai sync embeddings intercept failed: {}".format(e))
790
+ return OpenAIInterceptor(agent, mod.resources.chat.completions.Completions).attach('create', patched_method_name='wrap_create')
791
+
792
+ # 6. Embeddings — async (Gap 7)
793
+ try:
794
+ AsyncOpenAIEmbeddingsInterceptor(agent, mod.resources.embeddings.AsyncEmbeddings) \
795
+ .attach('create', patched_method_name='wrap_create')
796
+ except Exception as e:
797
+ agent.logger.exception("openai async embeddings intercept failed: {}".format(e))