Anthropic: stop_reason and stop_sequence not captured in span output #175

Description

@braintrust-bot

Summary

The Anthropic integration's _log_message_to_span function extracts only role and content from the response message, dropping stop_reason and stop_sequence. Every other comparable wrapper in this repo captures the model's stop/finish reason in the span output.

Inconsistency across wrappers

WrapperCaptures stop/finish reason?Location
OpenAI (chat completions)Yesfinish_reason in output choicesoai.py ~line 354 (streaming), ~line 204 (non-streaming)
OpenAI (responses)Yesstatus in response objectoai.py response wrapper
Google GenAIYesfinish_reason per candidategoogle_genai/tracing.py ~line 313
AgnoYesfinish_reason in aggregated outputagno/tracing.py ~line 293
AnthropicNoanthropic/tracing.py ~line 445

What is dropped

The Anthropic Message response object includes:

  • stop_reason: One of "end_turn", "max_tokens", "stop_sequence", "tool_use" — tells users why the model stopped generating
  • stop_sequence: The specific stop sequence matched (when stop_reason is "stop_sequence")
  • model: The resolved model name (e.g., claude-sonnet-4-5-20250929 when the request used claude-sonnet-4-5-latest)

The current code at _log_message_to_span (line ~445 of tracing.py) only extracts:

output= {
k: vfork, vin {"role": getattr(message, "role", None), "content": getattr(message, "content", None)}.items()
ifv
} orNone

Why this matters

  • stop_reason: "max_tokens" signals output truncation — a common debugging need
  • stop_reason: "tool_use" signals the model wants tool execution — critical for agent workflows
  • The resolved model name is needed for accurate cost tracking when using model aliases
  • Other wrappers in this repo already capture the equivalent field, so users may expect it from Anthropic too

Braintrust docs status

The Anthropic integration docs describe streaming and metric collection but do not mention stop_reason capture: not_found.

Upstream source

Anthropic Messages API reference documents stop_reason as a top-level field on every Message response.

Local files inspected

  • py/src/braintrust/integrations/anthropic/tracing.py_log_message_to_span (line ~445) only extracts role and content
  • py/src/braintrust/integrations/anthropic/_utils.pyextract_anthropic_usage extracts token metrics but not response-level fields
  • py/src/braintrust/oai.py — OpenAI wrapper captures finish_reason for comparison
  • py/src/braintrust/integrations/google_genai/tracing.py — Google GenAI captures finish_reason for comparison

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    Anthropic: stop_reason and stop_sequence not captured in span output #175

    Description

    @braintrust-bot

    Summary

    The Anthropic integration's _log_message_to_span function extracts only role and content from the response message, dropping stop_reason and stop_sequence. Every other comparable wrapper in this repo captures the model's stop/finish reason in the span output.

    Inconsistency across wrappers

    WrapperCaptures stop/finish reason?Location
    OpenAI (chat completions)Yesfinish_reason in output choicesoai.py ~line 354 (streaming), ~line 204 (non-streaming)
    OpenAI (responses)Yesstatus in response objectoai.py response wrapper
    Google GenAIYesfinish_reason per candidategoogle_genai/tracing.py ~line 313
    AgnoYesfinish_reason in aggregated outputagno/tracing.py ~line 293
    AnthropicNoanthropic/tracing.py ~line 445

    What is dropped

    The Anthropic Message response object includes:

    • stop_reason: One of "end_turn", "max_tokens", "stop_sequence", "tool_use" — tells users why the model stopped generating
    • stop_sequence: The specific stop sequence matched (when stop_reason is "stop_sequence")
    • model: The resolved model name (e.g., claude-sonnet-4-5-20250929 when the request used claude-sonnet-4-5-latest)

    The current code at _log_message_to_span (line ~445 of tracing.py) only extracts:

    output= {
    k: vfork, vin {"role": getattr(message, "role", None), "content": getattr(message, "content", None)}.items()
    ifv
    } orNone

    Why this matters

    • stop_reason: "max_tokens" signals output truncation — a common debugging need
    • stop_reason: "tool_use" signals the model wants tool execution — critical for agent workflows
    • The resolved model name is needed for accurate cost tracking when using model aliases
    • Other wrappers in this repo already capture the equivalent field, so users may expect it from Anthropic too

    Braintrust docs status

    The Anthropic integration docs describe streaming and metric collection but do not mention stop_reason capture: not_found.

    Upstream source

    Anthropic Messages API reference documents stop_reason as a top-level field on every Message response.

    Local files inspected

    • py/src/braintrust/integrations/anthropic/tracing.py_log_message_to_span (line ~445) only extracts role and content
    • py/src/braintrust/integrations/anthropic/_utils.pyextract_anthropic_usage extracts token metrics but not response-level fields
    • py/src/braintrust/oai.py — OpenAI wrapper captures finish_reason for comparison
    • py/src/braintrust/integrations/google_genai/tracing.py — Google GenAI captures finish_reason for comparison

    Metadata

    Metadata

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      Anthropic: stop_reason and stop_sequence not captured in span output #175

      Description

      @braintrust-bot

      Summary

      The Anthropic integration's _log_message_to_span function extracts only role and content from the response message, dropping stop_reason and stop_sequence. Every other comparable wrapper in this repo captures the model's stop/finish reason in the span output.

      Inconsistency across wrappers

      WrapperCaptures stop/finish reason?Location
      OpenAI (chat completions)Yesfinish_reason in output choicesoai.py ~line 354 (streaming), ~line 204 (non-streaming)
      OpenAI (responses)Yesstatus in response objectoai.py response wrapper
      Google GenAIYesfinish_reason per candidategoogle_genai/tracing.py ~line 313
      AgnoYesfinish_reason in aggregated outputagno/tracing.py ~line 293
      AnthropicNoanthropic/tracing.py ~line 445

      What is dropped

      The Anthropic Message response object includes:

      • stop_reason: One of "end_turn", "max_tokens", "stop_sequence", "tool_use" — tells users why the model stopped generating
      • stop_sequence: The specific stop sequence matched (when stop_reason is "stop_sequence")
      • model: The resolved model name (e.g., claude-sonnet-4-5-20250929 when the request used claude-sonnet-4-5-latest)

      The current code at _log_message_to_span (line ~445 of tracing.py) only extracts:

      output= {
      k: vfork, vin {"role": getattr(message, "role", None), "content": getattr(message, "content", None)}.items()
      ifv
      } orNone

      Why this matters

      • stop_reason: "max_tokens" signals output truncation — a common debugging need
      • stop_reason: "tool_use" signals the model wants tool execution — critical for agent workflows
      • The resolved model name is needed for accurate cost tracking when using model aliases
      • Other wrappers in this repo already capture the equivalent field, so users may expect it from Anthropic too

      Braintrust docs status

      The Anthropic integration docs describe streaming and metric collection but do not mention stop_reason capture: not_found.

      Upstream source

      Anthropic Messages API reference documents stop_reason as a top-level field on every Message response.

      Local files inspected

      • py/src/braintrust/integrations/anthropic/tracing.py_log_message_to_span (line ~445) only extracts role and content
      • py/src/braintrust/integrations/anthropic/_utils.pyextract_anthropic_usage extracts token metrics but not response-level fields
      • py/src/braintrust/oai.py — OpenAI wrapper captures finish_reason for comparison
      • py/src/braintrust/integrations/google_genai/tracing.py — Google GenAI captures finish_reason for comparison

      Metadata

      Metadata

      Type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        Anthropic: stop_reason and stop_sequence not captured in span output #175

        Description

        @braintrust-bot

        Summary

        The Anthropic integration's _log_message_to_span function extracts only role and content from the response message, dropping stop_reason and stop_sequence. Every other comparable wrapper in this repo captures the model's stop/finish reason in the span output.

        Inconsistency across wrappers

        WrapperCaptures stop/finish reason?Location
        OpenAI (chat completions)Yesfinish_reason in output choicesoai.py ~line 354 (streaming), ~line 204 (non-streaming)
        OpenAI (responses)Yesstatus in response objectoai.py response wrapper
        Google GenAIYesfinish_reason per candidategoogle_genai/tracing.py ~line 313
        AgnoYesfinish_reason in aggregated outputagno/tracing.py ~line 293
        AnthropicNoanthropic/tracing.py ~line 445

        What is dropped

        The Anthropic Message response object includes:

        • stop_reason: One of "end_turn", "max_tokens", "stop_sequence", "tool_use" — tells users why the model stopped generating
        • stop_sequence: The specific stop sequence matched (when stop_reason is "stop_sequence")
        • model: The resolved model name (e.g., claude-sonnet-4-5-20250929 when the request used claude-sonnet-4-5-latest)

        The current code at _log_message_to_span (line ~445 of tracing.py) only extracts:

        output= {
        k: vfork, vin {"role": getattr(message, "role", None), "content": getattr(message, "content", None)}.items()
        ifv
        } orNone

        Why this matters

        • stop_reason: "max_tokens" signals output truncation — a common debugging need
        • stop_reason: "tool_use" signals the model wants tool execution — critical for agent workflows
        • The resolved model name is needed for accurate cost tracking when using model aliases
        • Other wrappers in this repo already capture the equivalent field, so users may expect it from Anthropic too

        Braintrust docs status

        The Anthropic integration docs describe streaming and metric collection but do not mention stop_reason capture: not_found.

        Upstream source

        Anthropic Messages API reference documents stop_reason as a top-level field on every Message response.

        Local files inspected

        • py/src/braintrust/integrations/anthropic/tracing.py_log_message_to_span (line ~445) only extracts role and content
        • py/src/braintrust/integrations/anthropic/_utils.pyextract_anthropic_usage extracts token metrics but not response-level fields
        • py/src/braintrust/oai.py — OpenAI wrapper captures finish_reason for comparison
        • py/src/braintrust/integrations/google_genai/tracing.py — Google GenAI captures finish_reason for comparison

        Metadata

        Metadata

        Type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          Anthropic: stop_reason and stop_sequence not captured in span output #175

          Description

          @braintrust-bot

          Summary

          The Anthropic integration's _log_message_to_span function extracts only role and content from the response message, dropping stop_reason and stop_sequence. Every other comparable wrapper in this repo captures the model's stop/finish reason in the span output.

          Inconsistency across wrappers

          WrapperCaptures stop/finish reason?Location
          OpenAI (chat completions)Yesfinish_reason in output choicesoai.py ~line 354 (streaming), ~line 204 (non-streaming)
          OpenAI (responses)Yesstatus in response objectoai.py response wrapper
          Google GenAIYesfinish_reason per candidategoogle_genai/tracing.py ~line 313
          AgnoYesfinish_reason in aggregated outputagno/tracing.py ~line 293
          AnthropicNoanthropic/tracing.py ~line 445

          What is dropped

          The Anthropic Message response object includes:

          • stop_reason: One of "end_turn", "max_tokens", "stop_sequence", "tool_use" — tells users why the model stopped generating
          • stop_sequence: The specific stop sequence matched (when stop_reason is "stop_sequence")
          • model: The resolved model name (e.g., claude-sonnet-4-5-20250929 when the request used claude-sonnet-4-5-latest)

          The current code at _log_message_to_span (line ~445 of tracing.py) only extracts:

          output= {
          k: vfork, vin {"role": getattr(message, "role", None), "content": getattr(message, "content", None)}.items()
          ifv
          } orNone

          Why this matters

          • stop_reason: "max_tokens" signals output truncation — a common debugging need
          • stop_reason: "tool_use" signals the model wants tool execution — critical for agent workflows
          • The resolved model name is needed for accurate cost tracking when using model aliases
          • Other wrappers in this repo already capture the equivalent field, so users may expect it from Anthropic too

          Braintrust docs status

          The Anthropic integration docs describe streaming and metric collection but do not mention stop_reason capture: not_found.

          Upstream source

          Anthropic Messages API reference documents stop_reason as a top-level field on every Message response.

          Local files inspected

          • py/src/braintrust/integrations/anthropic/tracing.py_log_message_to_span (line ~445) only extracts role and content
          • py/src/braintrust/integrations/anthropic/_utils.pyextract_anthropic_usage extracts token metrics but not response-level fields
          • py/src/braintrust/oai.py — OpenAI wrapper captures finish_reason for comparison
          • py/src/braintrust/integrations/google_genai/tracing.py — Google GenAI captures finish_reason for comparison

          Metadata

          Metadata

          Type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            Anthropic: stop_reason and stop_sequence not captured in span output #175

            Description

            @braintrust-bot

            Summary

            The Anthropic integration's _log_message_to_span function extracts only role and content from the response message, dropping stop_reason and stop_sequence. Every other comparable wrapper in this repo captures the model's stop/finish reason in the span output.

            Inconsistency across wrappers

            WrapperCaptures stop/finish reason?Location
            OpenAI (chat completions)Yesfinish_reason in output choicesoai.py ~line 354 (streaming), ~line 204 (non-streaming)
            OpenAI (responses)Yesstatus in response objectoai.py response wrapper
            Google GenAIYesfinish_reason per candidategoogle_genai/tracing.py ~line 313
            AgnoYesfinish_reason in aggregated outputagno/tracing.py ~line 293
            AnthropicNoanthropic/tracing.py ~line 445

            What is dropped

            The Anthropic Message response object includes:

            • stop_reason: One of "end_turn", "max_tokens", "stop_sequence", "tool_use" — tells users why the model stopped generating
            • stop_sequence: The specific stop sequence matched (when stop_reason is "stop_sequence")
            • model: The resolved model name (e.g., claude-sonnet-4-5-20250929 when the request used claude-sonnet-4-5-latest)

            The current code at _log_message_to_span (line ~445 of tracing.py) only extracts:

            output= {
            k: vfork, vin {"role": getattr(message, "role", None), "content": getattr(message, "content", None)}.items()
            ifv
            } orNone

            Why this matters

            • stop_reason: "max_tokens" signals output truncation — a common debugging need
            • stop_reason: "tool_use" signals the model wants tool execution — critical for agent workflows
            • The resolved model name is needed for accurate cost tracking when using model aliases
            • Other wrappers in this repo already capture the equivalent field, so users may expect it from Anthropic too

            Braintrust docs status

            The Anthropic integration docs describe streaming and metric collection but do not mention stop_reason capture: not_found.

            Upstream source

            Anthropic Messages API reference documents stop_reason as a top-level field on every Message response.

            Local files inspected

            • py/src/braintrust/integrations/anthropic/tracing.py_log_message_to_span (line ~445) only extracts role and content
            • py/src/braintrust/integrations/anthropic/_utils.pyextract_anthropic_usage extracts token metrics but not response-level fields
            • py/src/braintrust/oai.py — OpenAI wrapper captures finish_reason for comparison
            • py/src/braintrust/integrations/google_genai/tracing.py — Google GenAI captures finish_reason for comparison

            Metadata

            Metadata

            Type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Anthropic: stop_reason and stop_sequence not captured in span output #175

              Description

              @braintrust-bot

              Summary

              The Anthropic integration's _log_message_to_span function extracts only role and content from the response message, dropping stop_reason and stop_sequence. Every other comparable wrapper in this repo captures the model's stop/finish reason in the span output.

              Inconsistency across wrappers

              WrapperCaptures stop/finish reason?Location
              OpenAI (chat completions)Yesfinish_reason in output choicesoai.py ~line 354 (streaming), ~line 204 (non-streaming)
              OpenAI (responses)Yesstatus in response objectoai.py response wrapper
              Google GenAIYesfinish_reason per candidategoogle_genai/tracing.py ~line 313
              AgnoYesfinish_reason in aggregated outputagno/tracing.py ~line 293
              AnthropicNoanthropic/tracing.py ~line 445

              What is dropped

              The Anthropic Message response object includes:

              • stop_reason: One of "end_turn", "max_tokens", "stop_sequence", "tool_use" — tells users why the model stopped generating
              • stop_sequence: The specific stop sequence matched (when stop_reason is "stop_sequence")
              • model: The resolved model name (e.g., claude-sonnet-4-5-20250929 when the request used claude-sonnet-4-5-latest)

              The current code at _log_message_to_span (line ~445 of tracing.py) only extracts:

              output= {
              k: vfork, vin {"role": getattr(message, "role", None), "content": getattr(message, "content", None)}.items()
              ifv
              } orNone

              Why this matters

              • stop_reason: "max_tokens" signals output truncation — a common debugging need
              • stop_reason: "tool_use" signals the model wants tool execution — critical for agent workflows
              • The resolved model name is needed for accurate cost tracking when using model aliases
              • Other wrappers in this repo already capture the equivalent field, so users may expect it from Anthropic too

              Braintrust docs status

              The Anthropic integration docs describe streaming and metric collection but do not mention stop_reason capture: not_found.

              Upstream source

              Anthropic Messages API reference documents stop_reason as a top-level field on every Message response.

              Local files inspected

              • py/src/braintrust/integrations/anthropic/tracing.py_log_message_to_span (line ~445) only extracts role and content
              • py/src/braintrust/integrations/anthropic/_utils.pyextract_anthropic_usage extracts token metrics but not response-level fields
              • py/src/braintrust/oai.py — OpenAI wrapper captures finish_reason for comparison
              • py/src/braintrust/integrations/google_genai/tracing.py — Google GenAI captures finish_reason for comparison

              Metadata

              Metadata

              Type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                Anthropic: stop_reason and stop_sequence not captured in span output #175

                Description

                @braintrust-bot

                Summary

                The Anthropic integration's _log_message_to_span function extracts only role and content from the response message, dropping stop_reason and stop_sequence. Every other comparable wrapper in this repo captures the model's stop/finish reason in the span output.

                Inconsistency across wrappers

                WrapperCaptures stop/finish reason?Location
                OpenAI (chat completions)Yesfinish_reason in output choicesoai.py ~line 354 (streaming), ~line 204 (non-streaming)
                OpenAI (responses)Yesstatus in response objectoai.py response wrapper
                Google GenAIYesfinish_reason per candidategoogle_genai/tracing.py ~line 313
                AgnoYesfinish_reason in aggregated outputagno/tracing.py ~line 293
                AnthropicNoanthropic/tracing.py ~line 445

                What is dropped

                The Anthropic Message response object includes:

                • stop_reason: One of "end_turn", "max_tokens", "stop_sequence", "tool_use" — tells users why the model stopped generating
                • stop_sequence: The specific stop sequence matched (when stop_reason is "stop_sequence")
                • model: The resolved model name (e.g., claude-sonnet-4-5-20250929 when the request used claude-sonnet-4-5-latest)

                The current code at _log_message_to_span (line ~445 of tracing.py) only extracts:

                output= {
                k: vfork, vin {"role": getattr(message, "role", None), "content": getattr(message, "content", None)}.items()
                ifv
                } orNone

                Why this matters

                • stop_reason: "max_tokens" signals output truncation — a common debugging need
                • stop_reason: "tool_use" signals the model wants tool execution — critical for agent workflows
                • The resolved model name is needed for accurate cost tracking when using model aliases
                • Other wrappers in this repo already capture the equivalent field, so users may expect it from Anthropic too

                Braintrust docs status

                The Anthropic integration docs describe streaming and metric collection but do not mention stop_reason capture: not_found.

                Upstream source

                Anthropic Messages API reference documents stop_reason as a top-level field on every Message response.

                Local files inspected

                • py/src/braintrust/integrations/anthropic/tracing.py_log_message_to_span (line ~445) only extracts role and content
                • py/src/braintrust/integrations/anthropic/_utils.pyextract_anthropic_usage extracts token metrics but not response-level fields
                • py/src/braintrust/oai.py — OpenAI wrapper captures finish_reason for comparison
                • py/src/braintrust/integrations/google_genai/tracing.py — Google GenAI captures finish_reason for comparison

                Metadata

                Metadata

                Type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions