fix(inference): use normalized role for Responses content kind (#3624) (#3626)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
oxoxDev
2026-06-15 13:29:34 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent e4666f9be4
commit eb80d249c6
2 changed files with 44 additions and 3 deletions
@@ -245,10 +245,18 @@ pub(crate) fn build_responses_prompt(
continue;
}
// Key the content-part kind on the NORMALIZED role, not the raw one.
// `normalize_responses_role` folds `tool` (and any non-user role) into
// `assistant`; the Responses API only accepts `output_text`/`refusal`
// for assistant items, so the kind must track the same normalized value.
// Reading `message.role` here instead let a `tool` turn become an
// assistant item carrying `input_text`, which OpenAI rejects with
// "Invalid value: 'input_text'" (Sentry TAURI-RUST-8FQ / GH #3624).
let role = normalize_responses_role(&message.role);
input.push(ResponsesInput {
role: normalize_responses_role(&message.role).to_string(),
role: role.to_string(),
content: vec![ResponsesContentPart {
kind: if message.role == "assistant" {
kind: if role == "assistant" {
"output_text".to_string()
} else {
"input_text".to_string()
@@ -862,14 +862,47 @@ fn build_responses_prompt_preserves_multi_turn_history() {
assert_eq!(input[1].role, "assistant");
assert_eq!(input[1].content[0].kind, "output_text");
assert_eq!(input[1].content[0].text, "ack 1");
// A `tool` turn normalizes to the `assistant` role, so its content part
// MUST be `output_text` — the Responses API rejects `input_text` on an
// assistant item (Sentry TAURI-RUST-8FQ / GH #3624).
assert_eq!(input[2].role, "assistant");
assert_eq!(input[2].content[0].kind, "input_text");
assert_eq!(input[2].content[0].kind, "output_text");
assert_eq!(input[2].content[0].text, "{\"result\":\"ok\"}");
assert_eq!(input[3].role, "user");
assert_eq!(input[3].content[0].kind, "input_text");
assert_eq!(input[3].content[0].text, "step 2");
}
/// Regression for Sentry TAURI-RUST-8FQ / GH #3624: the Responses API only
/// accepts `output_text`/`refusal` for assistant items. `normalize_responses_role`
/// folds `tool` into `assistant`, so the content kind must follow the normalized
/// role — never the raw one. No assistant-role item may carry `input_text`.
#[test]
fn build_responses_prompt_tool_role_uses_output_text() {
let messages = vec![
ChatMessage::assistant("calling a tool"),
ChatMessage::tool("{\"result\":\"ok\"}"),
ChatMessage::user("thanks"),
];
let (_instructions, input) = build_responses_prompt(&messages);
// The tool turn folds to assistant and must carry output_text.
assert_eq!(input[1].role, "assistant");
assert_eq!(input[1].content[0].kind, "output_text");
// Invariant: an assistant-role item never carries input_text.
for item in &input {
if item.role == "assistant" {
assert_eq!(
item.content[0].kind, "output_text",
"assistant-role item must use output_text, got {}",
item.content[0].kind
);
}
}
}
#[tokio::test]
async fn chat_via_responses_requires_non_system_message() {
let provider = make_provider("custom", "https://api.example.com", Some("test-key"));