package executor import ( "context" "io" "net/http" "net/http/httptest" "testing" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson") // Issue #5551: Tool schema with 13-branch oneOf causes ChatGPT Codex upstream// to abort silently. The executor must simplify the complex union without destroying// property names, enum values, or sibling properties.func TestCodexExecutorSimplifiesComplexOneOfToolSchema(t *testing.T) { var gotBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } gotBody = body w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"total_tokens\":0}}}\n\n")) })) defer server.Close() executor := NewCodexExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "codex", Attributes: map[string]string{ "api_key": "test", "base_url": server.URL, "plan_type": "pro", }, } // 13-branch oneOf reproduction payload from Issue #5551 requestPayload := []byte(`{ "model": "gpt-5.5", "messages": [{"role": "user", "content": "Reply with exactly: OK"}], "tools": [{ "type": "function", "function": { "name": "t1", "description": "test tool", "parameters": { "type": "object", "properties": { "action": { "type": "string", "enum": ["p.list","m.list","s.list","s.create","s.send","s.fork","s.status","s.messages","sch.list","sch.create","sch.run","sch.delete","sch.toggle"], "oneOf": [ {"const": "p.list", "description": "List projects"}, {"const": "m.list", "description": "List models"}, {"const": "s.list", "description": "List sessions"}, {"const": "s.create", "description": "Create session"}, {"const": "s.send", "description": "Send prompt"}, {"const": "s.fork", "description": "Fork session"}, {"const": "s.status", "description": "Session status"}, {"const": "s.messages", "description": "Session messages"}, {"const": "sch.list", "description": "List schedule"}, {"const": "sch.create", "description": "Create schedule"}, {"const": "sch.run", "description": "Run schedule"}, {"const": "sch.delete", "description": "Delete schedule"}, {"const": "sch.toggle", "description": "Toggle schedule"} ], "description": "Action to perform" }, "target_id": { "type": "string", "description": "Optional target" } }, "required": ["action"] } } }] }`) _, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.5", Payload: requestPayload, }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: false, }) if err != nil { t.Fatalf("Execute() error = %v", err) } tools := gjson.GetBytes(gotBody, "tools").Array() if len(tools) == 0 { t.Fatalf("expected tools in upstream payload, got: %s", gotBody) } // Find tool t1 var t1Tool gjson.Result for _, tool := range tools { if tool.Get("name").String() == "t1" { t1Tool = tool break } } if !t1Tool.Exists() { t1Tool = tools[0] } // Complex oneOf must be removed from action property if t1Tool.Get("parameters.properties.action.oneOf").Exists() { t.Fatalf("expected complex oneOf to be removed from tool parameters, got: %s", t1Tool.Get("parameters").Raw) } // Action type and enum must be preserved if t1Tool.Get("parameters.properties.action.type").String() != "string" { t.Fatalf("expected action.type = string, got: %s", t1Tool.Get("parameters.properties.action.type").String()) } if len(t1Tool.Get("parameters.properties.action.enum").Array()) != 13 { t.Fatalf("expected action.enum to retain all 13 items") } // Sibling property target_id must be preserved if t1Tool.Get("parameters.properties.target_id.type").String() != "string" { t.Fatalf("sibling property target_id was dropped") } // Required field must be preserved if t1Tool.Get("parameters.required.0").String() != "action" { t.Fatalf("required list was dropped") }} // Issue #5551: If ChatGPT Codex upstream terminates immediately with response.incomplete,// reason: "max_output_tokens", and output_tokens: 0 (no output produced), it must be treated// as an upstream failure rather than a silent successful completion.func TestCodexExecutorExecuteStream_ZeroTokenIncompleteResponseIsFailure(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte(`data: {"type":"response.incomplete","response":{"id":"resp_1","model":"gpt-5.5","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":10,"output_tokens":0,"total_tokens":10}}}` + "\n\n")) })) defer server.Close() executor := NewCodexExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "codex", Attributes: map[string]string{ "base_url": server.URL, "api_key": "test", }, } res, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.5", Payload: []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}]}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: true, }) if err != nil { // Error returned before streaming is also valid failure return } // If streaming started, chunks must surface an error rather than silent completion sawError := false for chunk := range res.Chunks { if chunk.Err != nil { sawError = true break } } if !sawError { t.Fatalf("expected stream to fail on zero-token response.incomplete, but it completed without error") }} // Issue #5551: If a stream emitted output text deltas before response.incomplete,// it produced partial output and must NOT be treated as a zero-token empty failure.func TestCodexExecutorExecuteStream_PartialDeltasIncompleteResponseIsSuccessful(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello world\"}\n\n")) _, _ = w.Write([]byte(`data: {"type":"response.incomplete","response":{"id":"resp_1","model":"gpt-5.5","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":10,"output_tokens":0,"total_tokens":10}}}` + "\n\n")) })) defer server.Close() executor := NewCodexExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "codex", Attributes: map[string]string{ "base_url": server.URL, "api_key": "test", }, } res, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.5", Payload: []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}]}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: true, }) if err != nil { t.Fatalf("ExecuteStream() unexpected initial error: %v", err) } sawDelta := false sawError := false for chunk := range res.Chunks { if chunk.Err != nil { sawError = true } if len(chunk.Payload) > 0 { sawDelta = true } } if sawError { t.Fatalf("expected partial output stream to finish without failure, but got chunk error") } if !sawDelta { t.Fatalf("expected to receive output delta") }} // Issue #5551: Delta seen during the buffering phase must be remembered when transitioning// to the streaming goroutine so zero-token response.incomplete is not falsely flagged as empty.func TestCodexExecutorExecuteStream_BufferingPartialDeltasIncompleteResponseIsSuccessful(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"Buffered chunk\"}\n\n")) _, _ = w.Write([]byte(`data: {"type":"response.incomplete","response":{"id":"resp_1","model":"gpt-5.5","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":10,"output_tokens":0,"total_tokens":10}}}` + "\n\n")) })) defer server.Close() executor := NewCodexExecutor(&config.Config{ Codex: config.CodexConfig{ StreamBootstrapBuffering: true, }, }) auth := &cliproxyauth.Auth{ Provider: "codex", Attributes: map[string]string{ "base_url": server.URL, "api_key": "test", }, } res, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.5", Payload: []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}]}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: true, }) if err != nil { t.Fatalf("ExecuteStream() unexpected initial error: %v", err) } sawDelta := false sawError := false for chunk := range res.Chunks { if chunk.Err != nil { sawError = true } if len(chunk.Payload) > 0 { sawDelta = true } } if sawError { t.Fatalf("expected buffered partial output stream to finish without failure, but got chunk error") } if !sawDelta { t.Fatalf("expected to receive buffered output delta") }} // Issue #5551: An empty string delta ("") before response.incomplete must NOT be treated as// meaningful output, and the stream must correctly surface the zero-token failure.func TestCodexExecutorExecuteStream_EmptyDeltaDoesNotBypassZeroTokenFailure(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") _, _ = w.Write([]byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"\"}\n\n")) _, _ = w.Write([]byte(`data: {"type":"response.incomplete","response":{"id":"resp_1","model":"gpt-5.5","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":10,"output_tokens":0,"total_tokens":10}}}` + "\n\n")) })) defer server.Close() executor := NewCodexExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "codex", Attributes: map[string]string{ "base_url": server.URL, "api_key": "test", }, } res, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ Model: "gpt-5.5", Payload: []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}]}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FromString("openai"), Stream: true, }) if err != nil { return } sawError := false for chunk := range res.Chunks { if chunk.Err != nil { sawError = true break } } if !sawError { t.Fatalf("expected stream with empty delta to fail on zero-token response.incomplete, but it succeeded") }}
package executor
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
)
// Issue #5551: Tool schema with 13-branch oneOf causes ChatGPT Codex upstream
// to abort silently. The executor must simplify the complex union without destroying
// property names, enum values, or sibling properties.
func TestCodexExecutorSimplifiesComplexOneOfToolSchema(t *testing.T) {
var gotBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, errRead := io.ReadAll(r.Body)
if errRead != nil {
t.Fatalf("read request body: %v", errRead)
}
gotBody = body
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"total_tokens\":0}}}\n\n"))
}))
defer server.Close()
executor := NewCodexExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Provider: "codex",
Attributes: map[string]string{
"api_key": "test",
"base_url": server.URL,
"plan_type": "pro",
},
}
// 13-branch oneOf reproduction payload from Issue #5551
requestPayload := []byte(`{
"model": "gpt-5.5",
"messages": [{"role": "user", "content": "Reply with exactly: OK"}],
"tools": [{
"type": "function",
"function": {
"name": "t1",
"description": "test tool",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["p.list","m.list","s.list","s.create","s.send","s.fork","s.status","s.messages","sch.list","sch.create","sch.run","sch.delete","sch.toggle"],
"oneOf": [
{"const": "p.list", "description": "List projects"},
{"const": "m.list", "description": "List models"},
{"const": "s.list", "description": "List sessions"},
{"const": "s.create", "description": "Create session"},
{"const": "s.send", "description": "Send prompt"},
{"const": "s.fork", "description": "Fork session"},
{"const": "s.status", "description": "Session status"},
{"const": "s.messages", "description": "Session messages"},
{"const": "sch.list", "description": "List schedule"},
{"const": "sch.create", "description": "Create schedule"},
{"const": "sch.run", "description": "Run schedule"},
{"const": "sch.delete", "description": "Delete schedule"},
{"const": "sch.toggle", "description": "Toggle schedule"}
],
"description": "Action to perform"
},
"target_id": {
"type": "string",
"description": "Optional target"
}
},
"required": ["action"]
}
}
}]
}`)
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.5",
Payload: requestPayload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
Stream: false,
})
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
tools := gjson.GetBytes(gotBody, "tools").Array()
if len(tools) == 0 {
t.Fatalf("expected tools in upstream payload, got: %s", gotBody)
}
// Find tool t1
var t1Tool gjson.Result
for _, tool := range tools {
if tool.Get("name").String() == "t1" {
t1Tool = tool
break
}
}
if !t1Tool.Exists() {
t1Tool = tools[0]
}
// Complex oneOf must be removed from action property
if t1Tool.Get("parameters.properties.action.oneOf").Exists() {
t.Fatalf("expected complex oneOf to be removed from tool parameters, got: %s", t1Tool.Get("parameters").Raw)
}
// Action type and enum must be preserved
if t1Tool.Get("parameters.properties.action.type").String() != "string" {
t.Fatalf("expected action.type = string, got: %s", t1Tool.Get("parameters.properties.action.type").String())
}
if len(t1Tool.Get("parameters.properties.action.enum").Array()) != 13 {
t.Fatalf("expected action.enum to retain all 13 items")
}
// Sibling property target_id must be preserved
if t1Tool.Get("parameters.properties.target_id.type").String() != "string" {
t.Fatalf("sibling property target_id was dropped")
}
// Required field must be preserved
if t1Tool.Get("parameters.required.0").String() != "action" {
t.Fatalf("required list was dropped")
}
}
// Issue #5551: If ChatGPT Codex upstream terminates immediately with response.incomplete,
// reason: "max_output_tokens", and output_tokens: 0 (no output produced), it must be treated
// as an upstream failure rather than a silent successful completion.
func TestCodexExecutorExecuteStream_ZeroTokenIncompleteResponseIsFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte(`data: {"type":"response.incomplete","response":{"id":"resp_1","model":"gpt-5.5","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":10,"output_tokens":0,"total_tokens":10}}}` + "\n\n"))
}))
defer server.Close()
executor := NewCodexExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Provider: "codex",
Attributes: map[string]string{
"base_url": server.URL,
"api_key": "test",
},
}
res, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.5",
Payload: []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}]}`),
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
Stream: true,
})
if err != nil {
// Error returned before streaming is also valid failure
return
}
// If streaming started, chunks must surface an error rather than silent completion
sawError := false
for chunk := range res.Chunks {
if chunk.Err != nil {
sawError = true
break
}
}
if !sawError {
t.Fatalf("expected stream to fail on zero-token response.incomplete, but it completed without error")
}
}
// Issue #5551: If a stream emitted output text deltas before response.incomplete,
// it produced partial output and must NOT be treated as a zero-token empty failure.
func TestCodexExecutorExecuteStream_PartialDeltasIncompleteResponseIsSuccessful(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello world\"}\n\n"))
_, _ = w.Write([]byte(`data: {"type":"response.incomplete","response":{"id":"resp_1","model":"gpt-5.5","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":10,"output_tokens":0,"total_tokens":10}}}` + "\n\n"))
}))
defer server.Close()
executor := NewCodexExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Provider: "codex",
Attributes: map[string]string{
"base_url": server.URL,
"api_key": "test",
},
}
res, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.5",
Payload: []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}]}`),
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
Stream: true,
})
if err != nil {
t.Fatalf("ExecuteStream() unexpected initial error: %v", err)
}
sawDelta := false
sawError := false
for chunk := range res.Chunks {
if chunk.Err != nil {
sawError = true
}
if len(chunk.Payload) > 0 {
sawDelta = true
}
}
if sawError {
t.Fatalf("expected partial output stream to finish without failure, but got chunk error")
}
if !sawDelta {
t.Fatalf("expected to receive output delta")
}
}
// Issue #5551: Delta seen during the buffering phase must be remembered when transitioning
// to the streaming goroutine so zero-token response.incomplete is not falsely flagged as empty.
func TestCodexExecutorExecuteStream_BufferingPartialDeltasIncompleteResponseIsSuccessful(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"Buffered chunk\"}\n\n"))
_, _ = w.Write([]byte(`data: {"type":"response.incomplete","response":{"id":"resp_1","model":"gpt-5.5","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":10,"output_tokens":0,"total_tokens":10}}}` + "\n\n"))
}))
defer server.Close()
executor := NewCodexExecutor(&config.Config{
Codex: config.CodexConfig{
StreamBootstrapBuffering: true,
},
})
auth := &cliproxyauth.Auth{
Provider: "codex",
Attributes: map[string]string{
"base_url": server.URL,
"api_key": "test",
},
}
res, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.5",
Payload: []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}]}`),
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
Stream: true,
})
if err != nil {
t.Fatalf("ExecuteStream() unexpected initial error: %v", err)
}
sawDelta := false
sawError := false
for chunk := range res.Chunks {
if chunk.Err != nil {
sawError = true
}
if len(chunk.Payload) > 0 {
sawDelta = true
}
}
if sawError {
t.Fatalf("expected buffered partial output stream to finish without failure, but got chunk error")
}
if !sawDelta {
t.Fatalf("expected to receive buffered output delta")
}
}
// Issue #5551: An empty string delta ("") before response.incomplete must NOT be treated as
// meaningful output, and the stream must correctly surface the zero-token failure.
func TestCodexExecutorExecuteStream_EmptyDeltaDoesNotBypassZeroTokenFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"\"}\n\n"))
_, _ = w.Write([]byte(`data: {"type":"response.incomplete","response":{"id":"resp_1","model":"gpt-5.5","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":10,"output_tokens":0,"total_tokens":10}}}` + "\n\n"))
}))
defer server.Close()
executor := NewCodexExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
Provider: "codex",
Attributes: map[string]string{
"base_url": server.URL,
"api_key": "test",
},
}
res, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.5",
Payload: []byte(`{"model":"gpt-5.5","messages":[{"role":"user","content":"hello"}]}`),
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai"),
Stream: true,
})
if err != nil {
return
}
sawError := false
for chunk := range res.Chunks {
if chunk.Err != nil {
sawError = true
break
}
}
if !sawError {
t.Fatalf("expected stream with empty delta to fail on zero-token response.incomplete, but it succeeded")
}
}