mirror of
https://mirror.skon.top/github.com/router-for-me/CLIProxyAPI
synced 2026-05-01 00:30:55 +08:00
Merge pull request #2962 from MoYeRanqianzhi/feat/codex-image-generation-tool-injection
feat(codex): inject image_generation tool + route aliases for Codex CLI image generation
This commit is contained in:
@@ -353,6 +353,15 @@ func (s *Server) setupRoutes() {
|
||||
v1.POST("/responses/compact", openaiResponsesHandlers.Compact)
|
||||
}
|
||||
|
||||
// Codex CLI direct route aliases (chatgpt_base_url compatible)
|
||||
codexDirect := s.engine.Group("/backend-api/codex")
|
||||
codexDirect.Use(AuthMiddleware(s.accessManager))
|
||||
{
|
||||
codexDirect.GET("/responses", openaiResponsesHandlers.ResponsesWebsocket)
|
||||
codexDirect.POST("/responses", openaiResponsesHandlers.Responses)
|
||||
codexDirect.POST("/responses/compact", openaiResponsesHandlers.Compact)
|
||||
}
|
||||
|
||||
// Gemini compatible API routes
|
||||
v1beta := s.engine.Group("/v1beta")
|
||||
v1beta.Use(AuthMiddleware(s.accessManager))
|
||||
|
||||
@@ -180,6 +180,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
||||
body, _ = sjson.DeleteBytes(body, "safety_identifier")
|
||||
body, _ = sjson.DeleteBytes(body, "stream_options")
|
||||
body = normalizeCodexInstructions(body)
|
||||
body = ensureImageGenerationTool(body)
|
||||
|
||||
url := strings.TrimSuffix(baseURL, "/") + "/responses"
|
||||
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
|
||||
@@ -326,6 +327,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||
body, _ = sjson.DeleteBytes(body, "stream")
|
||||
body = normalizeCodexInstructions(body)
|
||||
body = ensureImageGenerationTool(body)
|
||||
|
||||
url := strings.TrimSuffix(baseURL, "/") + "/responses/compact"
|
||||
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
|
||||
@@ -420,6 +422,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
||||
body, _ = sjson.DeleteBytes(body, "stream_options")
|
||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||
body = normalizeCodexInstructions(body)
|
||||
body = ensureImageGenerationTool(body)
|
||||
|
||||
url := strings.TrimSuffix(baseURL, "/") + "/responses"
|
||||
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
|
||||
@@ -821,6 +824,24 @@ func normalizeCodexInstructions(body []byte) []byte {
|
||||
return body
|
||||
}
|
||||
|
||||
var imageGenToolJSON = []byte(`{"type":"image_generation","output_format":"png"}`)
|
||||
var imageGenToolArrayJSON = []byte(`[{"type":"image_generation","output_format":"png"}]`)
|
||||
|
||||
func ensureImageGenerationTool(body []byte) []byte {
|
||||
tools := gjson.GetBytes(body, "tools")
|
||||
if !tools.Exists() || !tools.IsArray() {
|
||||
body, _ = sjson.SetRawBytes(body, "tools", imageGenToolArrayJSON)
|
||||
return body
|
||||
}
|
||||
for _, t := range tools.Array() {
|
||||
if t.Get("type").String() == "image_generation" {
|
||||
return body
|
||||
}
|
||||
}
|
||||
body, _ = sjson.SetRawBytes(body, "tools.-1", imageGenToolJSON)
|
||||
return body
|
||||
}
|
||||
|
||||
func isCodexModelCapacityError(errorBody []byte) bool {
|
||||
if len(errorBody) == 0 {
|
||||
return false
|
||||
|
||||
89
internal/runtime/executor/codex_executor_imagegen_test.go
Normal file
89
internal/runtime/executor/codex_executor_imagegen_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestEnsureImageGenerationTool_NoTools(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","input":"draw a cat"}`)
|
||||
result := ensureImageGenerationTool(body)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
if !tools.IsArray() {
|
||||
t.Fatalf("expected tools array, got %v", tools.Type)
|
||||
}
|
||||
arr := tools.Array()
|
||||
if len(arr) != 1 {
|
||||
t.Fatalf("expected 1 tool, got %d", len(arr))
|
||||
}
|
||||
if arr[0].Get("type").String() != "image_generation" {
|
||||
t.Fatalf("expected type=image_generation, got %s", arr[0].Get("type").String())
|
||||
}
|
||||
if arr[0].Get("output_format").String() != "png" {
|
||||
t.Fatalf("expected output_format=png, got %s", arr[0].Get("output_format").String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureImageGenerationTool_ExistingToolsWithoutImageGen(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"function","name":"get_weather","parameters":{}}]}`)
|
||||
result := ensureImageGenerationTool(body)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
arr := tools.Array()
|
||||
if len(arr) != 2 {
|
||||
t.Fatalf("expected 2 tools, got %d", len(arr))
|
||||
}
|
||||
if arr[0].Get("type").String() != "function" {
|
||||
t.Fatalf("expected first tool type=function, got %s", arr[0].Get("type").String())
|
||||
}
|
||||
if arr[1].Get("type").String() != "image_generation" {
|
||||
t.Fatalf("expected second tool type=image_generation, got %s", arr[1].Get("type").String())
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
arr := tools.Array()
|
||||
if len(arr) != 2 {
|
||||
t.Fatalf("expected 2 tools (no duplicate), got %d", len(arr))
|
||||
}
|
||||
if arr[0].Get("output_format").String() != "webp" {
|
||||
t.Fatalf("expected original output_format=webp preserved, got %s", arr[0].Get("output_format").String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureImageGenerationTool_EmptyToolsArray(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[]}`)
|
||||
result := ensureImageGenerationTool(body)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
arr := tools.Array()
|
||||
if len(arr) != 1 {
|
||||
t.Fatalf("expected 1 tool, got %d", len(arr))
|
||||
}
|
||||
if arr[0].Get("type").String() != "image_generation" {
|
||||
t.Fatalf("expected type=image_generation, got %s", arr[0].Get("type").String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureImageGenerationTool_WebSearchAndImageGen(t *testing.T) {
|
||||
body := []byte(`{"model":"gpt-5.4","tools":[{"type":"web_search"}]}`)
|
||||
result := ensureImageGenerationTool(body)
|
||||
|
||||
tools := gjson.GetBytes(result, "tools")
|
||||
arr := tools.Array()
|
||||
if len(arr) != 2 {
|
||||
t.Fatalf("expected 2 tools, got %d", len(arr))
|
||||
}
|
||||
if arr[0].Get("type").String() != "web_search" {
|
||||
t.Fatalf("expected first tool type=web_search, got %s", arr[0].Get("type").String())
|
||||
}
|
||||
if arr[1].Get("type").String() != "image_generation" {
|
||||
t.Fatalf("expected second tool type=image_generation, got %s", arr[1].Get("type").String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user