History

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.

Luis Pater committed 2 months ago

Commit 291cfb81 parent 93f526614 files · +901 −4Browse files

Unified · Split

14 files
  1. config.example.yaml +6 −0
  2. internal/api/server_options.go +1 −0
  3. internal/api/server_sdk_config_test.go +9 −0
  4. internal/client/codex/optimize-multi-agent-v2/optimize_multi_agent_v2.go +14 −2
  5. internal/client/codex/optimize-multi-agent-v2/orphan_delegation.go +122 −0
  6. internal/client/codex/optimize-multi-agent-v2/orphan_delegation_test.go +613 −0
  7. internal/config/config_types.go +2 −0
  8. internal/config/sdk_config.go +3 −0
  9. internal/runtime/executor/helps/codex_multi_agent_v2.go +12 −2
  10. internal/watcher/diff/config_diff.go +3 −0
  11. internal/watcher/diff/config_diff_test.go +8 −0
  12. sdk/api/handlers/openai/openai_responses_handlers.go +17 −0
  13. sdk/api/handlers/openai/openai_responses_multi_agent_test.go +90 −0
  14. sdk/api/handlers/openai/openai_responses_websocket.go +1 −0
config.example.yaml+6−0
@@ -262,6 +262,12 @@ codex: · 261 unmodified lines
262 # normalizes encrypted agent_message content for Codex, and converts agent_message input262 # normalizes encrypted agent_message content for Codex, and converts agent_message input
263 # into standard user messages for non-Codex upstream protocols.263 # into standard user messages for non-Codex upstream protocols.
264 optimize-multi-agent-v2: false264 optimize-multi-agent-v2: false
265 # When true, enable opt-in compatibility for orphan Codex delegation outputs.
266 # Converts orphan function_call_output items from codex_app/create_thread and
267 # codex_app/send_message_to_thread (which lack a valid call_id or matching function_call)
268 # into standard user messages across Responses and non-Codex protocols when the request
269 # header contains X-Openai-Subagent: collab_spawn.
270 orphan-delegation-compatibility: false
265 # Terminate and relay Codex Live WebRTC audio and DataChannel traffic in this process.271 # Terminate and relay Codex Live WebRTC audio and DataChannel traffic in this process.
266 # This requires inbound UDP reachability. Keep disabled to preserve direct media behavior.272 # This requires inbound UDP reachability. Keep disabled to preserve direct media behavior.
267 live-media-relay:273 live-media-relay:
internal/api/server_options.go+1−0
@@ -46,6 +46,7 @@ func effectiveSDKConfig(cfg *config.Config) *config.SDKConfig { · 45 unmodified lines
46 }46 }
47 sdkCfg := cfg.SDKConfig47 sdkCfg := cfg.SDKConfig
48 sdkCfg.CodexOptimizeMultiAgentV2 = cfg.Codex.OptimizeMultiAgentV248 sdkCfg.CodexOptimizeMultiAgentV2 = cfg.Codex.OptimizeMultiAgentV2
49 sdkCfg.CodexOrphanDelegationCompatibility = cfg.Codex.OrphanDelegationCompatibility
49 if cfg.CommercialMode {50 if cfg.CommercialMode {
50 sdkCfg.RequestLog = false51 sdkCfg.RequestLog = false
51 }52 }
internal/api/server_sdk_config_test.go+9−0
@@ -14,3 +14,12 @@ func TestEffectiveSDKConfigCopiesCodexOptimizeMultiAgentV2(t *testing.T) { · 13 unmodified lines
14 t.Fatalf("CodexOptimizeMultiAgentV2 = false, want true")14 t.Fatalf("CodexOptimizeMultiAgentV2 = false, want true")
15 }15 }
16}16}
17
18func TestEffectiveSDKConfigCopiesCodexOrphanDelegationCompatibility(t *testing.T) {
19 cfg := &config.Config{Codex: config.CodexConfig{OrphanDelegationCompatibility: true}}
20
21 sdkCfg := effectiveSDKConfig(cfg)
22 if sdkCfg == nil || !sdkCfg.CodexOrphanDelegationCompatibility {
23 t.Fatalf("CodexOrphanDelegationCompatibility = false, want true")
24 }
25}
internal/client/codex/optimize-multi-agent-v2/optimize_multi_agent_v2.go+14−2
@@ -71,11 +71,23 @@ func RewriteCodexMultiAgentV2Input(ctx context.Context, headers http.Header, pay · 70 unmodified lines
71 return rewriteCodexAgentMessageInput(payload)71 return rewriteCodexAgentMessageInput(payload)
72}72}
73 73
74// RewriteCodexOrphanDelegationInputForConfig applies RewriteCodexOrphanDelegationInput
75// based on cfg.Codex.OrphanDelegationCompatibility and the X-Openai-Subagent header.
76func RewriteCodexOrphanDelegationInputForConfig(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
77 if cfg == nil || !cfg.Codex.OrphanDelegationCompatibility {
78 return payload
79 }
80 return RewriteCodexOrphanDelegationInput(ctx, headers, payload, true)
81}
82
74// TranslateRequestWithCodexMultiAgentV2 normalizes official Codex multi-agent83// TranslateRequestWithCodexMultiAgentV2 normalizes official Codex multi-agent
75// input before translating it to a non-Codex target protocol.84// input before translating it to a non-Codex target protocol.
76func TranslateRequestWithCodexMultiAgentV2(ctx context.Context, headers http.Header, cfg *config.Config, from, to sdktranslator.Format, model string, payload []byte, stream bool) []byte {85func TranslateRequestWithCodexMultiAgentV2(ctx context.Context, headers http.Header, cfg *config.Config, from, to sdktranslator.Format, model string, payload []byte, stream bool) []byte {
77 if from == sdktranslator.FormatOpenAIResponse && to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {86 if from == sdktranslator.FormatOpenAIResponse {
78 payload = RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)87 payload = RewriteCodexOrphanDelegationInputForConfig(ctx, headers, payload, cfg)
88 if to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {
89 payload = RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
90 }
79 }91 }
80 return sdktranslator.TranslateRequest(from, to, model, payload, stream)92 return sdktranslator.TranslateRequest(from, to, model, payload, stream)
81}93}
internal/client/codex/optimize-multi-agent-v2/orphan_delegation.go+122−0
@@ -0,0 +1,122 @@
1package multiagentv2
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "strings"
8
9 "github.com/gin-gonic/gin"
10 "github.com/tidwall/gjson"
11 "github.com/tidwall/sjson"
12)
13
14const (
15 codexAppNamespace = "codex_app"
16 codexCreateThreadName = "create_thread"
17 codexSendMessageToThreadName = "send_message_to_thread"
18 codexAppCreateThreadTool = "codex_app__create_thread"
19 codexAppSendMessageTool = "codex_app__send_message_to_thread"
20 codexOpenAISubagentHeader = "X-Openai-Subagent"
21 codexCollabSpawnSubagent = "collab_spawn"
22)
23
24// RewriteCodexOrphanDelegationInput converts orphan Codex delegation outputs into
25// standard user messages when orphan delegation compatibility is enabled and the
26// request carries the X-Openai-Subagent: collab_spawn header.
27func RewriteCodexOrphanDelegationInput(ctx context.Context, headers http.Header, payload []byte, enabled bool) []byte {
28 if !enabled || len(payload) == 0 || !isCodexCollabSpawnSubagent(ctx, headers) {
29 return payload
30 }
31
32 input := gjson.GetBytes(payload, "input")
33 if !input.IsArray() {
34 return payload
35 }
36
37 inputItems := input.Array()
38 availableCalls := make(map[string]int)
39 for _, item := range inputItems {
40 if item.Get("type").String() == "function_call" {
41 callID := item.Get("call_id").String()
42 if strings.TrimSpace(callID) != "" {
43 availableCalls[callID]++
44 }
45 }
46 }
47
48 updated := payload
49 for itemIndex, item := range inputItems {
50 if item.Get("type").String() != "function_call_output" {
51 continue
52 }
53
54 callID := item.Get("call_id").String()
55 if strings.TrimSpace(callID) != "" && availableCalls[callID] > 0 {
56 // Paired with a function call in the same request; consume and preserve.
57 availableCalls[callID]--
58 continue
59 }
60
61 toolLabel, isTarget := matchCodexDelegationTool(item)
62 if !isTarget {
63 continue
64 }
65
66 // Orphan delegation output: downgrade to user message preserving exact output.
67 itemPath := fmt.Sprintf("input.%d", itemIndex)
68 userMessage := buildCodexOrphanUserMessage(toolLabel, item.Get("output"))
69 var errSet error
70 updated, errSet = sjson.SetRawBytes(updated, itemPath, userMessage)
71 if errSet != nil {
72 return payload
73 }
74 }
75
76 return updated
77}
78
79func codexSubagentHeader(ctx context.Context, headers http.Header) string {
80 if ctx != nil {
81 if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
82 return headerValueCaseInsensitive(ginCtx.Request.Header, codexOpenAISubagentHeader)
83 }
84 }
85 return headerValueCaseInsensitive(headers, codexOpenAISubagentHeader)
86}
87
88func isCodexCollabSpawnSubagent(ctx context.Context, headers http.Header) bool {
89 return strings.EqualFold(codexSubagentHeader(ctx, headers), codexCollabSpawnSubagent)
90}
91
92func matchCodexDelegationTool(item gjson.Result) (string, bool) {
93 if item.Get("namespace").String() != codexAppNamespace {
94 return "", false
95 }
96
97 switch item.Get("name").String() {
98 case codexCreateThreadName:
99 return codexAppCreateThreadTool, true
100 case codexSendMessageToThreadName:
101 return codexAppSendMessageTool, true
102 default:
103 return "", false
104 }
105}
106
107func buildCodexOrphanUserMessage(toolLabel string, output gjson.Result) []byte {
108 outputText := ""
109 if output.Exists() {
110 if output.Type == gjson.String {
111 outputText = output.String()
112 } else {
113 outputText = output.Raw
114 }
115 }
116
117 fullText := fmt.Sprintf("Tool output from %s:\n%s", toolLabel, outputText)
118
119 msg := []byte(`{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}`)
120 msg, _ = sjson.SetBytes(msg, "content.0.text", fullText)
121 return msg
122}
internal/client/codex/optimize-multi-agent-v2/orphan_delegation_test.go+613−0
@@ -0,0 +1,613 @@
1package multiagentv2
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "testing"
8
9 "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
10 sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
11 "github.com/tidwall/gjson"
12)
13
14func testRewriteCodexOrphan(payload []byte, enabled bool) []byte {
15 headers := http.Header{"X-Openai-Subagent": []string{"collab_spawn"}}
16 return RewriteCodexOrphanDelegationInput(context.Background(), headers, payload, enabled)
17}
18
19func TestRewriteCodexOrphanDelegationInput(t *testing.T) {
20 t.Run("disabled leaves payload unchanged", func(t *testing.T) {
21 payload := []byte(`{
22 "model": "deepseek-v4-pro",
23 "input": [
24 {
25 "type": "function_call_output",
26 "name": "create_thread",
27 "namespace": "codex_app",
28 "output": "<codex_delegation><message>handoff</message></codex_delegation>"
29 }
30 ]
31 }`)
32 got := testRewriteCodexOrphan(payload, false)
33 if string(got) != string(payload) {
34 t.Fatalf("expected payload unchanged when disabled, got: %s", string(got))
35 }
36 })
37
38 t.Run("missing subagent header leaves payload unchanged", func(t *testing.T) {
39 payload := []byte(`{
40 "model": "deepseek-v4-pro",
41 "input": [
42 {
43 "type": "function_call_output",
44 "name": "create_thread",
45 "namespace": "codex_app",
46 "output": "<codex_delegation><message>handoff</message></codex_delegation>"
47 }
48 ]
49 }`)
50 got := RewriteCodexOrphanDelegationInput(context.Background(), http.Header{}, payload, true)
51 if string(got) != string(payload) {
52 t.Fatalf("expected payload unchanged without header, got: %s", string(got))
53 }
54 })
55
56 t.Run("different subagent header leaves payload unchanged", func(t *testing.T) {
57 payload := []byte(`{
58 "model": "deepseek-v4-pro",
59 "input": [
60 {
61 "type": "function_call_output",
62 "name": "create_thread",
63 "namespace": "codex_app",
64 "output": "<codex_delegation><message>handoff</message></codex_delegation>"
65 }
66 ]
67 }`)
68 headers := http.Header{"X-Openai-Subagent": []string{"other_subagent"}}
69 got := RewriteCodexOrphanDelegationInput(context.Background(), headers, payload, true)
70 if string(got) != string(payload) {
71 t.Fatalf("expected payload unchanged with wrong header, got: %s", string(got))
72 }
73 })
74
75 t.Run("rewrites orphan create_thread without call_id", func(t *testing.T) {
76 payload := []byte(`{
77 "model": "deepseek-v4-pro",
78 "input": [
79 {
80 "type": "function_call_output",
81 "name": "create_thread",
82 "namespace": "codex_app",
83 "output": "<codex_delegation><message>handoff</message></codex_delegation>"
84 },
85 {
86 "type": "message",
87 "role": "user",
88 "content": [{"type": "input_text", "text": "please continue"}]
89 }
90 ]
91 }`)
92 got := testRewriteCodexOrphan(payload, true)
93 parsed := gjson.ParseBytes(got)
94
95 item0 := parsed.Get("input.0")
96 if item0.Get("type").String() != "message" {
97 t.Fatalf("input.0.type = %q, want %q", item0.Get("type").String(), "message")
98 }
99 if item0.Get("role").String() != "user" {
100 t.Fatalf("input.0.role = %q, want %q", item0.Get("role").String(), "user")
101 }
102 wantText := "Tool output from codex_app__create_thread:\n<codex_delegation><message>handoff</message></codex_delegation>"
103 if text := item0.Get("content.0.text").String(); text != wantText {
104 t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
105 }
106
107 item1 := parsed.Get("input.1")
108 if item1.Get("type").String() != "message" || item1.Get("content.0.text").String() != "please continue" {
109 t.Fatalf("input.1 corrupted: %s", item1.Raw)
110 }
111 })
112
113 t.Run("case-insensitive header key and value works", func(t *testing.T) {
114 payload := []byte(`{
115 "model": "deepseek-v4-pro",
116 "input": [
117 {
118 "type": "function_call_output",
119 "name": "create_thread",
120 "namespace": "codex_app",
121 "output": "msg"
122 }
123 ]
124 }`)
125 headers := http.Header{"x-openai-subagent": []string{"COLLAB_SPAWN"}}
126 got := RewriteCodexOrphanDelegationInput(context.Background(), headers, payload, true)
127 parsed := gjson.ParseBytes(got)
128
129 item0 := parsed.Get("input.0")
130 if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
131 t.Fatalf("case-insensitive header should rewrite, got: %s", item0.Raw)
132 }
133 })
134
135 t.Run("rewrites orphan send_message_to_thread with stale call_id", func(t *testing.T) {
136 payload := []byte(`{
137 "model": "deepseek-v4-pro",
138 "input": [
139 {
140 "type": "function_call_output",
141 "call_id": "call_stale_123",
142 "name": "send_message_to_thread",
143 "namespace": "codex_app",
144 "output": "<codex_delegation>msg</codex_delegation>"
145 }
146 ]
147 }`)
148 got := testRewriteCodexOrphan(payload, true)
149 parsed := gjson.ParseBytes(got)
150
151 item0 := parsed.Get("input.0")
152 if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
153 t.Fatalf("input.0 not rewritten: %s", item0.Raw)
154 }
155 wantText := "Tool output from codex_app__send_message_to_thread:\n<codex_delegation>msg</codex_delegation>"
156 if text := item0.Get("content.0.text").String(); text != wantText {
157 t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
158 }
159 })
160
161 t.Run("preserves paired create_thread tool call", func(t *testing.T) {
162 payload := []byte(`{
163 "model": "deepseek-v4-pro",
164 "input": [
165 {
166 "type": "function_call",
167 "call_id": "call_active_123",
168 "name": "create_thread",
169 "namespace": "codex_app",
170 "arguments": "{}"
171 },
172 {
173 "type": "function_call_output",
174 "call_id": "call_active_123",
175 "name": "create_thread",
176 "namespace": "codex_app",
177 "output": "<codex_delegation>valid</codex_delegation>"
178 }
179 ]
180 }`)
181 got := testRewriteCodexOrphan(payload, true)
182 parsed := gjson.ParseBytes(got)
183
184 item1 := parsed.Get("input.1")
185 if item1.Get("type").String() != "function_call_output" {
186 t.Fatalf("paired call output was modified: %s", item1.Raw)
187 }
188 if item1.Get("call_id").String() != "call_active_123" {
189 t.Fatalf("call_id changed: %s", item1.Raw)
190 }
191 })
192
193 t.Run("preserves non-whitelisted tools", func(t *testing.T) {
194 payload := []byte(`{
195 "model": "deepseek-v4-pro",
196 "input": [
197 {
198 "type": "function_call_output",
199 "name": "automation_update",
200 "namespace": "codex_app",
201 "output": "ignored"
202 },
203 {
204 "type": "function_call_output",
205 "name": "create_thread",
206 "namespace": "other_namespace",
207 "output": "ignored"
208 }
209 ]
210 }`)
211 got := testRewriteCodexOrphan(payload, true)
212 parsed := gjson.ParseBytes(got)
213
214 if parsed.Get("input.0.type").String() != "function_call_output" {
215 t.Fatalf("automation_update should not be rewritten: %s", parsed.Get("input.0").Raw)
216 }
217 if parsed.Get("input.1.type").String() != "function_call_output" {
218 t.Fatalf("other_namespace should not be rewritten: %s", parsed.Get("input.1").Raw)
219 }
220 })
221
222 t.Run("handles empty output", func(t *testing.T) {
223 payload := []byte(`{
224 "model": "deepseek-v4-pro",
225 "input": [
226 {
227 "type": "function_call_output",
228 "name": "create_thread",
229 "namespace": "codex_app",
230 "output": ""
231 }
232 ]
233 }`)
234 got := testRewriteCodexOrphan(payload, true)
235 parsed := gjson.ParseBytes(got)
236
237 item0 := parsed.Get("input.0")
238 if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
239 t.Fatalf("input.0 not rewritten: %s", item0.Raw)
240 }
241 wantText := "Tool output from codex_app__create_thread:\n"
242 if text := item0.Get("content.0.text").String(); text != wantText {
243 t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
244 }
245 })
246
247 t.Run("call_id whitespace difference is not paired", func(t *testing.T) {
248 payload := []byte(`{
249 "model": "deepseek-v4-pro",
250 "input": [
251 {
252 "type": "function_call",
253 "call_id": "call_1",
254 "name": "create_thread",
255 "namespace": "codex_app",
256 "arguments": "{}"
257 },
258 {
259 "type": "function_call_output",
260 "call_id": " call_1 ",
261 "name": "create_thread",
262 "namespace": "codex_app",
263 "output": "mismatch"
264 }
265 ]
266 }`)
267 got := testRewriteCodexOrphan(payload, true)
268 parsed := gjson.ParseBytes(got)
269
270 if parsed.Get("input.1.type").String() != "message" {
271 t.Fatalf("call_id with whitespace mismatch should be treated as orphan: %s", parsed.Get("input.1").Raw)
272 }
273 })
274
275 t.Run("preserves structured image output in delegation as exact text", func(t *testing.T) {
276 rawOutput := `[{"type":"input_text","text":"diagram"},{"type":"input_image","image_url":"https://example.com/img.png"}]`
277 payload := []byte(`{
278 "model": "deepseek-v4-pro",
279 "input": [
280 {
281 "type": "function_call_output",
282 "name": "create_thread",
283 "namespace": "codex_app",
284 "output": ` + rawOutput + `
285 }
286 ]
287 }`)
288 got := testRewriteCodexOrphan(payload, true)
289 parsed := gjson.ParseBytes(got)
290
291 item0 := parsed.Get("input.0")
292 if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
293 t.Fatalf("input.0 not rewritten: %s", item0.Raw)
294 }
295 wantText := "Tool output from codex_app__create_thread:\n" + rawOutput
296 if text := item0.Get("content.0.text").String(); text != wantText {
297 t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
298 }
299 })
300
301 t.Run("call and output in same request are paired regardless of order", func(t *testing.T) {
302 payload := []byte(`{
303 "model": "deepseek-v4-pro",
304 "input": [
305 {
306 "type": "function_call_output",
307 "call_id": "call_future_1",
308 "name": "create_thread",
309 "namespace": "codex_app",
310 "output": "early"
311 },
312 {
313 "type": "function_call",
314 "call_id": "call_future_1",
315 "name": "create_thread",
316 "namespace": "codex_app",
317 "arguments": "{}"
318 }
319 ]
320 }`)
321 got := testRewriteCodexOrphan(payload, true)
322 parsed := gjson.ParseBytes(got)
323
324 if parsed.Get("input.0.type").String() != "function_call_output" {
325 t.Fatalf("output should remain paired function_call_output: %s", parsed.Get("input.0").Raw)
326 }
327 if parsed.Get("input.1.type").String() != "function_call" {
328 t.Fatalf("call should remain function_call: %s", parsed.Get("input.1").Raw)
329 }
330 })
331
332 t.Run("duplicate output consumes call once; second output is orphan", func(t *testing.T) {
333 payload := []byte(`{
334 "model": "deepseek-v4-pro",
335 "input": [
336 {
337 "type": "function_call",
338 "call_id": "call_once",
339 "name": "create_thread",
340 "namespace": "codex_app",
341 "arguments": "{}"
342 },
343 {
344 "type": "function_call_output",
345 "call_id": "call_once",
346 "name": "create_thread",
347 "namespace": "codex_app",
348 "output": "first"
349 },
350 {
351 "type": "function_call_output",
352 "call_id": "call_once",
353 "name": "create_thread",
354 "namespace": "codex_app",
355 "output": "second"
356 }
357 ]
358 }`)
359 got := testRewriteCodexOrphan(payload, true)
360 parsed := gjson.ParseBytes(got)
361
362 if parsed.Get("input.1.type").String() != "function_call_output" {
363 t.Fatalf("first output should pair with call: %s", parsed.Get("input.1").Raw)
364 }
365 if parsed.Get("input.2.type").String() != "message" {
366 t.Fatalf("second output should be orphan: %s", parsed.Get("input.2").Raw)
367 }
368 })
369
370 t.Run("custom tool call does not pair with function call output", func(t *testing.T) {
371 payload := []byte(`{
372 "model": "deepseek-v4-pro",
373 "input": [
374 {
375 "type": "custom_tool_call",
376 "call_id": "call_custom",
377 "name": "create_thread",
378 "input": "{}"
379 },
380 {
381 "type": "function_call_output",
382 "call_id": "call_custom",
383 "name": "create_thread",
384 "namespace": "codex_app",
385 "output": "orphan"
386 }
387 ]
388 }`)
389 got := testRewriteCodexOrphan(payload, true)
390 parsed := gjson.ParseBytes(got)
391
392 if parsed.Get("input.1.type").String() != "message" {
393 t.Fatalf("function_call_output should not pair with custom_tool_call: %s", parsed.Get("input.1").Raw)
394 }
395 })
396
397 t.Run("assistant message tool_calls does not pair with Responses function_call_output", func(t *testing.T) {
398 payload := []byte(`{
399 "model": "deepseek-v4-pro",
400 "input": [
401 {
402 "type": "message",
403 "role": "assistant",
404 "tool_calls": [{"id": "call_in_msg", "type": "function", "function": {"name": "create_thread"}}]
405 },
406 {
407 "type": "function_call_output",
408 "call_id": "call_in_msg",
409 "name": "create_thread",
410 "namespace": "codex_app",
411 "output": "orphan"
412 }
413 ]
414 }`)
415 got := testRewriteCodexOrphan(payload, true)
416 parsed := gjson.ParseBytes(got)
417
418 if parsed.Get("input.1.type").String() != "message" {
419 t.Fatalf("function_call_output should not pair with assistant message tool_calls: %s", parsed.Get("input.1").Raw)
420 }
421 })
422
423 t.Run("exact namespace and name required", func(t *testing.T) {
424 payload := []byte(`{
425 "model": "deepseek-v4-pro",
426 "input": [
427 {
428 "type": "function_call_output",
429 "name": "codex_app__create_thread",
430 "output": "orphan"
431 },
432 {
433 "type": "function_call_output",
434 "name": "create_thread",
435 "namespace": " codex_app ",
436 "output": "orphan"
437 },
438 {
439 "type": "function_call_output",
440 "name": "other_tool",
441 "namespace": "codex_app",
442 "output": "orphan"
443 }
444 ]
445 }`)
446 got := testRewriteCodexOrphan(payload, true)
447 parsed := gjson.ParseBytes(got)
448
449 for i := 0; i < 3; i++ {
450 if itemType := parsed.Get(fmt.Sprintf("input.%d.type", i)).String(); itemType != "function_call_output" {
451 t.Fatalf("input.%d should not be rewritten: %s", i, parsed.Get(fmt.Sprintf("input.%d", i)).Raw)
452 }
453 }
454 })
455}
456
457func TestTranslateRequestWithCodexMultiAgentV2OrphanDelegation(t *testing.T) {
458 payload := []byte(`{
459 "model": "test-model",
460 "stream": false,
461 "input": [
462 {
463 "type": "function_call_output",
464 "name": "create_thread",
465 "namespace": "codex_app",
466 "output": "<codex_delegation><message>handoff</message></codex_delegation>"
467 },
468 {
469 "type": "message",
470 "role": "user",
471 "content": [{"type": "input_text", "text": "please continue"}]
472 }
473 ]
474 }`)
475 collabHeaders := http.Header{"X-Openai-Subagent": []string{"collab_spawn"}}
476
477 t.Run("enabled but missing X-Openai-Subagent header does not rewrite", func(t *testing.T) {
478 cfg := &config.Config{
479 Codex: config.CodexConfig{
480 OrphanDelegationCompatibility: true,
481 },
482 }
483 got := TranslateRequestWithCodexMultiAgentV2(context.Background(), http.Header{}, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", payload, false)
484 parsed := gjson.ParseBytes(got)
485
486 item0 := parsed.Get("input.0")
487 if item0.Get("type").String() != "function_call_output" {
488 t.Fatalf("input.0 = %s, want function_call_output when X-Openai-Subagent header is missing", item0.Raw)
489 }
490 })
491
492 t.Run("enabled with wrong X-Openai-Subagent header value does not rewrite", func(t *testing.T) {
493 cfg := &config.Config{
494 Codex: config.CodexConfig{
495 OrphanDelegationCompatibility: true,
496 },
497 }
498 headers := http.Header{"X-Openai-Subagent": []string{"other_agent"}}
499 got := TranslateRequestWithCodexMultiAgentV2(context.Background(), headers, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", payload, false)
500 parsed := gjson.ParseBytes(got)
501
502 item0 := parsed.Get("input.0")
503 if item0.Get("type").String() != "function_call_output" {
504 t.Fatalf("input.0 = %s, want function_call_output when X-Openai-Subagent header is wrong", item0.Raw)
505 }
506 })
507
508 t.Run("enabled translates orphan delegation to user message for responses target", func(t *testing.T) {
509 cfg := &config.Config{
510 Codex: config.CodexConfig{
511 OrphanDelegationCompatibility: true,
512 },
513 }
514 got := TranslateRequestWithCodexMultiAgentV2(context.Background(), collabHeaders, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", payload, false)
515 parsed := gjson.ParseBytes(got)
516
517 item0 := parsed.Get("input.0")
518 if item0.Get("type").String() != "message" || item0.Get("role").String() != "user" {
519 t.Fatalf("input.0 = %s, want message/user", item0.Raw)
520 }
521 wantText := "Tool output from codex_app__create_thread:\n<codex_delegation><message>handoff</message></codex_delegation>"
522 if text := item0.Get("content.0.text").String(); text != wantText {
523 t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
524 }
525 })
526
527 t.Run("enabled translates orphan delegation to user message for chat target without empty tool_call_id", func(t *testing.T) {
528 cfg := &config.Config{
529 Codex: config.CodexConfig{
530 OrphanDelegationCompatibility: true,
531 },
532 }
533 got := TranslateRequestWithCodexMultiAgentV2(context.Background(), collabHeaders, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "test-model", payload, false)
534 parsed := gjson.ParseBytes(got)
535
536 messages := parsed.Get("messages").Array()
537 if len(messages) < 2 {
538 t.Fatalf("expected at least 2 messages, got %d: %s", len(messages), string(got))
539 }
540 msg0 := messages[0]
541 if msg0.Get("role").String() != "user" {
542 t.Fatalf("messages.0.role = %q, want user (should not be tool message with empty id)", msg0.Get("role").String())
543 }
544 wantText := "Tool output from codex_app__create_thread:\n<codex_delegation><message>handoff</message></codex_delegation>"
545 var text string
546 if msg0.Get("content").IsArray() {
547 text = msg0.Get("content.0.text").String()
548 } else {
549 text = msg0.Get("content").String()
550 }
551 if text != wantText {
552 t.Fatalf("messages.0.content = %q, want %q", text, wantText)
553 }
554 })
555
556 t.Run("disabled leaves orphan delegation untranslated for responses target", func(t *testing.T) {
557 cfg := &config.Config{
558 Codex: config.CodexConfig{
559 OrphanDelegationCompatibility: false,
560 },
561 }
562 got := TranslateRequestWithCodexMultiAgentV2(context.Background(), collabHeaders, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", payload, false)
563 parsed := gjson.ParseBytes(got)
564
565 item0 := parsed.Get("input.0")
566 if item0.Get("type").String() != "function_call_output" {
567 t.Fatalf("input.0 = %s, want function_call_output when disabled", item0.Raw)
568 }
569 })
570
571 t.Run("interleaved valid tool pair and orphan delegation retains order and pairing", func(t *testing.T) {
572 interleaved := []byte(`{
573 "model": "test-model",
574 "input": [
575 {
576 "type": "function_call",
577 "call_id": "call_1",
578 "name": "lookup",
579 "arguments": "{\"q\":\"test\"}"
580 },
581 {
582 "type": "function_call_output",
583 "call_id": "call_1",
584 "name": "lookup",
585 "output": "result1"
586 },
587 {
588 "type": "function_call_output",
589 "name": "create_thread",
590 "namespace": "codex_app",
591 "output": "delegated"
592 }
593 ]
594 }`)
595 cfg := &config.Config{
596 Codex: config.CodexConfig{
597 OrphanDelegationCompatibility: true,
598 },
599 }
600 got := TranslateRequestWithCodexMultiAgentV2(context.Background(), collabHeaders, cfg, sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAIResponse, "test-model", interleaved, false)
601 parsed := gjson.ParseBytes(got)
602
603 if parsed.Get("input.0.type").String() != "function_call" {
604 t.Fatalf("input.0 should remain function_call")
605 }
606 if parsed.Get("input.1.type").String() != "function_call_output" || parsed.Get("input.1.call_id").String() != "call_1" {
607 t.Fatalf("input.1 should remain paired function_call_output")
608 }
609 if parsed.Get("input.2.type").String() != "message" || parsed.Get("input.2.role").String() != "user" {
610 t.Fatalf("input.2 should be downgraded to message/user: %s", parsed.Get("input.2").Raw)
611 }
612 })
613}
internal/config/config_types.go+2−0
@@ -158,6 +158,8 @@ type CodexConfig struct { · 157 unmodified lines
158 StreamBootstrapBuffering bool `yaml:"stream-bootstrap-buffering" json:"stream-bootstrap-buffering"`158 StreamBootstrapBuffering bool `yaml:"stream-bootstrap-buffering" json:"stream-bootstrap-buffering"`
159 // OptimizeMultiAgentV2 optimizes official Codex multi-agent requests.159 // OptimizeMultiAgentV2 optimizes official Codex multi-agent requests.
160 OptimizeMultiAgentV2 bool `yaml:"optimize-multi-agent-v2" json:"optimize-multi-agent-v2"`160 OptimizeMultiAgentV2 bool `yaml:"optimize-multi-agent-v2" json:"optimize-multi-agent-v2"`
161 // OrphanDelegationCompatibility enables opt-in compatibility for orphan Codex delegation outputs.
162 OrphanDelegationCompatibility bool `yaml:"orphan-delegation-compatibility" json:"orphan-delegation-compatibility"`
161 // LiveMediaRelay terminates and relays Codex Live WebRTC media in this process.163 // LiveMediaRelay terminates and relays Codex Live WebRTC media in this process.
162 LiveMediaRelay CodexLiveMediaRelayConfig `yaml:"live-media-relay" json:"live-media-relay"`164 LiveMediaRelay CodexLiveMediaRelayConfig `yaml:"live-media-relay" json:"live-media-relay"`
163}165}
internal/config/sdk_config.go+3−0
@@ -45,6 +45,9 @@ type SDKConfig struct { · 44 unmodified lines
45 // CodexOptimizeMultiAgentV2 mirrors the provider-wide runtime setting for API handlers.45 // CodexOptimizeMultiAgentV2 mirrors the provider-wide runtime setting for API handlers.
46 CodexOptimizeMultiAgentV2 bool `yaml:"-" json:"-"`46 CodexOptimizeMultiAgentV2 bool `yaml:"-" json:"-"`
47 47
48 // CodexOrphanDelegationCompatibility mirrors the provider-wide runtime setting for API handlers.
49 CodexOrphanDelegationCompatibility bool `yaml:"-" json:"-"`
50
48 // ClaudeCode configures Claude Code compatibility behavior.51 // ClaudeCode configures Claude Code compatibility behavior.
49 ClaudeCode ClaudeCodeConfig `yaml:"claude-code" json:"claude-code"`52 ClaudeCode ClaudeCodeConfig `yaml:"claude-code" json:"claude-code"`
50 53
internal/runtime/executor/helps/codex_multi_agent_v2.go+12−2
@@ -29,6 +29,13 @@ func RewriteCodexMultiAgentV2Input(ctx context.Context, headers http.Header, pay · 28 unmodified lines
29 return multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)29 return multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
30}30}
31 31
32// RewriteCodexOrphanDelegationInput converts orphan Codex delegation outputs into
33// standard user messages when orphan delegation compatibility is enabled and the
34// request carries the X-Openai-Subagent: collab_spawn header.
35func RewriteCodexOrphanDelegationInput(ctx context.Context, headers http.Header, payload []byte, cfg *config.Config) []byte {
36 return multiagentv2.RewriteCodexOrphanDelegationInputForConfig(ctx, headers, payload, cfg)
37}
38
32// TranslateRequestWithCodexMultiAgentV2 normalizes official Codex multi-agent39// TranslateRequestWithCodexMultiAgentV2 normalizes official Codex multi-agent
33// input before translating it to a non-Codex target protocol.40// input before translating it to a non-Codex target protocol.
34func TranslateRequestWithCodexMultiAgentV2(ctx context.Context, headers http.Header, cfg *config.Config, from, to sdktranslator.Format, model string, payload []byte, stream bool) []byte {41func 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 · 36 unmodified lines
71 if !isCompat {78 if !isCompat {
72 return TranslateRequestWithCodexMultiAgentV2(ctx, headers, cfg, from, to, model, payload, stream)79 return TranslateRequestWithCodexMultiAgentV2(ctx, headers, cfg, from, to, model, payload, stream)
73 }80 }
74 if from == sdktranslator.FormatOpenAIResponse && to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {81 if from == sdktranslator.FormatOpenAIResponse {
75 payload = multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)82 payload = RewriteCodexOrphanDelegationInput(ctx, headers, payload, cfg)
83 if to != sdktranslator.FormatCodex && to != sdktranslator.FormatOpenAIResponse {
84 payload = multiagentv2.RewriteCodexMultiAgentV2Input(ctx, headers, payload, cfg)
85 }
76 }86 }
77 87
78 var translated []byte88 var translated []byte
internal/watcher/diff/config_diff.go+3−0
@@ -121,6 +121,9 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string { · 120 unmodified lines
121 if oldCfg.Codex.OptimizeMultiAgentV2 != newCfg.Codex.OptimizeMultiAgentV2 {121 if oldCfg.Codex.OptimizeMultiAgentV2 != newCfg.Codex.OptimizeMultiAgentV2 {
122 changes = append(changes, fmt.Sprintf("codex.optimize-multi-agent-v2: %t -> %t", oldCfg.Codex.OptimizeMultiAgentV2, newCfg.Codex.OptimizeMultiAgentV2))122 changes = append(changes, fmt.Sprintf("codex.optimize-multi-agent-v2: %t -> %t", oldCfg.Codex.OptimizeMultiAgentV2, newCfg.Codex.OptimizeMultiAgentV2))
123 }123 }
124 if oldCfg.Codex.OrphanDelegationCompatibility != newCfg.Codex.OrphanDelegationCompatibility {
125 changes = append(changes, fmt.Sprintf("codex.orphan-delegation-compatibility: %t -> %t", oldCfg.Codex.OrphanDelegationCompatibility, newCfg.Codex.OrphanDelegationCompatibility))
126 }
124 if oldCfg.XAI.InjectXSearch != newCfg.XAI.InjectXSearch {127 if oldCfg.XAI.InjectXSearch != newCfg.XAI.InjectXSearch {
125 changes = append(changes, fmt.Sprintf("xai.inject-x-search: %t -> %t", oldCfg.XAI.InjectXSearch, newCfg.XAI.InjectXSearch))128 changes = append(changes, fmt.Sprintf("xai.inject-x-search: %t -> %t", oldCfg.XAI.InjectXSearch, newCfg.XAI.InjectXSearch))
126 }129 }
internal/watcher/diff/config_diff_test.go+8−0
@@ -204,6 +204,14 @@ func TestBuildConfigChangeDetails_CodexAlphaSearch(t *testing.T) { · 203 unmodified lines
204 expectContains(t, changes, "codex[0].alpha-search: false -> true")204 expectContains(t, changes, "codex[0].alpha-search: false -> true")
205}205}
206 206
207func TestBuildConfigChangeDetails_CodexOrphanDelegationCompatibility(t *testing.T) {
208 oldCfg := &config.Config{Codex: config.CodexConfig{OrphanDelegationCompatibility: false}}
209 newCfg := &config.Config{Codex: config.CodexConfig{OrphanDelegationCompatibility: true}}
210
211 changes := BuildConfigChangeDetails(oldCfg, newCfg)
212 expectContains(t, changes, "codex.orphan-delegation-compatibility: false -> true")
213}
214
207func TestBuildConfigChangeDetails_XAIKeys(t *testing.T) {215func TestBuildConfigChangeDetails_XAIKeys(t *testing.T) {
208 oldRetry := 1216 oldRetry := 1
209 newRetry := 0217 newRetry := 0
sdk/api/handlers/openai/openai_responses_handlers.go+17−0
@@ -511,6 +511,20 @@ func (h *OpenAIResponsesAPIHandler) prepareCodexMultiAgentV2Tools(c *gin.Context · 510 unmodified lines
511 return updated511 return updated
512}512}
513 513
514func (h *OpenAIResponsesAPIHandler) prepareCodexOrphanDelegation(c *gin.Context, payload []byte) []byte {
515 if h == nil || h.Cfg == nil || !h.Cfg.CodexOrphanDelegationCompatibility {
516 return payload
517 }
518 requestCtx := context.Background()
519 var requestHeaders http.Header
520 if c != nil && c.Request != nil {
521 requestCtx = c.Request.Context()
522 requestHeaders = c.Request.Header
523 }
524 requestCtx = context.WithValue(requestCtx, "gin", c)
525 return multiagentv2.RewriteCodexOrphanDelegationInput(requestCtx, requestHeaders, payload, true)
526}
527
514// Responses handles the /v1/responses endpoint.528// Responses handles the /v1/responses endpoint.
515// It determines whether the request is for a streaming or non-streaming response529// It determines whether the request is for a streaming or non-streaming response
516// and calls the appropriate handler based on the model provider.530// and calls the appropriate handler based on the model provider.
@@ -531,6 +545,7 @@ func (h *OpenAIResponsesAPIHandler) Responses(c *gin.Context) { · 14 unmodified lines
531 }545 }
532 546
533 rawJSON = h.prepareCodexMultiAgentV2Tools(c, rawJSON)547 rawJSON = h.prepareCodexMultiAgentV2Tools(c, rawJSON)
548 rawJSON = h.prepareCodexOrphanDelegation(c, rawJSON)
534 549
535 // Check if the client requested a streaming response.550 // Check if the client requested a streaming response.
536 streamResult := gjson.GetBytes(rawJSON, "stream")551 streamResult := gjson.GetBytes(rawJSON, "stream")
@@ -554,6 +569,8 @@ func (h *OpenAIResponsesAPIHandler) Compact(c *gin.Context) { · 17 unmodified lines
554 return569 return
555 }570 }
556 571
572 rawJSON = h.prepareCodexOrphanDelegation(c, rawJSON)
573
557 streamResult := gjson.GetBytes(rawJSON, "stream")574 streamResult := gjson.GetBytes(rawJSON, "stream")
558 if streamResult.Type == gjson.True {575 if streamResult.Type == gjson.True {
559 c.JSON(http.StatusBadRequest, handlers.ErrorResponse{576 c.JSON(http.StatusBadRequest, handlers.ErrorResponse{
sdk/api/handlers/openai/openai_responses_multi_agent_test.go+90−0
@@ -197,3 +197,93 @@ func TestPrepareCodexMultiAgentV2ToolsAtResponsesBoundarySkipsOtherClients(t *te · 196 unmodified lines
197 t.Fatal("other client unexpectedly received prepared marker")197 t.Fatal("other client unexpectedly received prepared marker")
198 }198 }
199}199}
200
201func newResponsesOrphanDelegationTestHandler(t *testing.T, executor *responsesMultiAgentCaptureExecutor) (*OpenAIResponsesAPIHandler, string) {
202 t.Helper()
203
204 modelID := "responses-orphan-test-model"
205 authID := "responses-orphan-test-auth"
206 manager := coreauth.NewManager(nil, nil, nil)
207 manager.RegisterExecutor(executor)
208 auth := &coreauth.Auth{ID: authID, Provider: "codex", Status: coreauth.StatusActive, ProxyURL: "direct"}
209 if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
210 t.Fatalf("Register auth: %v", errRegister)
211 }
212 registry.GetGlobalRegistry().RegisterClient(authID, auth.Provider, []*registry.ModelInfo{{ID: modelID}})
213 t.Cleanup(func() {
214 registry.GetGlobalRegistry().UnregisterClient(authID)
215 })
216
217 base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{CodexOrphanDelegationCompatibility: true}, manager)
218 return NewOpenAIResponsesAPIHandler(base), modelID
219}
220
221func TestResponsesOrphanCodexDelegationCompatibility(t *testing.T) {
222 t.Parallel()
223
224 executor := &responsesMultiAgentCaptureExecutor{}
225 handler, modelID := newResponsesOrphanDelegationTestHandler(t, executor)
226
227 router := gin.New()
228 router.POST("/v1/responses", handler.Responses)
229
230 payload := fmt.Sprintf(`{
231 "model": %q,
232 "stream": false,
233 "input": [
234 {
235 "type": "function_call_output",
236 "name": "create_thread",
237 "namespace": "codex_app",
238 "output": "<codex_delegation>msg</codex_delegation>"
239 },
240 {
241 "type": "message",
242 "role": "user",
243 "content": [{"type": "input_text", "text": "continue"}]
244 }
245 ]
246 }`, modelID)
247
248 request := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(payload))
249 request.Header.Set("X-Openai-Subagent", "collab_spawn")
250 recorder := httptest.NewRecorder()
251 router.ServeHTTP(recorder, request)
252 if recorder.Code != http.StatusOK {
253 t.Fatalf("status = %d, want 200; body=%s", recorder.Code, recorder.Body.String())
254 }
255
256 payloads := executor.Payloads()
257 if len(payloads) != 1 {
258 t.Fatalf("captured payload count = %d, want 1", len(payloads))
259 }
260 captured := payloads[0]
261 parsed := gjson.ParseBytes(captured)
262 if itemType := parsed.Get("input.0.type").String(); itemType != "message" {
263 t.Fatalf("input.0.type = %q, want message; captured=%s", itemType, captured)
264 }
265 if role := parsed.Get("input.0.role").String(); role != "user" {
266 t.Fatalf("input.0.role = %q, want user", role)
267 }
268 wantText := "Tool output from codex_app__create_thread:\n<codex_delegation>msg</codex_delegation>"
269 if text := parsed.Get("input.0.content.0.text").String(); text != wantText {
270 t.Fatalf("input.0.content.0.text = %q, want %q", text, wantText)
271 }
272
273 // Without X-Openai-Subagent header, payload should remain function_call_output
274 requestNoHeader := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(payload))
275 recorderNoHeader := httptest.NewRecorder()
276 router.ServeHTTP(recorderNoHeader, requestNoHeader)
277 if recorderNoHeader.Code != http.StatusOK {
278 t.Fatalf("status = %d, want 200; body=%s", recorderNoHeader.Code, recorderNoHeader.Body.String())
279 }
280 payloads = executor.Payloads()
281 if len(payloads) != 2 {
282 t.Fatalf("captured payload count = %d, want 2", len(payloads))
283 }
284 capturedNoHeader := payloads[1]
285 parsedNoHeader := gjson.ParseBytes(capturedNoHeader)
286 if itemType := parsedNoHeader.Get("input.0.type").String(); itemType != "function_call_output" {
287 t.Fatalf("input.0.type = %q, want function_call_output without header; captured=%s", itemType, capturedNoHeader)
288 }
289}
sdk/api/handlers/openai/openai_responses_websocket.go+1−0
@@ -522,6 +522,7 @@ func (h *OpenAIResponsesAPIHandler) ResponsesWebsocket(c *gin.Context) { · 521 unmodified lines
522 }522 }
523 523
524 requestJSON = h.prepareCodexMultiAgentV2Tools(c, requestJSON)524 requestJSON = h.prepareCodexMultiAgentV2Tools(c, requestJSON)
525 requestJSON = h.prepareCodexOrphanDelegation(c, requestJSON)
525 526
526 if !useUpstreamWebsocketPassthrough && shouldHandleResponsesWebsocketPrewarmLocally(payload, lastRequest, false) {527 if !useUpstreamWebsocketPassthrough && shouldHandleResponsesWebsocketPrewarmLocally(payload, lastRequest, false) {
527 if updated, errDelete := sjson.DeleteBytes(requestJSON, "generate"); errDelete == nil {528 if updated, errDelete := sjson.DeleteBytes(requestJSON, "generate"); errDelete == nil {