Stop flagging snake_case is_error results as tool errors in OTel span (#2971)

This commit is contained in:
Marcelo Trylesinski 2026-06-25 17:24:45 +02:00 • committed by GitHub
commit 96bf22e57a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 12 additions and 5 deletions

View file

@ -59,8 +59,12 @@ class OpenTelemetryMiddleware(ServerMiddleware[Any]):
span.set_status(StatusCode.ERROR, str(e))
raise
if ctx.method == "tools/call":
# Tool errors are detected pre-serialization, so only shapes that reach the wire as an error
# count: the model, or the camelCase alias (`is_error` is dropped by the alias-only wire
# validation). A raw-dict `isError` is matched as a literal bool only - non-bool coercible
# values (1, "true") would serialize to an error but are rare enough to leave undetected.
match result:
case CallToolResult(is_error=True) | {"isError": True} | {"is_error": True}:
case CallToolResult(is_error=True) | {"isError": True}:
span.set_attribute("error.type", "tool_error")
span.set_status(StatusCode.ERROR)
case _:

View file

@ -92,7 +92,9 @@ async def test_tool_error_model_result_sets_error_type(server: SrvT, spans: Span
@pytest.mark.anyio
async def test_tool_error_snake_case_dict_result_sets_error_type(server: SrvT, spans: SpanCapture):
async def test_snake_case_dict_result_is_not_a_tool_error(server: SrvT, spans: SpanCapture):
# `is_error` is alias-only on the wire, so serialization drops it; the result reaches the
# client as a success and the span must not contradict that.
async def err_tool(ctx: Ctx, params: CallToolRequestParams) -> dict[str, Any]:
return {"content": [], "is_error": True}
@ -100,11 +102,12 @@ async def test_tool_error_snake_case_dict_result_sets_error_type(server: SrvT, s
server.middleware.append(OpenTelemetryMiddleware())
async with connected_runner(server) as (client, _):
spans.clear()
await client.send_raw_request("tools/call", {"name": "mytool", "arguments": {}})
result = await client.send_raw_request("tools/call", {"name": "mytool", "arguments": {}})
assert result == {"content": []}
[span] = [s for s in spans.finished() if s.kind == SpanKind.SERVER]
assert span.attributes is not None
assert span.attributes["error.type"] == "tool_error"
assert span.status.status_code == StatusCode.ERROR
assert "error.type" not in span.attributes
assert span.status.status_code == StatusCode.UNSET
@pytest.mark.anyio