anthropic.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087
  1. import sys
  2. import json
  3. from collections.abc import Iterable
  4. from functools import wraps
  5. from typing import TYPE_CHECKING
  6. import sentry_sdk
  7. from sentry_sdk.ai.monitoring import record_token_usage
  8. from sentry_sdk.ai.utils import (
  9. GEN_AI_ALLOWED_MESSAGE_ROLES,
  10. set_data_normalized,
  11. normalize_message_roles,
  12. truncate_and_annotate_messages,
  13. get_start_span_function,
  14. transform_anthropic_content_part,
  15. )
  16. from sentry_sdk.consts import OP, SPANDATA
  17. from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
  18. from sentry_sdk.scope import should_send_default_pii
  19. from sentry_sdk.utils import (
  20. capture_internal_exceptions,
  21. event_from_exception,
  22. package_version,
  23. safe_serialize,
  24. reraise,
  25. )
  26. try:
  27. try:
  28. from anthropic import NotGiven
  29. except ImportError:
  30. NotGiven = None
  31. try:
  32. from anthropic import Omit
  33. except ImportError:
  34. Omit = None
  35. from anthropic import Stream, AsyncStream
  36. from anthropic.resources import AsyncMessages, Messages
  37. from anthropic.lib.streaming import (
  38. MessageStreamManager,
  39. MessageStream,
  40. AsyncMessageStreamManager,
  41. AsyncMessageStream,
  42. )
  43. from anthropic.types import (
  44. MessageStartEvent,
  45. MessageDeltaEvent,
  46. MessageStopEvent,
  47. ContentBlockStartEvent,
  48. ContentBlockDeltaEvent,
  49. ContentBlockStopEvent,
  50. )
  51. if TYPE_CHECKING:
  52. from anthropic.types import MessageStreamEvent, TextBlockParam
  53. except ImportError:
  54. raise DidNotEnable("Anthropic not installed")
  55. if TYPE_CHECKING:
  56. from typing import (
  57. Any,
  58. AsyncIterator,
  59. Iterator,
  60. Optional,
  61. Union,
  62. Callable,
  63. Awaitable,
  64. )
  65. from sentry_sdk.tracing import Span
  66. from sentry_sdk._types import TextPart
  67. from anthropic.types import (
  68. RawMessageStreamEvent,
  69. MessageParam,
  70. ModelParam,
  71. TextBlockParam,
  72. ToolUnionParam,
  73. )
  74. class _RecordedUsage:
  75. output_tokens: int = 0
  76. input_tokens: int = 0
  77. cache_write_input_tokens: "Optional[int]" = 0
  78. cache_read_input_tokens: "Optional[int]" = 0
  79. class _StreamSpanContext:
  80. """
  81. Sets accumulated data on the stream's span and finishes the span on exit.
  82. Is a no-op if the stream has no span set, i.e., when the span has already been finished.
  83. """
  84. def __init__(
  85. self,
  86. stream: "Union[Stream, MessageStream, AsyncStream, AsyncMessageStream]",
  87. # Flag to avoid unreachable branches when the stream state is known to be initialized (stream._model, etc. are set).
  88. guaranteed_streaming_state: bool = False,
  89. ) -> None:
  90. self._stream = stream
  91. self._guaranteed_streaming_state = guaranteed_streaming_state
  92. def __enter__(self) -> "_StreamSpanContext":
  93. return self
  94. def __exit__(
  95. self,
  96. exc_type: "Optional[type[BaseException]]",
  97. exc_val: "Optional[BaseException]",
  98. exc_tb: "Optional[Any]",
  99. ) -> None:
  100. with capture_internal_exceptions():
  101. if not hasattr(self._stream, "_span"):
  102. return
  103. if not self._guaranteed_streaming_state and not hasattr(
  104. self._stream, "_model"
  105. ):
  106. self._stream._span.__exit__(exc_type, exc_val, exc_tb)
  107. del self._stream._span
  108. return
  109. _set_streaming_output_data(
  110. span=self._stream._span,
  111. integration=self._stream._integration,
  112. model=self._stream._model,
  113. usage=self._stream._usage,
  114. content_blocks=self._stream._content_blocks,
  115. response_id=self._stream._response_id,
  116. finish_reason=self._stream._finish_reason,
  117. )
  118. self._stream._span.__exit__(exc_type, exc_val, exc_tb)
  119. del self._stream._span
  120. class AnthropicIntegration(Integration):
  121. identifier = "anthropic"
  122. origin = f"auto.ai.{identifier}"
  123. def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None:
  124. self.include_prompts = include_prompts
  125. @staticmethod
  126. def setup_once() -> None:
  127. version = package_version("anthropic")
  128. _check_minimum_version(AnthropicIntegration, version)
  129. """
  130. client.messages.create(stream=True) can return an instance of the Stream class, which implements the iterator protocol.
  131. Analogously, the function can return an AsyncStream, which implements the asynchronous iterator protocol.
  132. The private _iterator variable and the close() method are patched. During iteration over the _iterator generator,
  133. information from intercepted events is accumulated and used to populate output attributes on the AI Client Span.
  134. The span can be finished in two places:
  135. - When the user exits the context manager or directly calls close(), the patched close() finishes the span.
  136. - When iteration ends, the finally block in the _iterator wrapper finishes the span.
  137. Both paths may run. For example, the context manager exit can follow iterator exhaustion.
  138. """
  139. Messages.create = _wrap_message_create(Messages.create)
  140. Stream.close = _wrap_close(Stream.close)
  141. AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create)
  142. AsyncStream.close = _wrap_async_close(AsyncStream.close)
  143. """
  144. client.messages.stream() patches are analogous to the patches for client.messages.create(stream=True) described above.
  145. """
  146. Messages.stream = _wrap_message_stream(Messages.stream)
  147. MessageStreamManager.__enter__ = _wrap_message_stream_manager_enter(
  148. MessageStreamManager.__enter__
  149. )
  150. # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a
  151. # MessageStream inherits from Stream, so patching Stream is sufficient on these versions.
  152. if not issubclass(MessageStream, Stream):
  153. MessageStream.close = _wrap_close(MessageStream.close)
  154. AsyncMessages.stream = _wrap_async_message_stream(AsyncMessages.stream)
  155. AsyncMessageStreamManager.__aenter__ = (
  156. _wrap_async_message_stream_manager_aenter(
  157. AsyncMessageStreamManager.__aenter__
  158. )
  159. )
  160. # Before https://github.com/anthropics/anthropic-sdk-python/commit/b1a1c0354a9aca450a7d512fdbdeb59c0ead688a
  161. # AsyncMessageStream inherits from AsyncStream, so patching Stream is sufficient on these versions.
  162. if not issubclass(AsyncMessageStream, AsyncStream):
  163. AsyncMessageStream.close = _wrap_async_close(AsyncMessageStream.close)
  164. def _capture_exception(exc: "Any") -> None:
  165. event, hint = event_from_exception(
  166. exc,
  167. client_options=sentry_sdk.get_client().options,
  168. mechanism={"type": "anthropic", "handled": False},
  169. )
  170. sentry_sdk.capture_event(event, hint=hint)
  171. def _get_token_usage(result: "Messages") -> "tuple[int, int, int, int]":
  172. """
  173. Get token usage from the Anthropic response.
  174. Returns: (input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens)
  175. """
  176. input_tokens = 0
  177. output_tokens = 0
  178. cache_read_input_tokens = 0
  179. cache_write_input_tokens = 0
  180. if hasattr(result, "usage"):
  181. usage = result.usage
  182. if hasattr(usage, "input_tokens") and isinstance(usage.input_tokens, int):
  183. input_tokens = usage.input_tokens
  184. if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int):
  185. output_tokens = usage.output_tokens
  186. if hasattr(usage, "cache_read_input_tokens") and isinstance(
  187. usage.cache_read_input_tokens, int
  188. ):
  189. cache_read_input_tokens = usage.cache_read_input_tokens
  190. if hasattr(usage, "cache_creation_input_tokens") and isinstance(
  191. usage.cache_creation_input_tokens, int
  192. ):
  193. cache_write_input_tokens = usage.cache_creation_input_tokens
  194. # Anthropic's input_tokens excludes cached/cache_write tokens.
  195. # Normalize to total input tokens so downstream cost calculations
  196. # (input_tokens - cached) don't produce negative values.
  197. input_tokens += cache_read_input_tokens + cache_write_input_tokens
  198. return (
  199. input_tokens,
  200. output_tokens,
  201. cache_read_input_tokens,
  202. cache_write_input_tokens,
  203. )
  204. def _collect_ai_data(
  205. event: "MessageStreamEvent",
  206. model: "str | None",
  207. usage: "_RecordedUsage",
  208. content_blocks: "list[str]",
  209. response_id: "str | None" = None,
  210. finish_reason: "str | None" = None,
  211. ) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]":
  212. """
  213. Collect model information, token usage, and collect content blocks from the AI streaming response.
  214. """
  215. with capture_internal_exceptions():
  216. if hasattr(event, "type"):
  217. if event.type == "content_block_start":
  218. pass
  219. elif event.type == "content_block_delta":
  220. if hasattr(event.delta, "text"):
  221. content_blocks.append(event.delta.text)
  222. elif hasattr(event.delta, "partial_json"):
  223. content_blocks.append(event.delta.partial_json)
  224. elif event.type == "content_block_stop":
  225. pass
  226. # Token counting logic mirrors anthropic SDK, which also extracts already accumulated tokens.
  227. # https://github.com/anthropics/anthropic-sdk-python/blob/9c485f6966e10ae0ea9eabb3a921d2ea8145a25b/src/anthropic/lib/streaming/_messages.py#L433-L518
  228. if event.type == "message_start":
  229. model = event.message.model or model
  230. response_id = event.message.id
  231. incoming_usage = event.message.usage
  232. usage.output_tokens = incoming_usage.output_tokens
  233. usage.input_tokens = incoming_usage.input_tokens
  234. usage.cache_write_input_tokens = getattr(
  235. incoming_usage, "cache_creation_input_tokens", None
  236. )
  237. usage.cache_read_input_tokens = getattr(
  238. incoming_usage, "cache_read_input_tokens", None
  239. )
  240. return (
  241. model,
  242. usage,
  243. content_blocks,
  244. response_id,
  245. finish_reason,
  246. )
  247. # Counterintuitive, but message_delta contains cumulative token counts :)
  248. if event.type == "message_delta":
  249. usage.output_tokens = event.usage.output_tokens
  250. # Update other usage fields if they exist in the event
  251. input_tokens = getattr(event.usage, "input_tokens", None)
  252. if input_tokens is not None:
  253. usage.input_tokens = input_tokens
  254. cache_creation_input_tokens = getattr(
  255. event.usage, "cache_creation_input_tokens", None
  256. )
  257. if cache_creation_input_tokens is not None:
  258. usage.cache_write_input_tokens = cache_creation_input_tokens
  259. cache_read_input_tokens = getattr(
  260. event.usage, "cache_read_input_tokens", None
  261. )
  262. if cache_read_input_tokens is not None:
  263. usage.cache_read_input_tokens = cache_read_input_tokens
  264. # TODO: Record event.usage.server_tool_use
  265. if event.delta.stop_reason is not None:
  266. finish_reason = event.delta.stop_reason
  267. return (model, usage, content_blocks, response_id, finish_reason)
  268. return (
  269. model,
  270. usage,
  271. content_blocks,
  272. response_id,
  273. finish_reason,
  274. )
  275. def _transform_anthropic_content_block(
  276. content_block: "dict[str, Any]",
  277. ) -> "dict[str, Any]":
  278. """
  279. Transform an Anthropic content block using the Anthropic-specific transformer,
  280. with special handling for Anthropic's text-type documents.
  281. """
  282. # Handle Anthropic's text-type documents specially (not covered by shared function)
  283. if content_block.get("type") == "document":
  284. source = content_block.get("source")
  285. if isinstance(source, dict) and source.get("type") == "text":
  286. return {
  287. "type": "text",
  288. "text": source.get("data", ""),
  289. }
  290. # Use Anthropic-specific transformation
  291. result = transform_anthropic_content_part(content_block)
  292. return result if result is not None else content_block
  293. def _transform_system_instructions(
  294. system_instructions: "Union[str, Iterable[TextBlockParam]]",
  295. ) -> "list[TextPart]":
  296. if isinstance(system_instructions, str):
  297. return [
  298. {
  299. "type": "text",
  300. "content": system_instructions,
  301. }
  302. ]
  303. return [
  304. {
  305. "type": "text",
  306. "content": instruction["text"],
  307. }
  308. for instruction in system_instructions
  309. if isinstance(instruction, dict) and "text" in instruction
  310. ]
  311. def _set_common_input_data(
  312. span: "Span",
  313. integration: "AnthropicIntegration",
  314. max_tokens: "int",
  315. messages: "Iterable[MessageParam]",
  316. model: "ModelParam",
  317. system: "Optional[Union[str, Iterable[TextBlockParam]]]",
  318. temperature: "Optional[float]",
  319. top_k: "Optional[int]",
  320. top_p: "Optional[float]",
  321. tools: "Optional[Iterable[ToolUnionParam]]",
  322. ) -> None:
  323. """
  324. Set input data for the span based on the provided keyword arguments for the anthropic message creation.
  325. """
  326. span.set_data(SPANDATA.GEN_AI_SYSTEM, "anthropic")
  327. span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
  328. if (
  329. messages is not None
  330. and len(messages) > 0 # type: ignore
  331. and should_send_default_pii()
  332. and integration.include_prompts
  333. ):
  334. if isinstance(system, str) or isinstance(system, Iterable):
  335. span.set_data(
  336. SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
  337. json.dumps(_transform_system_instructions(system)),
  338. )
  339. normalized_messages = []
  340. for message in messages:
  341. if (
  342. message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER
  343. and "content" in message
  344. and isinstance(message["content"], (list, tuple))
  345. ):
  346. transformed_content = []
  347. for item in message["content"]:
  348. # Skip tool_result items - they can contain images/documents
  349. # with nested structures that are difficult to redact properly
  350. if isinstance(item, dict) and item.get("type") == "tool_result":
  351. continue
  352. # Transform content blocks (images, documents, etc.)
  353. transformed_content.append(
  354. _transform_anthropic_content_block(item)
  355. if isinstance(item, dict)
  356. else item
  357. )
  358. # If there are non-tool-result items, add them as a message
  359. if transformed_content:
  360. normalized_messages.append(
  361. {
  362. "role": message.get("role"),
  363. "content": transformed_content,
  364. }
  365. )
  366. else:
  367. # Transform content for non-list messages or assistant messages
  368. transformed_message = message.copy()
  369. if "content" in transformed_message:
  370. content = transformed_message["content"]
  371. if isinstance(content, (list, tuple)):
  372. transformed_message["content"] = [
  373. _transform_anthropic_content_block(item)
  374. if isinstance(item, dict)
  375. else item
  376. for item in content
  377. ]
  378. normalized_messages.append(transformed_message)
  379. role_normalized_messages = normalize_message_roles(normalized_messages)
  380. scope = sentry_sdk.get_current_scope()
  381. messages_data = truncate_and_annotate_messages(
  382. role_normalized_messages, span, scope
  383. )
  384. if messages_data is not None:
  385. set_data_normalized(
  386. span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
  387. )
  388. if max_tokens is not None and _is_given(max_tokens):
  389. span.set_data(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens)
  390. if model is not None and _is_given(model):
  391. span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model)
  392. if temperature is not None and _is_given(temperature):
  393. span.set_data(SPANDATA.GEN_AI_REQUEST_TEMPERATURE, temperature)
  394. if top_k is not None and _is_given(top_k):
  395. span.set_data(SPANDATA.GEN_AI_REQUEST_TOP_K, top_k)
  396. if top_p is not None and _is_given(top_p):
  397. span.set_data(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p)
  398. if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore
  399. span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools))
  400. def _set_create_input_data(
  401. span: "Span", kwargs: "dict[str, Any]", integration: "AnthropicIntegration"
  402. ) -> None:
  403. """
  404. Set input data for the span based on the provided keyword arguments for the anthropic message creation.
  405. """
  406. span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False))
  407. _set_common_input_data(
  408. span=span,
  409. integration=integration,
  410. max_tokens=kwargs.get("max_tokens"), # type: ignore
  411. messages=kwargs.get("messages"), # type: ignore
  412. model=kwargs.get("model"),
  413. system=kwargs.get("system"),
  414. temperature=kwargs.get("temperature"),
  415. top_k=kwargs.get("top_k"),
  416. top_p=kwargs.get("top_p"),
  417. tools=kwargs.get("tools"),
  418. )
  419. def _wrap_synchronous_message_iterator(
  420. stream: "Union[Stream, MessageStream]",
  421. iterator: "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]",
  422. ) -> "Iterator[Union[RawMessageStreamEvent, MessageStreamEvent]]":
  423. """
  424. Sets information received while iterating the response stream on the AI Client Span.
  425. Responsible for closing the AI Client Span unless the span has already been closed in the close() patch.
  426. """
  427. with _StreamSpanContext(stream, guaranteed_streaming_state=True):
  428. for event in iterator:
  429. # Message and content types are aliases for corresponding Raw* types, introduced in
  430. # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a
  431. if not isinstance(
  432. event,
  433. (
  434. MessageStartEvent,
  435. MessageDeltaEvent,
  436. MessageStopEvent,
  437. ContentBlockStartEvent,
  438. ContentBlockDeltaEvent,
  439. ContentBlockStopEvent,
  440. ),
  441. ):
  442. yield event
  443. continue
  444. _accumulate_event_data(stream, event)
  445. yield event
  446. async def _wrap_asynchronous_message_iterator(
  447. stream: "Union[AsyncStream, AsyncMessageStream]",
  448. iterator: "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]",
  449. ) -> "AsyncIterator[Union[RawMessageStreamEvent, MessageStreamEvent]]":
  450. """
  451. Sets information received while iterating the response stream on the AI Client Span.
  452. Responsible for closing the AI Client Span unless the span has already been closed in the close() patch.
  453. """
  454. with _StreamSpanContext(stream, guaranteed_streaming_state=True):
  455. async for event in iterator:
  456. # Message and content types are aliases for corresponding Raw* types, introduced in
  457. # https://github.com/anthropics/anthropic-sdk-python/commit/bc9d11cd2addec6976c46db10b7c89a8c276101a
  458. if not isinstance(
  459. event,
  460. (
  461. MessageStartEvent,
  462. MessageDeltaEvent,
  463. MessageStopEvent,
  464. ContentBlockStartEvent,
  465. ContentBlockDeltaEvent,
  466. ContentBlockStopEvent,
  467. ),
  468. ):
  469. yield event
  470. continue
  471. _accumulate_event_data(stream, event)
  472. yield event
  473. def _set_output_data(
  474. span: "Span",
  475. integration: "AnthropicIntegration",
  476. model: "str | None",
  477. input_tokens: "int | None",
  478. output_tokens: "int | None",
  479. cache_read_input_tokens: "int | None",
  480. cache_write_input_tokens: "int | None",
  481. content_blocks: "list[Any]",
  482. response_id: "str | None" = None,
  483. finish_reason: "str | None" = None,
  484. ) -> None:
  485. """
  486. Set output data for the span based on the AI response."""
  487. span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, model)
  488. if response_id is not None:
  489. span.set_data(SPANDATA.GEN_AI_RESPONSE_ID, response_id)
  490. if finish_reason is not None:
  491. span.set_data(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason])
  492. if should_send_default_pii() and integration.include_prompts:
  493. output_messages: "dict[str, list[Any]]" = {
  494. "response": [],
  495. "tool": [],
  496. }
  497. for output in content_blocks:
  498. if output["type"] == "text":
  499. output_messages["response"].append(output["text"])
  500. elif output["type"] == "tool_use":
  501. output_messages["tool"].append(output)
  502. if len(output_messages["tool"]) > 0:
  503. set_data_normalized(
  504. span,
  505. SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS,
  506. output_messages["tool"],
  507. unpack=False,
  508. )
  509. if len(output_messages["response"]) > 0:
  510. set_data_normalized(
  511. span, SPANDATA.GEN_AI_RESPONSE_TEXT, output_messages["response"]
  512. )
  513. record_token_usage(
  514. span,
  515. input_tokens=input_tokens,
  516. output_tokens=output_tokens,
  517. input_tokens_cached=cache_read_input_tokens,
  518. input_tokens_cache_write=cache_write_input_tokens,
  519. )
  520. def _sentry_patched_create_sync(f: "Any", *args: "Any", **kwargs: "Any") -> "Any":
  521. """
  522. Creates and manages an AI Client Span for both non-streaming and streaming calls.
  523. """
  524. integration = kwargs.pop("integration")
  525. if integration is None:
  526. return f(*args, **kwargs)
  527. if "messages" not in kwargs:
  528. return f(*args, **kwargs)
  529. try:
  530. iter(kwargs["messages"])
  531. except TypeError:
  532. return f(*args, **kwargs)
  533. model = kwargs.get("model", "")
  534. span = get_start_span_function()(
  535. op=OP.GEN_AI_CHAT,
  536. name=f"chat {model}".strip(),
  537. origin=AnthropicIntegration.origin,
  538. )
  539. span.__enter__()
  540. _set_create_input_data(span, kwargs, integration)
  541. try:
  542. result = f(*args, **kwargs)
  543. except Exception as exc:
  544. exc_info = sys.exc_info()
  545. with capture_internal_exceptions():
  546. _capture_exception(exc)
  547. span.__exit__(*exc_info)
  548. reraise(*exc_info)
  549. if isinstance(result, Stream):
  550. result._span = span
  551. result._integration = integration
  552. _initialize_data_accumulation_state(result)
  553. result._iterator = _wrap_synchronous_message_iterator(
  554. result,
  555. result._iterator,
  556. )
  557. return result
  558. with capture_internal_exceptions():
  559. if hasattr(result, "content"):
  560. (
  561. input_tokens,
  562. output_tokens,
  563. cache_read_input_tokens,
  564. cache_write_input_tokens,
  565. ) = _get_token_usage(result)
  566. content_blocks = []
  567. for content_block in result.content:
  568. if hasattr(content_block, "to_dict"):
  569. content_blocks.append(content_block.to_dict())
  570. elif hasattr(content_block, "model_dump"):
  571. content_blocks.append(content_block.model_dump())
  572. elif hasattr(content_block, "text"):
  573. content_blocks.append({"type": "text", "text": content_block.text})
  574. _set_output_data(
  575. span=span,
  576. integration=integration,
  577. model=getattr(result, "model", None),
  578. input_tokens=input_tokens,
  579. output_tokens=output_tokens,
  580. cache_read_input_tokens=cache_read_input_tokens,
  581. cache_write_input_tokens=cache_write_input_tokens,
  582. content_blocks=content_blocks,
  583. response_id=getattr(result, "id", None),
  584. finish_reason=getattr(result, "stop_reason", None),
  585. )
  586. span.__exit__(None, None, None)
  587. else:
  588. span.set_data("unknown_response", True)
  589. span.__exit__(None, None, None)
  590. return result
  591. async def _sentry_patched_create_async(
  592. f: "Any", *args: "Any", **kwargs: "Any"
  593. ) -> "Any":
  594. """
  595. Creates and manages an AI Client Span for both non-streaming and streaming calls.
  596. """
  597. integration = kwargs.pop("integration")
  598. if integration is None:
  599. return await f(*args, **kwargs)
  600. if "messages" not in kwargs:
  601. return await f(*args, **kwargs)
  602. try:
  603. iter(kwargs["messages"])
  604. except TypeError:
  605. return await f(*args, **kwargs)
  606. model = kwargs.get("model", "")
  607. span = get_start_span_function()(
  608. op=OP.GEN_AI_CHAT,
  609. name=f"chat {model}".strip(),
  610. origin=AnthropicIntegration.origin,
  611. )
  612. span.__enter__()
  613. _set_create_input_data(span, kwargs, integration)
  614. try:
  615. result = await f(*args, **kwargs)
  616. except Exception as exc:
  617. exc_info = sys.exc_info()
  618. with capture_internal_exceptions():
  619. _capture_exception(exc)
  620. span.__exit__(*exc_info)
  621. reraise(*exc_info)
  622. if isinstance(result, AsyncStream):
  623. result._span = span
  624. result._integration = integration
  625. _initialize_data_accumulation_state(result)
  626. result._iterator = _wrap_asynchronous_message_iterator(
  627. result,
  628. result._iterator,
  629. )
  630. return result
  631. with capture_internal_exceptions():
  632. if hasattr(result, "content"):
  633. (
  634. input_tokens,
  635. output_tokens,
  636. cache_read_input_tokens,
  637. cache_write_input_tokens,
  638. ) = _get_token_usage(result)
  639. content_blocks = []
  640. for content_block in result.content:
  641. if hasattr(content_block, "to_dict"):
  642. content_blocks.append(content_block.to_dict())
  643. elif hasattr(content_block, "model_dump"):
  644. content_blocks.append(content_block.model_dump())
  645. elif hasattr(content_block, "text"):
  646. content_blocks.append({"type": "text", "text": content_block.text})
  647. _set_output_data(
  648. span=span,
  649. integration=integration,
  650. model=getattr(result, "model", None),
  651. input_tokens=input_tokens,
  652. output_tokens=output_tokens,
  653. cache_read_input_tokens=cache_read_input_tokens,
  654. cache_write_input_tokens=cache_write_input_tokens,
  655. content_blocks=content_blocks,
  656. response_id=getattr(result, "id", None),
  657. finish_reason=getattr(result, "stop_reason", None),
  658. )
  659. span.__exit__(None, None, None)
  660. else:
  661. span.set_data("unknown_response", True)
  662. span.__exit__(None, None, None)
  663. return result
  664. def _wrap_message_create(f: "Any") -> "Any":
  665. @wraps(f)
  666. def _sentry_wrapped_create_sync(*args: "Any", **kwargs: "Any") -> "Any":
  667. integration = sentry_sdk.get_client().get_integration(AnthropicIntegration)
  668. kwargs["integration"] = integration
  669. return _sentry_patched_create_sync(f, *args, **kwargs)
  670. return _sentry_wrapped_create_sync
  671. def _initialize_data_accumulation_state(stream: "Union[Stream, MessageStream]") -> None:
  672. """
  673. Initialize fields for accumulating output on the Stream instance.
  674. """
  675. if not hasattr(stream, "_model"):
  676. stream._model = None
  677. stream._usage = _RecordedUsage()
  678. stream._content_blocks = []
  679. stream._response_id = None
  680. stream._finish_reason = None
  681. def _accumulate_event_data(
  682. stream: "Union[Stream, MessageStream]",
  683. event: "Union[RawMessageStreamEvent, MessageStreamEvent]",
  684. ) -> None:
  685. """
  686. Update accumulated output from a single stream event.
  687. """
  688. (model, usage, content_blocks, response_id, finish_reason) = _collect_ai_data(
  689. event,
  690. stream._model,
  691. stream._usage,
  692. stream._content_blocks,
  693. stream._response_id,
  694. stream._finish_reason,
  695. )
  696. stream._model = model
  697. stream._usage = usage
  698. stream._content_blocks = content_blocks
  699. stream._response_id = response_id
  700. stream._finish_reason = finish_reason
  701. def _set_streaming_output_data(
  702. span: "Span",
  703. integration: "AnthropicIntegration",
  704. model: "Optional[str]",
  705. usage: "_RecordedUsage",
  706. content_blocks: "list[str]",
  707. response_id: "Optional[str]",
  708. finish_reason: "Optional[str]",
  709. ) -> None:
  710. """
  711. Set output attributes on the AI Client Span.
  712. """
  713. # Anthropic's input_tokens excludes cached/cache_write tokens.
  714. # Normalize to total input tokens for correct cost calculations.
  715. total_input = (
  716. usage.input_tokens
  717. + (usage.cache_read_input_tokens or 0)
  718. + (usage.cache_write_input_tokens or 0)
  719. )
  720. _set_output_data(
  721. span=span,
  722. integration=integration,
  723. model=model,
  724. input_tokens=total_input,
  725. output_tokens=usage.output_tokens,
  726. cache_read_input_tokens=usage.cache_read_input_tokens,
  727. cache_write_input_tokens=usage.cache_write_input_tokens,
  728. content_blocks=[{"text": "".join(content_blocks), "type": "text"}],
  729. response_id=response_id,
  730. finish_reason=finish_reason,
  731. )
  732. def _wrap_close(
  733. f: "Callable[..., None]",
  734. ) -> "Callable[..., None]":
  735. """
  736. Closes the AI Client Span unless the finally block in `_wrap_synchronous_message_iterator()` runs first.
  737. """
  738. def close(self: "Union[Stream, MessageStream]") -> None:
  739. with _StreamSpanContext(self):
  740. return f(self)
  741. return close
  742. def _wrap_message_create_async(f: "Any") -> "Any":
  743. @wraps(f)
  744. async def _sentry_wrapped_create_async(*args: "Any", **kwargs: "Any") -> "Any":
  745. integration = sentry_sdk.get_client().get_integration(AnthropicIntegration)
  746. kwargs["integration"] = integration
  747. return await _sentry_patched_create_async(f, *args, **kwargs)
  748. return _sentry_wrapped_create_async
  749. def _wrap_async_close(
  750. f: "Callable[..., Awaitable[None]]",
  751. ) -> "Callable[..., Awaitable[None]]":
  752. """
  753. Closes the AI Client Span unless the finally block in `_wrap_asynchronous_message_iterator()` runs first.
  754. """
  755. async def close(self: "AsyncStream") -> None:
  756. with _StreamSpanContext(self):
  757. return await f(self)
  758. return close
  759. def _wrap_message_stream(f: "Any") -> "Any":
  760. """
  761. Attaches user-provided arguments to the returned context manager.
  762. The attributes are set on AI Client Spans in the patch for the context manager.
  763. """
  764. @wraps(f)
  765. def _sentry_patched_stream(*args: "Any", **kwargs: "Any") -> "MessageStreamManager":
  766. stream_manager = f(*args, **kwargs)
  767. stream_manager._max_tokens = kwargs.get("max_tokens")
  768. stream_manager._messages = kwargs.get("messages")
  769. stream_manager._model = kwargs.get("model")
  770. stream_manager._system = kwargs.get("system")
  771. stream_manager._temperature = kwargs.get("temperature")
  772. stream_manager._top_k = kwargs.get("top_k")
  773. stream_manager._top_p = kwargs.get("top_p")
  774. stream_manager._tools = kwargs.get("tools")
  775. return stream_manager
  776. return _sentry_patched_stream
  777. def _wrap_message_stream_manager_enter(f: "Any") -> "Any":
  778. """
  779. Creates and manages AI Client Spans.
  780. """
  781. @wraps(f)
  782. def _sentry_patched_enter(self: "MessageStreamManager") -> "MessageStream":
  783. if not hasattr(self, "_max_tokens"):
  784. return f(self)
  785. integration = sentry_sdk.get_client().get_integration(AnthropicIntegration)
  786. if integration is None:
  787. return f(self)
  788. if self._messages is None:
  789. return f(self)
  790. try:
  791. iter(self._messages)
  792. except TypeError:
  793. return f(self)
  794. span = get_start_span_function()(
  795. op=OP.GEN_AI_CHAT,
  796. name="chat" if self._model is None else f"chat {self._model}".strip(),
  797. origin=AnthropicIntegration.origin,
  798. )
  799. span.__enter__()
  800. span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
  801. _set_common_input_data(
  802. span=span,
  803. integration=integration,
  804. max_tokens=self._max_tokens,
  805. messages=self._messages,
  806. model=self._model,
  807. system=self._system,
  808. temperature=self._temperature,
  809. top_k=self._top_k,
  810. top_p=self._top_p,
  811. tools=self._tools,
  812. )
  813. try:
  814. stream = f(self)
  815. except Exception as exc:
  816. exc_info = sys.exc_info()
  817. with capture_internal_exceptions():
  818. _capture_exception(exc)
  819. span.__exit__(*exc_info)
  820. reraise(*exc_info)
  821. stream._span = span
  822. stream._integration = integration
  823. _initialize_data_accumulation_state(stream)
  824. stream._iterator = _wrap_synchronous_message_iterator(
  825. stream,
  826. stream._iterator,
  827. )
  828. return stream
  829. return _sentry_patched_enter
  830. def _wrap_async_message_stream(f: "Any") -> "Any":
  831. """
  832. Attaches user-provided arguments to the returned context manager.
  833. The attributes are set on AI Client Spans in the patch for the context manager.
  834. """
  835. @wraps(f)
  836. def _sentry_patched_stream(
  837. *args: "Any", **kwargs: "Any"
  838. ) -> "AsyncMessageStreamManager":
  839. stream_manager = f(*args, **kwargs)
  840. stream_manager._max_tokens = kwargs.get("max_tokens")
  841. stream_manager._messages = kwargs.get("messages")
  842. stream_manager._model = kwargs.get("model")
  843. stream_manager._system = kwargs.get("system")
  844. stream_manager._temperature = kwargs.get("temperature")
  845. stream_manager._top_k = kwargs.get("top_k")
  846. stream_manager._top_p = kwargs.get("top_p")
  847. stream_manager._tools = kwargs.get("tools")
  848. return stream_manager
  849. return _sentry_patched_stream
  850. def _wrap_async_message_stream_manager_aenter(f: "Any") -> "Any":
  851. """
  852. Creates and manages AI Client Spans.
  853. """
  854. @wraps(f)
  855. async def _sentry_patched_aenter(
  856. self: "AsyncMessageStreamManager",
  857. ) -> "AsyncMessageStream":
  858. if not hasattr(self, "_max_tokens"):
  859. return await f(self)
  860. integration = sentry_sdk.get_client().get_integration(AnthropicIntegration)
  861. if integration is None:
  862. return await f(self)
  863. if self._messages is None:
  864. return await f(self)
  865. try:
  866. iter(self._messages)
  867. except TypeError:
  868. return await f(self)
  869. span = get_start_span_function()(
  870. op=OP.GEN_AI_CHAT,
  871. name="chat" if self._model is None else f"chat {self._model}".strip(),
  872. origin=AnthropicIntegration.origin,
  873. )
  874. span.__enter__()
  875. span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
  876. _set_common_input_data(
  877. span=span,
  878. integration=integration,
  879. max_tokens=self._max_tokens,
  880. messages=self._messages,
  881. model=self._model,
  882. system=self._system,
  883. temperature=self._temperature,
  884. top_k=self._top_k,
  885. top_p=self._top_p,
  886. tools=self._tools,
  887. )
  888. try:
  889. stream = await f(self)
  890. except Exception as exc:
  891. exc_info = sys.exc_info()
  892. with capture_internal_exceptions():
  893. _capture_exception(exc)
  894. span.__exit__(*exc_info)
  895. reraise(*exc_info)
  896. stream._span = span
  897. stream._integration = integration
  898. _initialize_data_accumulation_state(stream)
  899. stream._iterator = _wrap_asynchronous_message_iterator(
  900. stream,
  901. stream._iterator,
  902. )
  903. return stream
  904. return _sentry_patched_aenter
  905. def _is_given(obj: "Any") -> bool:
  906. """
  907. Check for givenness safely across different anthropic versions.
  908. """
  909. if NotGiven is not None and isinstance(obj, NotGiven):
  910. return False
  911. if Omit is not None and isinstance(obj, Omit):
  912. return False
  913. return True