paid-python 1.0.4__tar.gz → 1.0.6__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 (118) hide show
  1. {paid_python-1.0.4 → paid_python-1.0.6}/PKG-INFO +1 -1
  2. {paid_python-1.0.4 → paid_python-1.0.6}/pyproject.toml +1 -1
  3. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/client_wrapper.py +2 -2
  4. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/anthropic_patches/patches.py +106 -5
  5. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/autoinstrumentation.py +8 -0
  6. paid_python-1.0.6/src/paid/tracing/gemini_patches/__init__.py +15 -0
  7. paid_python-1.0.6/src/paid/tracing/gemini_patches/patches.py +101 -0
  8. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/tracing.py +5 -3
  9. paid_python-1.0.6/src/paid/version.py +6 -0
  10. paid_python-1.0.4/src/paid/version.py +0 -3
  11. {paid_python-1.0.4 → paid_python-1.0.6}/LICENSE +0 -0
  12. {paid_python-1.0.4 → paid_python-1.0.6}/README.md +0 -0
  13. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/__init__.py +0 -0
  14. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/client.py +0 -0
  15. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/contacts/__init__.py +0 -0
  16. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/contacts/client.py +0 -0
  17. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/contacts/raw_client.py +0 -0
  18. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/__init__.py +0 -0
  19. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/api_error.py +0 -0
  20. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/datetime_utils.py +0 -0
  21. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/file.py +0 -0
  22. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/force_multipart.py +0 -0
  23. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/http_client.py +0 -0
  24. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/http_response.py +0 -0
  25. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/http_sse/__init__.py +0 -0
  26. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/http_sse/_api.py +0 -0
  27. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/http_sse/_decoders.py +0 -0
  28. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/http_sse/_exceptions.py +0 -0
  29. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/http_sse/_models.py +0 -0
  30. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/jsonable_encoder.py +0 -0
  31. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/pydantic_utilities.py +0 -0
  32. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/query_encoder.py +0 -0
  33. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/remove_none_from_dict.py +0 -0
  34. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/request_options.py +0 -0
  35. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/core/serialization.py +0 -0
  36. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/customers/__init__.py +0 -0
  37. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/customers/client.py +0 -0
  38. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/customers/raw_client.py +0 -0
  39. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/environment.py +0 -0
  40. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/errors/__init__.py +0 -0
  41. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/errors/bad_request_error.py +0 -0
  42. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/errors/forbidden_error.py +0 -0
  43. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/errors/internal_server_error.py +0 -0
  44. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/errors/not_found_error.py +0 -0
  45. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/invoices/__init__.py +0 -0
  46. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/invoices/client.py +0 -0
  47. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/invoices/raw_client.py +0 -0
  48. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/logger.py +0 -0
  49. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/orders/__init__.py +0 -0
  50. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/orders/client.py +0 -0
  51. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/orders/raw_client.py +0 -0
  52. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/products/__init__.py +0 -0
  53. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/products/client.py +0 -0
  54. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/products/raw_client.py +0 -0
  55. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/py.typed +0 -0
  56. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/signals/__init__.py +0 -0
  57. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/signals/client.py +0 -0
  58. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/signals/raw_client.py +0 -0
  59. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/__init__.py +0 -0
  60. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/anthropic_patches/__init__.py +0 -0
  61. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/context_data.py +0 -0
  62. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/context_manager.py +0 -0
  63. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/distributed_tracing.py +0 -0
  64. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/signal.py +0 -0
  65. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/__init__.py +0 -0
  66. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/anthropic/__init__.py +0 -0
  67. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/anthropic/anthropicWrapper.py +0 -0
  68. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/bedrock/__init__.py +0 -0
  69. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/bedrock/bedrockWrapper.py +0 -0
  70. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/gemini/__init__.py +0 -0
  71. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/gemini/geminiWrapper.py +0 -0
  72. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/langchain/__init__.py +0 -0
  73. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/langchain/paidLangChainCallback.py +0 -0
  74. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/mistral/__init__.py +0 -0
  75. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/mistral/mistralWrapper.py +0 -0
  76. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/openai/__init__.py +0 -0
  77. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/openai/openAiWrapper.py +0 -0
  78. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/openai_agents/__init__.py +0 -0
  79. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/openai_agents/openaiAgentsHook.py +0 -0
  80. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/tracing/wrappers/utils.py +0 -0
  81. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/__init__.py +0 -0
  82. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/attribution.py +0 -0
  83. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/bulk_signals_response.py +0 -0
  84. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/contact.py +0 -0
  85. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/contact_billing_address.py +0 -0
  86. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/contact_list_response.py +0 -0
  87. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/customer.py +0 -0
  88. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/customer_attribution.py +0 -0
  89. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/customer_billing_address.py +0 -0
  90. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/customer_by_external_id.py +0 -0
  91. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/customer_by_id.py +0 -0
  92. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/customer_creation_state.py +0 -0
  93. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/customer_list_response.py +0 -0
  94. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/empty_response.py +0 -0
  95. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/error_response.py +0 -0
  96. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice.py +0 -0
  97. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice_line.py +0 -0
  98. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice_line_payment_status.py +0 -0
  99. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice_lines_response.py +0 -0
  100. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice_list_response.py +0 -0
  101. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice_payment_status.py +0 -0
  102. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice_source.py +0 -0
  103. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice_status.py +0 -0
  104. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/invoice_tax_status.py +0 -0
  105. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/order.py +0 -0
  106. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/order_creation_state.py +0 -0
  107. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/order_line.py +0 -0
  108. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/order_lines_response.py +0 -0
  109. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/order_list_response.py +0 -0
  110. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/pagination.py +0 -0
  111. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/product.py +0 -0
  112. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/product_by_external_id.py +0 -0
  113. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/product_by_id.py +0 -0
  114. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/product_list_response.py +0 -0
  115. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/signal.py +0 -0
  116. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/update_contact_request.py +0 -0
  117. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/update_customer_request.py +0 -0
  118. {paid_python-1.0.4 → paid_python-1.0.6}/src/paid/types/update_product_request.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: paid-python
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary:
5
5
  Requires-Python: >=3.10,<3.14
6
6
  Classifier: Intended Audience :: Developers
@@ -4,7 +4,7 @@ dynamic = ["version"]
4
4
 
5
5
  [tool.poetry]
6
6
  name = "paid-python"
7
- version = "1.0.4"
7
+ version = "1.0.6"
8
8
  description = ""
9
9
  readme = "README.md"
10
10
  authors = []
@@ -24,12 +24,12 @@ class BaseClientWrapper:
24
24
  import platform
25
25
 
26
26
  headers: typing.Dict[str, str] = {
27
- "User-Agent": "paid-python/1.0.4",
27
+ "User-Agent": "paid-python/1.0.6",
28
28
  "X-Fern-Language": "Python",
29
29
  "X-Fern-Runtime": f"python/{platform.python_version()}",
30
30
  "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
31
31
  "X-Fern-SDK-Name": "paid-python",
32
- "X-Fern-SDK-Version": "1.0.4",
32
+ "X-Fern-SDK-Version": "1.0.6",
33
33
  **(self.get_custom_headers() or {}),
34
34
  }
35
35
  headers["Authorization"] = f"Bearer {self._get_token()}"
@@ -15,8 +15,13 @@ _original_async_messages_stream = None
15
15
  # Originals for the beta path, keyed by method name.
16
16
  _beta_originals: Dict[str, Any] = {}
17
17
 
18
+ # Originals for response-ID patches.
19
+ _response_id_originals: Dict[str, Any] = {}
20
+
18
21
  _BETA_MODULE = "anthropic.resources.beta.messages.messages"
19
22
 
23
+ _ATTR_RESPONSE_ID = "gen_ai.response.id"
24
+
20
25
 
21
26
  def instrument_anthropic() -> None:
22
27
  """Apply all Anthropic patches. Call after AnthropicInstrumentor().instrument()."""
@@ -24,6 +29,8 @@ def instrument_anthropic() -> None:
24
29
  _patch_message_stream_manager()
25
30
  _wrap_async_messages_stream()
26
31
  _patch_response_accumulator_for_beta()
32
+ _patch_response_id_extraction()
33
+ _patch_streaming_response_id_extraction()
27
34
  _wrap_beta_messages()
28
35
 
29
36
 
@@ -38,6 +45,7 @@ def uninstrument_anthropic() -> None:
38
45
  _original_async_messages_stream = None
39
46
 
40
47
  _uninstrument_beta_messages()
48
+ _uninstrument_response_id_patches()
41
49
 
42
50
  def _patch_stream_context_managers() -> None:
43
51
  try:
@@ -120,14 +128,26 @@ class _AsyncMessageStreamManagerProxy(ObjectProxy): # type: ignore[misc]
120
128
  def __init__(self, manager: Any, span: trace_api.Span) -> None:
121
129
  super().__init__(manager)
122
130
  self._self_span = span
131
+ self._self_stream: Any = None
123
132
 
124
133
  async def __aenter__(self): # type: ignore[misc]
125
- return await self.__wrapped__.__aenter__()
134
+ stream = await self.__wrapped__.__aenter__()
135
+ self._self_stream = stream
136
+ return stream
126
137
 
127
138
  async def __aexit__(self, exc_type, exc_val, exc_tb): # type: ignore[misc]
128
139
  try:
129
140
  return await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb)
130
141
  finally:
142
+ try:
143
+ # Try to capture response ID from the stream's final message snapshot
144
+ stream = self._self_stream
145
+ if stream is not None:
146
+ final_msg = getattr(stream, "_MessageStream__final_message_snapshot", None)
147
+ if final_msg is not None and hasattr(final_msg, "id") and final_msg.id:
148
+ self._self_span.set_attribute(_ATTR_RESPONSE_ID, final_msg.id)
149
+ except Exception:
150
+ logger.debug("Failed to capture response ID from async stream", exc_info=True)
131
151
  try:
132
152
  if exc_type:
133
153
  self._self_span.set_status(trace_api.Status(trace_api.StatusCode.ERROR, str(exc_val)))
@@ -178,7 +198,7 @@ def _wrap_async_messages_stream() -> None:
178
198
 
179
199
 
180
200
  def _patch_response_accumulator_for_beta() -> None:
181
- """Extend _MessageResponseAccumulator.process_chunk to handle beta event types."""
201
+ """Extend _MessageResponseAccumulator.process_chunk to handle beta event types and capture response IDs."""
182
202
  try:
183
203
  from openinference.instrumentation.anthropic._stream import _MessageResponseAccumulator
184
204
  except ImportError:
@@ -196,13 +216,21 @@ def _patch_response_accumulator_for_beta() -> None:
196
216
  logger.debug("Could not import beta event types, skipping beta accumulator patch")
197
217
  return
198
218
 
219
+ try:
220
+ from anthropic.types.raw_message_start_event import RawMessageStartEvent
221
+ except ImportError:
222
+ RawMessageStartEvent = None # type: ignore[assignment,misc]
223
+
199
224
  _original_process_chunk = _MessageResponseAccumulator.process_chunk
200
225
 
201
226
  def _process_chunk_with_beta(self, chunk): # type: ignore[misc]
202
- """Handles both regular and beta event types."""
227
+ """Handles both regular and beta event types, and captures response IDs."""
203
228
  if isinstance(chunk, BetaRawMessageStartEvent):
204
229
  self._is_null = False
205
230
  self._current_message_idx += 1
231
+ # Capture response ID from beta message start
232
+ if hasattr(chunk.message, "id") and chunk.message.id:
233
+ self._values += {"response_id": chunk.message.id}
206
234
  value = {
207
235
  "messages": {
208
236
  "index": str(self._current_message_idx),
@@ -254,11 +282,84 @@ def _patch_response_accumulator_for_beta() -> None:
254
282
  }
255
283
  self._values += value
256
284
  else:
257
- # Non-beta event — delegate to the original.
285
+ # Non-beta event — capture response ID from regular RawMessageStartEvent
286
+ # before delegating to the original handler.
287
+ if RawMessageStartEvent is not None and isinstance(chunk, RawMessageStartEvent):
288
+ if hasattr(chunk.message, "id") and chunk.message.id:
289
+ self._values += {"response_id": chunk.message.id}
258
290
  return _original_process_chunk(self, chunk)
259
291
 
260
292
  _MessageResponseAccumulator.process_chunk = _process_chunk_with_beta # type: ignore[method-assign]
261
- logger.debug("Patched _MessageResponseAccumulator.process_chunk for beta event types")
293
+ logger.debug("Patched _MessageResponseAccumulator.process_chunk for beta event types and response IDs")
294
+
295
+
296
+ def _patch_response_id_extraction() -> None:
297
+ """Patch openinference to extract response.id from non-streaming Anthropic messages."""
298
+ try:
299
+ from openinference.instrumentation.anthropic import _wrappers
300
+ except ImportError:
301
+ logger.debug("Could not import openinference anthropic _wrappers, skipping response ID patch")
302
+ return
303
+
304
+ _original = _wrappers._get_llm_model_name_from_response
305
+ _response_id_originals["_get_llm_model_name_from_response"] = _original
306
+
307
+ def _patched(message): # type: ignore[misc]
308
+ yield from _original(message)
309
+ if response_id := getattr(message, "id", None):
310
+ yield _ATTR_RESPONSE_ID, response_id
311
+
312
+ _wrappers._get_llm_model_name_from_response = _patched # type: ignore[attr-defined]
313
+ logger.debug("Patched _get_llm_model_name_from_response to also yield response ID")
314
+
315
+
316
+ def _patch_streaming_response_id_extraction() -> None:
317
+ """Patch openinference _MessageResponseExtractor to yield response ID from accumulated data."""
318
+ try:
319
+ from openinference.instrumentation.anthropic._stream import _MessageResponseExtractor
320
+ except ImportError:
321
+ logger.debug("Could not import _MessageResponseExtractor, skipping streaming response ID patch")
322
+ return
323
+
324
+ _original_get_extra = _MessageResponseExtractor.get_extra_attributes
325
+ _response_id_originals["get_extra_attributes"] = _original_get_extra
326
+
327
+ def _get_extra_with_response_id(self): # type: ignore[misc]
328
+ yield from _original_get_extra(self)
329
+ try:
330
+ result = self._response_accumulator._result()
331
+ if result and (response_id := result.get("response_id")):
332
+ yield _ATTR_RESPONSE_ID, response_id
333
+ except Exception:
334
+ pass
335
+
336
+ _MessageResponseExtractor.get_extra_attributes = _get_extra_with_response_id # type: ignore[method-assign]
337
+ logger.debug("Patched _MessageResponseExtractor.get_extra_attributes for response IDs")
338
+
339
+
340
+ def _uninstrument_response_id_patches() -> None:
341
+ """Restore original methods patched by _patch_response_id_extraction / _patch_streaming_response_id_extraction."""
342
+ if "_get_llm_model_name_from_response" in _response_id_originals:
343
+ try:
344
+ from openinference.instrumentation.anthropic import _wrappers
345
+
346
+ _wrappers._get_llm_model_name_from_response = _response_id_originals.pop( # type: ignore[attr-defined]
347
+ "_get_llm_model_name_from_response"
348
+ )
349
+ except Exception:
350
+ pass
351
+
352
+ if "get_extra_attributes" in _response_id_originals:
353
+ try:
354
+ from openinference.instrumentation.anthropic._stream import _MessageResponseExtractor
355
+
356
+ _MessageResponseExtractor.get_extra_attributes = _response_id_originals.pop( # type: ignore[method-assign]
357
+ "get_extra_attributes"
358
+ )
359
+ except Exception:
360
+ pass
361
+
362
+ _response_id_originals.clear()
262
363
 
263
364
 
264
365
  # ---------------------------------------------------------------------------
@@ -249,14 +249,22 @@ def _instrument_langchain() -> None:
249
249
  def _instrument_google_genai() -> None:
250
250
  """
251
251
  Instrument Google GenAI using openinference-instrumentation-google-genai.
252
+
253
+ Applies patches on top of the base instrumentor to capture response IDs — see
254
+ ``paid.tracing.gemini_patches`` for full details.
252
255
  """
253
256
  if not GOOGLE_GENAI_AVAILABLE:
254
257
  logger.warning("Google GenAI instrumentation library not available, skipping instrumentation")
255
258
  return
256
259
 
260
+ from .gemini_patches import instrument_google_genai
261
+
257
262
  logger.debug("[paid:autoinstrument] Instrumenting google-genai with GoogleGenAIInstrumentor, provider=%s",
258
263
  type(tracing.paid_tracer_provider).__name__)
259
264
  GoogleGenAIInstrumentor().instrument(tracer_provider=tracing.paid_tracer_provider)
265
+
266
+ instrument_google_genai()
267
+
260
268
  _initialized_instrumentors.append("google-genai")
261
269
  logger.info("Google GenAI auto-instrumentation enabled")
262
270
 
@@ -0,0 +1,15 @@
1
+ """Patches for openinference-instrumentation-google-genai.
2
+
3
+ Adds response ID extraction (gen_ai.response.id) to both non-streaming
4
+ and streaming Google GenAI spans.
5
+ """
6
+
7
+ from .patches import (
8
+ instrument_google_genai,
9
+ uninstrument_google_genai,
10
+ )
11
+
12
+ __all__ = [
13
+ "instrument_google_genai",
14
+ "uninstrument_google_genai",
15
+ ]
@@ -0,0 +1,101 @@
1
+ """Monkey-patches for openinference-instrumentation-google-genai.
2
+
3
+ Adds response ID extraction to both non-streaming and streaming spans.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from paid.logger import logger
9
+
10
+ _ATTR_RESPONSE_ID = "gen_ai.response.id"
11
+
12
+ # Store originals for uninstrumentation
13
+ _originals: dict = {}
14
+
15
+
16
+ def instrument_google_genai() -> None:
17
+ """Apply all Google GenAI patches. Call after GoogleGenAIInstrumentor().instrument()."""
18
+ _patch_response_id_extraction()
19
+ _patch_streaming_response_id_extraction()
20
+
21
+
22
+ def uninstrument_google_genai() -> None:
23
+ """Restore original Google GenAI methods."""
24
+ if "get_attributes_from_generate_content" in _originals:
25
+ try:
26
+ from openinference.instrumentation.google_genai import (
27
+ _response_attributes_extractor as mod,
28
+ )
29
+
30
+ mod._ResponseAttributesExtractor._get_attributes_from_generate_content = _originals.pop( # type: ignore[method-assign]
31
+ "get_attributes_from_generate_content"
32
+ )
33
+ except Exception:
34
+ pass
35
+
36
+ if "stream_get_extra_attributes" in _originals:
37
+ try:
38
+ from openinference.instrumentation.google_genai import _stream as stream_mod
39
+
40
+ stream_mod._ResponseExtractor.get_extra_attributes = _originals.pop( # type: ignore[method-assign]
41
+ "stream_get_extra_attributes"
42
+ )
43
+ except Exception:
44
+ pass
45
+
46
+ _originals.clear()
47
+
48
+
49
+ def _patch_response_id_extraction() -> None:
50
+ """Patch openinference to extract response_id from non-streaming Gemini responses."""
51
+ try:
52
+ from openinference.instrumentation.google_genai import (
53
+ _response_attributes_extractor as mod,
54
+ )
55
+ except ImportError:
56
+ logger.debug(
57
+ "Could not import openinference google_genai _response_attributes_extractor, "
58
+ "skipping response ID patch"
59
+ )
60
+ return
61
+
62
+ _original = mod._ResponseAttributesExtractor._get_attributes_from_generate_content
63
+ _originals["get_attributes_from_generate_content"] = _original
64
+
65
+ def _patched(self, response, request_parameters): # type: ignore[misc]
66
+ yield from _original(self, response=response, request_parameters=request_parameters)
67
+ if response_id := getattr(response, "response_id", None):
68
+ yield _ATTR_RESPONSE_ID, response_id
69
+
70
+ mod._ResponseAttributesExtractor._get_attributes_from_generate_content = _patched # type: ignore[method-assign]
71
+ logger.debug(
72
+ "Patched _ResponseAttributesExtractor._get_attributes_from_generate_content "
73
+ "to also yield response ID"
74
+ )
75
+
76
+
77
+ def _patch_streaming_response_id_extraction() -> None:
78
+ """Patch openinference _ResponseExtractor to yield response_id from accumulated streaming data."""
79
+ try:
80
+ from openinference.instrumentation.google_genai import _stream as stream_mod
81
+ except ImportError:
82
+ logger.debug(
83
+ "Could not import openinference google_genai _stream, "
84
+ "skipping streaming response ID patch"
85
+ )
86
+ return
87
+
88
+ _original_get_extra = stream_mod._ResponseExtractor.get_extra_attributes
89
+ _originals["stream_get_extra_attributes"] = _original_get_extra
90
+
91
+ def _get_extra_with_response_id(self): # type: ignore[misc]
92
+ yield from _original_get_extra(self)
93
+ try:
94
+ result = self._response_accumulator._result()
95
+ if result and (response_id := result.get("response_id")):
96
+ yield _ATTR_RESPONSE_ID, response_id
97
+ except Exception:
98
+ pass
99
+
100
+ stream_mod._ResponseExtractor.get_extra_attributes = _get_extra_with_response_id # type: ignore[method-assign]
101
+ logger.debug("Patched _ResponseExtractor.get_extra_attributes for response IDs")
@@ -19,6 +19,7 @@ from opentelemetry.trace import NonRecordingSpan, NoOpTracerProvider, SpanContex
19
19
  from opentelemetry.util.types import Attributes
20
20
 
21
21
  from paid.logger import logger
22
+ from paid.version import __version__ as _paid_version
22
23
 
23
24
  DEFAULT_COLLECTOR_ENDPOINT = (
24
25
  os.environ.get("PAID_OTEL_COLLECTOR_ENDPOINT") or "https://collector.agentpaid.io:4318/v1/traces"
@@ -223,6 +224,9 @@ class PaidSpanProcessor(SpanProcessor):
223
224
  if agent_id:
224
225
  span.set_attribute("external_agent_id", agent_id)
225
226
 
227
+ # Always stamp the SDK version so the backend can identify the source
228
+ span.set_attribute("paid.sdk.version", f"python-{_paid_version}")
229
+
226
230
  logger.debug(
227
231
  "[paid:span] on_start: name=%s, customer_id=%s, agent_id=%s",
228
232
  span.name, customer_id, agent_id,
@@ -445,13 +449,11 @@ def initialize_tracing(
445
449
 
446
450
  set_token(api_key)
447
451
 
448
- resource = Resource(attributes={"api.key": api_key})
449
452
  # Create isolated tracer provider for Paid - don't use or modify global provider
450
453
  # Pass explicit sampler and span_limits to avoid inheriting from OTEL env vars
451
454
  # (OTEL_TRACES_SAMPLER=always_off or OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=1
452
455
  # set by the client app would silently break this)
453
456
  paid_tracer_provider = TracerProvider(
454
- resource=resource,
455
457
  sampler=ALWAYS_ON,
456
458
  span_limits=SpanLimits(
457
459
  max_span_attributes=128,
@@ -482,7 +484,7 @@ def initialize_tracing(
482
484
  # client OTEL env vars (e.g. OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT)
483
485
  otlp_exporter = OTLPSpanExporter(
484
486
  endpoint=collector_endpoint,
485
- headers={"_paid": "1"}, # Non-empty to prevent env var OTEL_EXPORTER_OTLP_HEADERS leak (empty dict is falsy)
487
+ headers={"Authorization": f"Bearer {api_key}"},
486
488
  timeout=10, # Explicit timeout to prevent env var OTEL_EXPORTER_OTLP_TIMEOUT override
487
489
  )
488
490
 
@@ -0,0 +1,6 @@
1
+ from importlib import metadata
2
+
3
+ try:
4
+ __version__ = metadata.version("paid-python")
5
+ except metadata.PackageNotFoundError:
6
+ __version__ = "unknown"
@@ -1,3 +0,0 @@
1
- from importlib import metadata
2
-
3
- __version__ = metadata.version("paid-python")
File without changes
File without changes