Skip to content

Commit 6968fe5

Browse files
authored
fix(cohere): add tool spans for chat tool calls (#380)
Emit child TOOL spans for Cohere chat responses that include tool calls, covering both v1 and v2 response shapes. Rename the shared Cohere field accessor to document why tracing handles both dict and SDK model objects. resolves #377
1 parent c076f0e commit 6968fe5

4 files changed

Lines changed: 358 additions & 37 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
interactions:
2+
- request:
3+
body: '{"message":"Use the get_weather tool for Paris.","model":"command-a-03-2025","max_tokens":64,"tools":[{"name":"get_weather","description":"Get
4+
the weather for a city.","parameter_definitions":{"city":{"description":"City
5+
name","type":"str","required":true}}}],"force_single_step":true,"stream":false}'
6+
headers:
7+
Accept:
8+
- '*/*'
9+
Accept-Encoding:
10+
- gzip, deflate
11+
Connection:
12+
- keep-alive
13+
Content-Length:
14+
- '300'
15+
Host:
16+
- api.cohere.com
17+
User-Agent:
18+
- cohere/6.1.0
19+
X-Fern-Language:
20+
- Python
21+
X-Fern-Platform:
22+
- darwin/25.2.0
23+
X-Fern-Runtime:
24+
- python/3.12.12
25+
X-Fern-SDK-Name:
26+
- cohere
27+
X-Fern-SDK-Version:
28+
- 6.1.0
29+
content-type:
30+
- application/json
31+
method: POST
32+
uri: https://api.cohere.com/v1/chat
33+
response:
34+
body:
35+
string: '{"response_id":"490af82c-d74b-4774-96aa-a48abc77b39d","text":"I will
36+
use one or more of the available tools to find the answer","generation_id":"465b7c4c-823c-4932-bd72-cc91b3aa154d","chat_history":[{"role":"USER","message":"Use
37+
the get_weather tool for Paris."},{"role":"CHATBOT","message":"I will use
38+
one or more of the available tools to find the answer","tool_calls":[{"name":"get_weather","parameters":{"city":"Paris"}}]}],"finish_reason":"COMPLETE","meta":{"api_version":{"version":"1"},"billed_units":{"input_tokens":42,"output_tokens":22},"tokens":{"input_tokens":1462,"output_tokens":33},"cached_tokens":0},"tool_calls":[{"name":"get_weather","parameters":{"city":"Paris"}}]}'
39+
headers:
40+
Alt-Svc:
41+
- h3=":443"; ma=2592000
42+
Transfer-Encoding:
43+
- chunked
44+
Via:
45+
- 1.1 google
46+
access-control-expose-headers:
47+
- X-Debug-Trace-ID
48+
cache-control:
49+
- no-cache, no-store, no-transform, must-revalidate, private, max-age=0
50+
content-length:
51+
- '684'
52+
content-type:
53+
- application/json
54+
date:
55+
- Fri, 01 May 2026 17:35:39 GMT
56+
expires:
57+
- Thu, 01 Jan 1970 00:00:00 GMT
58+
num_chars:
59+
- '6894'
60+
num_tokens:
61+
- '64'
62+
pragma:
63+
- no-cache
64+
server:
65+
- envoy
66+
vary:
67+
- Origin,Accept-Encoding
68+
x-accel-expires:
69+
- '0'
70+
x-debug-trace-id:
71+
- 9a84e7af81536f5a0ae6409b072713da
72+
x-endpoint-monthly-call-limit:
73+
- '1000'
74+
x-envoy-upstream-service-time:
75+
- '988'
76+
x-trial-endpoint-call-limit:
77+
- '20'
78+
x-trial-endpoint-call-remaining:
79+
- '17'
80+
status:
81+
code: 200
82+
message: OK
83+
version: 1
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
interactions:
2+
- request:
3+
body: '{"model":"command-a-03-2025","messages":[{"role":"user","content":"Use
4+
the get_weather tool for Paris."}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get
5+
the weather for a city.","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],"max_tokens":64,"tool_choice":"REQUIRED","stream":false}'
6+
headers:
7+
Accept:
8+
- '*/*'
9+
Accept-Encoding:
10+
- gzip, deflate
11+
Connection:
12+
- keep-alive
13+
Content-Length:
14+
- '361'
15+
Host:
16+
- api.cohere.com
17+
User-Agent:
18+
- cohere/6.1.0
19+
X-Fern-Language:
20+
- Python
21+
X-Fern-Platform:
22+
- darwin/25.2.0
23+
X-Fern-Runtime:
24+
- python/3.12.12
25+
X-Fern-SDK-Name:
26+
- cohere
27+
X-Fern-SDK-Version:
28+
- 6.1.0
29+
content-type:
30+
- application/json
31+
method: POST
32+
uri: https://api.cohere.com/v2/chat
33+
response:
34+
body:
35+
string: '{"id":"74529407-6112-4e14-9cb3-41ab95fe7725","message":{"role":"assistant","tool_plan":"I
36+
will use one or more of the available tools to find the answer","tool_calls":[{"id":"get_weather_p1927913dgpp","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"Paris\"}"}}]},"finish_reason":"TOOL_CALL","usage":{"billed_units":{"input_tokens":37,"output_tokens":22},"tokens":{"input_tokens":1455,"output_tokens":33},"cached_tokens":0}}'
37+
headers:
38+
Alt-Svc:
39+
- h3=":443"; ma=2592000
40+
Transfer-Encoding:
41+
- chunked
42+
Via:
43+
- 1.1 google
44+
access-control-expose-headers:
45+
- X-Debug-Trace-ID
46+
cache-control:
47+
- no-cache, no-store, no-transform, must-revalidate, private, max-age=0
48+
content-length:
49+
- '451'
50+
content-type:
51+
- application/json
52+
date:
53+
- Fri, 01 May 2026 17:34:51 GMT
54+
expires:
55+
- Thu, 01 Jan 1970 00:00:00 GMT
56+
num_chars:
57+
- '6866'
58+
num_tokens:
59+
- '59'
60+
pragma:
61+
- no-cache
62+
server:
63+
- envoy
64+
vary:
65+
- Origin,Accept-Encoding
66+
x-accel-expires:
67+
- '0'
68+
x-debug-trace-id:
69+
- b3c262976868c73da50611172c4163a1
70+
x-endpoint-monthly-call-limit:
71+
- '1000'
72+
x-envoy-upstream-service-time:
73+
- '913'
74+
x-trial-endpoint-call-limit:
75+
- '20'
76+
x-trial-endpoint-call-remaining:
77+
- '18'
78+
status:
79+
code: 200
80+
message: OK
81+
version: 1

py/src/braintrust/integrations/cohere/test_cohere.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
V2RerankPatcher,
3131
)
3232
from braintrust.integrations.test_utils import assert_metrics_are_valid, verify_autoinstrument_script
33-
from braintrust.test_helpers import init_test_logger
33+
from braintrust.span_types import SpanTypeAttribute
34+
from braintrust.test_helpers import find_spans_by_type, init_test_logger
3435

3536

3637
pytest.importorskip("cohere")
@@ -260,6 +261,91 @@ def test_wrap_cohere_chat_v2_sync(memory_logger):
260261
assert_metrics_are_valid(span["metrics"], start, end)
261262

262263

264+
@pytest.mark.vcr
265+
def test_wrap_cohere_chat_v2_tool_call_spans(memory_logger):
266+
if os.environ.get("BRAINTRUST_TEST_PACKAGE_VERSION") != "latest":
267+
pytest.skip("v2 tool-call cassette is recorded for the latest Cohere SDK")
268+
269+
assert not memory_logger.pop()
270+
client = wrap_cohere(_v2_client(require_methods=("chat",)))
271+
272+
response = client.chat(
273+
model=CHAT_MODEL,
274+
messages=[{"role": "user", "content": "Use the get_weather tool for Paris."}],
275+
tools=[
276+
{
277+
"type": "function",
278+
"function": {
279+
"name": "get_weather",
280+
"description": "Get the weather for a city.",
281+
"parameters": {
282+
"type": "object",
283+
"properties": {"city": {"type": "string"}},
284+
"required": ["city"],
285+
},
286+
},
287+
}
288+
],
289+
tool_choice="REQUIRED",
290+
max_tokens=64,
291+
)
292+
293+
tool_calls = response.message.tool_calls
294+
assert tool_calls
295+
assert tool_calls[0].function.name == "get_weather"
296+
297+
spans = memory_logger.pop()
298+
llm_spans = find_spans_by_type(spans, SpanTypeAttribute.LLM)
299+
tool_spans = find_spans_by_type(spans, SpanTypeAttribute.TOOL)
300+
301+
assert len(llm_spans) == 1
302+
assert len(tool_spans) == 1
303+
tool_span = tool_spans[0]
304+
assert tool_span["span_attributes"]["name"] == "tool: get_weather"
305+
assert tool_span["span_parents"] == [llm_spans[0]["span_id"]]
306+
assert tool_span["metadata"]["tool_call_id"] == tool_calls[0].id
307+
assert tool_span["metadata"]["tool_type"] == "function"
308+
assert "Paris" in str(tool_span["input"])
309+
310+
311+
@pytest.mark.vcr
312+
def test_wrap_cohere_chat_v1_tool_call_spans(memory_logger):
313+
if os.environ.get("BRAINTRUST_TEST_PACKAGE_VERSION") != "latest":
314+
pytest.skip("v1 tool-call cassette is recorded for the latest Cohere SDK")
315+
316+
assert not memory_logger.pop()
317+
client = wrap_cohere(_v1_client())
318+
319+
response = client.chat(
320+
model=CHAT_MODEL,
321+
message="Use the get_weather tool for Paris.",
322+
tools=[
323+
{
324+
"name": "get_weather",
325+
"description": "Get the weather for a city.",
326+
"parameter_definitions": {"city": {"description": "City name", "type": "str", "required": True}},
327+
}
328+
],
329+
force_single_step=True,
330+
max_tokens=64,
331+
)
332+
333+
tool_calls = response.tool_calls
334+
assert tool_calls
335+
assert tool_calls[0].name == "get_weather"
336+
337+
spans = memory_logger.pop()
338+
llm_spans = find_spans_by_type(spans, SpanTypeAttribute.LLM)
339+
tool_spans = find_spans_by_type(spans, SpanTypeAttribute.TOOL)
340+
341+
assert len(llm_spans) == 1
342+
assert len(tool_spans) == 1
343+
tool_span = tool_spans[0]
344+
assert tool_span["span_attributes"]["name"] == "tool: get_weather"
345+
assert tool_span["span_parents"] == [llm_spans[0]["span_id"]]
346+
assert "Paris" in str(tool_span["input"])
347+
348+
263349
@pytest.mark.vcr
264350
def test_wrap_cohere_chat_v1_sync(memory_logger):
265351
assert not memory_logger.pop()

0 commit comments

Comments
 (0)