mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
fix(executor): enhance image generation tool handling for responses-lite requests
This commit is contained in:
@@ -38,6 +38,8 @@ const (
|
||||
codexUserAgent = "codex-tui/0.135.0 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10 (codex-tui; 0.135.0)"
|
||||
codexOriginator = "codex-tui"
|
||||
codexDefaultImageToolModel = "gpt-image-2"
|
||||
codexResponsesLiteHeader = "X-OpenAI-Internal-Codex-Responses-Lite"
|
||||
codexResponsesLiteMetadata = "client_metadata.ws_request_header_x_openai_internal_codex_responses_lite"
|
||||
)
|
||||
|
||||
var dataTag = []byte("data:")
|
||||
@@ -782,7 +784,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
||||
body, _ = sjson.DeleteBytes(body, "stream_options")
|
||||
body = normalizeCodexInstructions(body)
|
||||
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
|
||||
body = ensureImageGenerationTool(body, baseModel, auth)
|
||||
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
|
||||
}
|
||||
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body)
|
||||
body = normalizeCodexParallelToolCallsForTools(body)
|
||||
@@ -959,7 +961,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
||||
body, _ = sjson.DeleteBytes(body, "stream")
|
||||
body = normalizeCodexInstructions(body)
|
||||
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
|
||||
body = ensureImageGenerationTool(body, baseModel, auth)
|
||||
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
|
||||
}
|
||||
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body)
|
||||
body = normalizeCodexParallelToolCallsForTools(body)
|
||||
@@ -1070,7 +1072,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||
body = normalizeCodexInstructions(body)
|
||||
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
|
||||
body = ensureImageGenerationTool(body, baseModel, auth)
|
||||
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
|
||||
}
|
||||
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body)
|
||||
body = normalizeCodexParallelToolCallsForTools(body)
|
||||
@@ -1767,7 +1769,22 @@ func isImageGenerationFunctionTool(tool gjson.Result) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureImageGenerationTool(body []byte, baseModel string, auth *cliproxyauth.Auth) []byte {
|
||||
func isCodexResponsesLiteRequest(body []byte, headers http.Header) bool {
|
||||
if strings.EqualFold(strings.TrimSpace(headers.Get(codexResponsesLiteHeader)), "true") {
|
||||
return true
|
||||
}
|
||||
// Codex Desktop mirrors websocket-only request headers into client_metadata.
|
||||
value := gjson.GetBytes(body, codexResponsesLiteMetadata)
|
||||
if !value.Exists() {
|
||||
return false
|
||||
}
|
||||
return value.Type == gjson.True || value.Type == gjson.String && strings.EqualFold(strings.TrimSpace(value.String()), "true")
|
||||
}
|
||||
|
||||
func ensureImageGenerationTool(body []byte, baseModel string, auth *cliproxyauth.Auth, headers http.Header) []byte {
|
||||
if isCodexResponsesLiteRequest(body, headers) {
|
||||
return body
|
||||
}
|
||||
if strings.HasSuffix(baseModel, "spark") {
|
||||
return body
|
||||
}
|
||||
|
||||
@@ -1,15 +1,103 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func TestCodexExecutorExecuteResponsesLiteHeaderDoesNotInjectImageGenerationTool(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",
|
||||
},
|
||||
}
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-OpenAI-Internal-Codex-Responses-Lite", "true")
|
||||
|
||||
_, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
||||
Model: "gpt-5.6-sol",
|
||||
Payload: []byte(`{"model":"gpt-5.6-sol","input":"hello"}`),
|
||||
}, cliproxyexecutor.Options{
|
||||
SourceFormat: sdktranslator.FromString("openai-response"),
|
||||
Headers: headers,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
if tools := gjson.GetBytes(gotBody, "tools"); tools.Exists() {
|
||||
t.Fatalf("unexpected tools in responses-lite upstream payload: %s", tools.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureImageGenerationTool_ResponsesLiteMetadataDoesNotInjectTool(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.6-sol","client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":"true"},"input":[{"role":"user","content":"hello"}]}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.6-sol", nil, nil)
|
||||
|
||||
if string(result) != string(body) {
|
||||
t.Fatalf("expected responses-lite body to be unchanged, got %s", string(result))
|
||||
}
|
||||
if gjson.GetBytes(result, "tools").Exists() {
|
||||
t.Fatalf("expected no injected tools for responses-lite request, got %s", gjson.GetBytes(result, "tools").Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureImageGenerationTool_ResponsesLiteBooleanMetadataDoesNotInjectTool(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.6-sol","client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":true},"input":"hello"}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.6-sol", nil, nil)
|
||||
|
||||
if string(result) != string(body) {
|
||||
t.Fatalf("expected responses-lite body to be unchanged, got %s", string(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureImageGenerationTool_ResponsesLiteHeaderDoesNotInjectTool(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.6-sol","input":"hello"}`)
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-OpenAI-Internal-Codex-Responses-Lite", "true")
|
||||
result := ensureImageGenerationTool(body, "gpt-5.6-sol", nil, headers)
|
||||
|
||||
if string(result) != string(body) {
|
||||
t.Fatalf("expected responses-lite body to be unchanged, got %s", string(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureImageGenerationTool_ResponsesLiteFalseMetadataStillInjectsTool(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.6-sol","client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":"false"},"input":"hello"}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.6-sol", nil, nil)
|
||||
|
||||
if got := gjson.GetBytes(result, "tools.0.type").String(); got != "image_generation" {
|
||||
t.Fatalf("tools.0.type = %q, want image_generation; body=%s", got, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureImageGenerationTool_NoTools(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","input":"draw a cat"}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil, nil)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
if !tools.IsArray() {
|
||||
@@ -29,7 +117,7 @@ func TestEnsureImageGenerationTool_NoTools(t *testing.T) {
|
||||
|
||||
func TestEnsureImageGenerationTool_ExistingToolsWithoutImageGen(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"function","name":"get_weather","parameters":{}}]}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil, nil)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
arr := tools.Array()
|
||||
@@ -46,7 +134,7 @@ func TestEnsureImageGenerationTool_ExistingToolsWithoutImageGen(t *testing.T) {
|
||||
|
||||
func TestEnsureImageGenerationTool_AlreadyPresent(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"image_generation","output_format":"webp"},{"type":"function","name":"f1"}]}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil, nil)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
arr := tools.Array()
|
||||
@@ -60,7 +148,7 @@ func TestEnsureImageGenerationTool_AlreadyPresent(t *testing.T) {
|
||||
|
||||
func TestEnsureImageGenerationTool_ImageGenNamespaceDoesNotInjectTool(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"namespace","name":"image_gen","tools":[{"type":"function","name":"imagegen","parameters":{}}]}]}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil, nil)
|
||||
|
||||
if string(result) != string(body) {
|
||||
t.Fatalf("expected body to be unchanged, got %s", string(result))
|
||||
@@ -69,7 +157,7 @@ func TestEnsureImageGenerationTool_ImageGenNamespaceDoesNotInjectTool(t *testing
|
||||
|
||||
func TestEnsureImageGenerationTool_FlattenedImageGenFunctionDoesNotInjectTool(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"function","name":"image_gen.imagegen","parameters":{}}]}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil, nil)
|
||||
|
||||
if string(result) != string(body) {
|
||||
t.Fatalf("expected body to be unchanged, got %s", string(result))
|
||||
@@ -78,7 +166,7 @@ func TestEnsureImageGenerationTool_FlattenedImageGenFunctionDoesNotInjectTool(t
|
||||
|
||||
func TestEnsureImageGenerationTool_SimilarNamespaceStillInjectsTool(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"namespace","name":"image_tools","tools":[{"type":"function","name":"imagegen","parameters":{}}]}]}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil, nil)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools").Array()
|
||||
if len(tools) != 2 {
|
||||
@@ -91,7 +179,7 @@ func TestEnsureImageGenerationTool_SimilarNamespaceStillInjectsTool(t *testing.T
|
||||
|
||||
func TestEnsureImageGenerationTool_EmptyToolsArray(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[]}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil, nil)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
arr := tools.Array()
|
||||
@@ -105,7 +193,7 @@ func TestEnsureImageGenerationTool_EmptyToolsArray(t *testing.T) {
|
||||
|
||||
func TestEnsureImageGenerationTool_WebSearchAndImageGen(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"web_search"}]}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", nil, nil)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
arr := tools.Array()
|
||||
@@ -122,7 +210,7 @@ func TestEnsureImageGenerationTool_WebSearchAndImageGen(t *testing.T) {
|
||||
|
||||
func TestEnsureImageGenerationTool_GPT53CodexSparkDoesNotInjectTool(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.3-codex-spark","input":"draw a cat"}`)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.3-codex-spark", nil)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.3-codex-spark", nil, nil)
|
||||
|
||||
if string(result) != string(body) {
|
||||
t.Fatalf("expected body to be unchanged, got %s", string(result))
|
||||
@@ -138,7 +226,7 @@ func TestEnsureImageGenerationTool_FreeCodexAuthDoesNotInjectTool(t *testing.T)
|
||||
Provider: "codex",
|
||||
Attributes: map[string]string{"plan_type": "free"},
|
||||
}
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", freeAuth)
|
||||
result := ensureImageGenerationTool(body, "gpt-5.4", freeAuth, nil)
|
||||
|
||||
if string(result) != string(body) {
|
||||
t.Fatalf("expected body to be unchanged, got %s", string(result))
|
||||
|
||||
@@ -212,7 +212,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
||||
body, _ = sjson.DeleteBytes(body, "safety_identifier")
|
||||
body = normalizeCodexInstructions(body)
|
||||
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
|
||||
body = ensureImageGenerationTool(body, baseModel, auth)
|
||||
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
|
||||
}
|
||||
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex websockets executor", body)
|
||||
|
||||
@@ -434,7 +434,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||
body = normalizeCodexInstructions(body)
|
||||
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
|
||||
body = ensureImageGenerationTool(body, baseModel, auth)
|
||||
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
|
||||
}
|
||||
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex websockets executor", body)
|
||||
|
||||
|
||||
@@ -40,6 +40,64 @@ func TestBuildCodexWebsocketRequestBodyPreservesPreviousResponseID(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexWebsocketsExecuteResponsesLiteDoesNotInjectImageGenerationTool(t *testing.T) {
|
||||
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
|
||||
capturedPayload := make(chan []byte, 1)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("upgrade websocket: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_, payload, errRead := conn.ReadMessage()
|
||||
if errRead != nil {
|
||||
t.Fatalf("read upstream websocket message: %v", errRead)
|
||||
}
|
||||
capturedPayload <- bytes.Clone(payload)
|
||||
|
||||
completed := []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[],"usage":{"input_tokens":0,"output_tokens":0,"total_tokens":0}}}`)
|
||||
if errWrite := conn.WriteMessage(websocket.TextMessage, completed); errWrite != nil {
|
||||
t.Fatalf("write completed websocket message: %v", errWrite)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
exec := NewCodexWebsocketsExecutor(&config.Config{})
|
||||
auth := &cliproxyauth.Auth{
|
||||
Provider: "codex",
|
||||
Attributes: map[string]string{
|
||||
"api_key": "sk-test",
|
||||
"base_url": server.URL,
|
||||
"plan_type": "pro",
|
||||
},
|
||||
}
|
||||
req := cliproxyexecutor.Request{
|
||||
Model: "gpt-5.6-sol",
|
||||
Payload: []byte(`{"model":"gpt-5.6-sol","input":[{"type":"additional_tools","role":"developer","tools":[{"type":"custom","name":"exec"}]},{"role":"user","content":"hello"}],"client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":"true"}}`),
|
||||
}
|
||||
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("codex")}
|
||||
|
||||
if _, err := exec.Execute(context.Background(), auth, req, opts); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case payload := <-capturedPayload:
|
||||
if tools := gjson.GetBytes(payload, "tools"); tools.Exists() {
|
||||
t.Fatalf("unexpected tools in responses-lite upstream payload: %s", tools.Raw)
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "input.0.type").String(); got != "additional_tools" {
|
||||
t.Fatalf("input.0.type = %q, want additional_tools; payload=%s", got, payload)
|
||||
}
|
||||
if got := gjson.GetBytes(payload, "client_metadata.ws_request_header_x_openai_internal_codex_responses_lite").String(); got != "true" {
|
||||
t.Fatalf("responses-lite metadata = %q, want true; payload=%s", got, payload)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for upstream websocket payload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexWebsocketsExecutePreservesPreviousResponseIDUpstream(t *testing.T) {
|
||||
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
|
||||
capturedPayload := make(chan []byte, 1)
|
||||
|
||||
Reference in New Issue
Block a user