Skip to content

fix(claude_agent_sdk): add provider metadata, drop deep-copy serialization, allowlist hook fields#585

Merged
Abhijeet Prasad (AbhiPrasad) merged 2 commits into
mainfrom
fix-claude-agent-sdk-spec-and-serialization
Jul 17, 2026
Merged

fix(claude_agent_sdk): add provider metadata, drop deep-copy serialization, allowlist hook fields#585
Abhijeet Prasad (AbhiPrasad) merged 2 commits into
mainfrom
fix-claude-agent-sdk-spec-and-serialization

Conversation

@starfolkai

@starfolkai starfolkai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add metadata.provider = \"anthropic\" on the anthropic.messages.create LLM span so it satisfies the SKILL.md rule that every llm span carries both metadata.model and metadata.provider.
  • Rewrite _serialize_content_blocks to do a shallow field copy instead of dataclasses.asdict(). asdict recursively copy.deepcopys leaves (see the note in bt_json.py:61-65 where Braintrust avoids it for the same reason); nested ToolUseBlock.input dicts and any inner content dataclasses now pass through to bt_json at send time. The block type discriminator and the ToolResult flatten/prune rules are preserved.
  • Rewrite _serialize_hook_value around an allowlist of documented Claude Code hook input/output fields (session_id, hook_event_name, tool_name, tool_input, tool_response, prompt, message, stop_hook_active, trigger, custom_instructions, source, reason, agent_id, agent_type, continue, stopReason, suppressOutput, decision, hookSpecificOutput, systemMessage), applied only at the top level. cwd, transcript_path, agent_transcript_path, and anything else the SDK might add stay dropped by default; nested values inside tool_input / hookSpecificOutput pass through untouched.
  • Extend the existing cassette-backed test_calculator_with_multiple_operations to assert metadata.provider == \"anthropic\" and metadata.model on every LLM span. No new cassettes.

Test plan

  • cd py && BRAINTRUST_TEST_PACKAGE_VERSION=latest uv run pytest src/braintrust/integrations/claude_agent_sdk/test_claude_agent_sdk.py — 48 passed
  • Serialization unit tests (test_serialize_content_blocks_keeps_malformed_text_block_payload, test_serialize_tool_result_output_flattens_text_blocks_and_errors) still cover the shallow-copy refactor's flatten/type/is_error branches
  • Hook cassette tests (test_user_prompt_submit_hook_creates_function_span, test_tool_hooks_create_function_spans, test_hook_spans_parent_to_matching_tool_and_final_llm) exercise the allowlist end-to-end and still pass

🤖 Generated with Claude Code

Created by abhijeet

Slack thread

starfolkbot and others added 2 commits July 17, 2026 05:35
…ation, allowlist hook fields

- LLM spans now include `metadata.provider = "anthropic"` per the
  spec-required `metadata.model` + `metadata.provider` rule.
- `_serialize_content_blocks` replaces `dataclasses.asdict()` (which
  deep-copies leaves) with a shallow field copy. Nested content and
  ToolUseBlock inputs pass through to Braintrust's send-time serializer.
- `_serialize_hook_value` drops the recursive walk and `dataclasses.asdict`
  path; hook payloads are filtered at the top level against an allowlist of
  documented Claude Code hook input/output fields. `cwd`, `transcript_path`,
  and `agent_transcript_path` remain excluded by default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit a2989d3 into main Jul 17, 2026
82 checks passed
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) deleted the fix-claude-agent-sdk-spec-and-serialization branch July 17, 2026 16:01
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>
Abhijeet Prasad (AbhiPrasad) pushed a commit that referenced this pull request Jul 17, 2026
…op eager serialization (#590)

## Summary

Align the Google GenAI integration with
`.agents/skills/sdk-integrations/SKILL.md`. Audit turned up four issues:

- **Missing `metadata.provider` on every span** — spec requires every
`llm` span to carry both `metadata.model` and `metadata.provider`. Added
`metadata.provider = \"google\"` on `generate_content`,
`generate_content_stream`, `embed_content`, `generate_images`, and every
`interactions.*` span.
- **Tools in `input.config` instead of `metadata.tools`** — spec says
tool definitions live in `metadata.tools`. Route the serialized
Google-native tool declarations onto `metadata.tools` for the models
path, and route the interactions `tools` kwarg onto `metadata.tools` for
the interactions path. New `_config_for_input()` strips `tools` from the
config that flows into `input` via a shallow field iteration (no full
model_dump).
- **Excess serialization** — dropped `bt_safe_deep_copy(config)` in
`_serialize_input` and `_prepare_generate_images_traced_call` (span
logging already runs `bt_safe_deep_copy` at send time, so the eager copy
was duplicate work — same pattern as #583 for anthropic and #585 for
claude_agent_sdk). Also dropped the eager
`grounding_metadata.model_dump(exclude_none=True)` in
`_aggregate_generate_content_chunks`, and the eager
`_materialize_interaction_value(...)` on interaction tools.
- **Denylist → allowlist** in `_tool_span_input` / `_tool_span_output`.
Replaced `key not in {\"id\", \"name\", \"type\", \"signature\",
\"server_name\", ...}` with explicit `_TOOL_CALL_INPUT_FIELDS =
(\"arguments\", \"code\", \"language\", \"url\", \"query\")` and
`_TOOL_RESULT_OUTPUT_FIELDS = (\"result\", \"output\", \"outcome\",
\"content\")`. Any unrecognized field on a call/result payload is
dropped rather than passed through.

Also trimmed narrative docstrings/comments that just described what the
code does; kept the section dividers and the two \"why\" comments about
interaction tool span lifetime.

## Test plan

- [x] Extended existing VCR-backed tests to assert `metadata.provider ==
\"google\"`, `metadata.tools` presence for both
`models.generate_content` and `interactions.create` tool cases, and
negative assertions that tools do not leak into `input.config` /
`input`.
- [x] `cd py && CI=1 BRAINTRUST_TEST_PACKAGE_VERSION=latest pytest
src/braintrust/integrations/google_genai/test_google_genai.py` — 45
passed
- [x] `cd py && CI=1 BRAINTRUST_TEST_PACKAGE_VERSION=1.75.0 pytest
src/braintrust/integrations/google_genai/test_google_genai.py` — 45
passed
- [x] `cd py && CI=1 BRAINTRUST_TEST_PACKAGE_VERSION=1.30.0 pytest
src/braintrust/integrations/google_genai/test_google_genai.py` — 38
passed, 7 pre-existing skips (interactions API unavailable on that
version)
- [x] **No cassettes re-recorded, no new mocks/fakes** — used the
existing VCR-backed suite.

🤖 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/p1784308772517939?thread_ts=1784308772.517939&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 21, 2026
…ation (#608)

## Summary

Aligns the OpenRouter integration with
`.agents/skills/sdk-integrations/SKILL.md`, following #604 (mistral),
#603 (livekit_agents), #594 (litellm), #593 (langchain), and #585
(claude_agent_sdk). Audit found two issues:

- **Excess serialization.** `sanitize_openrouter_logged_value` called
`bt_safe_deep_copy` on every span input, every kwargs dict, every
response (for both output and metadata extraction), and every usage
dict. Braintrust already runs `bt_safe_deep_copy` at log time in
`Span.log_internal` (`logger.py:4699`), so every one of those was a
duplicate walk. Dropped the wrapper; reads now go through a small
`_get_field(obj, key)` helper (dict-or-attribute) and pydantic responses
flow straight to `span.log(...)`.

- **Denylist metadata → per-surface allowlist.** `_OMITTED_KEYS` was a
small denylist (`execute`, `render`, `nextTurnParams`,
`requireApproval`); the old `_build_openrouter_metadata` then dumped
every *other* kwarg — including `messages`, `api_key`, `extra_headers`,
and unknown kwargs — into `metadata`. Replaced with three request
allowlists (`_CHAT_REQUEST_KEYS`, `_EMBEDDINGS_REQUEST_KEYS`,
`_RESPONSES_REQUEST_KEYS`) and three response allowlists
(`_CHAT_RESPONSE_KEYS`, `_EMBEDDINGS_RESPONSE_KEYS`,
`_RESPONSES_RESPONSE_KEYS`), mirroring `litellm/_SAFE_METADATA_KEYS` and
mistral's `_*_METADATA_KEYS` family. `is_byok` is still extracted from
`usage` (it's still nested there).

Also collapsed the three near-identical `_finalize_*_response` functions
into one parameterized `_finalize_response(..., *, output,
response_keys)`, and factored the shared model/provider stamping
(`_stamp_model_and_provider`) used by both request and response metadata
builders.

Net: 1 file changed, +262 / −178 (net −0 after adding the allowlist
tuples, but the tracing surface itself shrank meaningfully).

## Test plan

- [x] **No new mocks/fakes.** All coverage stays on the existing
`@pytest.mark.vcr` cassette-backed tests in
`integrations/openrouter/test_openrouter.py` (chat sync/stream/async,
embeddings, responses sync + async-stream, `setup_*_creates_spans`,
`setup_*_is_idempotent`, auto_instrument subprocess).
- [x] **No cassette re-recording.** HTTP behavior is unchanged; only
in-process span shaping was touched.
- [x] `cd py && nox -s "test_openrouter(latest)"` — 9/9 passed.
- [x] `cd py && nox -s "test_openrouter(0.6.0)"` — 9/9 passed.
- [x] `ruff check` + `ruff format --check` — clean.

🤖 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/p1784653510318699?thread_ts=1784653510.318699&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