Skip to content

fix(anthropic): drop redundant bt_safe_deep_copy passes in tracing#583

Merged
Abhijeet Prasad (AbhiPrasad) merged 1 commit into
mainfrom
fix/anthropic-drop-redundant-deep-copies
Jul 16, 2026
Merged

fix(anthropic): drop redundant bt_safe_deep_copy passes in tracing#583
Abhijeet Prasad (AbhiPrasad) merged 1 commit into
mainfrom
fix/anthropic-drop-redundant-deep-copies

Conversation

@starfolkai

@starfolkai starfolkai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • SpanImpl.log_internal already runs bt_safe_deep_copy on every logged event (see logger.py:4699), so the eager copies in _normalize_anthropic_input, _get_input_from_kwargs, and _redact_server_tool_output were duplicating work on every span open/log.
  • Removed the three bt_safe_deep_copy calls and dropped the now-unused import. The remaining walks (_normalize_anthropic_data, _process_input_attachments, inlined redaction) already build fresh dicts/lists and never mutate user data, so behavior is preserved.
  • Mirrors what google_genai/tracing.py does today and matches the sdk-integrations skill guidance: "Do not over-serialize. Braintrust serializes at send/log time."

Test plan

  • pytest src/braintrust/integrations/anthropic/test_anthropic.py on anthropic==0.116.0 (matrix latest) — 48 passed
  • pytest src/braintrust/integrations/anthropic/test_anthropic.py on anthropic==0.48.0 — 39 passed, 9 legitimately skipped for features unavailable on that version
  • No cassettes re-recorded, no new mocks/fakes added — used the existing VCR-backed tests

🤖 Generated with Claude Code

Created by abhijeet

Slack thread

Braintrust already deep-copies via bt_safe_deep_copy in SpanImpl.log_internal,
so the eager copies in the Anthropic integration were duplicating work on every
span open/log. Remove them from _normalize_anthropic_input,
_get_input_from_kwargs, and _redact_server_tool_output; the remaining walks
(_normalize_anthropic_data, _process_input_attachments, and the inlined
redaction) already build fresh dicts/lists and never mutate user data.

This mirrors what google_genai/tracing.py does and matches the sdk-integrations
skill guidance ("Do not over-serialize. Braintrust serializes at send/log
time."). All existing cassette-backed tests pass on both matrix versions
(latest=0.116.0 and 0.48.0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit 63b8ed2 into main Jul 16, 2026
82 checks passed
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) deleted the fix/anthropic-drop-redundant-deep-copies branch July 16, 2026 21:35
Abhijeet Prasad (AbhiPrasad) pushed a commit that referenced this pull request Jul 17, 2026
…586)

## Summary
- `_iter_tool_calls` ran `_try_to_dict(output)` (Pydantic
`model_dump("python")`) on every chat/stream response just to probe for
`tool_calls`, even when none were present — an extra recursive walk on
top of the one `SpanImpl.log_internal` already runs via
`bt_safe_deep_copy`.
- Replaced the eager conversion with `_get_field(output, "tool_calls")`,
which already handles both dict and Pydantic-object shapes. Downstream
tool-call helpers (`_tool_call_name`, `_tool_call_input`,
`_tool_call_metadata`) all read via `_get_field`, so behavior is
preserved.
- Matches the `sdk-integrations` skill guidance ("Do not over-serialize.
Braintrust serializes at send/log time.") and mirrors #583 (anthropic).

## Test plan
- [x] `nox -s "test_cohere(latest)"` (cohere==7.0.5) — 21 passed
- [x] `nox -s "test_cohere(5.0.0)"` — 10 passed, 11 legitimately skipped
for v2/audio surfaces not present on 5.0.0
- No cassettes re-recorded, no new tests — used the existing VCR-backed
coverage in `test_cohere.py`, which already asserts tool-call span
parent/child structure for both v1
(`test_wrap_cohere_chat_v1_tool_call_spans`) and v2
(`test_wrap_cohere_chat_v2_tool_call_spans`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- sfk:created-approved-by -->
Created by abhijeet

<!-- sfk:slack-thread -->
[Slack
thread](https://starfolkai.slack.com/archives/C0AQDETAVT3/p1784304169602969?thread_ts=1784304169.602969&cid=C0AQDETAVT3)

Co-authored-by: Starfolk <noreply@starfolk.ai>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Abhijeet Prasad (AbhiPrasad) pushed a commit that referenced this pull request Jul 17, 2026
…er serialization (#587)

## Summary

Align the CrewAI integration with
`.agents/skills/sdk-integrations/SKILL.md`. Audit turned up five issues:

- **Missing `metadata.provider` on `crewai.llm` spans** — spec requires
every `llm` span to carry both `metadata.model` and `metadata.provider`.
Added `_provider_from_source()`, which reads the `provider` string off
the emitting CrewAI `LLM` object (`"openai"` / `"anthropic"` /
`"bedrock"` / ...).
- **Tools in `input` instead of `metadata.tools`** — spec says tool
definitions live in `metadata.tools`, not `input`. Moved both the
LLM-call `tools` and the agent `tools` onto `metadata`.
- **Token double-counting on `crewai.llm`** — the previous rule only
skipped tokens when the LiteLLM integration was patched, but CrewAI 1.x
routes `gpt-*` to a native `openai` client, `claude-*` to a native
`anthropic` client, etc. When those integrations are patched (as
`auto_instrument()` does by default), they emit a leaf span with tokens
and `crewai.llm` double-counted at every ancestor in the trace-tree
rollup. The SKILL is explicit here: "Do not add 'if OpenAI is patched,
skip metrics' checks — define clear ownership instead." CrewAI is always
an orchestration layer that delegates to a provider SDK, so the clear
rule is: **`crewai.llm` never owns tokens**. Same pattern `pydantic_ai`
already uses for its wrapper spans. Dropped `_litellm_owns_leaf_span` +
the token-name/prefix maps + the `extra_metrics` plumbing on `_end_span`
accordingly.
- **Excess serialization** — dropped eager `_try_to_dict` /
`_normalize_output` / `_normalize_tools` calls across kickoff, task,
agent, LLM, and tool output paths. `bt_json._to_bt_safe` at log time
already handles Pydantic v2 (`model_dump`), Pydantic v1 (`dict`), and
dataclasses; the eager pre-pass was wasted work (same pattern as #585
for claude_agent_sdk and #583 for anthropic).

All other metadata extractors (`_causal_metadata`, `_crew_metadata`,
`_task_metadata`, `_LLM_CONFIG_FIELDS`) were already allowlist-based —
no changes needed there.

## Test plan

- [x] Extended `test_kickoff_llm_event_tree_parents_and_shape` to emit
with a real `LLM` source and assert `metadata.provider == "openai"`.
- [x] Added `test_llm_tools_route_to_metadata_not_input`
(positive-in-metadata + negative-not-in-input).
- [x] Replaced the two conditional token tests with a single
`test_llm_never_emits_token_metrics` that asserts no token key
(`tokens`, `prompt_tokens`, `completion_tokens`, `prompt_cached_tokens`,
`completion_reasoning_tokens`) leaks onto `crewai.llm` regardless of
which provider integrations are patched.
- [x] **No new cassettes.** The test file docstring documents why VCR is
impractical for CrewAI (pytest-vcr + httpcore + CrewAI's native
`OpenAICompletion` `model_post_init` interaction bug). Direct-event
tests remain the source of truth here; the LiteLLM `mock_response` smoke
test still exercises the full `crew.kickoff()` path.
- [x] `cd py && BRAINTRUST_TEST_PACKAGE_VERSION=latest pytest
src/braintrust/integrations/crewai/test_crewai.py -v` — 17 passed
- [x] `cd py && nox -s pylint` — success
- [x] `pre-commit run` on the changed files — passed (ruff format, ruff
check, codespell, EOF/trailing-whitespace)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- sfk:created-approved-by -->
Created by abhijeet

<!-- sfk:slack-thread -->
[Slack
thread](https://starfolkai.slack.com/archives/C0AQDETAVT3/p1784304223300019?thread_ts=1784304223.300019&cid=C0AQDETAVT3)

---------

Co-authored-by: Starfolk <noreply@starfolk.ai>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants