Skip to content

Commit abd56b4

Browse files
committed
strip deferred tool descriptions and schema
1 parent 48788ce commit abd56b4

9 files changed

Lines changed: 319 additions & 399 deletions

File tree

ddtrace/llmobs/_integrations/anthropic.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,11 @@ def _extract_tools(self, tools: Optional[Any]) -> list[ToolDefinition]:
271271

272272
tool_definitions = []
273273
for tool in tools:
274+
is_deferred = bool(_get_attr(tool, "defer_loading", False))
274275
tool_def = ToolDefinition(
275-
name=tool.get("name", ""), description=tool.get("description", ""), schema=tool.get("input_schema", {})
276+
name=tool.get("name", ""),
277+
description="" if is_deferred else tool.get("description", ""),
278+
schema={} if is_deferred else tool.get("input_schema", {}),
276279
)
277280
tool_definitions.append(tool_def)
278281
return tool_definitions

ddtrace/llmobs/_integrations/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,9 @@ def _openai_get_tool_definitions(tools: list[Any]) -> list[ToolDefinition]:
10101010
)
10111011
if not any(tool_definition.values()):
10121012
continue
1013+
if _get_attr(tool, "defer_loading", False):
1014+
tool_definition["description"] = ""
1015+
tool_definition["schema"] = {}
10131016
tool_definitions.append(tool_definition)
10141017
return tool_definitions
10151018

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
interactions:
2+
- request:
3+
body: '{"max_tokens": 200, "messages": [{"role": "user", "content": "What is the
4+
weather in San Francisco, CA?"}], "model": "claude-sonnet-4-6", "tools": [{"name":
5+
"get_weather", "description": "Get the weather for a specific location", "input_schema":
6+
{"type": "object", "properties": {"location": {"type": "string"}}}}, {"name":
7+
"search_logs", "description": "Search Datadog logs", "input_schema": {"type":
8+
"object", "properties": {"query": {"type": "string"}}}, "defer_loading": true}]}'
9+
headers:
10+
accept:
11+
- application/json
12+
accept-encoding:
13+
- gzip, deflate
14+
anthropic-version:
15+
- '2023-06-01'
16+
connection:
17+
- keep-alive
18+
content-length:
19+
- '482'
20+
content-type:
21+
- application/json
22+
host:
23+
- api.anthropic.com
24+
user-agent:
25+
- Anthropic/Python 0.28.1
26+
x-stainless-arch:
27+
- arm64
28+
x-stainless-async:
29+
- 'false'
30+
x-stainless-lang:
31+
- python
32+
x-stainless-os:
33+
- MacOS
34+
x-stainless-package-version:
35+
- 0.28.1
36+
x-stainless-runtime:
37+
- CPython
38+
x-stainless-runtime-version:
39+
- 3.10.15
40+
method: POST
41+
uri: https://api.anthropic.com/v1/messages
42+
response:
43+
body:
44+
string: !!binary |
45+
H4sIAAAAAAAAA3RRXU8CMRD8K6bPkEDkI/ImJjyYkChEMBjTrL31rtqPs91CkPDf3V48FY1v152Z
46+
ndm5g7C+QCMmQhlIBXajdw6pO+iOREfoggEbS9nrrwbr2/s4nS/03cuFvR7P6v3qfcgc2teYWRgj
47+
lMiD4E0eQIw6EjjikfKOkL8mD4eWT94bmSK2Lvmd2Geu7LK+2W3iYrFNVidab1bTilkObNaVSHKH
48+
QBWGLHV14rUHYbwC0t4xYwnubBbAKR2V75xdXYojJwBjWDH58i90QEXieHzsiEi+lgEhNvofyRog
49+
4ltCp1jkkjGfwwIJtIntLDW38/YmkCT/io7B4eg8W6sKpeL9OaA8ZfRanOHiP6zVZgOsK7QYwMih
50+
/cv/RvvVb5Rb8IlO4o35GgxbrVCSzvWI/McKCEXT7TOGfLgs0efmjX8Cw419AAAA//8DAOHUFD80
51+
AgAA
52+
headers:
53+
CF-RAY:
54+
- 9e6a1c896dc4f793-EWR
55+
Connection:
56+
- keep-alive
57+
Content-Encoding:
58+
- gzip
59+
Content-Security-Policy:
60+
- default-src 'none'; frame-ancestors 'none'
61+
Content-Type:
62+
- application/json
63+
Date:
64+
- Fri, 03 Apr 2026 18:24:41 GMT
65+
Server:
66+
- cloudflare
67+
Transfer-Encoding:
68+
- chunked
69+
X-Robots-Tag:
70+
- none
71+
anthropic-organization-id:
72+
- 4257e925-ee99-4ee8-9c62-8e53716d5203
73+
anthropic-ratelimit-input-tokens-limit:
74+
- '20000000'
75+
anthropic-ratelimit-input-tokens-remaining:
76+
- '20000000'
77+
anthropic-ratelimit-input-tokens-reset:
78+
- '2026-04-03T18:24:41Z'
79+
anthropic-ratelimit-output-tokens-limit:
80+
- '4000000'
81+
anthropic-ratelimit-output-tokens-remaining:
82+
- '4000000'
83+
anthropic-ratelimit-output-tokens-reset:
84+
- '2026-04-03T18:24:41Z'
85+
anthropic-ratelimit-requests-limit:
86+
- '20000'
87+
anthropic-ratelimit-requests-remaining:
88+
- '19998'
89+
anthropic-ratelimit-requests-reset:
90+
- '2026-04-03T18:24:39Z'
91+
anthropic-ratelimit-tokens-limit:
92+
- '24000000'
93+
anthropic-ratelimit-tokens-remaining:
94+
- '24000000'
95+
anthropic-ratelimit-tokens-reset:
96+
- '2026-04-03T18:24:41Z'
97+
cf-cache-status:
98+
- DYNAMIC
99+
request-id:
100+
- req_011CZhJiAbXbnNgXdG6uaWwN
101+
server-timing:
102+
- x-originResponse;dur=1444
103+
set-cookie:
104+
- _cfuvid=NuIj9jojCbmmrSz8Fjqy_4SCIVBTXUtvAGSR7UMo98E-1775240679.9067852-1.0.1.1-_ng7wmOL8YHtigejdGPA1JjjSMn417BVQPhuW3t0hos;
105+
HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com
106+
strict-transport-security:
107+
- max-age=31536000; includeSubDomains; preload
108+
vary:
109+
- Accept-Encoding
110+
x-envoy-upstream-service-time:
111+
- '1443'
112+
status:
113+
code: 200
114+
message: OK
115+
version: 1

tests/contrib/anthropic/test_anthropic_llmobs.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ def test_beta_server_tool_use_stream(
169169
{"name": "tool_search_tool_regex", "description": "", "schema": {}},
170170
{
171171
"name": "get_weather",
172-
"description": "Get the weather for a specific location",
173-
"schema": {"type": "object", "properties": {"location": {"type": "string"}}},
172+
"description": "",
173+
"schema": {},
174174
},
175175
],
176176
)
@@ -276,8 +276,8 @@ def test_beta_server_tool_use_non_stream(
276276
{"name": "tool_search_tool_regex", "description": "", "schema": {}},
277277
{
278278
"name": "get_weather",
279-
"description": "Get the weather for a specific location",
280-
"schema": {"type": "object", "properties": {"location": {"type": "string"}}},
279+
"description": "",
280+
"schema": {},
281281
},
282282
],
283283
)
@@ -1637,3 +1637,54 @@ def test_thinking_in_input_messages(
16371637
tool_definitions=EXPECTED_TOOL_DEFINITIONS,
16381638
)
16391639
)
1640+
1641+
def test_deferred_tool_schema_stripped_in_span(
1642+
self, anthropic, ddtrace_global_config, mock_llmobs_writer, test_spans, request_vcr
1643+
):
1644+
"""Regression test: deferred tools (defer_loading=True) should have description and schema
1645+
stripped from LLMObs spans to avoid inflating payload size. Non-deferred tools keep their
1646+
full definitions.
1647+
"""
1648+
llm = anthropic.Anthropic()
1649+
with request_vcr.use_cassette("anthropic_completion_tools_deferred.yaml"):
1650+
llm.messages.create(
1651+
model="claude-sonnet-4-6",
1652+
max_tokens=200,
1653+
messages=[{"role": "user", "content": "What is the weather in San Francisco, CA?"}],
1654+
tools=[
1655+
{
1656+
"name": "get_weather",
1657+
"description": "Get the weather for a specific location",
1658+
"input_schema": {
1659+
"type": "object",
1660+
"properties": {"location": {"type": "string"}},
1661+
},
1662+
},
1663+
{
1664+
"name": "search_logs",
1665+
"description": "Search Datadog logs",
1666+
"input_schema": {
1667+
"type": "object",
1668+
"properties": {"query": {"type": "string"}},
1669+
},
1670+
"defer_loading": True,
1671+
},
1672+
],
1673+
)
1674+
1675+
assert mock_llmobs_writer.enqueue.call_count == 1
1676+
span_event = mock_llmobs_writer.enqueue.call_args[0][0]
1677+
assert span_event["meta"]["tool_definitions"] == [
1678+
{
1679+
"name": "get_weather",
1680+
"description": "Get the weather for a specific location",
1681+
"schema": {"type": "object", "properties": {"location": {"type": "string"}}},
1682+
},
1683+
{
1684+
"name": "search_logs",
1685+
"description": "",
1686+
"schema": {},
1687+
},
1688+
]
1689+
1690+

tests/contrib/openai/test_openai_llmobs.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2549,6 +2549,43 @@ def test_response_reasoning_tokens(self, openai, mock_llmobs_writer, test_spans)
25492549
)
25502550
)
25512551

2552+
@pytest.mark.skipif(
2553+
parse_version(openai_module.version.VERSION) < (1, 1), reason="Tool calls available after v1.1.0"
2554+
)
2555+
def test_deferred_tool_schema_stripped_in_span(self, openai, ddtrace_global_config, mock_llmobs_writer, test_spans):
2556+
"""Regression test: deferred tools (defer_loading=True) should have description and schema
2557+
stripped from LLMObs spans to avoid inflating payload size. Non-deferred tools keep their
2558+
full definitions.
2559+
"""
2560+
deferred_tool = {
2561+
"type": "function",
2562+
"function": {
2563+
"name": "search_logs",
2564+
"description": "Search Datadog logs",
2565+
"parameters": {
2566+
"type": "object",
2567+
"properties": {"query": {"type": "string", "description": "Search query"}},
2568+
"required": ["query"],
2569+
},
2570+
},
2571+
"defer_loading": True,
2572+
}
2573+
with get_openai_vcr(subdirectory_name="v1").use_cassette("chat_completion_tool_call.yaml"):
2574+
model = "gpt-3.5-turbo"
2575+
client = openai.OpenAI()
2576+
client.chat.completions.create(
2577+
tools=[chat_completion_custom_functions[0], deferred_tool],
2578+
model=model,
2579+
messages=[{"role": "user", "content": chat_completion_input_description}],
2580+
user="ddtrace-test",
2581+
)
2582+
assert mock_llmobs_writer.enqueue.call_count == 1
2583+
span_event = mock_llmobs_writer.enqueue.call_args[0][0]
2584+
assert span_event["meta"]["tool_definitions"] == [
2585+
EXPECTED_TOOL_DEFINITIONS[0],
2586+
{"name": "search_logs", "description": "", "schema": {}},
2587+
]
2588+
25522589

25532590
@pytest.mark.parametrize(
25542591
"ddtrace_global_config",
@@ -2622,3 +2659,5 @@ def test_est_tokens():
26222659
)
26232660
== 97
26242661
) # oracle: 92
2662+
2663+

tests/llmobs/llmobs_cassettes/openai/openai_responses_post_aaef3e68.json

Lines changed: 48 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)