mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-06 08:02:22 +08:00
feat(codex): support orphan delegation compatibility via orphan-delegation-compatibility
- Add `codex.orphan-delegation-compatibility` configuration option and mirror it to SDK configuration. - Convert orphan Codex delegation outputs into standard user messages for requests with `X-Openai-Subagent: collab_spawn`. - Integrate orphan delegation rewriting into OpenAI responses request handling pipeline. Closes: #5401
This commit is contained in:
@@ -262,6 +262,12 @@ codex:
|
||||
# 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 opt-in compatibility for orphan Codex delegation outputs.
|
||||
# Converts orphan function_call_output items from codex_app/create_thread and
|
||||
# codex_app/send_message_to_thread (which lack a valid call_id or matching function_call)
|
||||
# into standard user messages across Responses and non-Codex protocols when the request
|
||||
# header contains X-Openai-Subagent: collab_spawn.
|
||||
orphan-delegation-compatibility: false
|
||||
# Terminate and relay Codex Live WebRTC audio and DataChannel traffic in this process.
|
||||
# This requires inbound UDP reachability. Keep disabled to preserve direct media behavior.
|
||||
live-media-relay:
|
||||
|
||||
@@ -46,6 +46,7 @@ func effectiveSDKConfig(cfg *config.Config) *config.SDKConfig {
|
||||
}
|
||||
sdkCfg := cfg.SDKConfig
|
||||
sdkCfg.CodexOptimizeMultiAgentV2 = cfg.Codex.OptimizeMultiAgentV2
|
||||
sdkCfg.CodexOrphanDelegationCompatibility = cfg.Codex.OrphanDelegationCompatibility
|
||||
if cfg.CommercialMode {
|
||||
sdkCfg.RequestLog = false
|
||||
}
|
||||
|
||||
@@ -14,3 +14,12 @@ func TestEffectiveSDKConfigCopiesCodexOptimizeMultiAgentV2(t *testing.T) {
|
||||
t.Fatalf("CodexOptimizeMultiAgentV2 = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveSDKConfigCopiesCodexOrphanDelegationCompatibility(t *testing.T) {
|
||||
cfg := &config.Config{Codex: config.CodexConfig{OrphanDelegationCompatibility: true}}
|
||||
|
||||
sdkCfg := effectiveSDKConfig(cfg)
|
||||
if sdkCfg == nil || !sdkCfg.CodexOrphanDelegationCompatibility {
|
||||
t.Fatalf("CodexOrphanDelegationCompatibility = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +71,23 @@ func RewriteCodexMultiAgentV2Input(ctx context.Context, headers http.Header, pay
|
||||
return rewriteCodexAgentMessageInput(payload)
|
||||
}
|
||||
|
||||
// RewriteCodexOrphanDelegationInputForConfig applies RewriteCodexOrphanDelegationInput
|
||||
// based on cfg.Codex.OrphanDelegationCompatibility and the X-Openai-Subagent header.
|
||||
func RewriteCodexOrphanDelegationInputForConfig(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
|
||||
if cfg == nil || !cfg.Codex.OrphanDelegationCompatibility {
|
||||
return payload
|
||||
}
|
||||
return RewriteCodexOrphanDelegationInput(ctx, headers, payload, true)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if from == sdktranslator.FormatOpenAIResponse {
|
||||
payload = RewriteCodexOrphanDelegationInputForConfig(ctx, headers, payload, cfg)
|
||||
if to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {
|
||||
payload = RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
|
||||
}
|
||||
}
|
||||
return sdktranslator.TranslateRequest(from, to, model, payload, stream)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package multiagentv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
codexAppNamespace = "codex_app"
|
||||
codexCreateThreadName = "create_thread"
|
||||
codexSendMessageToThreadName = "send_message_to_thread"
|
||||
codexAppCreateThreadTool = "codex_app__create_thread"
|
||||
codexAppSendMessageTool = "codex_app__send_message_to_thread"
|
||||
codexOpenAISubagentHeader = "X-Openai-Subagent"
|
||||
codexCollabSpawnSubagent = "collab_spawn"
|
||||
)
|
||||
|
||||
// RewriteCodexOrphanDelegationInput converts orphan Codex delegation outputs into
|
||||
// standard user messages when orphan delegation compatibility is enabled and the
|
||||
// request carries the X-Openai-Subagent: collab_spawn header.
|
||||
func RewriteCodexOrphanDelegationInput(ctx context.Context, headers http.Header, payload []byte, enabled bool) []byte {
|
||||
if !enabled || len(payload) == 0 || !isCodexCollabSpawnSubagent(ctx, headers) {
|
||||
return payload
|
||||
}
|
||||
|
||||
input := gjson.GetBytes(payload, "input")
|
||||
if !input.IsArray() {
|
||||
return payload
|
||||
}
|
||||
|
||||
inputItems := input.Array()
|
||||
availableCalls := make(map[string]int)
|
||||
for _, item := range inputItems {
|
||||
if item.Get("type").String() == "function_call" {
|
||||
callID := item.Get("call_id").String()
|
||||
if strings.TrimSpace(callID) != "" {
|
||||
availableCalls[callID]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updated := payload
|
||||
for itemIndex, item := range inputItems {
|
||||
if item.Get("type").String() != "function_call_output" {
|
||||
continue
|
||||
}
|
||||
|
||||
callID := item.Get("call_id").String()
|
||||
if strings.TrimSpace(callID) != "" && availableCalls[callID] > 0 {
|
||||
// Paired with a function call in the same request; consume and preserve.
|
||||
availableCalls[callID]--
|
||||
continue
|
||||
}
|
||||
|
||||
toolLabel, isTarget := matchCodexDelegationTool(item)
|
||||
if !isTarget {
|
||||
continue
|
||||
}
|
||||
|
||||
// Orphan delegation output: downgrade to user message preserving exact output.
|
||||
itemPath := fmt.Sprintf("input.%d", itemIndex)
|
||||
userMessage := buildCodexOrphanUserMessage(toolLabel, item.Get("output"))
|
||||
var errSet error
|
||||
updated, errSet = sjson.SetRawBytes(updated, itemPath, userMessage)
|
||||
if errSet != nil {
|
||||
return payload
|
||||
}
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
func codexSubagentHeader(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, codexOpenAISubagentHeader)
|
||||
}
|
||||
}
|
||||
return headerValueCaseInsensitive(headers, codexOpenAISubagentHeader)
|
||||
}
|
||||
|
||||
func isCodexCollabSpawnSubagent(ctx context.Context, headers http.Header) bool {
|
||||
return strings.EqualFold(codexSubagentHeader(ctx, headers), codexCollabSpawnSubagent)
|
||||
}
|
||||
|
||||
func matchCodexDelegationTool(item gjson.Result) (string, bool) {
|
||||
if item.Get("namespace").String() != codexAppNamespace {
|
||||
return "", false
|
||||
}
|
||||
|
||||
switch item.Get("name").String() {
|
||||
case codexCreateThreadName:
|
||||
return codexAppCreateThreadTool, true
|
||||
case codexSendMessageToThreadName:
|
||||
return codexAppSendMessageTool, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func buildCodexOrphanUserMessage(toolLabel string, output gjson.Result) []byte {
|
||||
outputText := ""
|
||||
if output.Exists() {
|
||||
if output.Type == gjson.String {
|
||||
outputText = output.String()
|
||||
} else {
|
||||
outputText = output.Raw
|
||||
}
|
||||
}
|
||||
|
||||
fullText := fmt.Sprintf("Tool output from %s:\n%s", toolLabel, outputText)
|
||||
|
||||
msg := []byte(`{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}`)
|
||||
msg, _ = sjson.SetBytes(msg, "content.0.text", fullText)
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
package multiagentv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func testRewriteCodexOrphan(payload []byte, enabled bool) []byte {
|
||||
headers := http.Header{"X-Openai-Subagent": []string{"collab_spawn"}}
|
||||
return RewriteCodexOrphanDelegationInput(context.Background(), headers, payload, enabled)
|
||||
}
|
||||
|
||||
func TestRewriteCodexOrphanDelegationInput(t *testing.T) {
|
||||
t.Run("disabled leaves payload unchanged", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "<codex_delegation><message>handoff</message></codex_delegation>"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, false)
|
||||
if string(got) != string(payload) {
|
||||
t.Fatalf("expected payload unchanged when disabled, got: %s", string(got))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing subagent header leaves payload unchanged", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "<codex_delegation><message>handoff</message></codex_delegation>"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := RewriteCodexOrphanDelegationInput(context.Background(), http.Header{}, payload, true)
|
||||
if string(got) != string(payload) {
|
||||
t.Fatalf("expected payload unchanged without header, got: %s", string(got))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("different subagent header leaves payload unchanged", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "<codex_delegation><message>handoff</message></codex_delegation>"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
headers := http.Header{"X-Openai-Subagent": []string{"other_subagent"}}
|
||||
got := RewriteCodexOrphanDelegationInput(context.Background(), headers, payload, true)
|
||||
if string(got) != string(payload) {
|
||||
t.Fatalf("expected payload unchanged with wrong header, got: %s", string(got))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rewrites orphan create_thread without call_id", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "<codex_delegation><message>handoff</message></codex_delegation>"
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "please continue"}]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "message" {
|
||||
t.Fatalf("input.0.type = %q, want %q", item0.Get("type").String(), "message")
|
||||
}
|
||||
if item0.Get("role").String() != "user" {
|
||||
t.Fatalf("input.0.role = %q, want %q", item0.Get("role").String(), "user")
|
||||
}
|
||||
wantText := "Tool output from codex_app__create_thread:\n<codex_delegation><message>handoff</message></codex_delegation>"
|
||||
if text := item0.Get("content.0.text").String(); text != wantText {
|
||||
t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
|
||||
}
|
||||
|
||||
item1 := parsed.Get("input.1")
|
||||
if item1.Get("type").String() != "message" || item1.Get("content.0.text").String() != "please continue" {
|
||||
t.Fatalf("input.1 corrupted: %s", item1.Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("case-insensitive header key and value works", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "msg"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
headers := http.Header{"x-openai-subagent": []string{"COLLAB_SPAWN"}}
|
||||
got := RewriteCodexOrphanDelegationInput(context.Background(), headers, payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
|
||||
t.Fatalf("case-insensitive header should rewrite, got: %s", item0.Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rewrites orphan send_message_to_thread with stale call_id", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_stale_123",
|
||||
"name": "send_message_to_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "<codex_delegation>msg</codex_delegation>"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
|
||||
t.Fatalf("input.0 not rewritten: %s", item0.Raw)
|
||||
}
|
||||
wantText := "Tool output from codex_app__send_message_to_thread:\n<codex_delegation>msg</codex_delegation>"
|
||||
if text := item0.Get("content.0.text").String(); text != wantText {
|
||||
t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves paired create_thread tool call", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_active_123",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"arguments": "{}"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_active_123",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "<codex_delegation>valid</codex_delegation>"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item1 := parsed.Get("input.1")
|
||||
if item1.Get("type").String() != "function_call_output" {
|
||||
t.Fatalf("paired call output was modified: %s", item1.Raw)
|
||||
}
|
||||
if item1.Get("call_id").String() != "call_active_123" {
|
||||
t.Fatalf("call_id changed: %s", item1.Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves non-whitelisted tools", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "automation_update",
|
||||
"namespace": "codex_app",
|
||||
"output": "ignored"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "other_namespace",
|
||||
"output": "ignored"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
if parsed.Get("input.0.type").String() != "function_call_output" {
|
||||
t.Fatalf("automation_update should not be rewritten: %s", parsed.Get("input.0").Raw)
|
||||
}
|
||||
if parsed.Get("input.1.type").String() != "function_call_output" {
|
||||
t.Fatalf("other_namespace should not be rewritten: %s", parsed.Get("input.1").Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles empty output", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
|
||||
t.Fatalf("input.0 not rewritten: %s", item0.Raw)
|
||||
}
|
||||
wantText := "Tool output from codex_app__create_thread:\n"
|
||||
if text := item0.Get("content.0.text").String(); text != wantText {
|
||||
t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("call_id whitespace difference is not paired", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_1",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"arguments": "{}"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": " call_1 ",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "mismatch"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
if parsed.Get("input.1.type").String() != "message" {
|
||||
t.Fatalf("call_id with whitespace mismatch should be treated as orphan: %s", parsed.Get("input.1").Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves structured image output in delegation as exact text", func(t *testing.T) {
|
||||
rawOutput := `[{"type":"input_text","text":"diagram"},{"type":"input_image","image_url":"https://example.com/img.png"}]`
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": ` + rawOutput + `
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
|
||||
t.Fatalf("input.0 not rewritten: %s", item0.Raw)
|
||||
}
|
||||
wantText := "Tool output from codex_app__create_thread:\n" + rawOutput
|
||||
if text := item0.Get("content.0.text").String(); text != wantText {
|
||||
t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("call and output in same request are paired regardless of order", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_future_1",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "early"
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_future_1",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"arguments": "{}"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
if parsed.Get("input.0.type").String() != "function_call_output" {
|
||||
t.Fatalf("output should remain paired function_call_output: %s", parsed.Get("input.0").Raw)
|
||||
}
|
||||
if parsed.Get("input.1.type").String() != "function_call" {
|
||||
t.Fatalf("call should remain function_call: %s", parsed.Get("input.1").Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate output consumes call once; second output is orphan", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_once",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"arguments": "{}"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_once",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "first"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_once",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "second"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
if parsed.Get("input.1.type").String() != "function_call_output" {
|
||||
t.Fatalf("first output should pair with call: %s", parsed.Get("input.1").Raw)
|
||||
}
|
||||
if parsed.Get("input.2.type").String() != "message" {
|
||||
t.Fatalf("second output should be orphan: %s", parsed.Get("input.2").Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("custom tool call does not pair with function call output", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "custom_tool_call",
|
||||
"call_id": "call_custom",
|
||||
"name": "create_thread",
|
||||
"input": "{}"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_custom",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "orphan"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
if parsed.Get("input.1.type").String() != "message" {
|
||||
t.Fatalf("function_call_output should not pair with custom_tool_call: %s", parsed.Get("input.1").Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assistant message tool_calls does not pair with Responses function_call_output", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"tool_calls": [{"id": "call_in_msg", "type": "function", "function": {"name": "create_thread"}}]
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_in_msg",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "orphan"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
if parsed.Get("input.1.type").String() != "message" {
|
||||
t.Fatalf("function_call_output should not pair with assistant message tool_calls: %s", parsed.Get("input.1").Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exact namespace and name required", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "deepseek-v4-pro",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "codex_app__create_thread",
|
||||
"output": "orphan"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": " codex_app ",
|
||||
"output": "orphan"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "other_tool",
|
||||
"namespace": "codex_app",
|
||||
"output": "orphan"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
got := testRewriteCodexOrphan(payload, true)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
if itemType := parsed.Get(fmt.Sprintf("input.%d.type", i)).String(); itemType != "function_call_output" {
|
||||
t.Fatalf("input.%d should not be rewritten: %s", i, parsed.Get(fmt.Sprintf("input.%d", i)).Raw)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTranslateRequestWithCodexMultiAgentV2OrphanDelegation(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "test-model",
|
||||
"stream": false,
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "<codex_delegation><message>handoff</message></codex_delegation>"
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "please continue"}]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
collabHeaders := http.Header{"X-Openai-Subagent": []string{"collab_spawn"}}
|
||||
|
||||
t.Run("enabled but missing X-Openai-Subagent header does not rewrite", func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Codex: config.CodexConfig{
|
||||
OrphanDelegationCompatibility: true,
|
||||
},
|
||||
}
|
||||
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), http.Header{}, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", payload, false)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "function_call_output" {
|
||||
t.Fatalf("input.0 = %s, want function_call_output when X-Openai-Subagent header is missing", item0.Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("enabled with wrong X-Openai-Subagent header value does not rewrite", func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Codex: config.CodexConfig{
|
||||
OrphanDelegationCompatibility: true,
|
||||
},
|
||||
}
|
||||
headers := http.Header{"X-Openai-Subagent": []string{"other_agent"}}
|
||||
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), headers, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", payload, false)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "function_call_output" {
|
||||
t.Fatalf("input.0 = %s, want function_call_output when X-Openai-Subagent header is wrong", item0.Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("enabled translates orphan delegation to user message for responses target", func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Codex: config.CodexConfig{
|
||||
OrphanDelegationCompatibility: true,
|
||||
},
|
||||
}
|
||||
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), collabHeaders, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", payload, false)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
|
||||
t.Fatalf("input.0 = %s, want message/user", item0.Raw)
|
||||
}
|
||||
wantText := "Tool output from codex_app__create_thread:\n<codex_delegation><message>handoff</message></codex_delegation>"
|
||||
if text := item0.Get("content.0.text").String(); text != wantText {
|
||||
t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("enabled translates orphan delegation to user message for chat target without empty tool_call_id", func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Codex: config.CodexConfig{
|
||||
OrphanDelegationCompatibility: true,
|
||||
},
|
||||
}
|
||||
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), collabHeaders, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "test-model", payload, false)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
messages := parsed.Get("messages").Array()
|
||||
if len(messages) < 2 {
|
||||
t.Fatalf("expected at least 2 messages, got %d: %s", len(messages), string(got))
|
||||
}
|
||||
msg0 := messages[0]
|
||||
if msg0.Get("role").String() != "user" {
|
||||
t.Fatalf("messages.0.role = %q, want user (should not be tool message with empty id)", msg0.Get("role").String())
|
||||
}
|
||||
wantText := "Tool output from codex_app__create_thread:\n<codex_delegation><message>handoff</message></codex_delegation>"
|
||||
var text string
|
||||
if msg0.Get("content").IsArray() {
|
||||
text = msg0.Get("content.0.text").String()
|
||||
} else {
|
||||
text = msg0.Get("content").String()
|
||||
}
|
||||
if text != wantText {
|
||||
t.Fatalf("messages.0.content = %q, want %q", text, wantText)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disabled leaves orphan delegation untranslated for responses target", func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Codex: config.CodexConfig{
|
||||
OrphanDelegationCompatibility: false,
|
||||
},
|
||||
}
|
||||
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), collabHeaders, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", payload, false)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
item0 := parsed.Get("input.0")
|
||||
if item0.Get("type").String() != "function_call_output" {
|
||||
t.Fatalf("input.0 = %s, want function_call_output when disabled", item0.Raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("interleaved valid tool pair and orphan delegation retains order and pairing", func(t *testing.T) {
|
||||
interleaved := []byte(`{
|
||||
"model": "test-model",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_1",
|
||||
"name": "lookup",
|
||||
"arguments": "{\"q\":\"test\"}"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_1",
|
||||
"name": "lookup",
|
||||
"output": "result1"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "delegated"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
cfg := &config.Config{
|
||||
Codex: config.CodexConfig{
|
||||
OrphanDelegationCompatibility: true,
|
||||
},
|
||||
}
|
||||
got := TranslateRequestWithCodexMultiAgentV2(context.Background(), collabHeaders, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", interleaved, false)
|
||||
parsed := gjson.ParseBytes(got)
|
||||
|
||||
if parsed.Get("input.0.type").String() != "function_call" {
|
||||
t.Fatalf("input.0 should remain function_call")
|
||||
}
|
||||
if parsed.Get("input.1.type").String() != "function_call_output" || parsed.Get("input.1.call_id").String() != "call_1" {
|
||||
t.Fatalf("input.1 should remain paired function_call_output")
|
||||
}
|
||||
if parsed.Get("input.2.type").String() != "message" || parsed.Get("input.2.role").String() != "user" {
|
||||
t.Fatalf("input.2 should be downgraded to message/user: %s", parsed.Get("input.2").Raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -158,6 +158,8 @@ type CodexConfig struct {
|
||||
StreamBootstrapBuffering bool `yaml:"stream-bootstrap-buffering" json:"stream-bootstrap-buffering"`
|
||||
// OptimizeMultiAgentV2 optimizes official Codex multi-agent requests.
|
||||
OptimizeMultiAgentV2 bool `yaml:"optimize-multi-agent-v2" json:"optimize-multi-agent-v2"`
|
||||
// OrphanDelegationCompatibility enables opt-in compatibility for orphan Codex delegation outputs.
|
||||
OrphanDelegationCompatibility bool `yaml:"orphan-delegation-compatibility" json:"orphan-delegation-compatibility"`
|
||||
// LiveMediaRelay terminates and relays Codex Live WebRTC media in this process.
|
||||
LiveMediaRelay CodexLiveMediaRelayConfig `yaml:"live-media-relay" json:"live-media-relay"`
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ type SDKConfig struct {
|
||||
// CodexOptimizeMultiAgentV2 mirrors the provider-wide runtime setting for API handlers.
|
||||
CodexOptimizeMultiAgentV2 bool `yaml:"-" json:"-"`
|
||||
|
||||
// CodexOrphanDelegationCompatibility mirrors the provider-wide runtime setting for API handlers.
|
||||
CodexOrphanDelegationCompatibility bool `yaml:"-" json:"-"`
|
||||
|
||||
// ClaudeCode configures Claude Code compatibility behavior.
|
||||
ClaudeCode ClaudeCodeConfig `yaml:"claude-code" json:"claude-code"`
|
||||
|
||||
|
||||
@@ -29,6 +29,13 @@ func RewriteCodexMultiAgentV2Input(ctx context.Context, headers http.Header, pay
|
||||
return multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
|
||||
}
|
||||
|
||||
// RewriteCodexOrphanDelegationInput converts orphan Codex delegation outputs into
|
||||
// standard user messages when orphan delegation compatibility is enabled and the
|
||||
// request carries the X-Openai-Subagent: collab_spawn header.
|
||||
func RewriteCodexOrphanDelegationInput(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
|
||||
return multiagentv2.RewriteCodexOrphanDelegationInputForConfig(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 {
|
||||
@@ -71,8 +78,11 @@ func TranslateRequestWithAPIKeyModelCompatibility(ctx context.Context, headers h
|
||||
if !isCompat {
|
||||
return TranslateRequestWithCodexMultiAgentV2(ctx, headers, cfg, from, to, model, payload, stream)
|
||||
}
|
||||
if from == sdktranslator.FormatOpenAIResponse && to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {
|
||||
payload = multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
|
||||
if from == sdktranslator.FormatOpenAIResponse {
|
||||
payload = RewriteCodexOrphanDelegationInput(ctx, headers, payload, cfg)
|
||||
if to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {
|
||||
payload = multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
var translated []byte
|
||||
|
||||
@@ -121,6 +121,9 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
||||
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.Codex.OrphanDelegationCompatibility != newCfg.Codex.OrphanDelegationCompatibility {
|
||||
changes = append(changes, fmt.Sprintf("codex.orphan-delegation-compatibility: %t -> %t", oldCfg.Codex.OrphanDelegationCompatibility, newCfg.Codex.OrphanDelegationCompatibility))
|
||||
}
|
||||
if oldCfg.XAI.InjectXSearch != newCfg.XAI.InjectXSearch {
|
||||
changes = append(changes, fmt.Sprintf("xai.inject-x-search: %t -> %t", oldCfg.XAI.InjectXSearch, newCfg.XAI.InjectXSearch))
|
||||
}
|
||||
|
||||
@@ -204,6 +204,14 @@ func TestBuildConfigChangeDetails_CodexAlphaSearch(t *testing.T) {
|
||||
expectContains(t, changes, "codex[0].alpha-search: false -> true")
|
||||
}
|
||||
|
||||
func TestBuildConfigChangeDetails_CodexOrphanDelegationCompatibility(t *testing.T) {
|
||||
oldCfg := &config.Config{Codex: config.CodexConfig{OrphanDelegationCompatibility: false}}
|
||||
newCfg := &config.Config{Codex: config.CodexConfig{OrphanDelegationCompatibility: true}}
|
||||
|
||||
changes := BuildConfigChangeDetails(oldCfg, newCfg)
|
||||
expectContains(t, changes, "codex.orphan-delegation-compatibility: false -> true")
|
||||
}
|
||||
|
||||
func TestBuildConfigChangeDetails_XAIKeys(t *testing.T) {
|
||||
oldRetry := 1
|
||||
newRetry := 0
|
||||
|
||||
@@ -511,6 +511,20 @@ func (h *OpenAIResponsesAPIHandler) prepareCodexMultiAgentV2Tools(c *gin.Context
|
||||
return updated
|
||||
}
|
||||
|
||||
func (h *OpenAIResponsesAPIHandler) prepareCodexOrphanDelegation(c *gin.Context, payload []byte) []byte {
|
||||
if h == nil || h.Cfg == nil || !h.Cfg.CodexOrphanDelegationCompatibility {
|
||||
return payload
|
||||
}
|
||||
requestCtx := context.Background()
|
||||
var requestHeaders http.Header
|
||||
if c != nil && c.Request != nil {
|
||||
requestCtx = c.Request.Context()
|
||||
requestHeaders = c.Request.Header
|
||||
}
|
||||
requestCtx = context.WithValue(requestCtx, "gin", c)
|
||||
return multiagentv2.RewriteCodexOrphanDelegationInput(requestCtx, requestHeaders, payload, true)
|
||||
}
|
||||
|
||||
// Responses handles the /v1/responses endpoint.
|
||||
// It determines whether the request is for a streaming or non-streaming response
|
||||
// and calls the appropriate handler based on the model provider.
|
||||
@@ -531,6 +545,7 @@ func (h *OpenAIResponsesAPIHandler) Responses(c *gin.Context) {
|
||||
}
|
||||
|
||||
rawJSON = h.prepareCodexMultiAgentV2Tools(c, rawJSON)
|
||||
rawJSON = h.prepareCodexOrphanDelegation(c, rawJSON)
|
||||
|
||||
// Check if the client requested a streaming response.
|
||||
streamResult := gjson.GetBytes(rawJSON, "stream")
|
||||
@@ -554,6 +569,8 @@ func (h *OpenAIResponsesAPIHandler) Compact(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
rawJSON = h.prepareCodexOrphanDelegation(c, rawJSON)
|
||||
|
||||
streamResult := gjson.GetBytes(rawJSON, "stream")
|
||||
if streamResult.Type == gjson.True {
|
||||
c.JSON(http.StatusBadRequest, handlers.ErrorResponse{
|
||||
|
||||
@@ -197,3 +197,93 @@ func TestPrepareCodexMultiAgentV2ToolsAtResponsesBoundarySkipsOtherClients(t *te
|
||||
t.Fatal("other client unexpectedly received prepared marker")
|
||||
}
|
||||
}
|
||||
|
||||
func newResponsesOrphanDelegationTestHandler(t *testing.T, executor *responsesMultiAgentCaptureExecutor) (*OpenAIResponsesAPIHandler, string) {
|
||||
t.Helper()
|
||||
|
||||
modelID := "responses-orphan-test-model"
|
||||
authID := "responses-orphan-test-auth"
|
||||
manager := coreauth.NewManager(nil, nil, nil)
|
||||
manager.RegisterExecutor(executor)
|
||||
auth := &coreauth.Auth{ID: authID, Provider: "codex", Status: coreauth.StatusActive, ProxyURL: "direct"}
|
||||
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
|
||||
t.Fatalf("Register auth: %v", errRegister)
|
||||
}
|
||||
registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: modelID}})
|
||||
t.Cleanup(func() {
|
||||
registry.GetGlobalRegistry().UnregisterClient(authID)
|
||||
})
|
||||
|
||||
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{CodexOrphanDelegationCompatibility: true}, manager)
|
||||
return NewOpenAIResponsesAPIHandler(base), modelID
|
||||
}
|
||||
|
||||
func TestResponsesOrphanCodexDelegationCompatibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
executor := &responsesMultiAgentCaptureExecutor{}
|
||||
handler, modelID := newResponsesOrphanDelegationTestHandler(t, executor)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/v1/responses", handler.Responses)
|
||||
|
||||
payload := fmt.Sprintf(`{
|
||||
"model": %q,
|
||||
"stream": false,
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"name": "create_thread",
|
||||
"namespace": "codex_app",
|
||||
"output": "<codex_delegation>msg</codex_delegation>"
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "continue"}]
|
||||
}
|
||||
]
|
||||
}`, modelID)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(payload))
|
||||
request.Header.Set("X-Openai-Subagent", "collab_spawn")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
payloads := executor.Payloads()
|
||||
if len(payloads) != 1 {
|
||||
t.Fatalf("captured payload count = %d, want 1", len(payloads))
|
||||
}
|
||||
captured := payloads[0]
|
||||
parsed := gjson.ParseBytes(captured)
|
||||
if itemType := parsed.Get("input.0.type").String(); itemType != "message" {
|
||||
t.Fatalf("input.0.type = %q, want message; captured=%s", itemType, captured)
|
||||
}
|
||||
if role := parsed.Get("input.0.role").String(); role != "user" {
|
||||
t.Fatalf("input.0.role = %q, want user", role)
|
||||
}
|
||||
wantText := "Tool output from codex_app__create_thread:\n<codex_delegation>msg</codex_delegation>"
|
||||
if text := parsed.Get("input.0.content.0.text").String(); text != wantText {
|
||||
t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
|
||||
}
|
||||
|
||||
// Without X-Openai-Subagent header, payload should remain function_call_output
|
||||
requestNoHeader := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(payload))
|
||||
recorderNoHeader := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorderNoHeader, requestNoHeader)
|
||||
if recorderNoHeader.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", recorderNoHeader.Code, recorderNoHeader.Body.String())
|
||||
}
|
||||
payloads = executor.Payloads()
|
||||
if len(payloads) != 2 {
|
||||
t.Fatalf("captured payload count = %d, want 2", len(payloads))
|
||||
}
|
||||
capturedNoHeader := payloads[1]
|
||||
parsedNoHeader := gjson.ParseBytes(capturedNoHeader)
|
||||
if itemType := parsedNoHeader.Get("input.0.type").String(); itemType != "function_call_output" {
|
||||
t.Fatalf("input.0.type = %q, want function_call_output without header; captured=%s", itemType, capturedNoHeader)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,6 +522,7 @@ func (h *OpenAIResponsesAPIHandler) ResponsesWebsocket(c *gin.Context) {
|
||||
}
|
||||
|
||||
requestJSON = h.prepareCodexMultiAgentV2Tools(c, requestJSON)
|
||||
requestJSON = h.prepareCodexOrphanDelegation(c, requestJSON)
|
||||
|
||||
if !useUpstreamWebsocketPassthrough && shouldHandleResponsesWebsocketPrewarmLocally(payload, lastRequest, false) {
|
||||
if updated, errDelete := sjson.DeleteBytes(requestJSON, "generate"); errDelete == nil {
|
||||
|
||||
Reference in New Issue
Block a user