fix(codex): use resolved compat for multi-agent v2

This commit is contained in:
hkfires
2026-09-28 19:01:06 +08:00
parent b9971ee3df
commit d3cd466ddb
6 changed files with 124 additions and 9 deletions

View File

@@ -69,7 +69,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
body = sanitizeOpenAIResponsesReasoningEncryptedContentWithCompat(ctx, "codex executor", body, isCompat)
body = normalizeCodexParallelToolCalls(body, opts.Headers)
body = helps.NormalizeCodexToolSchemas(body)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, baseModel)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, isCompat)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return resp, errReplay
@@ -242,7 +242,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
body = sanitizeOpenAIResponsesReasoningEncryptedContentWithCompat(ctx, "codex executor", body, isCompat)
body = normalizeCodexParallelToolCalls(body, opts.Headers)
body = helps.NormalizeCodexToolSchemas(body)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, baseModel)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, isCompat)
reporter.SetTranslatedReasoningEffort(body, to.String())
url := strings.TrimSuffix(baseURL, "/") + "/responses/compact"

View File

@@ -10,6 +10,7 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
@@ -202,6 +203,120 @@ func TestCodexExecutorIsCompatConvertsAgentMessage(t *testing.T) {
}
}
func TestCodexExecutorsMultiAgentV2UsesSelectedHomeModel(t *testing.T) {
capturedPayload := make(chan []byte, 1)
upgrader := websocket.Upgrader{}
completed := []byte(`{"type":"response.completed","response":{"id":"resp_1","object":"response","status":"completed","output":[]}}`)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
if websocket.IsWebSocketUpgrade(request) {
conn, errUpgrade := upgrader.Upgrade(w, request, nil)
if errUpgrade != nil {
t.Errorf("upgrade websocket: %v", errUpgrade)
return
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Errorf("close websocket: %v", errClose)
}
}()
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
t.Errorf("read websocket request: %v", errRead)
return
}
capturedPayload <- payload
if errWrite := conn.WriteMessage(websocket.TextMessage, completed); errWrite != nil {
t.Errorf("write websocket response: %v", errWrite)
}
return
}
payload, errRead := io.ReadAll(request.Body)
if errRead != nil {
t.Errorf("read request: %v", errRead)
http.Error(w, errRead.Error(), http.StatusInternalServerError)
return
}
capturedPayload <- payload
if request.URL.Path == "/responses/compact" {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"resp_1","object":"response.compaction","output":[]}`))
return
}
w.Header().Set("Content-Type", "text/event-stream")
_, _ = fmt.Fprintf(w, "data: %s\n\n", completed)
}))
t.Cleanup(server.Close)
for _, tc := range []struct {
name, model string
isCompat bool
}{
{name: "native alias", model: "gpt-5.4"},
{name: "compat alias", model: "gpt-5.4", isCompat: true},
{name: "native suffix", model: "gpt-5.4(high)"},
{name: "compat suffix", model: "gpt-5.4(high)", isCompat: true},
} {
for _, mode := range []string{"execute", "stream", "compact", "websocket execute", "websocket stream"} {
t.Run(tc.name+"/"+mode, func(t *testing.T) {
cfg := &config.Config{Home: config.HomeConfig{Enabled: true}, Codex: config.CodexConfig{OptimizeMultiAgentV2: true}}
var executor cliproxyauth.ProviderExecutor = NewCodexExecutor(cfg)
if strings.HasPrefix(mode, "websocket") {
executor = NewCodexWebsocketsExecutor(cfg)
}
auth := &cliproxyauth.Auth{
ID: t.Name(), Provider: "codex", Prefix: "tenant",
Attributes: map[string]string{
"base_url": server.URL, "api_key": "test", "auth_kind": cliproxyauth.AuthKindAPIKey,
"home_upstream_model": tc.model,
},
Metadata: map[string]any{"credential_options": map[string]any{"models": []config.CodexModel{
{Name: "gpt-5.4", Alias: "other", IsCompat: !tc.isCompat},
{Name: tc.model, Alias: "chosen", IsCompat: tc.isCompat},
}}},
}
req := cliproxyexecutor.Request{
Model: tc.model, Payload: codexSpawnAgentTestPayload(),
Metadata: map[string]any{"cliproxy.resolved_home_model_info": &registry.ModelInfo{
ID: "gpt-5.4", IsCompat: tc.isCompat, Thinking: &registry.ThinkingSupport{Levels: []string{"high"}},
}},
}
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("openai-response")}
ctx := codexSpawnAgentTestContext()
if strings.HasSuffix(mode, "stream") {
result, errStream := executor.ExecuteStream(ctx, auth, req, opts)
if errStream != nil {
t.Fatal(errStream)
}
for chunk := range result.Chunks {
if chunk.Err != nil {
t.Fatal(chunk.Err)
}
}
} else {
if mode == "compact" {
opts.Alt = "responses/compact"
}
if _, errExecute := executor.Execute(ctx, auth, req, opts); errExecute != nil {
t.Fatal(errExecute)
}
}
upstreamPayload := <-capturedPayload
message := gjson.GetBytes(upstreamPayload, "input.1")
if tc.isCompat {
if message.Get("type").String() != "message" || message.Get("role").String() != "user" {
t.Fatalf("selected compat model must receive a user message: %s", message.Raw)
}
if message.Get("author").Exists() || message.Get("recipient").Exists() || message.Get("internal_chat_message_metadata_passthrough").Exists() {
t.Fatalf("compat message retained internal metadata: %s", message.Raw)
}
} else {
assertCodexSpawnAgentRequestMessage(t, upstreamPayload, true)
}
})
}
}
}
func TestCodexExecutor_IsCompat_StripsAuthorAndRecipient_Issue6136(t *testing.T) {
var upstreamBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {

View File

@@ -77,7 +77,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
body = sanitizeOpenAIResponsesReasoningEncryptedContentWithCompat(ctx, "codex executor", body, isCompat)
body = normalizeCodexParallelToolCalls(body, opts.Headers)
body = helps.NormalizeCodexToolSchemas(body)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, baseModel)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, isCompat)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return nil, errReplay

View File

@@ -67,7 +67,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
body = normalizeCodexWebsocketParallelToolCalls(body, opts.Headers)
body = helps.NormalizeCodexToolSchemas(body)
multiAgentV2Conflict := helps.HasCodexMultiAgentV2NamespaceConflict(body)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, baseModel)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, isCompat)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return resp, errReplay

View File

@@ -786,7 +786,7 @@ func (e *CodexWebsocketsExecutor) prepareCodexWebsocketStream(ctx context.Contex
body = normalizeCodexWebsocketParallelToolCalls(body, opts.Headers)
body = helps.NormalizeCodexToolSchemas(body)
multiAgentV2Conflict := helps.HasCodexMultiAgentV2NamespaceConflict(body)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, baseModel)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, isCompat)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return nil, errReplay

View File

@@ -166,16 +166,16 @@ func OptimizeCodexMultiAgentV2Request(ctx context.Context, headers http.Header,
}
// OptimizeCodexMultiAgentV2RequestForAuth applies the standard Codex MultiAgentV2
// request optimization and, when the selected codex-api-key model has is-compat
// enabled, also converts agent_message items into portable message/user input,
// request optimization and uses the execution attempt's resolved compatibility
// flag to convert agent_message items into portable message/user input,
// proactively stripping author, recipient, and internal passthrough metadata.
func OptimizeCodexMultiAgentV2RequestForAuth(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config, auth *cliproxyauth.Auth, model string) ([]byte, bool) {
func OptimizeCodexMultiAgentV2RequestForAuth(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config, auth *cliproxyauth.Auth, isCompat bool) ([]byte, bool) {
if auth != nil && auth.AuthKind() == cliproxyauth.AuthKindAPIKey {
cfg = cfg.ForAPIKey()
}
payload = multiagentv2.RewriteCodexOrphanDelegationInputForConfig(ctx, headers, payload, cfg)
updated, optimized := multiagentv2.OptimizeCodexMultiAgentV2Request(ctx, headers, payload, cfg)
if cliproxyauth.CodexAPIKeyModelIsCompat(cfg, auth, model) {
if isCompat {
updated = multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, updated, cfg, true)
}
return updated, optimized