fix(codex): convert system role to developer for codex input

This commit is contained in:
hkfires
2026-02-01 15:37:37 +08:00
parent fe3ebe3532
commit 354f6582b2
2 changed files with 293 additions and 0 deletions

View File

@@ -2,7 +2,9 @@ package responses
import ( import (
"bytes" "bytes"
"fmt"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
@@ -20,5 +22,31 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte,
rawJSON, _ = sjson.DeleteBytes(rawJSON, "top_p") rawJSON, _ = sjson.DeleteBytes(rawJSON, "top_p")
rawJSON, _ = sjson.DeleteBytes(rawJSON, "service_tier") rawJSON, _ = sjson.DeleteBytes(rawJSON, "service_tier")
// Convert role "system" to "developer" in input array to comply with Codex API requirements.
rawJSON = convertSystemRoleToDeveloper(rawJSON)
return rawJSON return rawJSON
} }
// convertSystemRoleToDeveloper traverses the input array and converts any message items
// with role "system" to role "developer". This is necessary because Codex API does not
// accept "system" role in the input array.
func convertSystemRoleToDeveloper(rawJSON []byte) []byte {
inputResult := gjson.GetBytes(rawJSON, "input")
if !inputResult.IsArray() {
return rawJSON
}
inputArray := inputResult.Array()
result := rawJSON
// Directly modify role values for items with "system" role
for i := 0; i < len(inputArray); i++ {
rolePath := fmt.Sprintf("input.%d.role", i)
if gjson.GetBytes(result, rolePath).String() == "system" {
result, _ = sjson.SetBytes(result, rolePath, "developer")
}
}
return result
}

View File

@@ -0,0 +1,265 @@
package responses
import (
"testing"
"github.com/tidwall/gjson"
)
// TestConvertSystemRoleToDeveloper_BasicConversion tests the basic system -> developer role conversion
func TestConvertSystemRoleToDeveloper_BasicConversion(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": "You are a pirate."}]
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Say hello."}]
}
]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that system role was converted to developer
firstItemRole := gjson.Get(outputStr, "input.0.role")
if firstItemRole.String() != "developer" {
t.Errorf("Expected role 'developer', got '%s'", firstItemRole.String())
}
// Check that user role remains unchanged
secondItemRole := gjson.Get(outputStr, "input.1.role")
if secondItemRole.String() != "user" {
t.Errorf("Expected role 'user', got '%s'", secondItemRole.String())
}
// Check content is preserved
firstItemContent := gjson.Get(outputStr, "input.0.content.0.text")
if firstItemContent.String() != "You are a pirate." {
t.Errorf("Expected content 'You are a pirate.', got '%s'", firstItemContent.String())
}
}
// TestConvertSystemRoleToDeveloper_MultipleSystemMessages tests conversion with multiple system messages
func TestConvertSystemRoleToDeveloper_MultipleSystemMessages(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": "You are helpful."}]
},
{
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": "Be concise."}]
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Hello"}]
}
]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that both system roles were converted
firstRole := gjson.Get(outputStr, "input.0.role")
if firstRole.String() != "developer" {
t.Errorf("Expected first role 'developer', got '%s'", firstRole.String())
}
secondRole := gjson.Get(outputStr, "input.1.role")
if secondRole.String() != "developer" {
t.Errorf("Expected second role 'developer', got '%s'", secondRole.String())
}
// Check that user role is unchanged
thirdRole := gjson.Get(outputStr, "input.2.role")
if thirdRole.String() != "user" {
t.Errorf("Expected third role 'user', got '%s'", thirdRole.String())
}
}
// TestConvertSystemRoleToDeveloper_NoSystemMessages tests that requests without system messages are unchanged
func TestConvertSystemRoleToDeveloper_NoSystemMessages(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Hello"}]
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Hi there!"}]
}
]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that user and assistant roles are unchanged
firstRole := gjson.Get(outputStr, "input.0.role")
if firstRole.String() != "user" {
t.Errorf("Expected role 'user', got '%s'", firstRole.String())
}
secondRole := gjson.Get(outputStr, "input.1.role")
if secondRole.String() != "assistant" {
t.Errorf("Expected role 'assistant', got '%s'", secondRole.String())
}
}
// TestConvertSystemRoleToDeveloper_EmptyInput tests that empty input arrays are handled correctly
func TestConvertSystemRoleToDeveloper_EmptyInput(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": []
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that input is still an empty array
inputArray := gjson.Get(outputStr, "input")
if !inputArray.IsArray() {
t.Error("Input should still be an array")
}
if len(inputArray.Array()) != 0 {
t.Errorf("Expected empty array, got %d items", len(inputArray.Array()))
}
}
// TestConvertSystemRoleToDeveloper_NoInputField tests that requests without input field are unchanged
func TestConvertSystemRoleToDeveloper_NoInputField(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"stream": false
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that other fields are still set correctly
stream := gjson.Get(outputStr, "stream")
if !stream.Bool() {
t.Error("Stream should be set to true by conversion")
}
store := gjson.Get(outputStr, "store")
if store.Bool() {
t.Error("Store should be set to false by conversion")
}
}
// TestConvertOpenAIResponsesRequestToCodex_OriginalIssue tests the exact issue reported by the user
func TestConvertOpenAIResponsesRequestToCodex_OriginalIssue(t *testing.T) {
// This is the exact input that was failing with "System messages are not allowed"
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "system",
"content": "You are a pirate. Always respond in pirate speak."
},
{
"type": "message",
"role": "user",
"content": "Say hello."
}
],
"stream": false
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Verify system role was converted to developer
firstRole := gjson.Get(outputStr, "input.0.role")
if firstRole.String() != "developer" {
t.Errorf("Expected role 'developer', got '%s'", firstRole.String())
}
// Verify stream was set to true (as required by Codex)
stream := gjson.Get(outputStr, "stream")
if !stream.Bool() {
t.Error("Stream should be set to true")
}
// Verify other required fields for Codex
store := gjson.Get(outputStr, "store")
if store.Bool() {
t.Error("Store should be false")
}
parallelCalls := gjson.Get(outputStr, "parallel_tool_calls")
if !parallelCalls.Bool() {
t.Error("parallel_tool_calls should be true")
}
include := gjson.Get(outputStr, "include")
if !include.IsArray() || len(include.Array()) != 1 {
t.Error("include should be an array with one element")
} else if include.Array()[0].String() != "reasoning.encrypted_content" {
t.Errorf("Expected include[0] to be 'reasoning.encrypted_content', got '%s'", include.Array()[0].String())
}
}
// TestConvertSystemRoleToDeveloper_AssistantRole tests that assistant role is preserved
func TestConvertSystemRoleToDeveloper_AssistantRole(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": "You are helpful."}]
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Hello"}]
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Hi!"}]
}
]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check system -> developer
firstRole := gjson.Get(outputStr, "input.0.role")
if firstRole.String() != "developer" {
t.Errorf("Expected first role 'developer', got '%s'", firstRole.String())
}
// Check user unchanged
secondRole := gjson.Get(outputStr, "input.1.role")
if secondRole.String() != "user" {
t.Errorf("Expected second role 'user', got '%s'", secondRole.String())
}
// Check assistant unchanged
thirdRole := gjson.Get(outputStr, "input.2.role")
if thirdRole.String() != "assistant" {
t.Errorf("Expected third role 'assistant', got '%s'", thirdRole.String())
}
}