feat(executor): replace sdktranslator.TranslateRequest with helps.TranslateRequestWithCodexMultiAgentV2

- Updated Kimi and Gemini executors to use `TranslateRequestWithCodexMultiAgentV2` for improved multi-agent v2 optimization.
- Enhanced configuration and API server to support `CodexOptimizeMultiAgentV2`.
- Added codex multi-agent V2 optimizations in `config`, `executor`, and `watcher` modules.
This commit is contained in:
Luis Pater
2026-07-25 00:32:37 +08:00
parent 42f36b94e0
commit 84bf9376e5
25 changed files with 1912 additions and 59 deletions
+5
View File
@@ -212,6 +212,11 @@ codex:
# Some superstitious users believe request tracking identifiers can be used
# as evidence for TOS enforcement bans; this option only satisfies those odd concerns.
identity-confuse: false
# When true, optimize Codex Desktop and codex-tui requests for multi-agent v2.
# This refreshes Codex spawn_agent model details, removes message parameter encryption,
# normalizes encrypted agent_message content for Codex, and converts agent_message input
# into standard user messages for non-Codex upstream protocols.
optimize-multi-agent-v2: false
# When true, enable authentication for the WebSocket API (/v1/ws).
ws-auth: true
+2 -1
View File
@@ -107,6 +107,7 @@ func effectiveSDKConfig(cfg *config.Config) *config.SDKConfig {
return nil
}
sdkCfg := cfg.SDKConfig
sdkCfg.CodexOptimizeMultiAgentV2 = cfg.Codex.OptimizeMultiAgentV2
if cfg.CommercialMode {
sdkCfg.RequestLog = false
}
@@ -1366,7 +1367,7 @@ func (s *Server) handleHomeCodexClientModels(c *gin.Context) {
models = append(models, model)
}
c.JSON(http.StatusOK, openai.CodexClientModelsResponse(models))
c.JSON(http.StatusOK, openai.CodexClientModelsResponseWithMultiAgentV2(models, s.cfg.Codex.OptimizeMultiAgentV2))
}
func (s *Server) geminiModelsHandler(geminiHandler *gemini.GeminiAPIHandler) gin.HandlerFunc {
+16
View File
@@ -0,0 +1,16 @@
package api
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
)
func TestEffectiveSDKConfigCopiesCodexOptimizeMultiAgentV2(t *testing.T) {
cfg := &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}}
sdkCfg := effectiveSDKConfig(cfg)
if sdkCfg == nil || !sdkCfg.CodexOptimizeMultiAgentV2 {
t.Fatalf("CodexOptimizeMultiAgentV2 = false, want true")
}
}
@@ -0,0 +1,746 @@
package multiagentv2
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
const (
codexSpawnAgentDescriptionMarker = "Spawns an agent"
codexSpawnAgentModelsHeading = "Available model overrides (optional; inherited parent model is preferred):"
codexCollaborationNamespace = "collaboration"
codexOptimizedCollaborationNamespace = "collaboration-optimize"
codexOptimizedCollaborationNamePrefix = codexOptimizedCollaborationNamespace + "__"
)
type codexSpawnAgentModel struct {
id string
description string
reasoningEfforts []string
defaultReasoningEffort string
serviceTiers []string
priority int
displayName string
}
type codexClientModelsCatalog struct {
Models []map[string]any `json:"models"`
}
// RewriteCodexSpawnAgentDescription optimizes spawn_agent definitions for
// official Codex clients when multi-agent v2 optimization is enabled.
func RewriteCodexSpawnAgentDescription(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
updated, _ := OptimizeCodexMultiAgentV2Request(ctx, headers, payload, cfg)
return updated
}
// RewriteCodexMultiAgentV2Input converts official Codex multi-agent input into
// standard Responses API messages when multi-agent v2 optimization is enabled.
func RewriteCodexMultiAgentV2Input(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
if !codexMultiAgentV2Enabled(ctx, headers, cfg) {
return payload
}
return rewriteCodexAgentMessageInput(payload)
}
// TranslateRequestWithCodexMultiAgentV2 normalizes official Codex multi-agent
// input before translating it to a non-Codex target protocol.
func TranslateRequestWithCodexMultiAgentV2(ctx context.Context, headers http.Header, cfg *config.Config, from, to sdktranslator.Format, model string, payload []byte, stream bool) []byte {
if from == sdktranslator.FormatOpenAIResponse && to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {
payload = RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
}
return sdktranslator.TranslateRequest(from, to, model, payload, stream)
}
// OptimizeCodexMultiAgentV2Request rewrites an eligible spawn_agent request and
// reports whether the collaboration namespace was renamed for upstream use.
func OptimizeCodexMultiAgentV2Request(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) ([]byte, bool) {
if !codexMultiAgentV2Enabled(ctx, headers, cfg) {
return payload, false
}
updated := rewriteCodexAgentMessageContent(payload)
toolPaths := codexSpawnAgentToolPaths(updated)
if len(toolPaths) == 0 || hasCodexOptimizedCollaborationConflict(updated) {
return updated, false
}
models := codexSpawnAgentModelsForRequest(ctx, headers, cfg.Home.Enabled)
updated = rewriteCodexSpawnAgentTools(updated, toolPaths, models)
return optimizeCodexCollaborationNamespace(updated, toolPaths)
}
func codexMultiAgentV2Enabled(ctx context.Context, headers http.Header, cfg *config.Config) bool {
return cfg != nil && cfg.Codex.OptimizeMultiAgentV2 && isCodexMultiAgentClient(codexClientUserAgent(ctx, headers))
}
func codexClientUserAgent(ctx context.Context, headers http.Header) string {
if ctx != nil {
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
return headerValueCaseInsensitive(ginCtx.Request.Header, "User-Agent")
}
}
return headerValueCaseInsensitive(headers, "User-Agent")
}
func headerValueCaseInsensitive(headers http.Header, name string) string {
if headers == nil {
return ""
}
if value := strings.TrimSpace(headers.Get(name)); value != "" {
return value
}
for key, values := range headers {
if !strings.EqualFold(key, name) {
continue
}
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
}
return ""
}
func isCodexMultiAgentClient(userAgent string) bool {
userAgent = strings.TrimSpace(userAgent)
return strings.HasPrefix(userAgent, "Codex Desktop/") || strings.HasPrefix(userAgent, "codex-tui/")
}
func codexSpawnAgentModelsForRequest(ctx context.Context, headers http.Header, homeEnabled bool) []codexSpawnAgentModel {
availableModels := registry.GetGlobalRegistry().GetAvailableModels("openai")
if homeEnabled {
availableModels = codexHomeAvailableModels(ctx, headers)
}
return codexSpawnAgentModelsFromSources(availableModels, registry.GetCodexClientModelsJSON(), func(modelID string) *registry.ModelInfo {
return registry.LookupModelInfo(modelID)
})
}
func codexHomeAvailableModels(ctx context.Context, headers http.Header) []map[string]any {
client := home.Current()
if client == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
requestHeaders := headers
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
requestHeaders = ginCtx.Request.Header
}
query := make(url.Values)
query.Set("client_version", "")
raw, errGet := client.GetModels(ctx, requestHeaders, query)
if errGet != nil {
return nil
}
return decodeCodexHomeAvailableModels(raw)
}
func decodeCodexHomeAvailableModels(raw []byte) []map[string]any {
var sections map[string][]map[string]any
if err := json.Unmarshal(raw, &sections); err != nil || len(sections) == 0 {
return nil
}
seen := make(map[string]struct{})
models := make([]map[string]any, 0, 256)
for _, sectionModels := range sections {
for _, model := range sectionModels {
modelID := mapString(model, "id")
if modelID == "" {
modelID = strings.TrimPrefix(mapString(model, "name"), "models/")
}
if modelID == "" {
continue
}
if _, exists := seen[modelID]; exists {
continue
}
seen[modelID] = struct{}{}
displayName := mapString(model, "display_name")
if displayName == "" {
displayName = mapString(model, "displayName")
}
entry := map[string]any{"id": modelID}
if displayName != "" {
entry["display_name"] = displayName
entry["description"] = displayName
}
models = append(models, entry)
}
}
sort.Slice(models, func(i, j int) bool {
return mapString(models[i], "id") < mapString(models[j], "id")
})
return models
}
func codexSpawnAgentModelsFromSources(availableModels []map[string]any, catalogJSON []byte, lookupModel func(string) *registry.ModelInfo) []codexSpawnAgentModel {
var catalog codexClientModelsCatalog
if err := json.Unmarshal(catalogJSON, &catalog); err != nil || len(catalog.Models) == 0 {
return nil
}
templates := make(map[string]map[string]any, len(catalog.Models))
var defaultTemplate map[string]any
for _, model := range catalog.Models {
modelID := mapString(model, "slug")
if modelID == "" {
continue
}
templates[modelID] = model
if modelID == "gpt-5.5" {
defaultTemplate = model
}
}
if defaultTemplate == nil {
return nil
}
seen := make(map[string]struct{}, len(availableModels))
templateModels := make([]codexSpawnAgentModel, 0, len(availableModels))
synthesizedModels := make([]codexSpawnAgentModel, 0, len(availableModels))
for _, availableModel := range availableModels {
modelID := mapString(availableModel, "id")
if modelID == "" {
continue
}
if _, exists := seen[modelID]; exists {
continue
}
seen[modelID] = struct{}{}
if template, ok := templates[modelID]; ok {
templateModels = append(templateModels, codexSpawnAgentModelFromMetadata(modelID, template))
continue
}
profile := codexSpawnAgentModelFromMetadata(modelID, defaultTemplate)
profile.id = modelID
profile.description = mapString(availableModel, "description")
profile.displayName = mapString(availableModel, "display_name")
if profile.displayName == "" {
profile.displayName = modelID
}
if lookupModel != nil {
if info := lookupModel(modelID); info != nil {
if strings.TrimSpace(info.Description) != "" {
profile.description = strings.TrimSpace(info.Description)
}
applyCodexSpawnAgentThinking(&profile, info.Thinking)
}
}
if profile.description == "" {
profile.description = modelID
}
profile.serviceTiers = nil
synthesizedModels = append(synthesizedModels, profile)
}
sort.SliceStable(templateModels, func(i, j int) bool {
if templateModels[i].priority == templateModels[j].priority {
return templateModels[i].id < templateModels[j].id
}
return templateModels[i].priority < templateModels[j].priority
})
sort.SliceStable(synthesizedModels, func(i, j int) bool {
left := strings.ToLower(synthesizedModels[i].displayName)
right := strings.ToLower(synthesizedModels[j].displayName)
if left == right {
return synthesizedModels[i].id < synthesizedModels[j].id
}
return left < right
})
return append(templateModels, synthesizedModels...)
}
func codexSpawnAgentModelFromMetadata(modelID string, metadata map[string]any) codexSpawnAgentModel {
profile := codexSpawnAgentModel{
id: modelID,
description: mapString(metadata, "description"),
displayName: mapString(metadata, "display_name"),
priority: mapInt(metadata, "priority"),
}
profile.reasoningEfforts, profile.defaultReasoningEffort = codexReasoningMetadata(metadata)
profile.serviceTiers = codexServiceTierIDs(metadata)
return profile
}
func applyCodexSpawnAgentThinking(profile *codexSpawnAgentModel, thinking *registry.ThinkingSupport) {
if profile == nil || thinking == nil || len(thinking.Levels) == 0 {
return
}
efforts := make([]string, 0, len(thinking.Levels))
defaultEffort := ""
firstEffort := ""
for _, rawEffort := range thinking.Levels {
effort := normalizeCodexReasoningEffort(rawEffort)
if effort == "" {
continue
}
if firstEffort == "" {
firstEffort = effort
}
if (defaultEffort == "" && effort != "none") || effort == "medium" {
defaultEffort = effort
}
efforts = append(efforts, effort)
}
if len(efforts) == 0 {
return
}
if defaultEffort == "" {
defaultEffort = firstEffort
}
profile.reasoningEfforts = efforts
profile.defaultReasoningEffort = defaultEffort
}
func codexReasoningMetadata(metadata map[string]any) ([]string, string) {
rawLevels, _ := metadata["supported_reasoning_levels"].([]any)
efforts := make([]string, 0, len(rawLevels))
allowed := make(map[string]struct{}, len(rawLevels))
for _, rawLevel := range rawLevels {
level, _ := rawLevel.(map[string]any)
effort := normalizeCodexReasoningEffort(mapString(level, "effort"))
if effort == "" {
continue
}
efforts = append(efforts, effort)
allowed[effort] = struct{}{}
}
if len(efforts) == 0 {
return nil, ""
}
defaultEffort := normalizeCodexReasoningEffort(mapString(metadata, "default_reasoning_level"))
if _, ok := allowed[defaultEffort]; !ok {
defaultEffort = efforts[0]
}
return efforts, defaultEffort
}
func normalizeCodexReasoningEffort(effort string) string {
effort = strings.ToLower(strings.TrimSpace(effort))
switch effort {
case "none", "low", "medium", "high", "xhigh", "max", "ultra":
return effort
default:
return ""
}
}
func codexServiceTierIDs(metadata map[string]any) []string {
rawTiers, _ := metadata["service_tiers"].([]any)
tiers := make([]string, 0, len(rawTiers))
seen := make(map[string]struct{}, len(rawTiers))
for _, rawTier := range rawTiers {
tier, _ := rawTier.(map[string]any)
tierID := mapString(tier, "id")
if tierID == "" {
continue
}
if _, exists := seen[tierID]; exists {
continue
}
seen[tierID] = struct{}{}
tiers = append(tiers, tierID)
}
return tiers
}
func mapString(values map[string]any, key string) string {
if values == nil {
return ""
}
value, _ := values[key].(string)
return strings.TrimSpace(value)
}
func mapInt(values map[string]any, key string) int {
if values == nil {
return 0
}
switch value := values[key].(type) {
case int:
return value
case int64:
return int(value)
case float64:
return int(value)
default:
return 0
}
}
func rewriteCodexSpawnAgentDescription(payload []byte, models []codexSpawnAgentModel) []byte {
return rewriteCodexSpawnAgentTools(payload, codexSpawnAgentToolPaths(payload), models)
}
func rewriteCodexSpawnAgentTools(payload []byte, toolPaths []string, models []codexSpawnAgentModel) []byte {
if len(toolPaths) == 0 {
return payload
}
modelList := formatCodexSpawnAgentModels(models)
updated := payload
for _, toolPath := range toolPaths {
descriptionPath := toolPath + ".description"
description := gjson.GetBytes(updated, descriptionPath)
if description.Type == gjson.String && modelList != "" {
rewritten := replaceCodexSpawnAgentModels(description.String(), modelList)
if rewritten != description.String() {
var errSet error
updated, errSet = sjson.SetBytes(updated, descriptionPath, rewritten)
if errSet != nil {
return payload
}
}
}
var errDelete error
updated, errDelete = sjson.DeleteBytes(updated, toolPath+".parameters.properties.message.encrypted")
if errDelete != nil {
return payload
}
}
return updated
}
func hasCodexOptimizedCollaborationConflict(payload []byte) bool {
if codexToolsHaveOptimizedCollaborationConflict(gjson.GetBytes(payload, "tools")) {
return true
}
input := gjson.GetBytes(payload, "input")
if !input.IsArray() {
return false
}
for _, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) == "additional_tools" && codexToolsHaveOptimizedCollaborationConflict(item.Get("tools")) {
return true
}
}
return false
}
func codexToolsHaveOptimizedCollaborationConflict(tools gjson.Result) bool {
if !tools.IsArray() {
return false
}
for _, tool := range tools.Array() {
name := strings.TrimSpace(tool.Get("name").String())
if name == codexOptimizedCollaborationNamespace || strings.HasPrefix(name, codexOptimizedCollaborationNamePrefix) {
return true
}
if strings.TrimSpace(tool.Get("type").String()) == "namespace" && codexToolsHaveOptimizedCollaborationConflict(tool.Get("tools")) {
return true
}
}
return false
}
func optimizeCodexCollaborationNamespace(payload []byte, toolPaths []string) ([]byte, bool) {
updated := payload
optimized := false
for _, toolPath := range toolPaths {
separatorIndex := strings.LastIndex(toolPath, ".tools.")
if separatorIndex < 0 {
continue
}
namespacePath := toolPath[:separatorIndex]
namespace := gjson.GetBytes(updated, namespacePath)
if strings.TrimSpace(namespace.Get("type").String()) != "namespace" || strings.TrimSpace(namespace.Get("name").String()) != codexCollaborationNamespace {
continue
}
var errSet error
updated, errSet = sjson.SetBytes(updated, namespacePath+".name", codexOptimizedCollaborationNamespace)
if errSet != nil {
return payload, false
}
optimized = true
}
return updated, optimized
}
// RestoreCodexMultiAgentV2Response restores optimized collaboration namespace
// values before an upstream response is translated and returned to the client.
func RestoreCodexMultiAgentV2Response(payload []byte, optimized bool) []byte {
if !optimized || len(payload) == 0 || !gjson.ValidBytes(payload) {
return payload
}
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.UseNumber()
var value any
if errDecode := decoder.Decode(&value); errDecode != nil {
return payload
}
if !restoreCodexCollaborationValue(value) {
return payload
}
restored, errMarshal := json.Marshal(value)
if errMarshal != nil {
return payload
}
return restored
}
func restoreCodexCollaborationValue(value any) bool {
changed := false
switch typed := value.(type) {
case []any:
for _, item := range typed {
if restoreCodexCollaborationValue(item) {
changed = true
}
}
case map[string]any:
itemType := strings.TrimSpace(mapString(typed, "type"))
isToolCall := itemType == "function_call" || itemType == "custom_tool_call"
if isToolCall {
if namespace, ok := typed["namespace"].(string); ok && namespace == codexOptimizedCollaborationNamespace {
typed["namespace"] = codexCollaborationNamespace
changed = true
}
}
if name, ok := typed["name"].(string); ok {
switch {
case name == codexOptimizedCollaborationNamespace && itemType == "namespace":
typed["name"] = codexCollaborationNamespace
changed = true
case isToolCall && strings.HasPrefix(name, codexOptimizedCollaborationNamePrefix):
typed["name"] = codexCollaborationNamespace + "__" + strings.TrimPrefix(name, codexOptimizedCollaborationNamePrefix)
changed = true
}
}
for key, child := range typed {
if key == "arguments" || key == "input" || key == "output" && (itemType == "function_call_output" || itemType == "custom_tool_call_output") {
continue
}
if restoreCodexCollaborationValue(child) {
changed = true
}
}
}
return changed
}
func rewriteCodexAgentMessageInput(payload []byte) []byte {
input := gjson.GetBytes(payload, "input")
if !input.IsArray() {
return payload
}
updated := rewriteCodexAgentMessageContent(payload)
for itemIndex, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) != "agent_message" {
continue
}
itemPath := fmt.Sprintf("input.%d", itemIndex)
var errSet error
updated, errSet = sjson.SetBytes(updated, itemPath+".role", "user")
if errSet != nil {
return payload
}
updated, errSet = sjson.SetBytes(updated, itemPath+".type", "message")
if errSet != nil {
return payload
}
}
return updated
}
func rewriteCodexAgentMessageContent(payload []byte) []byte {
input := gjson.GetBytes(payload, "input")
if !input.IsArray() {
return payload
}
updated := payload
for itemIndex, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) != "agent_message" {
continue
}
content := item.Get("content")
if !content.IsArray() {
continue
}
for partIndex, part := range content.Array() {
if strings.TrimSpace(part.Get("type").String()) != "encrypted_content" {
continue
}
encryptedContent := part.Get("encrypted_content")
if encryptedContent.Type != gjson.String {
continue
}
partPath := fmt.Sprintf("input.%d.content.%d", itemIndex, partIndex)
var errSet error
updated, errSet = sjson.SetBytes(updated, partPath+".type", "input_text")
if errSet != nil {
return payload
}
updated, errSet = sjson.SetBytes(updated, partPath+".text", encryptedContent.String())
if errSet != nil {
return payload
}
updated, errSet = sjson.DeleteBytes(updated, partPath+".encrypted_content")
if errSet != nil {
return payload
}
}
}
return updated
}
func codexSpawnAgentToolPaths(payload []byte) []string {
paths := make([]string, 0, 1)
collectCodexSpawnAgentToolPaths(gjson.GetBytes(payload, "tools"), "tools", &paths)
input := gjson.GetBytes(payload, "input")
if input.IsArray() {
for index, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) != "additional_tools" {
continue
}
collectCodexSpawnAgentToolPaths(item.Get("tools"), fmt.Sprintf("input.%d.tools", index), &paths)
}
}
return paths
}
func collectCodexSpawnAgentToolPaths(tools gjson.Result, path string, paths *[]string) {
if !tools.IsArray() {
return
}
for index, tool := range tools.Array() {
toolPath := fmt.Sprintf("%s.%d", path, index)
toolType := strings.TrimSpace(tool.Get("type").String())
if toolType == "function" && strings.TrimSpace(tool.Get("name").String()) == "spawn_agent" {
*paths = append(*paths, toolPath)
}
if toolType == "namespace" {
collectCodexSpawnAgentToolPaths(tool.Get("tools"), toolPath+".tools", paths)
}
}
}
func formatCodexSpawnAgentModels(models []codexSpawnAgentModel) string {
var modelList strings.Builder
for _, model := range models {
modelID := strings.Join(strings.Fields(model.id), " ")
if modelID == "" {
continue
}
modelList.WriteString("- ")
modelList.WriteString(markdownCode(modelID))
modelList.WriteString(": ")
hasDetails := false
if description := strings.Join(strings.Fields(model.description), " "); description != "" {
writeSentence(&modelList, description)
hasDetails = true
}
if len(model.reasoningEfforts) > 0 {
if hasDetails {
modelList.WriteByte(' ')
}
modelList.WriteString("Reasoning efforts: ")
for index, effort := range model.reasoningEfforts {
if index > 0 {
modelList.WriteString(", ")
}
modelList.WriteString(effort)
if effort == model.defaultReasoningEffort {
modelList.WriteString(" (default)")
}
}
modelList.WriteByte('.')
hasDetails = true
}
if len(model.serviceTiers) > 0 {
if hasDetails {
modelList.WriteByte(' ')
}
modelList.WriteString("Service tiers: ")
modelList.WriteString(strings.Join(model.serviceTiers, ", "))
modelList.WriteByte('.')
}
modelList.WriteByte('\n')
}
return strings.TrimSuffix(modelList.String(), "\n")
}
func markdownCode(value string) string {
if strings.Contains(value, "`") {
return "`` " + value + " ``"
}
return "`" + value + "`"
}
func writeSentence(builder *strings.Builder, value string) {
builder.WriteString(value)
if !strings.ContainsAny(value[len(value)-1:], ".!?") {
builder.WriteByte('.')
}
}
func replaceCodexSpawnAgentModels(description, modelList string) string {
if modelList == "" {
return description
}
cleaned, headingIndent := removeCodexSpawnAgentModelSections(description)
section := headingIndent + codexSpawnAgentModelsHeading + "\n" + modelList + "\n"
markerIndex := strings.Index(cleaned, codexSpawnAgentDescriptionMarker)
if markerIndex >= 0 {
markerLineStart := strings.LastIndex(cleaned[:markerIndex], "\n") + 1
return cleaned[:markerLineStart] + section + cleaned[markerLineStart:]
}
separator := ""
if cleaned != "" && !strings.HasSuffix(cleaned, "\n") {
separator = "\n\n"
}
return cleaned + separator + strings.TrimSuffix(section, "\n")
}
func removeCodexSpawnAgentModelSections(description string) (string, string) {
lines := strings.SplitAfter(description, "\n")
var cleaned strings.Builder
headingIndent := ""
for index := 0; index < len(lines); {
line := lines[index]
trimmedLine := strings.TrimSpace(line)
if trimmedLine != codexSpawnAgentModelsHeading {
cleaned.WriteString(line)
index++
continue
}
if headingIndent == "" {
headingIndex := strings.Index(line, codexSpawnAgentModelsHeading)
if headingIndex > 0 {
headingIndent = line[:headingIndex]
}
}
index++
for index < len(lines) && strings.HasPrefix(strings.TrimSpace(lines[index]), "- ") {
index++
}
}
return cleaned.String(), headingIndent
}
@@ -0,0 +1,589 @@
package multiagentv2
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
_ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
)
func TestIsCodexMultiAgentClient(t *testing.T) {
t.Parallel()
tests := []struct {
name string
userAgent string
want bool
}{
{
name: "Codex Desktop",
userAgent: "Codex Desktop/0.146.0-alpha.3 (Mac OS 26.5.2; arm64) unknown (Codex Desktop; 26.721.30844)",
want: true,
},
{
name: "codex tui",
userAgent: "codex-tui/0.145.0 (Mac OS 26.5.2; arm64) iTerm.app/3.6.11 (codex-tui; 0.145.0)",
want: true,
},
{
name: "other client",
userAgent: "curl/8.7.1",
want: false,
},
{
name: "embedded token",
userAgent: "proxy Codex Desktop/0.146.0",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := isCodexMultiAgentClient(tt.userAgent); got != tt.want {
t.Fatalf("isCodexMultiAgentClient(%q) = %v, want %v", tt.userAgent, got, tt.want)
}
})
}
}
func TestCodexSpawnAgentModelsFromSourcesIncludesModelMetadata(t *testing.T) {
t.Parallel()
catalog := []byte(`{"models":[
{"slug":"model-template","display_name":"Template","description":"Template model.","default_reasoning_level":"low","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"}],"service_tiers":[{"id":"priority"}],"priority":1},
{"slug":"gpt-5.5","display_name":"Default","description":"Default model.","default_reasoning_level":"medium","supported_reasoning_levels":[{"effort":"low"},{"effort":"medium"},{"effort":"high"}],"service_tiers":[{"id":"priority"}],"priority":2}
]}`)
available := []map[string]any{
{"id": "custom-model", "display_name": "Custom", "description": "Registry description."},
{"id": "model-template"},
{"id": "custom-model", "description": "duplicate"},
}
lookup := func(modelID string) *registry.ModelInfo {
if modelID != "custom-model" {
return nil
}
return &registry.ModelInfo{
Description: "Dynamic model.",
Thinking: &registry.ThinkingSupport{
Levels: []string{"none", "low", "medium", "high"},
},
}
}
models := codexSpawnAgentModelsFromSources(available, catalog, lookup)
if len(models) != 2 {
t.Fatalf("model count = %d, want 2", len(models))
}
if got := models[0]; got.id != "model-template" || got.description != "Template model." || got.defaultReasoningEffort != "low" {
t.Fatalf("template model = %+v", got)
}
if got := strings.Join(models[0].serviceTiers, ","); got != "priority" {
t.Fatalf("template service tiers = %q, want priority", got)
}
custom := models[1]
if custom.id != "custom-model" || custom.description != "Dynamic model." {
t.Fatalf("custom model = %+v", custom)
}
if got := strings.Join(custom.reasoningEfforts, ","); got != "none,low,medium,high" {
t.Fatalf("custom reasoning efforts = %q", got)
}
if custom.defaultReasoningEffort != "medium" {
t.Fatalf("custom default reasoning effort = %q, want medium", custom.defaultReasoningEffort)
}
if len(custom.serviceTiers) != 0 {
t.Fatalf("custom service tiers = %v, want none", custom.serviceTiers)
}
}
func TestDecodeCodexHomeAvailableModels(t *testing.T) {
t.Parallel()
raw := []byte(`{
"codex":[{"id":"model-b","display_name":"Model B"},{"id":"model-a"}],
"other":[{"name":"models/model-c","displayName":"Model C"},{"id":"model-a","display_name":"duplicate"}]
}`)
models := decodeCodexHomeAvailableModels(raw)
if len(models) != 3 {
t.Fatalf("model count = %d, want 3", len(models))
}
if got := mapString(models[0], "id"); got != "model-a" {
t.Fatalf("first model ID = %q, want model-a", got)
}
if got := mapString(models[1], "description"); got != "Model B" {
t.Fatalf("model-b description = %q, want Model B", got)
}
if got := mapString(models[2], "id"); got != "model-c" {
t.Fatalf("last model ID = %q, want model-c", got)
}
if got := decodeCodexHomeAvailableModels([]byte(`{"error":{"type":"no_credentials"}}`)); got != nil {
t.Fatalf("error envelope decoded as models: %#v", got)
}
}
func TestRewriteCodexSpawnAgentDescriptionNormalizesModelList(t *testing.T) {
t.Parallel()
payload := []byte(`{
"input":[{
"type":"additional_tools",
"role":"developer",
"tools":[{
"type":"namespace",
"name":"collaboration",
"tools":[
{"type":"function","name":"send_message","description":"unchanged"},
{"type":"function","name":"spawn_agent","description":"\n Available model overrides (optional; inherited parent model is preferred):\n- old duplicate\n- old duplicate\n Spawns an agent to work on a task.","parameters":{"type":"object","properties":{"message":{"type":"string","encrypted":true}}}}
]
}]
}]
}`)
models := []codexSpawnAgentModel{
{
id: "model-alpha",
description: "Alpha model.",
reasoningEfforts: []string{"low", "medium", "high"},
defaultReasoningEffort: "medium",
serviceTiers: []string{"priority"},
},
{
id: "model-beta",
description: "Beta model",
reasoningEfforts: []string{"low", "high"},
defaultReasoningEffort: "low",
},
}
got := rewriteCodexSpawnAgentDescription(payload, models)
description := gjson.GetBytes(got, "input.0.tools.0.tools.1.description").String()
wantAlpha := "- `model-alpha`: Alpha model. Reasoning efforts: low, medium (default), high. Service tiers: priority."
wantBeta := "- `model-beta`: Beta model. Reasoning efforts: low (default), high."
if !strings.Contains(description, wantAlpha) || !strings.Contains(description, wantBeta) {
t.Fatalf("description does not contain model metadata:\n%s", description)
}
if strings.Contains(description, "old duplicate") {
t.Fatalf("stale model list was not replaced: %q", description)
}
for _, modelID := range []string{"model-alpha", "model-beta"} {
if count := strings.Count(description, "`"+modelID+"`"); count != 1 {
t.Fatalf("model %q reference count = %d, want 1", modelID, count)
}
}
if strings.Index(description, "`model-beta`") > strings.Index(description, codexSpawnAgentDescriptionMarker) {
t.Fatalf("model list was not inserted before spawn instructions: %q", description)
}
if gotDescription := gjson.GetBytes(got, "input.0.tools.0.tools.0.description").String(); gotDescription != "unchanged" {
t.Fatalf("non-spawn tool description = %q, want unchanged", gotDescription)
}
if encrypted := gjson.GetBytes(got, "input.0.tools.0.tools.1.parameters.properties.message.encrypted"); encrypted.Exists() {
t.Fatalf("spawn_agent message encrypted was not removed: %s", encrypted.Raw)
}
}
func TestRewriteCodexSpawnAgentDescriptionTopLevelWithoutMarker(t *testing.T) {
t.Parallel()
payload := []byte(`{"tools":[{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent","description":"Create a worker."}]}]}`)
models := []codexSpawnAgentModel{{
id: "model-a",
description: "Model A.",
reasoningEfforts: []string{"medium"},
defaultReasoningEffort: "medium",
}}
got := rewriteCodexSpawnAgentDescription(payload, models)
description := gjson.GetBytes(got, "tools.0.tools.0.description").String()
wantSuffix := codexSpawnAgentModelsHeading + "\n- `model-a`: Model A. Reasoning efforts: medium (default)."
if !strings.HasPrefix(description, "Create a worker.\n\n") || !strings.HasSuffix(description, wantSuffix) {
t.Fatalf("description = %q, want original text followed by model list", description)
}
}
func TestCodexSpawnAgentToolPathsIgnoreInvalidContainers(t *testing.T) {
t.Parallel()
payload := []byte(`{
"input":[{"type":"message","tools":[{"type":"function","name":"spawn_agent","description":"message"}]}],
"tools":[
{"type":"function","name":"wrapper","tools":[{"type":"function","name":"spawn_agent","description":"child"}]},
{"type":"custom","name":"spawn_agent","description":"custom"},
{"type":"namespace","name":"spawn_agent","description":"namespace"}
]
}`)
if paths := codexSpawnAgentToolPaths(payload); len(paths) != 0 {
t.Fatalf("invalid container paths = %v, want none", paths)
}
}
func TestOptimizeCodexMultiAgentV2RequestSkipsNamespaceConflict(t *testing.T) {
t.Parallel()
payload := []byte(`{"tools":[{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent"}]},{"type":"namespace","name":"collaboration-optimize","tools":[]}]}`)
headers := http.Header{"User-Agent": []string{"codex-tui/0.145.0"}}
cfg := &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}}
got, optimized := OptimizeCodexMultiAgentV2Request(context.Background(), headers, payload, cfg)
if optimized {
t.Fatal("namespace conflict unexpectedly enabled optimization")
}
if string(got) != string(payload) {
t.Fatalf("namespace conflict changed payload: %s", got)
}
}
func TestOptimizeCodexCollaborationNamespaceWithoutModels(t *testing.T) {
t.Parallel()
payload := []byte(`{"tools":[{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent"}]}]}`)
toolPaths := codexSpawnAgentToolPaths(payload)
got, optimized := optimizeCodexCollaborationNamespace(payload, toolPaths)
if !optimized {
t.Fatal("collaboration namespace was not optimized")
}
if namespace := gjson.GetBytes(got, "tools.0.name").String(); namespace != codexOptimizedCollaborationNamespace {
t.Fatalf("namespace = %q, want collaboration-optimize", namespace)
}
}
func TestRewriteCodexSpawnAgentDescriptionWithoutModelsStillRemovesEncrypted(t *testing.T) {
t.Parallel()
payload := []byte(`{"tools":[{"type":"function","name":"spawn_agent","description":"unchanged","parameters":{"properties":{"message":{"encrypted":true}}}}]}`)
got := rewriteCodexSpawnAgentDescription(payload, nil)
if description := gjson.GetBytes(got, "tools.0.description").String(); description != "unchanged" {
t.Fatalf("description = %q, want unchanged", description)
}
if encrypted := gjson.GetBytes(got, "tools.0.parameters.properties.message.encrypted"); encrypted.Exists() {
t.Fatalf("message encrypted was not removed: %s", encrypted.Raw)
}
}
func TestRewriteCodexSpawnAgentDescriptionLeavesPayloadWithoutToolUnchanged(t *testing.T) {
t.Parallel()
payload := []byte(`{"tools":[{"type":"function","name":"other","description":"unchanged"}]}`)
models := []codexSpawnAgentModel{{id: "model-a", description: "Model A."}}
got := rewriteCodexSpawnAgentDescription(payload, models)
if string(got) != string(payload) {
t.Fatalf("payload changed without spawn_agent tool: %s", got)
}
}
func TestRewriteCodexSpawnAgentDescriptionEnabledOptimizesTool(t *testing.T) {
modelID := "codex-spawn-agent-test-model"
clientID := "codex-spawn-agent-test-client"
modelRegistry := registry.GetGlobalRegistry()
modelRegistry.RegisterClient(clientID, "codex", []*registry.ModelInfo{{
ID: modelID,
Description: "Test agent model.",
Thinking: &registry.ThinkingSupport{
Levels: []string{"low", "medium", "high"},
},
}})
defer modelRegistry.UnregisterClient(clientID)
payload := []byte(`{"tools":[{"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent","description":"Spawns an agent.","parameters":{"properties":{"message":{"type":"string","encrypted":true}}}}]}]}`)
headers := http.Header{"User-Agent": []string{"Codex Desktop/0.146.0-alpha.3"}}
cfg := &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}}
got, optimized := OptimizeCodexMultiAgentV2Request(context.Background(), headers, payload, cfg)
if !optimized {
t.Fatal("collaboration namespace was not marked optimized")
}
if namespace := gjson.GetBytes(got, "tools.0.name").String(); namespace != codexOptimizedCollaborationNamespace {
t.Fatalf("namespace = %q, want %q", namespace, codexOptimizedCollaborationNamespace)
}
description := gjson.GetBytes(got, "tools.0.tools.0.description").String()
want := "- `" + modelID + "`: Test agent model. Reasoning efforts: low, medium (default), high."
if !strings.Contains(description, want) {
t.Fatalf("description does not contain dynamic model metadata: %q", description)
}
if encrypted := gjson.GetBytes(got, "tools.0.tools.0.parameters.properties.message.encrypted"); encrypted.Exists() {
t.Fatalf("spawn_agent message encrypted was not removed: %s", encrypted.Raw)
}
}
func TestOptimizeCodexMultiAgentV2RequestNormalizesAgentMessageContentOnly(t *testing.T) {
t.Parallel()
payload := []byte(`{"input":[{"type":"agent_message","id":"amsg_1","author":"/root","recipient":"/root/worker","content":[{"type":"input_text","text":"Payload:\n"},{"type":"encrypted_content","encrypted_content":"delegated task"}],"internal_chat_message_metadata_passthrough":{"turn_id":"turn_1"}}]}`)
headers := http.Header{"User-Agent": []string{"Codex Desktop/0.146.0-alpha.3"}}
cfg := &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}}
got, namespaceOptimized := OptimizeCodexMultiAgentV2Request(context.Background(), headers, payload, cfg)
if namespaceOptimized {
t.Fatal("payload without spawn_agent unexpectedly optimized a namespace")
}
message := gjson.GetBytes(got, "input.0")
if message.Get("type").String() != "agent_message" || message.Get("role").Exists() {
t.Fatalf("outer agent message changed: %s", got)
}
if message.Get("content.1.type").String() != "input_text" || message.Get("content.1.text").String() != "delegated task" {
t.Fatalf("encrypted content was not normalized: %s", got)
}
if message.Get("content.1.encrypted_content").Exists() {
t.Fatalf("encrypted_content was preserved: %s", got)
}
if message.Get("author").String() != "/root" || message.Get("recipient").String() != "/root/worker" || message.Get("internal_chat_message_metadata_passthrough.turn_id").String() != "turn_1" {
t.Fatalf("agent message metadata changed: %s", got)
}
for _, tt := range []struct {
name string
headers http.Header
cfg *config.Config
}{
{name: "disabled", headers: headers, cfg: &config.Config{}},
{name: "unrelated client", headers: http.Header{"User-Agent": []string{"curl/8.7.1"}}, cfg: cfg},
} {
t.Run(tt.name, func(t *testing.T) {
unchanged, _ := OptimizeCodexMultiAgentV2Request(context.Background(), tt.headers, payload, tt.cfg)
if string(unchanged) != string(payload) {
t.Fatalf("ineligible request changed: %s", unchanged)
}
})
}
}
func TestRestoreCodexMultiAgentV2Response(t *testing.T) {
t.Parallel()
payload := []byte(`{
"type":"response.completed",
"response":{
"output":[
{"type":"function_call","name":"spawn_agent","namespace":"collaboration-optimize","arguments":{"namespace":"collaboration-optimize","name":"collaboration-optimize__opaque"}},
{"type":"function_call","name":"collaboration-optimize__send_message"},
{"type":"message","namespace":"collaboration-optimize","name":"collaboration-optimize__plain"}
],
"tools":[{"type":"namespace","name":"collaboration-optimize"}]
}
}`)
got := RestoreCodexMultiAgentV2Response(payload, true)
if namespace := gjson.GetBytes(got, "response.output.0.namespace").String(); namespace != codexCollaborationNamespace {
t.Fatalf("function namespace = %q, want collaboration", namespace)
}
if name := gjson.GetBytes(got, "response.output.1.name").String(); name != "collaboration__send_message" {
t.Fatalf("qualified function name = %q, want collaboration__send_message", name)
}
if name := gjson.GetBytes(got, "response.tools.0.name").String(); name != codexCollaborationNamespace {
t.Fatalf("namespace tool name = %q, want collaboration", name)
}
if namespace := gjson.GetBytes(got, "response.output.0.arguments.namespace").String(); namespace != codexOptimizedCollaborationNamespace {
t.Fatalf("opaque arguments namespace was unexpectedly rewritten: %q", namespace)
}
if namespace := gjson.GetBytes(got, "response.output.2.namespace").String(); namespace != codexOptimizedCollaborationNamespace {
t.Fatalf("ordinary namespace field was unexpectedly rewritten: %q", namespace)
}
if name := gjson.GetBytes(got, "response.output.2.name").String(); name != "collaboration-optimize__plain" {
t.Fatalf("ordinary name field was unexpectedly rewritten: %q", name)
}
if unchanged := RestoreCodexMultiAgentV2Response(payload, false); string(unchanged) != string(payload) {
t.Fatalf("inactive restore changed payload: %s", unchanged)
}
}
func TestRewriteCodexMultiAgentV2InputRewritesAgentMessage(t *testing.T) {
t.Parallel()
payload := []byte(`{"model":"gpt-5.4","input":[{
"type":"agent_message",
"id":"amsg_019f92ae-84fd-76f0-aa66-5a722dee382e",
"author":"/root",
"recipient":"/root/arithmetic_problem",
"content":[
{"type":"input_text","text":"Message Type: NEW_TASK\nTask name: /root/arithmetic_problem\nSender: /root\nPayload:\n"},
{"type":"encrypted_content","encrypted_content":"请出一道四则运算题,并给出答案。全程使用简体中文,题目简洁。"}
],
"internal_chat_message_metadata_passthrough":{"turn_id":"019f92ae-7eae-7371-957e-8f6f734edddc"}
}]}`)
headers := http.Header{"User-Agent": []string{"Codex Desktop/0.146.0-alpha.3"}}
cfg := &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}}
got := RewriteCodexMultiAgentV2Input(context.Background(), headers, payload, cfg)
if messageType := gjson.GetBytes(got, "input.0.type").String(); messageType != "message" {
t.Fatalf("type = %q, want message; payload=%s", messageType, got)
}
if role := gjson.GetBytes(got, "input.0.role").String(); role != "user" {
t.Fatalf("role = %q, want user; payload=%s", role, got)
}
if partType := gjson.GetBytes(got, "input.0.content.1.type").String(); partType != "input_text" {
t.Fatalf("content[1].type = %q, want input_text; payload=%s", partType, got)
}
if text := gjson.GetBytes(got, "input.0.content.1.text").String(); text != "请出一道四则运算题,并给出答案。全程使用简体中文,题目简洁。" {
t.Fatalf("content[1].text = %q; payload=%s", text, got)
}
if encrypted := gjson.GetBytes(got, "input.0.content.1.encrypted_content"); encrypted.Exists() {
t.Fatalf("content[1].encrypted_content was preserved: %s", got)
}
if author := gjson.GetBytes(got, "input.0.author").String(); author != "/root" {
t.Fatalf("author = %q, want /root", author)
}
if turnID := gjson.GetBytes(got, "input.0.internal_chat_message_metadata_passthrough.turn_id").String(); turnID != "019f92ae-7eae-7371-957e-8f6f734edddc" {
t.Fatalf("turn_id = %q", turnID)
}
}
func TestRewriteCodexMultiAgentV2InputConditions(t *testing.T) {
t.Parallel()
payload := []byte(`{"input":[{"type":"agent_message","content":[{"type":"encrypted_content","encrypted_content":"task"}]}]}`)
tests := []struct {
name string
cfg *config.Config
userAgent string
want bool
}{
{
name: "Codex Desktop enabled",
cfg: &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}},
userAgent: "Codex Desktop/0.146.0-alpha.3",
want: true,
},
{
name: "codex tui enabled",
cfg: &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}},
userAgent: "codex-tui/0.145.0",
want: true,
},
{
name: "optimization disabled",
cfg: &config.Config{},
userAgent: "codex-tui/0.145.0",
},
{
name: "unrelated client",
cfg: &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}},
userAgent: "curl/8.7.1",
},
{
name: "nil config",
userAgent: "Codex Desktop/0.146.0-alpha.3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
headers := http.Header{"User-Agent": []string{tt.userAgent}}
got := RewriteCodexMultiAgentV2Input(context.Background(), headers, payload, tt.cfg)
if rewritten := gjson.GetBytes(got, "input.0.type").String() == "message"; rewritten != tt.want {
t.Fatalf("rewritten = %v, want %v; payload=%s", rewritten, tt.want, got)
}
})
}
}
func TestTranslateRequestWithCodexMultiAgentV2Conditions(t *testing.T) {
payload := []byte(`{"model":"test-model","input":[{"type":"agent_message","content":[{"type":"encrypted_content","encrypted_content":"task"}]}]}`)
enabledCfg := &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}}
eligibleHeaders := http.Header{"User-Agent": []string{"Codex Desktop/0.146.0-alpha.3"}}
translations := []struct {
name string
to sdktranslator.Format
path string
want string
model string
}{
{name: "Claude", to: sdktranslator.FormatClaude, path: "messages.0.content", want: "task", model: "claude-sonnet-4-5"},
{name: "Gemini", to: sdktranslator.FormatGemini, path: "contents.0.parts.0.text", want: "task", model: "gemini-2.5-pro"},
{name: "Antigravity", to: sdktranslator.FormatAntigravity, path: "request.contents.0.parts.0.text", want: "task", model: "gemini-2.5-pro"},
{name: "OpenAI", to: sdktranslator.FormatOpenAI, path: "messages.0.content.0.text", want: "task", model: "chat-model"},
{name: "Interactions", to: sdktranslator.FormatInteractions, path: "input.0.content.0.text", want: "task", model: "interaction-model"},
}
for _, tt := range translations {
t.Run(tt.name, func(t *testing.T) {
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), eligibleHeaders, enabledCfg, sdktranslator.FormatOpenAIResponse, tt.to, tt.model, payload, false)
if value := gjson.GetBytes(got, tt.path).String(); value != tt.want {
t.Fatalf("%s = %q, want %q; output=%s", tt.path, value, tt.want, got)
}
})
}
t.Run("disabled optimization", func(t *testing.T) {
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), eligibleHeaders, &config.Config{}, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "chat-model", payload, false)
if count := gjson.GetBytes(got, "messages.#").Int(); count != 0 {
t.Fatalf("disabled optimization translated agent_message; output=%s", got)
}
})
t.Run("unrelated client", func(t *testing.T) {
headers := http.Header{"User-Agent": []string{"curl/8.7.1"}}
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), headers, enabledCfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "chat-model", payload, false)
if count := gjson.GetBytes(got, "messages.#").Int(); count != 0 {
t.Fatalf("unrelated client agent_message was translated; output=%s", got)
}
})
t.Run("non-Responses source", func(t *testing.T) {
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), eligibleHeaders, enabledCfg, sdktranslator.FormatOpenAI, sdktranslator.FormatOpenAI, "test-model", payload, false)
if messageType := gjson.GetBytes(got, "input.0.type").String(); messageType != "agent_message" {
t.Fatalf("non-Responses source changed agent_message; output=%s", got)
}
})
for _, target := range []sdktranslator.Format{sdktranslator.FormatCodex, sdktranslator.FormatOpenAIResponse} {
t.Run("excluded target "+target.String(), func(t *testing.T) {
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), eligibleHeaders, enabledCfg, sdktranslator.FormatOpenAIResponse, target, "test-model", payload, false)
if messageType := gjson.GetBytes(got, "input.0.type").String(); messageType != "agent_message" {
t.Fatalf("target %s changed agent_message; output=%s", target, got)
}
})
}
}
func TestRewriteCodexSpawnAgentDescriptionDisabledLeavesPayloadUnchanged(t *testing.T) {
t.Parallel()
payload := []byte(`{"tools":[{"type":"function","name":"spawn_agent","description":"unchanged","parameters":{"properties":{"message":{"encrypted":true}}}}]}`)
headers := http.Header{"User-Agent": []string{"codex-tui/0.145.0"}}
got := RewriteCodexSpawnAgentDescription(context.Background(), headers, payload, &config.Config{})
if string(got) != string(payload) {
t.Fatalf("disabled optimization changed payload: %s", got)
}
}
func TestRewriteCodexSpawnAgentDescriptionIgnoresOtherUserAgent(t *testing.T) {
t.Parallel()
payload := []byte(`{"tools":[{"type":"function","name":"spawn_agent","description":"unchanged"}]}`)
headers := http.Header{"User-Agent": []string{"curl/8.7.1"}}
cfg := &config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}}
got := RewriteCodexSpawnAgentDescription(context.Background(), headers, payload, cfg)
if string(got) != string(payload) {
t.Fatalf("payload changed for unrelated User-Agent: %s", got)
}
}
func TestReplaceCodexSpawnAgentModelsNormalizesSectionsAndPreservesInstructions(t *testing.T) {
t.Parallel()
description := codexSpawnAgentModelsHeading + "\n- `old-model`: old\nKeep this multi-agent instruction.\nSpawns an agent.\n" + codexSpawnAgentModelsHeading
got := replaceCodexSpawnAgentModels(description, "- `new-model`: New model.")
if strings.Contains(got, "old-model") {
t.Fatalf("old model list was preserved: %q", got)
}
if count := strings.Count(got, codexSpawnAgentModelsHeading); count != 1 {
t.Fatalf("model heading count = %d, want 1: %q", count, got)
}
if !strings.Contains(got, "Keep this multi-agent instruction.") {
t.Fatalf("following instruction was removed: %q", got)
}
}
func TestCodexClientUserAgentPrefersGinRequest(t *testing.T) {
gin.SetMode(gin.TestMode)
request := httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
request.Header.Set("User-Agent", "codex-tui/0.145.0")
ginCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
ginCtx.Request = request
ctx := context.WithValue(context.Background(), "gin", ginCtx)
headers := http.Header{"User-Agent": []string{"overridden-client/1.0"}}
if got := codexClientUserAgent(ctx, headers); got != "codex-tui/0.145.0" {
t.Fatalf("codexClientUserAgent() = %q, want gin request User-Agent", got)
}
}
@@ -37,6 +37,7 @@ func TestLoadConfigOptional_CodexIdentityConfuse(t *testing.T) {
configYAML := []byte(`
codex:
identity-confuse: true
optimize-multi-agent-v2: true
`)
if err := os.WriteFile(configPath, configYAML, 0o600); err != nil {
t.Fatalf("failed to write config: %v", err)
@@ -50,4 +51,7 @@ codex:
if !cfg.Codex.IdentityConfuse {
t.Fatalf("IdentityConfuse = false, want true")
}
if !cfg.Codex.OptimizeMultiAgentV2 {
t.Fatalf("OptimizeMultiAgentV2 = false, want true")
}
}
+2
View File
@@ -291,6 +291,8 @@ type CodexHeaderDefaults struct {
// CodexConfig configures provider-wide Codex request behavior.
type CodexConfig struct {
IdentityConfuse bool `yaml:"identity-confuse" json:"identity-confuse"`
// OptimizeMultiAgentV2 optimizes official Codex multi-agent requests.
OptimizeMultiAgentV2 bool `yaml:"optimize-multi-agent-v2" json:"optimize-multi-agent-v2"`
}
// TLSConfig holds HTTPS server settings.
+3
View File
@@ -42,6 +42,9 @@ type SDKConfig struct {
// RequestLog enables or disables detailed request logging functionality.
RequestLog bool `yaml:"request-log" json:"request-log"`
// CodexOptimizeMultiAgentV2 mirrors the provider-wide runtime setting for API handlers.
CodexOptimizeMultiAgentV2 bool `yaml:"-" json:"-"`
// APIKeys is a list of keys for authenticating clients to this proxy server.
APIKeys []string `yaml:"api-keys" json:"api-keys"`
@@ -131,7 +131,7 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
reporter := helps.NewExecutorUsageReporter(ctx, e, baseModel, auth)
defer reporter.TrackFailure(ctx, &err)
translatedReq, body, err := e.translateRequest(req, opts, false)
translatedReq, body, err := e.translateRequest(ctx, req, opts, false)
if err != nil {
return resp, err
}
@@ -200,7 +200,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
reporter := helps.NewExecutorUsageReporter(ctx, e, baseModel, auth)
defer reporter.TrackFailure(ctx, &err)
translatedReq, body, err := e.translateRequest(req, opts, true)
translatedReq, body, err := e.translateRequest(ctx, req, opts, true)
if err != nil {
return nil, err
}
@@ -381,7 +381,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
// CountTokens counts tokens for the given request using the AI Studio API.
func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName
_, body, err := e.translateRequest(req, opts, false)
_, body, err := e.translateRequest(ctx, req, opts, false)
if err != nil {
return cliproxyexecutor.Response{}, err
}
@@ -449,7 +449,7 @@ type translatedPayload struct {
toFormat sdktranslator.Format
}
func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts cliproxyexecutor.Options, stream bool) ([]byte, translatedPayload, error) {
func (e *AIStudioExecutor) translateRequest(ctx context.Context, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, stream bool) ([]byte, translatedPayload, error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName
from := opts.SourceFormat
@@ -459,8 +459,8 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream)
payload := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, stream)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, stream)
payload := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, stream)
payload, err := thinking.ApplyThinking(payload, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
return nil, translatedPayload{}, err
@@ -653,8 +653,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
if updatedAuth != nil {
auth = updatedAuth
}
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, false)
translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -874,8 +874,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
if updatedAuth != nil {
auth = updatedAuth
}
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true)
translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true)
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -1344,8 +1344,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
auth = updatedAuth
}
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true)
translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true)
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -1680,7 +1680,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
}
// Prepare payload once (doesn't depend on baseURL)
payload := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
payload := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
payload, err := thinking.ApplyThinking(payload, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
+5 -5
View File
@@ -287,8 +287,8 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, stream)
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, stream)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, stream)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, stream)
body = helps.SetStringIfDifferent(body, "model", upstreamModel)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
@@ -482,8 +482,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true)
body = helps.SetStringIfDifferent(body, "model", upstreamModel)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
@@ -778,7 +778,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
to := sdktranslator.FromString("claude")
// Use streaming translation to preserve function calling, except for claude.
stream := from != to
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, stream)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, stream)
body = helps.SetStringIfDifferent(body, "model", upstreamModel)
if rebuildMidSystemMessageEnabled(e.cfg, auth) {
body = rebuildMidSystemMessagesToTopLevel(body)
@@ -1149,6 +1149,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
}
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body)
body = normalizeCodexParallelToolCalls(body, opts.Headers)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2Request(ctx, opts.Headers, body, e.cfg)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return resp, errReplay
@@ -1218,6 +1219,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
}
eventData := bytes.TrimSpace(line[5:])
eventData = helps.RestoreCodexMultiAgentV2Response(eventData, optimizeMultiAgentV2)
eventType := gjson.GetBytes(eventData, "type").String()
if streamErr, terminalBody, ok := codexTerminalFailureErr(eventData); ok {
@@ -1308,6 +1310,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
body = normalizeCodexInstructions(body)
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body)
body = normalizeCodexParallelToolCalls(body, opts.Headers)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2Request(ctx, opts.Headers, body, e.cfg)
reporter.SetTranslatedReasoningEffort(body, to.String())
url := strings.TrimSuffix(baseURL, "/") + "/responses/compact"
@@ -1364,6 +1367,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
}
upstreamData := applyCodexIdentityConfuseResponsePayload(data, identityState)
helps.AppendAPIResponseChunk(ctx, e.cfg, upstreamData)
upstreamData = helps.RestoreCodexMultiAgentV2Response(upstreamData, optimizeMultiAgentV2)
reporter.Publish(ctx, helps.ParseOpenAIUsage(upstreamData))
reporter.EnsurePublished(ctx)
var param any
@@ -1420,6 +1424,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
}
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex executor", body)
body = normalizeCodexParallelToolCalls(body, opts.Headers)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2Request(ctx, opts.Headers, body, e.cfg)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return nil, errReplay
@@ -1501,6 +1506,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
if bytes.HasPrefix(line, dataTag) {
data := bytes.TrimSpace(line[5:])
data = helps.RestoreCodexMultiAgentV2Response(data, optimizeMultiAgentV2)
translatedLine = append([]byte("data: "), data...)
eventType := gjson.GetBytes(data, "type").String()
if streamErr, terminalBody, ok := codexTerminalFailureErr(data); ok {
if errClearReplay := clearCodexReasoningReplayOnInvalidSignature(ctx, replayScope, streamErr.StatusCode(), terminalBody); errClearReplay != nil {
@@ -0,0 +1,208 @@
package executor
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
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 TestCodexExecutorOptimizeMultiAgentV2(t *testing.T) {
modelID := "codex-executor-spawn-agent-test-model"
clientID := "codex-executor-spawn-agent-test-client"
modelRegistry := registry.GetGlobalRegistry()
modelRegistry.RegisterClient(clientID, "codex", []*registry.ModelInfo{{
ID: modelID,
Description: "Executor test model.",
Thinking: &registry.ThinkingSupport{
Levels: []string{"low", "medium", "high"},
},
}})
defer modelRegistry.UnregisterClient(clientID)
var upstreamBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
upstreamBody, _ = io.ReadAll(request.Body)
if request.URL.Path == "/responses/compact" {
w.Header().Set("Content-Type", "application/json")
namespace := gjson.GetBytes(upstreamBody, "input.0.tools.0.name").String()
compact := fmt.Sprintf(`{"id":"resp_1","object":"response.compaction","output":[{"type":"function_call","name":"spawn_agent","namespace":%q,"arguments":"{}","call_id":"call_1"}]}`, namespace)
_, _ = w.Write([]byte(compact))
return
}
w.Header().Set("Content-Type", "text/event-stream")
namespace := gjson.GetBytes(upstreamBody, "input.0.tools.0.name").String()
completed := fmt.Sprintf(`data: {"type":"response.completed","response":{"id":"resp_1","object":"response","status":"completed","output":[{"type":"function_call","name":"spawn_agent","namespace":%q,"arguments":"{}","call_id":"call_1"}]}}`+"\n\n", namespace)
_, _ = w.Write([]byte(completed))
}))
defer server.Close()
payload := codexSpawnAgentTestPayload()
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"base_url": server.URL,
"api_key": "test",
}}
tests := []struct {
name string
enabled bool
mode string
}{
{name: "execute enabled", enabled: true, mode: "execute"},
{name: "execute disabled", enabled: false, mode: "execute"},
{name: "stream enabled", enabled: true, mode: "stream"},
{name: "stream disabled", enabled: false, mode: "stream"},
{name: "compact enabled", enabled: true, mode: "compact"},
{name: "compact disabled", enabled: false, mode: "compact"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
upstreamBody = nil
executor := NewCodexExecutor(&config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: tt.enabled}})
ctx := codexSpawnAgentTestContext()
headers := http.Header{"User-Agent": []string{"overridden-client/1.0"}}
req := cliproxyexecutor.Request{Model: "gpt-5.4", Payload: payload}
opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("openai-response"), Headers: headers}
var clientPayload []byte
switch tt.mode {
case "stream":
result, errExecute := executor.ExecuteStream(ctx, auth, req, opts)
if errExecute != nil {
t.Fatalf("ExecuteStream() error = %v", errExecute)
}
for chunk := range result.Chunks {
clientPayload = append(clientPayload, chunk.Payload...)
}
case "compact":
opts.Alt = "responses/compact"
response, errExecute := executor.Execute(ctx, auth, req, opts)
if errExecute != nil {
t.Fatalf("compact Execute() error = %v", errExecute)
}
clientPayload = response.Payload
default:
response, errExecute := executor.Execute(ctx, auth, req, opts)
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
clientPayload = response.Payload
}
assertCodexSpawnAgentOptimization(t, upstreamBody, modelID, tt.enabled)
assertCodexSpawnAgentRequestMessage(t, upstreamBody, tt.enabled)
assertCodexSpawnAgentClientNamespace(t, clientPayload)
})
}
}
func codexSpawnAgentTestPayload() []byte {
return []byte(`{
"model":"gpt-5.4",
"input":[{
"type":"additional_tools",
"role":"developer",
"tools":[{
"type":"namespace",
"name":"collaboration",
"tools":[{
"type":"function",
"name":"spawn_agent",
"description":"Available model overrides (optional; inherited parent model is preferred):\n- old-model\nSpawns an agent.",
"parameters":{"type":"object","properties":{"message":{"type":"string","encrypted":true}}}
}]
}]
},{
"type":"agent_message",
"id":"amsg_1",
"author":"/root",
"recipient":"/root/worker",
"content":[
{"type":"input_text","text":"Payload:\n"},
{"type":"encrypted_content","encrypted_content":"delegated task"}
],
"internal_chat_message_metadata_passthrough":{"turn_id":"turn_1"}
}]
}`)
}
func codexSpawnAgentTestContext() context.Context {
request := httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
request.Header.Set("User-Agent", "codex-tui/0.145.0")
ginCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
ginCtx.Request = request
return context.WithValue(context.Background(), "gin", ginCtx)
}
func assertCodexSpawnAgentClientNamespace(t *testing.T, payload []byte) {
t.Helper()
if strings.Contains(string(payload), "collaboration-optimize") {
t.Fatalf("optimized namespace leaked to client: %s", payload)
}
if !strings.Contains(string(payload), `"namespace":"collaboration"`) {
t.Fatalf("restored collaboration namespace missing from client payload: %s", payload)
}
}
func assertCodexSpawnAgentRequestMessage(t *testing.T, payload []byte, enabled bool) {
t.Helper()
message := gjson.GetBytes(payload, "input.1")
if message.Get("type").String() != "agent_message" || message.Get("role").Exists() {
t.Fatalf("Codex executor changed outer agent message: %s", payload)
}
if message.Get("author").String() != "/root" || message.Get("recipient").String() != "/root/worker" || message.Get("internal_chat_message_metadata_passthrough.turn_id").String() != "turn_1" {
t.Fatalf("Codex executor changed agent message metadata: %s", payload)
}
if enabled {
if message.Get("content.1.type").String() != "input_text" || message.Get("content.1.text").String() != "delegated task" {
t.Fatalf("Codex executor did not normalize agent message content: %s", payload)
}
if message.Get("content.1.encrypted_content").Exists() {
t.Fatalf("Codex executor preserved encrypted_content: %s", payload)
}
return
}
if message.Get("content.1.type").String() != "encrypted_content" || message.Get("content.1.encrypted_content").String() != "delegated task" {
t.Fatalf("disabled optimization changed agent message content: %s", payload)
}
}
func assertCodexSpawnAgentOptimization(t *testing.T, payload []byte, modelID string, enabled bool) {
t.Helper()
namespace := gjson.GetBytes(payload, "input.0.tools.0.name").String()
description := gjson.GetBytes(payload, "input.0.tools.0.tools.0.description").String()
encrypted := gjson.GetBytes(payload, "input.0.tools.0.tools.0.parameters.properties.message.encrypted")
if enabled {
if namespace != "collaboration-optimize" {
t.Fatalf("optimized namespace = %q, want collaboration-optimize", namespace)
}
wantModel := "- `" + modelID + "`: Executor test model. Reasoning efforts: low, medium (default), high."
if !strings.Contains(description, wantModel) {
t.Fatalf("description does not contain model metadata: %q", description)
}
if encrypted.Exists() {
t.Fatalf("message encrypted was not removed: %s", encrypted.Raw)
}
return
}
if namespace != "collaboration" {
t.Fatalf("disabled namespace = %q, want collaboration", namespace)
}
if !strings.Contains(description, "- old-model") {
t.Fatalf("disabled optimization changed description: %q", description)
}
if !encrypted.Bool() {
t.Fatalf("disabled optimization removed message encrypted: %s", encrypted.Raw)
}
}
@@ -489,6 +489,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
}
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex websockets executor", body)
body = normalizeCodexWebsocketParallelToolCalls(body, opts.Headers)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2Request(ctx, opts.Headers, body, e.cfg)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return resp, errReplay
@@ -703,6 +704,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
reporter.MarkFirstResponseByte()
payload = applyCodexIdentityConfuseResponsePayload(payload, identityState)
helps.AppendAPIWebsocketResponse(ctx, e.cfg, payload)
payload = helps.RestoreCodexMultiAgentV2Response(payload, optimizeMultiAgentV2)
if wsErr, ok := parseCodexWebsocketError(payload); ok {
if sess != nil {
@@ -788,6 +790,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
}
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex websockets executor", body)
body = normalizeCodexWebsocketParallelToolCalls(body, opts.Headers)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2Request(ctx, opts.Headers, body, e.cfg)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return nil, errReplay
@@ -1062,6 +1065,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
reporter.MarkFirstResponseByte()
payload = applyCodexIdentityConfuseResponsePayload(payload, identityState)
helps.AppendAPIWebsocketResponse(ctx, e.cfg, payload)
payload = helps.RestoreCodexMultiAgentV2Response(payload, optimizeMultiAgentV2)
if wsErr, ok := parseCodexWebsocketError(payload); ok {
terminateReason = "upstream_error"
@@ -0,0 +1,98 @@
package executor
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/websocket"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
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 TestCodexWebsocketsExecutorOptimizeMultiAgentV2(t *testing.T) {
modelID := "codex-websocket-spawn-agent-test-model"
clientID := "codex-websocket-spawn-agent-test-client"
modelRegistry := registry.GetGlobalRegistry()
modelRegistry.RegisterClient(clientID, "codex", []*registry.ModelInfo{{
ID: modelID,
Description: "Executor test model.",
Thinking: &registry.ThinkingSupport{
Levels: []string{"low", "medium", "high"},
},
}})
defer modelRegistry.UnregisterClient(clientID)
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
capturedPayload := make(chan []byte, 2)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
conn, errUpgrade := upgrader.Upgrade(w, request, nil)
if errUpgrade != nil {
t.Errorf("upgrade websocket: %v", errUpgrade)
return
}
defer func() { _ = conn.Close() }()
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
t.Errorf("read websocket request: %v", errRead)
return
}
capturedPayload <- payload
namespace := gjson.GetBytes(payload, "input.0.tools.0.name").String()
completed := []byte(fmt.Sprintf(`{"type":"response.completed","response":{"id":"resp_1","object":"response","status":"completed","output":[{"type":"function_call","name":"spawn_agent","namespace":%q,"arguments":"{}","call_id":"call_1"}]}}`, namespace))
if errWrite := conn.WriteMessage(websocket.TextMessage, completed); errWrite != nil {
t.Errorf("write websocket response: %v", errWrite)
}
}))
defer server.Close()
auth := &cliproxyauth.Auth{Attributes: map[string]string{
"base_url": server.URL,
"api_key": "test",
}}
req := cliproxyexecutor.Request{Model: "gpt-5.4", Payload: codexSpawnAgentTestPayload()}
opts := cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai-response"),
Headers: http.Header{"User-Agent": []string{"overridden-client/1.0"}},
}
for _, tt := range []struct {
name string
enabled bool
stream bool
}{
{name: "execute enabled", enabled: true},
{name: "execute disabled", enabled: false},
{name: "stream enabled", enabled: true, stream: true},
{name: "stream disabled", enabled: false, stream: true},
} {
t.Run(tt.name, func(t *testing.T) {
executor := NewCodexWebsocketsExecutor(&config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: tt.enabled}})
var clientPayload []byte
if tt.stream {
result, errExecute := executor.ExecuteStream(codexSpawnAgentTestContext(), auth, req, opts)
if errExecute != nil {
t.Fatalf("ExecuteStream() error = %v", errExecute)
}
for chunk := range result.Chunks {
clientPayload = append(clientPayload, chunk.Payload...)
}
} else {
response, errExecute := executor.Execute(codexSpawnAgentTestContext(), auth, req, opts)
if errExecute != nil {
t.Fatalf("Execute() error = %v", errExecute)
}
clientPayload = response.Payload
}
upstreamPayload := <-capturedPayload
assertCodexSpawnAgentOptimization(t, upstreamPayload, modelID, tt.enabled)
assertCodexSpawnAgentRequestMessage(t, upstreamPayload, tt.enabled)
assertCodexSpawnAgentClientNamespace(t, clientPayload)
})
}
}
+13 -13
View File
@@ -145,8 +145,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, false)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -258,8 +258,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -387,7 +387,7 @@ func (e *GeminiExecutor) executeInteractions(ctx context.Context, auth *cliproxy
reporter := helps.NewExecutorUsageReporter(ctx, e, targetName, auth)
defer reporter.TrackFailure(ctx, &err)
body := translateGeminiInteractionsRequestBody(targetName, req.Payload, opts, false)
body := translateGeminiInteractionsRequestBody(ctx, e.cfg, targetName, req.Payload, opts, false)
if gjson.GetBytes(body, "model").Exists() && targetName != "" {
body = helps.SetStringIfDifferent(body, "model", targetName)
}
@@ -398,7 +398,7 @@ func (e *GeminiExecutor) executeInteractions(ctx context.Context, auth *cliproxy
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
requestPath := helps.PayloadRequestPath(opts)
fromProtocol := opts.SourceFormat.String()
originalTranslated := geminiInteractionsPayloadConfigSource(targetName, req.Payload, opts, false)
originalTranslated := geminiInteractionsPayloadConfigSource(ctx, e.cfg, targetName, req.Payload, opts, false)
body = helps.ApplyPayloadConfigWithRequest(e.cfg, targetName, "interactions", fromProtocol, "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
baseURL := resolveGeminiBaseURL(auth)
@@ -463,7 +463,7 @@ func (e *GeminiExecutor) executeInteractionsStream(ctx context.Context, auth *cl
reporter := helps.NewExecutorUsageReporter(ctx, e, targetName, auth)
defer reporter.TrackFailure(ctx, &err)
body := translateGeminiInteractionsRequestBody(targetName, req.Payload, opts, true)
body := translateGeminiInteractionsRequestBody(ctx, e.cfg, targetName, req.Payload, opts, true)
if gjson.GetBytes(body, "model").Exists() && targetName != "" {
body = helps.SetStringIfDifferent(body, "model", targetName)
}
@@ -474,7 +474,7 @@ func (e *GeminiExecutor) executeInteractionsStream(ctx context.Context, auth *cl
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
requestPath := helps.PayloadRequestPath(opts)
fromProtocol := opts.SourceFormat.String()
originalTranslated := geminiInteractionsPayloadConfigSource(targetName, req.Payload, opts, true)
originalTranslated := geminiInteractionsPayloadConfigSource(ctx, e.cfg, targetName, req.Payload, opts, true)
body = helps.ApplyPayloadConfigWithRequest(e.cfg, targetName, "interactions", fromProtocol, "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
body = helps.SetBoolIfDifferent(body, "stream", true)
baseURL := resolveGeminiBaseURL(auth)
@@ -619,7 +619,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
from := opts.SourceFormat
responseFormat := cliproxyexecutor.ResponseFormatOrSource(opts)
to := sdktranslator.FromString("gemini")
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
translatedReq := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -779,19 +779,19 @@ func nativeInteractionsSourceFormat(format sdktranslator.Format) bool {
}
}
func translateGeminiInteractionsRequestBody(model string, payload []byte, opts cliproxyexecutor.Options, stream bool) []byte {
func translateGeminiInteractionsRequestBody(ctx context.Context, cfg *config.Config, model string, payload []byte, opts cliproxyexecutor.Options, stream bool) []byte {
if opts.SourceFormat == "" || opts.SourceFormat == sdktranslator.FormatInteractions {
return bytes.Clone(payload)
}
return sdktranslator.TranslateRequest(opts.SourceFormat, sdktranslator.FormatInteractions, model, payload, stream)
return helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, cfg, opts.SourceFormat, sdktranslator.FormatInteractions, model, payload, stream)
}
func geminiInteractionsPayloadConfigSource(model string, payload []byte, opts cliproxyexecutor.Options, stream bool) []byte {
func geminiInteractionsPayloadConfigSource(ctx context.Context, cfg *config.Config, model string, payload []byte, opts cliproxyexecutor.Options, stream bool) []byte {
source := opts.OriginalRequest
if len(source) == 0 {
source = payload
}
return translateGeminiInteractionsRequestBody(model, source, opts, stream)
return translateGeminiInteractionsRequestBody(ctx, cfg, model, source, opts, stream)
}
func isNativeInteractionsAuth(auth *cliproxyauth.Auth) bool {
@@ -328,8 +328,8 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
body = sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, false)
body = helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -453,8 +453,8 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, false)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -568,8 +568,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -714,8 +714,8 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -852,7 +852,7 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
responseFormat := cliproxyexecutor.ResponseFormatOrSource(opts)
to := sdktranslator.FromString("gemini")
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
translatedReq := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -943,7 +943,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
responseFormat := cliproxyexecutor.ResponseFormatOrSource(opts)
to := sdktranslator.FromString("gemini")
translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
translatedReq := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -0,0 +1,40 @@
package helps
import (
"context"
"net/http"
multiagentv2 "github.com/router-for-me/CLIProxyAPI/v7/internal/client/codex/optimize-multi-agent-v2"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
)
// RewriteCodexSpawnAgentDescription optimizes spawn_agent definitions for
// official Codex clients when multi-agent v2 optimization is enabled.
func RewriteCodexSpawnAgentDescription(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
return multiagentv2.RewriteCodexSpawnAgentDescription(ctx, headers, payload, cfg)
}
// RewriteCodexMultiAgentV2Input converts official Codex multi-agent input into
// standard Responses API messages when multi-agent v2 optimization is enabled.
func RewriteCodexMultiAgentV2Input(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
return multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
}
// TranslateRequestWithCodexMultiAgentV2 normalizes official Codex multi-agent
// input before translating it to a non-Codex target protocol.
func TranslateRequestWithCodexMultiAgentV2(ctx context.Context, headers http.Header, cfg *config.Config, from, to sdktranslator.Format, model string, payload []byte, stream bool) []byte {
return multiagentv2.TranslateRequestWithCodexMultiAgentV2(ctx, headers, cfg, from, to, model, payload, stream)
}
// OptimizeCodexMultiAgentV2Request rewrites an eligible spawn_agent request and
// reports whether the collaboration namespace was renamed for upstream use.
func OptimizeCodexMultiAgentV2Request(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) ([]byte, bool) {
return multiagentv2.OptimizeCodexMultiAgentV2Request(ctx, headers, payload, cfg)
}
// RestoreCodexMultiAgentV2Response restores optimized collaboration namespace
// values before an upstream response is translated and returned to the client.
func RestoreCodexMultiAgentV2Response(payload []byte, optimized bool) []byte {
return multiagentv2.RestoreCodexMultiAgentV2Response(payload, optimized)
}
+4 -4
View File
@@ -103,8 +103,8 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
originalPayloadSource = opts.OriginalRequest
}
originalPayload := bytes.Clone(originalPayloadSource)
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, false)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, bytes.Clone(req.Payload), false)
// Strip kimi- prefix and any [1m] suffix for upstream API
upstreamModel := normalizeKimiUpstreamModel(baseModel)
@@ -212,8 +212,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
originalPayloadSource = opts.OriginalRequest
}
originalPayload := bytes.Clone(originalPayloadSource)
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), true)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true)
body := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, bytes.Clone(req.Payload), true)
// Strip kimi- prefix and any [1m] suffix for upstream API
upstreamModel := normalizeKimiUpstreamModel(baseModel)
@@ -111,8 +111,8 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, opts.Stream)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, opts.Stream)
translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, opts.Stream)
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -312,8 +312,8 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
originalTranslated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, originalPayload, true)
translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, true)
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -583,7 +583,7 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau
from := opts.SourceFormat
responseFormat := cliproxyexecutor.ResponseFormatOrSource(opts)
to := sdktranslator.FromString("openai")
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
translated := helps.TranslateRequestWithCodexMultiAgentV2(ctx, opts.Headers, e.cfg, from, to, baseModel, req.Payload, false)
modelForCounting := baseModel
@@ -1028,6 +1028,7 @@ func (e *XAIExecutor) prepareResponsesRequestTo(ctx context.Context, req cliprox
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
body, _ = sjson.DeleteBytes(body, "safety_identifier")
body, _ = sjson.DeleteBytes(body, "stream_options")
body = helps.RewriteCodexMultiAgentV2Input(ctx, opts.Headers, body, e.cfg)
namespaceTools := collectXAINamespaceToolRefs(body)
// Collect before normalizeXAITools flattens namespace wrappers so keys match
// the post-restore (namespace, short-name) shape used by the response filter.
@@ -329,6 +329,56 @@ func TestXAIExecutorExecuteShapesResponsesRequest(t *testing.T) {
}
}
func TestXAIExecutorPrepareResponsesRequestRewritesCodexAgentMessage(t *testing.T) {
t.Parallel()
exec := NewXAIExecutor(&config.Config{Codex: config.CodexConfig{OptimizeMultiAgentV2: true}})
payload := []byte(`{
"model":"grok-4.5",
"input":[{
"type":"agent_message",
"id":"amsg_019f92c3-6d77-7880-a6e4-f920867dc6a0",
"author":"/root",
"recipient":"/root/arithmetic_question",
"content":[
{"type":"input_text","text":"Message Type: NEW_TASK\nTask name: /root/arithmetic_question\nSender: /root\nPayload:\n"},
{"type":"encrypted_content","encrypted_content":"请出一道四则运算题。只回复题目本身,不要解答;使用中文。"}
],
"internal_chat_message_metadata_passthrough":{"turn_id":"019f92c3-6772-7213-8aac-8bd154d528f1"}
}]
}`)
prepared, errPrepare := exec.prepareResponsesRequest(context.Background(), cliproxyexecutor.Request{
Model: "grok-4.5",
Payload: payload,
}, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatOpenAIResponse,
Headers: http.Header{"User-Agent": []string{"Codex Desktop/0.146.0-alpha.3.1"}},
}, true)
if errPrepare != nil {
t.Fatalf("prepareResponsesRequest() error = %v", errPrepare)
}
message := gjson.GetBytes(prepared.body, "input.0")
if message.Get("type").String() != "message" || message.Get("role").String() != "user" {
t.Fatalf("agent message was not rewritten: %s", prepared.body)
}
if message.Get("content.1.type").String() != "input_text" {
t.Fatalf("content[1].type = %q, want input_text; body=%s", message.Get("content.1.type").String(), prepared.body)
}
if text := message.Get("content.1.text").String(); text != "请出一道四则运算题。只回复题目本身,不要解答;使用中文。" {
t.Fatalf("content[1].text = %q; body=%s", text, prepared.body)
}
if message.Get("content.1.encrypted_content").Exists() {
t.Fatalf("encrypted_content was preserved: %s", prepared.body)
}
if message.Get("id").String() != "amsg_019f92c3-6d77-7880-a6e4-f920867dc6a0" || message.Get("author").String() != "/root" || message.Get("recipient").String() != "/root/arithmetic_question" {
t.Fatalf("agent message identity fields changed: %s", prepared.body)
}
if turnID := message.Get("internal_chat_message_metadata_passthrough.turn_id").String(); turnID != "019f92c3-6772-7213-8aac-8bd154d528f1" {
t.Fatalf("turn_id = %q; body=%s", turnID, prepared.body)
}
}
func TestXAIExecutorExecuteRestoresAdditionalToolsNamespaceCalls(t *testing.T) {
var gotBody []byte
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+3
View File
@@ -105,6 +105,9 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
if oldCfg.Codex.IdentityConfuse != newCfg.Codex.IdentityConfuse {
changes = append(changes, fmt.Sprintf("codex.identity-confuse: %t -> %t", oldCfg.Codex.IdentityConfuse, newCfg.Codex.IdentityConfuse))
}
if oldCfg.Codex.OptimizeMultiAgentV2 != newCfg.Codex.OptimizeMultiAgentV2 {
changes = append(changes, fmt.Sprintf("codex.optimize-multi-agent-v2: %t -> %t", oldCfg.Codex.OptimizeMultiAgentV2, newCfg.Codex.OptimizeMultiAgentV2))
}
if oldCfg.Routing.Strategy != newCfg.Routing.Strategy {
changes = append(changes, fmt.Sprintf("routing.strategy: %s -> %s", oldCfg.Routing.Strategy, newCfg.Routing.Strategy))
+17 -7
View File
@@ -35,20 +35,27 @@ var codexClientAllowedReasoningLevels = map[string]struct{}{
}
func (h *OpenAIAPIHandler) codexClientModelsResponse() map[string]any {
return codexClientModelsResponse(h.Models(), registry.GetGlobalRegistry().GetModelProviders)
optimizeMultiAgentV2 := h != nil && h.Cfg != nil && h.Cfg.CodexOptimizeMultiAgentV2
return codexClientModelsResponse(h.Models(), registry.GetGlobalRegistry().GetModelProviders, optimizeMultiAgentV2)
}
func CodexClientModelsResponse(models []map[string]any) map[string]any {
return codexClientModelsResponse(models, nil)
return codexClientModelsResponse(models, nil, false)
}
func codexClientModelsResponse(models []map[string]any, providersForModel codexClientModelProvidersFunc) map[string]any {
// CodexClientModelsResponseWithMultiAgentV2 builds the Codex client model response
// and advertises multi-agent v2 for synthesized models when enabled.
func CodexClientModelsResponseWithMultiAgentV2(models []map[string]any, enabled bool) map[string]any {
return codexClientModelsResponse(models, nil, enabled)
}
func codexClientModelsResponse(models []map[string]any, providersForModel codexClientModelProvidersFunc, optimizeMultiAgentV2 bool) map[string]any {
return map[string]any{
"models": buildCodexClientModels(models, providersForModel),
"models": buildCodexClientModels(models, providersForModel, optimizeMultiAgentV2),
}
}
func buildCodexClientModels(models []map[string]any, providersForModel codexClientModelProvidersFunc) []map[string]any {
func buildCodexClientModels(models []map[string]any, providersForModel codexClientModelProvidersFunc, optimizeMultiAgentV2 bool) []map[string]any {
templates, defaultTemplate, err := loadCodexClientModelTemplates()
if err != nil || defaultTemplate == nil {
return nil
@@ -72,7 +79,7 @@ func buildCodexClientModels(models []map[string]any, providersForModel codexClie
}
entry := cloneCodexClientModelMap(defaultTemplate)
applyCodexClientModelMetadata(entry, id, model)
applyCodexClientModelMetadata(entry, id, model, optimizeMultiAgentV2)
applyCodexClientSearchToolSupport(entry, id, false, providersForModel)
sanitizeCodexClientReasoningMetadata(entry)
applyCodexClientVisibilityOverride(entry, id)
@@ -214,7 +221,7 @@ func applyCodexClientSearchToolSupport(entry map[string]any, id string, template
}
}
func applyCodexClientModelMetadata(entry map[string]any, id string, model map[string]any) {
func applyCodexClientModelMetadata(entry map[string]any, id string, model map[string]any, optimizeMultiAgentV2 bool) {
info := registry.LookupModelInfo(id)
displayName := stringModelValue(model, "display_name")
@@ -252,6 +259,9 @@ func applyCodexClientModelMetadata(entry map[string]any, id string, model map[st
entry["display_name"] = displayName
entry["description"] = description
entry["prefer_websockets"] = false
if optimizeMultiAgentV2 {
entry["multi_agent_version"] = "v2"
}
entry["service_tiers"] = []any{}
delete(entry, "apply_patch_tool_type")
delete(entry, "upgrade")
@@ -3,7 +3,9 @@ package openai
import (
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
)
func TestCodexClientModelsResponse_InputModalitiesFromRegistry(t *testing.T) {
@@ -189,7 +191,7 @@ func TestCodexClientModelsResponse_RequiresTemplateAndCodexProvidersForSearchToo
{"id": "gpt-5.6-sol"},
}, func(id string) []string {
return providers[id]
})
}, false)
models, ok := resp["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", resp["models"])
@@ -293,3 +295,67 @@ func TestLoadCodexClientModelTemplatesRefreshesOnRevision(t *testing.T) {
t.Fatalf("cached display_name = %q, want Second", got)
}
}
func TestApplyCodexClientModelMetadataPreservesMultiAgentVersionWhenDisabled(t *testing.T) {
entry := map[string]any{"multi_agent_version": "v1"}
model := map[string]any{"id": "custom-model"}
applyCodexClientModelMetadata(entry, "custom-model", model, false)
if got := entry["multi_agent_version"]; got != "v1" {
t.Fatalf("disabled multi_agent_version = %#v, want preserved v1", got)
}
applyCodexClientModelMetadata(entry, "custom-model", model, true)
if got := entry["multi_agent_version"]; got != "v2" {
t.Fatalf("enabled multi_agent_version = %#v, want v2", got)
}
}
func TestCodexClientModelsResponseMultiAgentV2FollowsConfig(t *testing.T) {
modelID := "codex-client-multi-agent-v2-test"
clientID := "codex-client-multi-agent-v2-test-client"
modelRegistry := registry.GetGlobalRegistry()
modelRegistry.RegisterClient(clientID, "openai-compatibility", []*registry.ModelInfo{{ID: modelID}})
t.Cleanup(func() {
modelRegistry.UnregisterClient(clientID)
})
base := handlers.NewBaseAPIHandlers(&config.SDKConfig{}, nil)
handler := NewOpenAIAPIHandler(base)
for _, tt := range []struct {
name string
enabled bool
}{
{name: "disabled", enabled: false},
{name: "enabled", enabled: true},
} {
t.Run(tt.name, func(t *testing.T) {
base.Cfg.CodexOptimizeMultiAgentV2 = tt.enabled
response := handler.codexClientModelsResponse()
models, ok := response["models"].([]map[string]any)
if !ok {
t.Fatalf("models type = %T, want []map[string]any", response["models"])
}
var entry map[string]any
for _, model := range models {
if stringModelValue(model, "slug") == modelID {
entry = model
break
}
}
if entry == nil {
t.Fatalf("missing synthesized model %q", modelID)
}
value, exists := entry["multi_agent_version"]
if tt.enabled {
if !exists || value != "v2" {
t.Fatalf("multi_agent_version = %#v, want v2", value)
}
return
}
if !exists || value != nil {
t.Fatalf("multi_agent_version = %#v, want preserved null", value)
}
})
}
}