Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions internal/adapter/provider/cliproxyapi_codex/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ import (
"time"

"github.com/awsl-project/maxx/internal/adapter/provider"
"github.com/awsl-project/maxx/internal/codexutil"
"github.com/awsl-project/maxx/internal/domain"
"github.com/awsl-project/maxx/internal/flow"
"github.com/awsl-project/maxx/internal/usage"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/exec"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

// TokenCache caches access tokens
Expand Down Expand Up @@ -254,26 +253,7 @@ func (a *CLIProxyAPICodexAdapter) Execute(c *flow.Ctx, p *domain.Provider) error
}

func sanitizeCodexPayload(body []byte) []byte {
if input := gjson.GetBytes(body, "input"); input.IsArray() {
for i, item := range input.Array() {
itemType := item.Get("type").String()
if itemType != "message" {
if item.Get("role").Exists() {
body, _ = sjson.DeleteBytes(body, fmt.Sprintf("input.%d.role", i))
}
}
if itemType == "function_call" {
if id := item.Get("id").String(); id != "" && !strings.HasPrefix(id, "fc_") {
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+id)
}
}
if itemType == "function_call_output" {
if !item.Get("output").Exists() {
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.output", i), "")
}
}
}
}
body = codexutil.NormalizeCodexInput(body)
return body
}

Expand Down
22 changes: 2 additions & 20 deletions internal/adapter/provider/codex/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/awsl-project/maxx/internal/adapter/provider"
cliproxyapi "github.com/awsl-project/maxx/internal/adapter/provider/cliproxyapi_codex"
"github.com/awsl-project/maxx/internal/codexutil"
"github.com/awsl-project/maxx/internal/domain"
"github.com/awsl-project/maxx/internal/flow"
"github.com/awsl-project/maxx/internal/usage"
Expand Down Expand Up @@ -572,26 +573,7 @@ func applyCodexRequestTuning(c *flow.Ctx, body []byte) (string, []byte) {
if !gjson.GetBytes(body, "instructions").Exists() {
body, _ = sjson.SetBytes(body, "instructions", "")
}
if input := gjson.GetBytes(body, "input"); input.IsArray() {
for i, item := range input.Array() {
itemType := item.Get("type").String()
if itemType != "message" {
if item.Get("role").Exists() {
body, _ = sjson.DeleteBytes(body, fmt.Sprintf("input.%d.role", i))
}
}
if itemType == "function_call" {
if id := item.Get("id").String(); id != "" && !strings.HasPrefix(id, "fc_") {
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+id)
}
}
if itemType == "function_call_output" {
if !item.Get("output").Exists() {
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.output", i), "")
}
}
}
}
body = codexutil.NormalizeCodexInput(body)

return cacheID, body
}
Expand Down
15 changes: 13 additions & 2 deletions internal/adapter/provider/codex/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package codex

import (
"net/http"
"strings"
"testing"

"github.com/awsl-project/maxx/internal/domain"
Expand All @@ -14,7 +15,7 @@ func TestApplyCodexRequestTuning(t *testing.T) {
c.Set(flow.KeyOriginalClientType, domain.ClientTypeClaude)
c.Set(flow.KeyOriginalRequestBody, []byte(`{"metadata":{"user_id":"user-123"}}`))

body := []byte(`{"model":"gpt-5","stream":false,"instructions":"x","previous_response_id":"r1","prompt_cache_retention":123,"safety_identifier":"s1","max_output_tokens":77,"input":[{"type":"message","role":"user","content":"hi"},{"type":"function_call","role":"assistant","name":"t","arguments":"{}"},{"role":"tool","call_id":"c1","output":"ok"}]}`)
body := []byte(`{"model":"gpt-5","stream":false,"instructions":"x","previous_response_id":"r1","prompt_cache_retention":123,"safety_identifier":"s1","max_output_tokens":77,"input":[{"type":"message","role":"user","content":"hi"},{"type":"function_call","role":"assistant","name":"t","arguments":"{}","id":"toolu_01"},{"type":"function_call","name":"t2","arguments":"{}"},{"type":"function_call_output","call_id":"c1"},{"role":"tool","call_id":"c1","output":"ok"}]}`)
cacheID, tuned := applyCodexRequestTuning(c, body)

if cacheID == "" {
Expand Down Expand Up @@ -44,9 +45,19 @@ func TestApplyCodexRequestTuning(t *testing.T) {
if gjson.GetBytes(tuned, "input.0.role").String() != "user" {
t.Fatalf("expected role to be preserved for message input")
}
if gjson.GetBytes(tuned, "input.1.role").Exists() || gjson.GetBytes(tuned, "input.2.role").Exists() {
if gjson.GetBytes(tuned, "input.1.role").Exists() || gjson.GetBytes(tuned, "input.2.role").Exists() || gjson.GetBytes(tuned, "input.3.role").Exists() || gjson.GetBytes(tuned, "input.4.role").Exists() {
t.Fatalf("expected role to be removed for non-message inputs")
}
if gjson.GetBytes(tuned, "input.1.id").String() != "fc_toolu_01" {
t.Fatalf("expected function_call id to be prefixed with fc_")
}
missingID := gjson.GetBytes(tuned, "input.2.id").String()
if !strings.HasPrefix(missingID, "fc_") || missingID == "fc_" {
t.Fatalf("expected generated function_call id to be set and prefixed with fc_")
}
if gjson.GetBytes(tuned, "input.3.output").String() != "" {
t.Fatalf("expected missing function_call_output output to default to empty string")
}
}

func TestApplyCodexHeadersFiltersSensitiveAndPreservesUA(t *testing.T) {
Expand Down
43 changes: 43 additions & 0 deletions internal/codexutil/normalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package codexutil

import (
"fmt"
"strings"

"github.com/google/uuid"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

func NormalizeCodexInput(body []byte) []byte {
input := gjson.GetBytes(body, "input")
if !input.IsArray() {
return body
}

for i, item := range input.Array() {
itemType := item.Get("type").String()
if itemType != "message" {
if item.Get("role").Exists() {
body, _ = sjson.DeleteBytes(body, fmt.Sprintf("input.%d.role", i))
}
}
if itemType == "function_call" {
id := strings.TrimSpace(item.Get("id").String())
switch {
case strings.HasPrefix(id, "fc_"):
case id == "":
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+uuid.NewString())
default:
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+id)
}
}
if itemType == "function_call_output" {
if !item.Get("output").Exists() {
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.output", i), "")
}
Comment on lines +25 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

归一化对“带空白的已前缀 ID”和 output:null 仍有边界遗漏

当前逻辑在 id 已是 fc_ 前缀时不会回写去空白值;同时只判断 output 是否存在会漏掉 {"output":null}。这会让“看起来已规范化”的输入仍带异常值。

建议修复示例
 		if itemType == "function_call" {
-			id := strings.TrimSpace(item.Get("id").String())
+			rawID := item.Get("id").String()
+			id := strings.TrimSpace(rawID)
 			switch {
 			case strings.HasPrefix(id, "fc_"):
+				if rawID != id {
+					body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), id)
+				}
 			case id == "":
 				body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+uuid.NewString())
 			default:
 				body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+id)
 			}
 		}
 		if itemType == "function_call_output" {
-			if !item.Get("output").Exists() {
+			out := item.Get("output")
+			if !out.Exists() || out.Raw == "null" {
 				body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.output", i), "")
 			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if itemType == "function_call" {
id := strings.TrimSpace(item.Get("id").String())
switch {
case strings.HasPrefix(id, "fc_"):
case id == "":
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+uuid.NewString())
default:
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+id)
}
}
if itemType == "function_call_output" {
if !item.Get("output").Exists() {
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.output", i), "")
}
if itemType == "function_call" {
rawID := item.Get("id").String()
id := strings.TrimSpace(rawID)
switch {
case strings.HasPrefix(id, "fc_"):
if rawID != id {
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), id)
}
case id == "":
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+uuid.NewString())
default:
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+id)
}
}
if itemType == "function_call_output" {
out := item.Get("output")
if !out.Exists() || out.Raw == "null" {
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.output", i), "")
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/codexutil/normalize.go` around lines 25 - 38, 在处理 function_call
的分支中,确保即便 id 已有 "fc_" 前缀但包含前后空白也会被修正:对 item.Get("id").String() 做
strings.TrimSpace(id) 后,如果 strings.HasPrefix(trimmed, "fc_") 仍需用 sjson.SetBytes
将去空白后的值写回(例如 "fc_"+trimmedWithoutPrefix 或直接 trimmed),并保持现有对空 id 的生成逻辑;在处理
function_call_output 的分支,不仅检查 item.Get("output").Exists(),还要识别 output 为 JSON
null(例如通过 item.Get("output").Type == gjson.Null 或检查 item.Get("output").Raw ==
"null")并在这两种情况下把 output 写回为 ""(使用 sjson.SetBytes),以消除 {"output":null} 的边界情况。

}
}

return body
}