fix(openai): normalize codex responses payloads
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
@@ -153,6 +154,9 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
|
||||
if normalizeCodexTools(reqBody) {
|
||||
result.Modified = true
|
||||
}
|
||||
if normalizeCodexToolChoice(reqBody) {
|
||||
result.Modified = true
|
||||
}
|
||||
|
||||
if v, ok := reqBody["prompt_cache_key"].(string); ok {
|
||||
result.PromptCacheKey = strings.TrimSpace(v)
|
||||
@@ -173,6 +177,14 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
|
||||
|
||||
// 续链场景保留 item_reference 与 id,避免 call_id 上下文丢失。
|
||||
if input, ok := reqBody["input"].([]any); ok {
|
||||
if normalizedInput, modified := normalizeCodexToolRoleMessages(input); modified {
|
||||
input = normalizedInput
|
||||
result.Modified = true
|
||||
}
|
||||
if normalizedInput, modified := normalizeCodexMessageContentText(input); modified {
|
||||
input = normalizedInput
|
||||
result.Modified = true
|
||||
}
|
||||
input = filterCodexInput(input, needsToolContinuation)
|
||||
reqBody["input"] = input
|
||||
result.Modified = true
|
||||
@@ -197,6 +209,183 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeCodexToolChoice(reqBody map[string]any) bool {
|
||||
choice, ok := reqBody["tool_choice"]
|
||||
if !ok || choice == nil {
|
||||
return false
|
||||
}
|
||||
choiceMap, ok := choice.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
choiceType := strings.TrimSpace(firstNonEmptyString(choiceMap["type"]))
|
||||
if choiceType == "" || codexToolsContainType(reqBody["tools"], choiceType) {
|
||||
return false
|
||||
}
|
||||
reqBody["tool_choice"] = "auto"
|
||||
return true
|
||||
}
|
||||
|
||||
func codexToolsContainType(rawTools any, toolType string) bool {
|
||||
tools, ok := rawTools.([]any)
|
||||
if !ok || strings.TrimSpace(toolType) == "" {
|
||||
return false
|
||||
}
|
||||
for _, rawTool := range tools {
|
||||
tool, ok := rawTool.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(firstNonEmptyString(tool["type"])) == toolType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeCodexToolRoleMessages(input []any) ([]any, bool) {
|
||||
if len(input) == 0 {
|
||||
return input, false
|
||||
}
|
||||
|
||||
modified := false
|
||||
normalized := make([]any, 0, len(input))
|
||||
for _, item := range input {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
normalized = append(normalized, item)
|
||||
continue
|
||||
}
|
||||
role, _ := m["role"].(string)
|
||||
if strings.TrimSpace(role) != "tool" {
|
||||
normalized = append(normalized, item)
|
||||
continue
|
||||
}
|
||||
|
||||
callID := firstNonEmptyString(m["call_id"], m["tool_call_id"], m["id"])
|
||||
callID = strings.TrimSpace(callID)
|
||||
if callID == "" {
|
||||
// Responses does not accept role:"tool". If no call id is available,
|
||||
// preserve the text as a user message instead of sending invalid input.
|
||||
fallback := make(map[string]any, len(m))
|
||||
for key, value := range m {
|
||||
fallback[key] = value
|
||||
}
|
||||
fallback["role"] = "user"
|
||||
delete(fallback, "tool_call_id")
|
||||
normalized = append(normalized, fallback)
|
||||
modified = true
|
||||
continue
|
||||
}
|
||||
|
||||
output := extractTextFromContent(m["content"])
|
||||
if output == "" {
|
||||
if value, ok := m["output"].(string); ok {
|
||||
output = value
|
||||
}
|
||||
}
|
||||
if output == "" && m["content"] != nil {
|
||||
if b, err := json.Marshal(m["content"]); err == nil {
|
||||
output = string(b)
|
||||
}
|
||||
}
|
||||
|
||||
normalized = append(normalized, map[string]any{
|
||||
"type": "function_call_output",
|
||||
"call_id": callID,
|
||||
"output": output,
|
||||
})
|
||||
modified = true
|
||||
}
|
||||
if !modified {
|
||||
return input, false
|
||||
}
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
func normalizeCodexMessageContentText(input []any) ([]any, bool) {
|
||||
if len(input) == 0 {
|
||||
return input, false
|
||||
}
|
||||
|
||||
modified := false
|
||||
normalized := make([]any, 0, len(input))
|
||||
for _, item := range input {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok || strings.TrimSpace(firstNonEmptyString(m["type"])) != "message" {
|
||||
normalized = append(normalized, item)
|
||||
continue
|
||||
}
|
||||
parts, ok := m["content"].([]any)
|
||||
if !ok {
|
||||
normalized = append(normalized, item)
|
||||
continue
|
||||
}
|
||||
|
||||
var newItem map[string]any
|
||||
var newParts []any
|
||||
ensureItemCopy := func() {
|
||||
if newItem != nil {
|
||||
return
|
||||
}
|
||||
newItem = make(map[string]any, len(m))
|
||||
for key, value := range m {
|
||||
newItem[key] = value
|
||||
}
|
||||
newParts = make([]any, len(parts))
|
||||
copy(newParts, parts)
|
||||
}
|
||||
|
||||
for i, rawPart := range parts {
|
||||
part, ok := rawPart.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
text, hasText := part["text"]
|
||||
if !hasText {
|
||||
continue
|
||||
}
|
||||
if _, ok := text.(string); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ensureItemCopy()
|
||||
newPart := make(map[string]any, len(part))
|
||||
for key, value := range part {
|
||||
newPart[key] = value
|
||||
}
|
||||
newPart["text"] = stringifyCodexContentText(text)
|
||||
newParts[i] = newPart
|
||||
modified = true
|
||||
}
|
||||
|
||||
if newItem != nil {
|
||||
newItem["content"] = newParts
|
||||
normalized = append(normalized, newItem)
|
||||
continue
|
||||
}
|
||||
normalized = append(normalized, item)
|
||||
}
|
||||
if !modified {
|
||||
return input, false
|
||||
}
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
func stringifyCodexContentText(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case nil:
|
||||
return ""
|
||||
default:
|
||||
if b, err := json.Marshal(v); err == nil {
|
||||
return string(b)
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCodexModel(model string) string {
|
||||
model = strings.TrimSpace(model)
|
||||
if model == "" {
|
||||
@@ -729,6 +918,22 @@ func filterCodexInput(input []any, preserveReferences bool) []any {
|
||||
delete(newItem, "call_id")
|
||||
}
|
||||
|
||||
if codexInputItemRequiresName(typ) {
|
||||
if strings.TrimSpace(firstNonEmptyString(m["name"])) == "" {
|
||||
name := firstNonEmptyString(m["tool_name"])
|
||||
if name == "" {
|
||||
if function, ok := m["function"].(map[string]any); ok {
|
||||
name = firstNonEmptyString(function["name"])
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
name = "tool"
|
||||
}
|
||||
ensureCopy()
|
||||
newItem["name"] = name
|
||||
}
|
||||
}
|
||||
|
||||
if !preserveReferences {
|
||||
ensureCopy()
|
||||
delete(newItem, "id")
|
||||
@@ -756,6 +961,15 @@ func isCodexToolCallItemType(typ string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func codexInputItemRequiresName(typ string) bool {
|
||||
switch strings.TrimSpace(typ) {
|
||||
case "function_call", "custom_tool_call", "mcp_tool_call":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCodexTools(reqBody map[string]any) bool {
|
||||
rawTools, ok := reqBody["tools"]
|
||||
if !ok || rawTools == nil {
|
||||
|
||||
Reference in New Issue
Block a user