provider: normalize Codex system input messages to developer

This commit is contained in:
he_sk authored and yetone committed 2026-09-24 16:44:07 +08:00
1 parent c8130269c6
commit c879d8fffe
2 files changed
+72 -1

No files matched your search

+62 -1
View File
@@ -505,7 +505,33 @@ func TestCodexAccountUpstream(t *testing.T) {
`data: {"type":"response.created","response":{"id":"r1","model":"gpt-5.5"}}`,
`data: {"type":"response.output_text.delta","delta":"pong"}`,
`data: {"type":"response.completed","response":{"id":"r1","usage":{"input_tokens":7,"output_tokens":1}}}`)}
up := httptest.NewServer(f)
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
t.Error(err)
w.WriteHeader(http.StatusBadRequest)
return
}
var request struct {
Input []struct {
Role string `json:"role"`
} `json:"input"`
}
if err := json.Unmarshal(body, &request); err != nil {
t.Error(err)
w.WriteHeader(http.StatusBadRequest)
return
}
for _, item := range request.Input {
if item.Role == "system" {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, `{"detail":"System messages are not allowed"}`)
return
}
}
r.Body = io.NopCloser(bytes.NewReader(body))
f.ServeHTTP(w, r)
}))
defer up.Close()
old := provider.CodexBase
provider.CodexBase = up.URL + "/backend-api/codex"
@@ -542,6 +568,41 @@ func TestCodexAccountUpstream(t *testing.T) {
if _, isList := upstream["input"].([]any); !isList {
t.Fatalf("relayed input not a list: %s", f.got)
}
for _, tc := range []struct {
name string
path string
body string
}{
{"anthropic", "/v1/messages", `{"model":"codex/gpt-5.5","max_tokens":20,"system":"Top-level instructions","messages":[{"role":"system","content":"First instruction"},{"role":"user","content":"ping"},{"role":"system","content":"Later instruction"}]}`},
{"responses passthrough", "/v1/responses", `{"model":"codex/gpt-5.5","stream":true,"instructions":"Top-level instructions","input":[{"type":"message","role":"system","content":[{"type":"input_text","text":"First instruction"}]},{"role":"user","content":"ping"},{"role":"system","content":"Later instruction"}]}`},
} {
t.Run(tc.name, func(t *testing.T) {
code, body := post(t, tc.path, tc.body)
if code != http.StatusOK || !strings.Contains(body, "pong") {
t.Fatalf("system instructions rejected: %d %s", code, body)
}
var request struct {
Instructions string `json:"instructions"`
Input []struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
} `json:"input"`
}
if err := json.Unmarshal(f.got, &request); err != nil {
t.Fatal(err)
}
if request.Instructions != "Top-level instructions" || len(request.Input) != 3 {
t.Fatalf("instructions lost or reordered: %s", f.got)
}
for i, want := range []struct{ role, text string }{{"developer", "First instruction"}, {"user", "ping"}, {"developer", "Later instruction"}} {
item := request.Input[i]
if item.Role != want.role || stringOrText(item.Content) != want.text {
t.Errorf("input[%d]: role=%q content=%s, want %s %q", i, item.Role, item.Content, want.role, want.text)
}
}
})
}
}
func TestConversationID(t *testing.T) {
+10
View File
@@ -740,6 +740,16 @@ func codexBody(body []byte) []byte {
m["input"] = []any{map[string]any{"type": "message", "role": "user",
"content": []any{map[string]any{"type": "input_text", "text": s}}}}
}
// Codex rejects system messages in input; developer preserves their
// instruction role without moving them out of conversation order.
if input, ok := m["input"].([]any); ok {
for _, item := range input {
msg, ok := item.(map[string]any)
if ok && msg["role"] == "system" && (msg["type"] == nil || msg["type"] == "message") {
msg["role"] = "developer"
}
}
}
out, err := json.Marshal(m)
if err != nil {
return body