Skip to content

Commit fd93783

Browse files
authored
- Store must be set to false (#252)
- 处理:Codex 请求强制输出 store:false,并在 Claude→Codex 转换中显式设置。 - 相关:internal/converter/types_codex.go, internal/converter/ claude_to_codex.go - Unsupported parameter: max_output_tokens - 处理:上游前把 max_output_tokens 改写为 max_tokens 并移除旧字段。 - 相关:internal/adapter/provider/codex/adapter.go, internal/adapter/provider/ codex/adapter_test.go - Unknown parameter: input[i].role - 处理:非 message 类型的 input 项去掉 role,保留 message 的 role;同时去掉 Claude→Codex 中 function_call 的 role。 - 相关:internal/adapter/provider/codex/adapter.go, internal/adapter/provider/ cliproxyapi_codex/adapter.go, internal/converter/claude_to_codex.go - Invalid input[i].id ... Expected an ID that begins with 'fc' - 处理:function_call 的 id 统一前缀 fc_;并在清洗阶段兜底修正。 - 相关:internal/converter/claude_to_codex.go, internal/adapter/provider/codex/ adapter.go, internal/adapter/provider/cliproxyapi_codex/adapter.go - Missing required parameter: input[i].output - 处理:tool_result 内容统一转成字符串;若缺 output 字段则补空字符串。 - 相关:internal/converter/claude_to_codex.go, internal/adapter/provider/codex/ adapter.go, internal/adapter/provider/cliproxyapi_codex/adapter.go
1 parent 36388d8 commit fd93783

File tree

4 files changed

+110
-22
lines changed

4 files changed

+110
-22
lines changed

internal/adapter/provider/cliproxyapi_codex/adapter.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
2222
"github.com/router-for-me/CLIProxyAPI/v6/sdk/exec"
2323
"github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
24+
"github.com/tidwall/gjson"
25+
"github.com/tidwall/sjson"
2426
)
2527

2628
// TokenCache caches access tokens
@@ -227,6 +229,11 @@ func (a *CLIProxyAPICodexAdapter) Execute(c *flow.Ctx, p *domain.Provider) error
227229
return domain.NewProxyErrorWithMessage(err, true, fmt.Sprintf("failed to get access token: %v", err))
228230
}
229231

232+
// Normalize Codex payload for upstream compatibility.
233+
if len(requestBody) > 0 {
234+
requestBody = sanitizeCodexPayload(requestBody)
235+
}
236+
230237
// 构建 executor 请求
231238
execReq := executor.Request{
232239
Model: model,
@@ -246,6 +253,30 @@ func (a *CLIProxyAPICodexAdapter) Execute(c *flow.Ctx, p *domain.Provider) error
246253
return a.executeNonStream(c, w, execReq, execOpts)
247254
}
248255

256+
func sanitizeCodexPayload(body []byte) []byte {
257+
if input := gjson.GetBytes(body, "input"); input.IsArray() {
258+
for i, item := range input.Array() {
259+
itemType := item.Get("type").String()
260+
if itemType != "message" {
261+
if item.Get("role").Exists() {
262+
body, _ = sjson.DeleteBytes(body, fmt.Sprintf("input.%d.role", i))
263+
}
264+
}
265+
if itemType == "function_call" {
266+
if id := item.Get("id").String(); id != "" && !strings.HasPrefix(id, "fc_") {
267+
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+id)
268+
}
269+
}
270+
if itemType == "function_call_output" {
271+
if !item.Get("output").Exists() {
272+
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.output", i), "")
273+
}
274+
}
275+
}
276+
}
277+
return body
278+
}
279+
249280
func (a *CLIProxyAPICodexAdapter) executeNonStream(c *flow.Ctx, w http.ResponseWriter, execReq executor.Request, execOpts executor.Options) error {
250281
ctx := context.Background()
251282
if c.Request != nil {

internal/adapter/provider/codex/adapter.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,9 +561,37 @@ func applyCodexRequestTuning(c *flow.Ctx, body []byte) (string, []byte) {
561561
body, _ = sjson.DeleteBytes(body, "previous_response_id")
562562
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
563563
body, _ = sjson.DeleteBytes(body, "safety_identifier")
564+
if maxOut := gjson.GetBytes(body, "max_output_tokens"); maxOut.Exists() {
565+
if !gjson.GetBytes(body, "max_tokens").Exists() {
566+
if updated, err := sjson.SetBytes(body, "max_tokens", maxOut.Value()); err == nil {
567+
body = updated
568+
}
569+
}
570+
body, _ = sjson.DeleteBytes(body, "max_output_tokens")
571+
}
564572
if !gjson.GetBytes(body, "instructions").Exists() {
565573
body, _ = sjson.SetBytes(body, "instructions", "")
566574
}
575+
if input := gjson.GetBytes(body, "input"); input.IsArray() {
576+
for i, item := range input.Array() {
577+
itemType := item.Get("type").String()
578+
if itemType != "message" {
579+
if item.Get("role").Exists() {
580+
body, _ = sjson.DeleteBytes(body, fmt.Sprintf("input.%d.role", i))
581+
}
582+
}
583+
if itemType == "function_call" {
584+
if id := item.Get("id").String(); id != "" && !strings.HasPrefix(id, "fc_") {
585+
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.id", i), "fc_"+id)
586+
}
587+
}
588+
if itemType == "function_call_output" {
589+
if !item.Get("output").Exists() {
590+
body, _ = sjson.SetBytes(body, fmt.Sprintf("input.%d.output", i), "")
591+
}
592+
}
593+
}
594+
}
567595

568596
return cacheID, body
569597
}

internal/adapter/provider/codex/adapter_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func TestApplyCodexRequestTuning(t *testing.T) {
1414
c.Set(flow.KeyOriginalClientType, domain.ClientTypeClaude)
1515
c.Set(flow.KeyOriginalRequestBody, []byte(`{"metadata":{"user_id":"user-123"}}`))
1616

17-
body := []byte(`{"model":"gpt-5","stream":false,"instructions":"x","previous_response_id":"r1","prompt_cache_retention":123,"safety_identifier":"s1"}`)
17+
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"}]}`)
1818
cacheID, tuned := applyCodexRequestTuning(c, body)
1919

2020
if cacheID == "" {
@@ -35,6 +35,18 @@ func TestApplyCodexRequestTuning(t *testing.T) {
3535
if gjson.GetBytes(tuned, "safety_identifier").Exists() {
3636
t.Fatalf("expected safety_identifier to be removed")
3737
}
38+
if gjson.GetBytes(tuned, "max_output_tokens").Exists() {
39+
t.Fatalf("expected max_output_tokens to be removed")
40+
}
41+
if gjson.GetBytes(tuned, "max_tokens").Int() != 77 {
42+
t.Fatalf("expected max_tokens to be set from max_output_tokens")
43+
}
44+
if gjson.GetBytes(tuned, "input.0.role").String() != "user" {
45+
t.Fatalf("expected role to be preserved for message input")
46+
}
47+
if gjson.GetBytes(tuned, "input.1.role").Exists() || gjson.GetBytes(tuned, "input.2.role").Exists() {
48+
t.Fatalf("expected role to be removed for non-message inputs")
49+
}
3850
}
3951

4052
func TestApplyCodexHeadersFiltersSensitiveAndPreservesUA(t *testing.T) {

internal/converter/claude_to_codex.go

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func (c *claudeToCodexRequest) Transform(body []byte, model string, stream bool)
2828
MaxOutputTokens: req.MaxTokens,
2929
Temperature: req.Temperature,
3030
TopP: req.TopP,
31+
Store: false,
3132
}
3233

3334
shortMap := map[string]string{}
@@ -90,26 +91,34 @@ func (c *claudeToCodexRequest) Transform(body []byte, model string, stream bool)
9091
} else {
9192
name = shortenNameIfNeeded(name)
9293
}
93-
id, _ := m["id"].(string)
94-
inputData := m["input"]
95-
argJSON, _ := json.Marshal(inputData)
96-
input = append(input, CodexInputItem{
97-
Type: "function_call",
98-
ID: id,
99-
CallID: id,
100-
Name: name,
101-
Role: "assistant",
102-
Arguments: string(argJSON),
103-
})
94+
id, _ := m["id"].(string)
95+
callID := id
96+
callItemID := ""
97+
if callID != "" {
98+
if strings.HasPrefix(callID, "fc_") {
99+
callItemID = callID
100+
} else {
101+
callItemID = "fc_" + callID
102+
}
103+
}
104+
inputData := m["input"]
105+
argJSON, _ := json.Marshal(inputData)
106+
input = append(input, CodexInputItem{
107+
Type: "function_call",
108+
ID: callItemID,
109+
CallID: callID,
110+
Name: name,
111+
Arguments: string(argJSON),
112+
})
104113
continue
105-
case "tool_result":
106-
toolUseID, _ := m["tool_use_id"].(string)
107-
resultContent, _ := m["content"].(string)
108-
input = append(input, CodexInputItem{
109-
Type: "function_call_output",
110-
CallID: toolUseID,
111-
Output: resultContent,
112-
})
114+
case "tool_result":
115+
toolUseID, _ := m["tool_use_id"].(string)
116+
resultContent := convertClaudeToolResultContentToString(m["content"])
117+
input = append(input, CodexInputItem{
118+
Type: "function_call_output",
119+
CallID: toolUseID,
120+
Output: resultContent,
121+
})
113122
continue
114123
}
115124
}
@@ -192,10 +201,18 @@ func (c *claudeToCodexResponse) Transform(body []byte) ([]byte, error) {
192201
})
193202
case "tool_use":
194203
argJSON, _ := json.Marshal(block.Input)
204+
callID := block.ID
205+
itemID := block.ID
206+
if callID != "" {
207+
if strings.HasPrefix(callID, "fc_") {
208+
callID = strings.TrimPrefix(callID, "fc_")
209+
}
210+
itemID = "fc_" + callID
211+
}
195212
codexResp.Output = append(codexResp.Output, CodexOutput{
196213
Type: "function_call",
197-
ID: block.ID,
198-
CallID: block.ID,
214+
ID: itemID,
215+
CallID: callID,
199216
Name: block.Name,
200217
Arguments: string(argJSON),
201218
Status: "completed",

0 commit comments

Comments
 (0)