mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, §ions); 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 ®istry.ModelInfo{
|
||||
Description: "Dynamic model.",
|
||||
Thinking: ®istry.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: ®istry.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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: ®istry.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: ®istry.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user