Skip to content

[bot] Streaming aggregator and output types drop the structured output refusal field #50

@braintrust-bot

Description

@braintrust-bot

Summary

When OpenAI returns a safety refusal for a structured output request, the refusal text is sent in a dedicated refusal field on the message (both in non-streaming and streaming responses). The SDK's StreamDelta, ChatMessage, and streaming aggregation logic have no refusal field, so refusal content is silently dropped from logged span output. Users cannot distinguish a refused structured output from an empty response in their Braintrust traces.

What is missing

OpenAI's Chat Completions API includes a refusal field on assistant messages when using response_format: { type: "json_schema", ... }:

Non-streaming response:

{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "refusal": "I'm sorry, I cannot assist with that request."
    },
    "finish_reason": "stop"
  }]
}

Streaming delta:

{"choices": [{"delta": {"role": "assistant", "refusal": ""}}]}
{"choices": [{"delta": {"refusal": "I'm sorry, I cannot"}}]}
{"choices": [{"delta": {"refusal": " assist with that request."}}]}

Currently in the SDK:

  1. StreamDelta (src/stream.rs:651-656) only has role and content fields — no refusal field, so refusal text in streaming deltas is silently ignored during deserialization
  2. ChatMessage (src/stream.rs:268-275) only has role, content, and tool_calls — no refusal field, so even if captured during aggregation, it cannot be represented in the output
  3. aggregate() (src/stream.rs:727-807) only concatenates delta.content — no logic to accumulate delta.refusal

This means when a model refuses a structured output request:

  • The refusal message is lost from the logged span
  • The span output shows content: "" (empty) with no indication of why
  • Users cannot filter or alert on refusals in Braintrust dashboards

Braintrust docs status

supported — Braintrust's OpenAI integration page documents structured output support with response_format: { type: "json_schema" }. The refusal field is not specifically mentioned. Other Braintrust SDKs capture the full message object including refusal when present.

Upstream sources

Relationship to existing issues

Local files inspected

  • src/stream.rs:651-656StreamDelta struct has only role and content; no refusal field
  • src/stream.rs:268-275ChatMessage struct has role, content, tool_calls; no refusal field
  • src/stream.rs:779-789aggregate() only reads delta.role and delta.content; no refusal handling
  • Full codebase grep for "refusal", "content_filter", "structured_output" — zero results

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions