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:
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
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
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-656 — StreamDelta struct has only role and content; no refusal field
src/stream.rs:268-275 — ChatMessage struct has role, content, tool_calls; no refusal field
src/stream.rs:779-789 — aggregate() only reads delta.role and delta.content; no refusal handling
- Full codebase grep for "refusal", "content_filter", "structured_output" — zero results
Summary
When OpenAI returns a safety refusal for a structured output request, the refusal text is sent in a dedicated
refusalfield on the message (both in non-streaming and streaming responses). The SDK'sStreamDelta,ChatMessage, and streaming aggregation logic have norefusalfield, 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
refusalfield on assistant messages when usingresponse_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:
StreamDelta(src/stream.rs:651-656) only hasroleandcontentfields — norefusalfield, sorefusaltext in streaming deltas is silently ignored during deserializationChatMessage(src/stream.rs:268-275) only hasrole,content, andtool_calls— norefusalfield, so even if captured during aggregation, it cannot be represented in the outputaggregate()(src/stream.rs:727-807) only concatenatesdelta.content— no logic to accumulatedelta.refusalThis means when a model refuses a structured output request:
content: ""(empty) with no indication of whyBraintrust 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 includingrefusalwhen present.Upstream sources
refusalfield: https://platform.openai.com/docs/guides/structured-outputsrefusalfield on message: https://platform.openai.com/docs/api-reference/chat/objectRelationship to existing issues
tool_callsfield inStreamDelta. This is about the missingrefusalfield — a different message property for a different feature (structured output safety).refusalfield exists in the Chat Completions API which the SDK already partially supports.Local files inspected
src/stream.rs:651-656—StreamDeltastruct has onlyroleandcontent; norefusalfieldsrc/stream.rs:268-275—ChatMessagestruct hasrole,content,tool_calls; norefusalfieldsrc/stream.rs:779-789—aggregate()only readsdelta.roleanddelta.content; no refusal handling