fix(provider): use output_text content type for assistant messages in Codex history
The OpenAI Responses API requires assistant messages to use content type "output_text" while user messages use "input_text". The prior implementation used "input_text" for both roles, causing 400 errors on multi-turn history. Extract build_responses_input() helper for testability and add 3 unit tests covering role→content-type mapping, default instructions, and unknown roles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1b57be7223
commit
5dd11e6b0f
1 changed files with 105 additions and 27 deletions
|
|
@ -123,6 +123,44 @@ fn normalize_model_id(model: &str) -> &str {
|
||||||
model.rsplit('/').next().unwrap_or(model)
|
model.rsplit('/').next().unwrap_or(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_responses_input(messages: &[ChatMessage]) -> (String, Vec<ResponsesInput>) {
|
||||||
|
let mut system_parts: Vec<&str> = Vec::new();
|
||||||
|
let mut input: Vec<ResponsesInput> = Vec::new();
|
||||||
|
|
||||||
|
for msg in messages {
|
||||||
|
match msg.role.as_str() {
|
||||||
|
"system" => system_parts.push(&msg.content),
|
||||||
|
"user" => {
|
||||||
|
input.push(ResponsesInput {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: vec![ResponsesInputContent {
|
||||||
|
kind: "input_text".to_string(),
|
||||||
|
text: msg.content.clone(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"assistant" => {
|
||||||
|
input.push(ResponsesInput {
|
||||||
|
role: "assistant".to_string(),
|
||||||
|
content: vec![ResponsesInputContent {
|
||||||
|
kind: "output_text".to_string(),
|
||||||
|
text: msg.content.clone(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let instructions = if system_parts.is_empty() {
|
||||||
|
DEFAULT_CODEX_INSTRUCTIONS.to_string()
|
||||||
|
} else {
|
||||||
|
system_parts.join("\n\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
(instructions, input)
|
||||||
|
}
|
||||||
|
|
||||||
fn clamp_reasoning_effort(model: &str, effort: &str) -> String {
|
fn clamp_reasoning_effort(model: &str, effort: &str) -> String {
|
||||||
let id = normalize_model_id(model);
|
let id = normalize_model_id(model);
|
||||||
if (id.starts_with("gpt-5.2") || id.starts_with("gpt-5.3")) && effort == "minimal" {
|
if (id.starts_with("gpt-5.2") || id.starts_with("gpt-5.3")) && effort == "minimal" {
|
||||||
|
|
@ -429,33 +467,7 @@ impl Provider for OpenAiCodexProvider {
|
||||||
model: &str,
|
model: &str,
|
||||||
_temperature: f64,
|
_temperature: f64,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let mut system_parts: Vec<&str> = Vec::new();
|
let (instructions, input) = build_responses_input(messages);
|
||||||
let mut input: Vec<ResponsesInput> = Vec::new();
|
|
||||||
|
|
||||||
for msg in messages {
|
|
||||||
match msg.role.as_str() {
|
|
||||||
"system" => {
|
|
||||||
system_parts.push(&msg.content);
|
|
||||||
}
|
|
||||||
"user" | "assistant" => {
|
|
||||||
input.push(ResponsesInput {
|
|
||||||
role: msg.role.clone(),
|
|
||||||
content: vec![ResponsesInputContent {
|
|
||||||
kind: "input_text".to_string(),
|
|
||||||
text: msg.content.clone(),
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let instructions = if system_parts.is_empty() {
|
|
||||||
DEFAULT_CODEX_INSTRUCTIONS.to_string()
|
|
||||||
} else {
|
|
||||||
system_parts.join("\n\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
self.send_responses_request(input, instructions, model)
|
self.send_responses_request(input, instructions, model)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -566,4 +578,70 @@ data: [DONE]
|
||||||
|
|
||||||
assert_eq!(parse_sse_text(payload).unwrap().as_deref(), Some("Done"));
|
assert_eq!(parse_sse_text(payload).unwrap().as_deref(), Some("Done"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_responses_input_maps_content_types_by_role() {
|
||||||
|
let messages = vec![
|
||||||
|
ChatMessage {
|
||||||
|
role: "system".into(),
|
||||||
|
content: "You are helpful.".into(),
|
||||||
|
},
|
||||||
|
ChatMessage {
|
||||||
|
role: "user".into(),
|
||||||
|
content: "Hi".into(),
|
||||||
|
},
|
||||||
|
ChatMessage {
|
||||||
|
role: "assistant".into(),
|
||||||
|
content: "Hello!".into(),
|
||||||
|
},
|
||||||
|
ChatMessage {
|
||||||
|
role: "user".into(),
|
||||||
|
content: "Thanks".into(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let (instructions, input) = build_responses_input(&messages);
|
||||||
|
assert_eq!(instructions, "You are helpful.");
|
||||||
|
assert_eq!(input.len(), 3);
|
||||||
|
|
||||||
|
let json: Vec<Value> = input
|
||||||
|
.iter()
|
||||||
|
.map(|item| serde_json::to_value(item).unwrap())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(json[0]["role"], "user");
|
||||||
|
assert_eq!(json[0]["content"][0]["type"], "input_text");
|
||||||
|
assert_eq!(json[1]["role"], "assistant");
|
||||||
|
assert_eq!(json[1]["content"][0]["type"], "output_text");
|
||||||
|
assert_eq!(json[2]["role"], "user");
|
||||||
|
assert_eq!(json[2]["content"][0]["type"], "input_text");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_responses_input_uses_default_instructions_without_system() {
|
||||||
|
let messages = vec![ChatMessage {
|
||||||
|
role: "user".into(),
|
||||||
|
content: "Hello".into(),
|
||||||
|
}];
|
||||||
|
let (instructions, input) = build_responses_input(&messages);
|
||||||
|
assert_eq!(instructions, DEFAULT_CODEX_INSTRUCTIONS);
|
||||||
|
assert_eq!(input.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_responses_input_ignores_unknown_roles() {
|
||||||
|
let messages = vec![
|
||||||
|
ChatMessage {
|
||||||
|
role: "tool".into(),
|
||||||
|
content: "result".into(),
|
||||||
|
},
|
||||||
|
ChatMessage {
|
||||||
|
role: "user".into(),
|
||||||
|
content: "Go".into(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let (instructions, input) = build_responses_input(&messages);
|
||||||
|
assert_eq!(instructions, DEFAULT_CODEX_INSTRUCTIONS);
|
||||||
|
assert_eq!(input.len(), 1);
|
||||||
|
let json = serde_json::to_value(&input[0]).unwrap();
|
||||||
|
assert_eq!(json["role"], "user");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue