mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-06 16:15:50 +08:00
Merge PR #4239: fix(codex): proxy GPT-5.6 standalone search
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -532,6 +533,7 @@ func (s *Server) setupRoutes() {
|
||||
v1.GET("/responses", openaiResponsesHandlers.ResponsesWebsocket)
|
||||
v1.POST("/responses", openaiResponsesHandlers.Responses)
|
||||
v1.POST("/responses/compact", openaiResponsesHandlers.Compact)
|
||||
v1.POST("/alpha/search", s.codexAlphaSearch)
|
||||
}
|
||||
|
||||
openaiV1 := s.engine.Group("/openai/v1")
|
||||
@@ -621,6 +623,71 @@ func (s *Server) setupRoutes() {
|
||||
// Management routes are registered lazily by registerManagementRoutes when a secret is configured.
|
||||
}
|
||||
|
||||
// codexAlphaSearch forwards the standalone search endpoint used by current
|
||||
// Codex clients. Unlike /responses, this payload is already in Codex search
|
||||
// format and must not pass through a protocol translator.
|
||||
func (s *Server) codexAlphaSearch(c *gin.Context) {
|
||||
if s == nil || s.handlers == nil || s.handlers.AuthManager == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Codex auth manager unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
var selected *auth.Auth
|
||||
for _, candidate := range s.handlers.AuthManager.List() {
|
||||
if candidate != nil && candidate.Provider == "codex" && !candidate.Disabled && !candidate.Unavailable {
|
||||
selected = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if selected == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "No available Codex OAuth credential"})
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(c.Request.Body, 16<<20))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read search request"})
|
||||
return
|
||||
}
|
||||
headers := make(http.Header)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("Accept", "application/json")
|
||||
headers.Set("Originator", "codex_cli_rs")
|
||||
for _, name := range []string{"Version", "User-Agent", "Session_id", "X-Client-Request-Id"} {
|
||||
if value := strings.TrimSpace(c.GetHeader(name)); value != "" {
|
||||
headers.Set(name, value)
|
||||
}
|
||||
}
|
||||
if accountID, ok := selected.Metadata["account_id"].(string); ok && strings.TrimSpace(accountID) != "" {
|
||||
headers.Set("Chatgpt-Account-Id", accountID)
|
||||
}
|
||||
|
||||
req, err := s.handlers.AuthManager.NewHttpRequest(
|
||||
c.Request.Context(), selected, http.MethodPost,
|
||||
"https://chatgpt.com/backend-api/codex/alpha/search", body, headers,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
resp, err := s.handlers.AuthManager.HttpRequest(c.Request.Context(), selected, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
upstreamBody, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to read Codex search response"})
|
||||
return
|
||||
}
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
c.Header("Content-Type", contentType)
|
||||
}
|
||||
c.Status(resp.StatusCode)
|
||||
_, _ = c.Writer.Write(upstreamBody)
|
||||
}
|
||||
|
||||
// AttachWebsocketRoute registers a websocket upgrade handler on the primary Gin engine.
|
||||
// The handler is served as-is without additional middleware beyond the standard stack already configured.
|
||||
func (s *Server) AttachWebsocketRoute(path string, handler http.Handler) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -19,9 +21,53 @@ import (
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
||||
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
||||
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
|
||||
)
|
||||
|
||||
type codexSearchCaptureExecutor struct {
|
||||
request *http.Request
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (e *codexSearchCaptureExecutor) Identifier() string { return "codex" }
|
||||
|
||||
func (e *codexSearchCaptureExecutor) Execute(context.Context, *auth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
|
||||
return coreexecutor.Response{}, nil
|
||||
}
|
||||
|
||||
func (e *codexSearchCaptureExecutor) ExecuteStream(context.Context, *auth.Auth, coreexecutor.Request, coreexecutor.Options) (*coreexecutor.StreamResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *codexSearchCaptureExecutor) Refresh(_ context.Context, a *auth.Auth) (*auth.Auth, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (e *codexSearchCaptureExecutor) CountTokens(context.Context, *auth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
|
||||
return coreexecutor.Response{}, nil
|
||||
}
|
||||
|
||||
func (e *codexSearchCaptureExecutor) PrepareRequest(req *http.Request, a *auth.Auth) error {
|
||||
token, _ := a.Metadata["access_token"].(string)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *codexSearchCaptureExecutor) HttpRequest(_ context.Context, _ *auth.Auth, req *http.Request) (*http.Response, error) {
|
||||
e.request = req.Clone(req.Context())
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.body = body
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"results":[{"url":"https://example.com"}]}`)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T) *Server {
|
||||
t.Helper()
|
||||
return newTestServerWithOptions(t)
|
||||
@@ -93,6 +139,53 @@ func TestHealthz(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCodexAlphaSearchForwardsRequest(t *testing.T) {
|
||||
server := newTestServer(t)
|
||||
executor := &codexSearchCaptureExecutor{}
|
||||
server.handlers.AuthManager.RegisterExecutor(executor)
|
||||
credential := &auth.Auth{
|
||||
ID: "codex-auth",
|
||||
Provider: "codex",
|
||||
Status: auth.StatusActive,
|
||||
Metadata: map[string]any{"access_token": "codex-token", "account_id": "account-123"},
|
||||
}
|
||||
if _, err := server.handlers.AuthManager.Register(context.Background(), credential); err != nil {
|
||||
t.Fatalf("register Codex auth: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/alpha/search", strings.NewReader(`{"query":"GPT-5.6"}`))
|
||||
req.Header.Set("Authorization", "Bearer test-key")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Session_id", "session-123")
|
||||
rr := httptest.NewRecorder()
|
||||
server.engine.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d; body=%s", rr.Code, http.StatusOK, rr.Body.String())
|
||||
}
|
||||
if executor.request == nil {
|
||||
t.Fatal("Codex executor did not receive a request")
|
||||
}
|
||||
if got, want := executor.request.URL.String(), "https://chatgpt.com/backend-api/codex/alpha/search"; got != want {
|
||||
t.Fatalf("upstream URL = %q, want %q", got, want)
|
||||
}
|
||||
if got, want := string(executor.body), `{"query":"GPT-5.6"}`; got != want {
|
||||
t.Fatalf("upstream body = %q, want %q", got, want)
|
||||
}
|
||||
if got := executor.request.Header.Get("Authorization"); got != "Bearer codex-token" {
|
||||
t.Fatalf("Authorization = %q", got)
|
||||
}
|
||||
if got := executor.request.Header.Get("Chatgpt-Account-Id"); got != "account-123" {
|
||||
t.Fatalf("Chatgpt-Account-Id = %q", got)
|
||||
}
|
||||
if got := executor.request.Header.Get("Session_id"); got != "session-123" {
|
||||
t.Fatalf("Session_id = %q", got)
|
||||
}
|
||||
if got := rr.Header().Get("Content-Type"); got != "application/json" {
|
||||
t.Fatalf("response Content-Type = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagementResponseExposesPluginSupportHeaderForCORS(t *testing.T) {
|
||||
t.Setenv("MANAGEMENT_PASSWORD", "test-management-key")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user