shinmentakezo07 commited on
Commit
bb8f6a6
·
1 Parent(s): f4bac68

feat(codex): add planner-reviewer agent mode

Browse files
internal/runtime/executor/codex_agent_pipeline.go ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package executor
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "fmt"
7
+ "io"
8
+ "net/http"
9
+ "strings"
10
+
11
+ cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
12
+ cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
13
+ sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
14
+ "github.com/tidwall/gjson"
15
+ "github.com/tidwall/sjson"
16
+ )
17
+
18
+ type codexAgentMode string
19
+
20
+ const (
21
+ codexAgentModeNone codexAgentMode = ""
22
+ codexAgentModePlannerReviewer codexAgentMode = "planner-reviewer"
23
+ )
24
+
25
+ type codexAgentConfig struct {
26
+ Mode codexAgentMode
27
+ }
28
+
29
+ func (c codexAgentConfig) Enabled() bool { return c.Mode != codexAgentModeNone }
30
+
31
+ type codexCompletedPassResult struct {
32
+ Completed []byte
33
+ Request []byte
34
+ Headers http.Header
35
+ Usage usageDetail
36
+ UsageOK bool
37
+ }
38
+
39
+ func parseCodexAgentConfig(bodies ...[]byte) codexAgentConfig {
40
+ for _, body := range bodies {
41
+ if len(body) == 0 || !gjson.ValidBytes(body) {
42
+ continue
43
+ }
44
+ for _, path := range []string{"_cliproxy.agent_mode", "agent_mode"} {
45
+ raw := strings.TrimSpace(gjson.GetBytes(body, path).String())
46
+ if raw == "" {
47
+ continue
48
+ }
49
+ switch normalizeCodexAgentMode(raw) {
50
+ case codexAgentModePlannerReviewer:
51
+ return codexAgentConfig{Mode: codexAgentModePlannerReviewer}
52
+ }
53
+ }
54
+ }
55
+ return codexAgentConfig{}
56
+ }
57
+
58
+ func normalizeCodexAgentMode(v string) codexAgentMode {
59
+ s := strings.ToLower(strings.TrimSpace(v))
60
+ s = strings.ReplaceAll(s, "_", "-")
61
+ switch s {
62
+ case "planner-reviewer", "plannerreviewer":
63
+ return codexAgentModePlannerReviewer
64
+ default:
65
+ return codexAgentModeNone
66
+ }
67
+ }
68
+
69
+ func codexAgentModeSupportedForStreaming(body []byte) bool {
70
+ cfg := parseCodexAgentConfig(body)
71
+ return !cfg.Enabled()
72
+ }
73
+
74
+ func codexAgentCompatibilityIssue(body []byte) string {
75
+ if len(body) == 0 || !gjson.ValidBytes(body) {
76
+ return "request body is not valid JSON"
77
+ }
78
+ if tools := gjson.GetBytes(body, "tools"); tools.Exists() {
79
+ if !tools.IsArray() || len(tools.Array()) > 0 {
80
+ return "tool-enabled requests are not supported"
81
+ }
82
+ }
83
+ if gjson.GetBytes(body, "text.format").Exists() {
84
+ return "structured text.format responses are not supported"
85
+ }
86
+ if gjson.GetBytes(body, "response_format").Exists() {
87
+ return "structured response_format responses are not supported"
88
+ }
89
+ return ""
90
+ }
91
+
92
+ func codexExtractTaskText(body []byte) (string, bool) {
93
+ if len(body) == 0 || !gjson.ValidBytes(body) {
94
+ return "", false
95
+ }
96
+ root := gjson.ParseBytes(body)
97
+ var segments []string
98
+ unsupported := false
99
+
100
+ if inst := strings.TrimSpace(root.Get("instructions").String()); inst != "" {
101
+ segments = append(segments, "Instructions:\n"+inst)
102
+ }
103
+
104
+ input := root.Get("input")
105
+ if !input.Exists() {
106
+ return strings.TrimSpace(strings.Join(segments, "\n\n")), !unsupported
107
+ }
108
+
109
+ if input.IsArray() {
110
+ for _, item := range input.Array() {
111
+ itemType := strings.TrimSpace(item.Get("type").String())
112
+ switch itemType {
113
+ case "message":
114
+ role := strings.TrimSpace(item.Get("role").String())
115
+ if role == "" {
116
+ role = "user"
117
+ }
118
+ texts, okText := codexCollectMessageContentText(item.Get("content"))
119
+ if !okText {
120
+ unsupported = true
121
+ }
122
+ if len(texts) > 0 {
123
+ segments = append(segments, strings.ToUpper(role)+":\n"+strings.Join(texts, "\n"))
124
+ }
125
+ case "function_call":
126
+ name := strings.TrimSpace(item.Get("name").String())
127
+ args := strings.TrimSpace(item.Get("arguments").String())
128
+ if name != "" || args != "" {
129
+ segments = append(segments, "FUNCTION_CALL:\nname="+name+"\nargs="+args)
130
+ }
131
+ case "function_call_output":
132
+ out := strings.TrimSpace(item.Get("output").String())
133
+ if out != "" {
134
+ segments = append(segments, "FUNCTION_OUTPUT:\n"+out)
135
+ }
136
+ default:
137
+ if txt := strings.TrimSpace(item.Get("text").String()); txt != "" {
138
+ segments = append(segments, txt)
139
+ } else if itemType != "" {
140
+ unsupported = true
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ return strings.TrimSpace(strings.Join(segments, "\n\n")), !unsupported
147
+ }
148
+
149
+ func codexCollectMessageContentText(content gjson.Result) ([]string, bool) {
150
+ var parts []string
151
+ if !content.Exists() {
152
+ return parts, true
153
+ }
154
+ if content.IsArray() {
155
+ unsupported := false
156
+ for _, part := range content.Array() {
157
+ partType := strings.TrimSpace(part.Get("type").String())
158
+ switch partType {
159
+ case "input_text", "output_text", "text":
160
+ if txt := strings.TrimSpace(part.Get("text").String()); txt != "" {
161
+ parts = append(parts, txt)
162
+ }
163
+ case "":
164
+ if txt := strings.TrimSpace(part.String()); txt != "" {
165
+ parts = append(parts, txt)
166
+ }
167
+ default:
168
+ unsupported = true
169
+ }
170
+ }
171
+ return parts, !unsupported
172
+ }
173
+ if txt := strings.TrimSpace(content.String()); txt != "" {
174
+ return []string{txt}, true
175
+ }
176
+ return parts, true
177
+ }
178
+
179
+ func codexExtractCompletedMessageAndReasoning(payload []byte) (message string, reasoning string) {
180
+ root := gjson.ParseBytes(payload)
181
+ if root.Get("type").String() != "response.completed" {
182
+ return "", ""
183
+ }
184
+ output := root.Get("response.output")
185
+ if !output.Exists() || !output.IsArray() {
186
+ return "", ""
187
+ }
188
+ var msgParts []string
189
+ var reasoningParts []string
190
+ for _, item := range output.Array() {
191
+ switch item.Get("type").String() {
192
+ case "message":
193
+ content := item.Get("content")
194
+ if content.IsArray() {
195
+ for _, part := range content.Array() {
196
+ if part.Get("type").String() == "output_text" {
197
+ if txt := strings.TrimSpace(part.Get("text").String()); txt != "" {
198
+ msgParts = append(msgParts, txt)
199
+ }
200
+ }
201
+ }
202
+ }
203
+ case "reasoning":
204
+ if summary := item.Get("summary"); summary.IsArray() {
205
+ for _, part := range summary.Array() {
206
+ if txt := strings.TrimSpace(part.Get("text").String()); txt != "" {
207
+ reasoningParts = append(reasoningParts, txt)
208
+ }
209
+ }
210
+ }
211
+ if len(reasoningParts) == 0 {
212
+ if txt := strings.TrimSpace(item.Get("content").String()); txt != "" {
213
+ reasoningParts = append(reasoningParts, txt)
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return strings.TrimSpace(strings.Join(msgParts, "\n")), strings.TrimSpace(strings.Join(reasoningParts, "\n"))
219
+ }
220
+
221
+ func codexAgentTruncate(s string, max int) string {
222
+ if max <= 0 {
223
+ max = 12000
224
+ }
225
+ s = strings.TrimSpace(s)
226
+ if len(s) <= max {
227
+ return s
228
+ }
229
+ const suffix = "\n\n[truncated by server-side agent pipeline]"
230
+ if max <= len(suffix)+16 {
231
+ return s[:max]
232
+ }
233
+ return s[:max-len(suffix)] + suffix
234
+ }
235
+
236
+ func codexBuildAgentPassBody(baseBody []byte, phase string, originalTask string, plannerOutput string, reviewerOutput string) ([]byte, error) {
237
+ if len(baseBody) == 0 || !gjson.ValidBytes(baseBody) {
238
+ return nil, fmt.Errorf("codex agent pipeline: invalid base request body")
239
+ }
240
+ out := bytes.Clone(baseBody)
241
+
242
+ // Remove client/local controls and features incompatible with internal review passes.
243
+ for _, path := range []string{
244
+ "_cliproxy",
245
+ "agent_mode",
246
+ "tools",
247
+ "tool_choice",
248
+ "parallel_tool_calls",
249
+ "text.format",
250
+ "response_format",
251
+ "previous_response_id",
252
+ "prompt_cache_key",
253
+ } {
254
+ out, _ = sjson.DeleteBytes(out, path)
255
+ }
256
+ out, _ = sjson.SetBytes(out, "stream", true)
257
+
258
+ instructions := codexAgentPhaseInstructions(phase)
259
+ out, _ = sjson.SetBytes(out, "instructions", instructions)
260
+
261
+ prompt := codexAgentPhasePrompt(phase, originalTask, plannerOutput, reviewerOutput)
262
+ inputJSON := fmt.Sprintf(`[{"type":"message","role":"user","content":[{"type":"input_text","text":%q}]}]`, prompt)
263
+ out, _ = sjson.SetRawBytes(out, "input", []byte(inputJSON))
264
+
265
+ return out, nil
266
+ }
267
+
268
+ func codexAgentPhaseInstructions(phase string) string {
269
+ var base string
270
+ switch phase {
271
+ case "planner":
272
+ base = "You are an internal planning agent. Produce a strong plan and draft direction. Do not mention hidden reasoning. Be explicit and structured. Output planning artifacts only, not the final user answer."
273
+ case "reviewer":
274
+ base = "You are an internal reviewer agent. Critique the plan/draft rigorously, identify gaps and edge cases, and propose fixes. Do not produce the final user answer."
275
+ case "final":
276
+ base = "You are the final responder. Use the plan and review to produce the best final answer for the user. Do not mention the internal planner/reviewer workflow unless the user explicitly asks."
277
+ default:
278
+ base = "You are an internal agent pass."
279
+ }
280
+
281
+ sections := []string{base, codexDeepEngineeringStandardsPrompt()}
282
+ if phase == "final" {
283
+ sections = append(sections, codexNormalResponseFormatPrompt())
284
+ }
285
+ return strings.TrimSpace(strings.Join(sections, "\n\n"))
286
+ }
287
+
288
+ func codexAgentPhasePrompt(phase string, originalTask string, plannerOutput string, reviewerOutput string) string {
289
+ task := codexAgentTruncate(originalTask, 16000)
290
+ planner := codexAgentTruncate(plannerOutput, 12000)
291
+ reviewer := codexAgentTruncate(reviewerOutput, 12000)
292
+
293
+ switch phase {
294
+ case "planner":
295
+ return strings.TrimSpace(`Original user request and context:
296
+ ` + task + `
297
+
298
+ Produce:
299
+ 1. A solution plan
300
+ 2. Key technical risks and edge cases
301
+ 3. A draft answer/implementation direction
302
+
303
+ Prefer depth over brevity, but keep it actionable.`)
304
+ case "reviewer":
305
+ return strings.TrimSpace(`Original user request and context:
306
+ ` + task + `
307
+
308
+ Planner output:
309
+ ` + planner + `
310
+
311
+ Review the planner output critically. Identify incorrect assumptions, missing edge cases, scalability/performance concerns, accessibility concerns (if UI), and maintainability risks.
312
+
313
+ Produce:
314
+ 1. Findings (ordered by severity)
315
+ 2. Suggested corrections
316
+ 3. What the final answer must include`)
317
+ case "final":
318
+ return strings.TrimSpace(`Original user request and context:
319
+ ` + task + `
320
+
321
+ Planner output:
322
+ ` + planner + `
323
+
324
+ Reviewer output:
325
+ ` + reviewer + `
326
+
327
+ Produce the final answer for the user. Incorporate valid reviewer feedback. Keep it technically rigorous and directly useful.`)
328
+ default:
329
+ return task
330
+ }
331
+ }
332
+
333
+ func addUsageDetails(a usageDetail, b usageDetail) usageDetail {
334
+ return usageDetail{
335
+ InputTokens: a.InputTokens + b.InputTokens,
336
+ OutputTokens: a.OutputTokens + b.OutputTokens,
337
+ ReasoningTokens: a.ReasoningTokens + b.ReasoningTokens,
338
+ CachedTokens: a.CachedTokens + b.CachedTokens,
339
+ TotalTokens: a.TotalTokens + b.TotalTokens,
340
+ }
341
+ }
342
+
343
+ func findCodexCompletedEventFromSSEData(data []byte) ([]byte, bool) {
344
+ lines := bytes.Split(data, []byte("\n"))
345
+ for _, line := range lines {
346
+ if payload, ok := codexCompletedEventPayload(line); ok {
347
+ return payload, true
348
+ }
349
+ }
350
+ return nil, false
351
+ }
352
+
353
+ func (e *CodexExecutorRefactored) executeCodexCompletedHTTPPass(ctx context.Context, auth *cliproxyauth.Auth, apiKey string, url string, from sdktranslator.Format, req cliproxyexecutor.Request, body []byte, usePromptCache bool) (codexCompletedPassResult, error) {
354
+ result := codexCompletedPassResult{Request: bytes.Clone(body)}
355
+
356
+ var httpReq *http.Request
357
+ var err error
358
+ if usePromptCache {
359
+ httpReq, err = e.cacheHelper(ctx, from, url, req, body)
360
+ } else {
361
+ httpReq, err = http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
362
+ }
363
+ if err != nil {
364
+ return result, err
365
+ }
366
+
367
+ provider := NewCodexProvider(e.cfg)
368
+ provider.ApplyHeaders(httpReq, auth, apiKey, true)
369
+
370
+ var authID, authLabel, authType, authValue string
371
+ if auth != nil {
372
+ authID = auth.ID
373
+ authLabel = auth.Label
374
+ authType, authValue = auth.AccountInfo()
375
+ }
376
+ recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
377
+ URL: url,
378
+ Method: http.MethodPost,
379
+ Headers: httpReq.Header.Clone(),
380
+ Body: body,
381
+ Provider: e.Identifier(),
382
+ AuthID: authID,
383
+ AuthLabel: authLabel,
384
+ AuthType: authType,
385
+ AuthValue: authValue,
386
+ })
387
+
388
+ httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
389
+ httpResp, err := httpClient.Do(httpReq)
390
+ if err != nil {
391
+ recordAPIResponseError(ctx, e.cfg, err)
392
+ return result, err
393
+ }
394
+ defer httpResp.Body.Close()
395
+
396
+ result.Headers = httpResp.Header.Clone()
397
+ recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, result.Headers.Clone())
398
+ if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
399
+ b, _ := io.ReadAll(httpResp.Body)
400
+ appendAPIResponseChunk(ctx, e.cfg, b)
401
+ logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
402
+ return result, statusErr{code: httpResp.StatusCode, msg: string(b)}
403
+ }
404
+
405
+ data, err := io.ReadAll(httpResp.Body)
406
+ if err != nil {
407
+ recordAPIResponseError(ctx, e.cfg, err)
408
+ return result, err
409
+ }
410
+ appendAPIResponseChunk(ctx, e.cfg, data)
411
+
412
+ completed, ok := findCodexCompletedEventFromSSEData(data)
413
+ if !ok {
414
+ return result, statusErr{code: 408, msg: "stream error: stream disconnected before completion: stream closed before response.completed"}
415
+ }
416
+ result.Completed = completed
417
+ if detail, okUsage := parseCodexUsage(completed); okUsage {
418
+ result.Usage = detail
419
+ result.UsageOK = true
420
+ }
421
+ return result, nil
422
+ }
423
+
424
+ func (e *CodexExecutorRefactored) executeCodexPlannerReviewerPipeline(ctx context.Context, auth *cliproxyauth.Auth, apiKey string, baseURL string, from sdktranslator.Format, req cliproxyexecutor.Request, normalizedBody []byte) (codexCompletedPassResult, error) {
425
+ if issue := codexAgentCompatibilityIssue(normalizedBody); issue != "" {
426
+ return codexCompletedPassResult{}, statusErr{code: http.StatusBadRequest, msg: "codex agent_mode planner-reviewer unsupported: " + issue}
427
+ }
428
+
429
+ taskText, taskSupported := codexExtractTaskText(normalizedBody)
430
+ if !taskSupported {
431
+ return codexCompletedPassResult{}, statusErr{code: http.StatusBadRequest, msg: "codex agent_mode planner-reviewer unsupported: request contains unsupported non-text input parts"}
432
+ }
433
+ if strings.TrimSpace(taskText) == "" {
434
+ return codexCompletedPassResult{}, statusErr{code: http.StatusBadRequest, msg: "codex agent_mode planner-reviewer unsupported: unable to extract textual task content"}
435
+ }
436
+
437
+ url := strings.TrimSuffix(baseURL, "/") + "/responses"
438
+
439
+ plannerBody, err := codexBuildAgentPassBody(normalizedBody, "planner", taskText, "", "")
440
+ if err != nil {
441
+ return codexCompletedPassResult{}, err
442
+ }
443
+ plannerRes, err := e.executeCodexCompletedHTTPPass(ctx, auth, apiKey, url, from, req, plannerBody, false)
444
+ if err != nil {
445
+ return codexCompletedPassResult{}, err
446
+ }
447
+ plannerText, plannerReasoning := codexExtractCompletedMessageAndReasoning(plannerRes.Completed)
448
+ if plannerReasoning != "" && plannerText != "" {
449
+ plannerText = plannerText + "\n\nReviewer note from planner reasoning summary:\n" + plannerReasoning
450
+ } else if plannerText == "" {
451
+ plannerText = plannerReasoning
452
+ }
453
+ if strings.TrimSpace(plannerText) == "" {
454
+ return codexCompletedPassResult{}, statusErr{code: http.StatusBadGateway, msg: "codex agent_mode planner-reviewer failed: planner pass returned no text"}
455
+ }
456
+
457
+ reviewerBody, err := codexBuildAgentPassBody(normalizedBody, "reviewer", taskText, plannerText, "")
458
+ if err != nil {
459
+ return codexCompletedPassResult{}, err
460
+ }
461
+ reviewerRes, err := e.executeCodexCompletedHTTPPass(ctx, auth, apiKey, url, from, req, reviewerBody, false)
462
+ if err != nil {
463
+ return codexCompletedPassResult{}, err
464
+ }
465
+ reviewerText, reviewerReasoning := codexExtractCompletedMessageAndReasoning(reviewerRes.Completed)
466
+ if reviewerReasoning != "" && reviewerText != "" {
467
+ reviewerText = reviewerText + "\n\nReviewer reasoning summary:\n" + reviewerReasoning
468
+ } else if reviewerText == "" {
469
+ reviewerText = reviewerReasoning
470
+ }
471
+ if strings.TrimSpace(reviewerText) == "" {
472
+ return codexCompletedPassResult{}, statusErr{code: http.StatusBadGateway, msg: "codex agent_mode planner-reviewer failed: reviewer pass returned no text"}
473
+ }
474
+
475
+ finalBody, err := codexBuildAgentPassBody(normalizedBody, "final", taskText, plannerText, reviewerText)
476
+ if err != nil {
477
+ return codexCompletedPassResult{}, err
478
+ }
479
+ finalRes, err := e.executeCodexCompletedHTTPPass(ctx, auth, apiKey, url, from, req, finalBody, false)
480
+ if err != nil {
481
+ return codexCompletedPassResult{}, err
482
+ }
483
+ finalRes.Request = finalBody
484
+ finalRes.Usage = addUsageDetails(finalRes.Usage, plannerRes.Usage)
485
+ finalRes.Usage = addUsageDetails(finalRes.Usage, reviewerRes.Usage)
486
+ finalRes.UsageOK = finalRes.UsageOK || plannerRes.UsageOK || reviewerRes.UsageOK
487
+ return finalRes, nil
488
+ }
internal/runtime/executor/codex_agent_pipeline_test.go ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package executor
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+
7
+ "github.com/tidwall/gjson"
8
+ )
9
+
10
+ func TestParseCodexAgentConfig(t *testing.T) {
11
+ tests := []struct {
12
+ name string
13
+ body []byte
14
+ want codexAgentMode
15
+ }{
16
+ {
17
+ name: "cliproxy nested",
18
+ body: []byte(`{"_cliproxy":{"agent_mode":"planner-reviewer"}}`),
19
+ want: codexAgentModePlannerReviewer,
20
+ },
21
+ {
22
+ name: "top-level alias underscore",
23
+ body: []byte(`{"agent_mode":"planner_reviewer"}`),
24
+ want: codexAgentModePlannerReviewer,
25
+ },
26
+ {
27
+ name: "unknown mode",
28
+ body: []byte(`{"_cliproxy":{"agent_mode":"foo"}}`),
29
+ want: codexAgentModeNone,
30
+ },
31
+ }
32
+
33
+ for _, tt := range tests {
34
+ t.Run(tt.name, func(t *testing.T) {
35
+ got := parseCodexAgentConfig(tt.body)
36
+ if got.Mode != tt.want {
37
+ t.Fatalf("mode = %q, want %q", got.Mode, tt.want)
38
+ }
39
+ })
40
+ }
41
+ }
42
+
43
+ func TestCodexAgentCompatibilityIssue(t *testing.T) {
44
+ if issue := codexAgentCompatibilityIssue([]byte(`{"tools":[{"name":"x"}]}`)); !strings.Contains(issue, "tool") {
45
+ t.Fatalf("expected tool compatibility issue, got %q", issue)
46
+ }
47
+ if issue := codexAgentCompatibilityIssue([]byte(`{"text":{"format":{"type":"json_schema"}}}`)); !strings.Contains(issue, "structured") {
48
+ t.Fatalf("expected structured compatibility issue, got %q", issue)
49
+ }
50
+ if issue := codexAgentCompatibilityIssue([]byte(`{"input":[]}`)); issue != "" {
51
+ t.Fatalf("unexpected issue for simple text request: %q", issue)
52
+ }
53
+ }
54
+
55
+ func TestCodexBuildAgentPassBody(t *testing.T) {
56
+ base := []byte(`{
57
+ "model":"gpt-5",
58
+ "stream":true,
59
+ "reasoning":{"effort":"high"},
60
+ "tools":[{"type":"function","name":"x"}],
61
+ "_cliproxy":{"agent_mode":"planner-reviewer"},
62
+ "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}]
63
+ }`)
64
+
65
+ out, err := codexBuildAgentPassBody(base, "planner", "original task", "", "")
66
+ if err != nil {
67
+ t.Fatalf("codexBuildAgentPassBody error: %v", err)
68
+ }
69
+
70
+ if got := gjson.GetBytes(out, "tools"); got.Exists() {
71
+ t.Fatalf("expected tools to be stripped, got %s", got.Raw)
72
+ }
73
+ if got := gjson.GetBytes(out, "_cliproxy"); got.Exists() {
74
+ t.Fatalf("expected _cliproxy to be stripped, got %s", got.Raw)
75
+ }
76
+ if !gjson.GetBytes(out, "stream").Bool() {
77
+ t.Fatalf("expected stream=true in internal pass body")
78
+ }
79
+ if got := gjson.GetBytes(out, "reasoning.effort").String(); got != "high" {
80
+ t.Fatalf("reasoning.effort = %q, want %q", got, "high")
81
+ }
82
+ prompt := gjson.GetBytes(out, "input.0.content.0.text").String()
83
+ if !strings.Contains(prompt, "original task") {
84
+ t.Fatalf("expected planner prompt to include original task, got %q", prompt)
85
+ }
86
+ instructions := gjson.GetBytes(out, "instructions").String()
87
+ if !strings.Contains(strings.ToLower(instructions), "planning agent") {
88
+ t.Fatalf("expected planner instructions, got %q", instructions)
89
+ }
90
+ }
91
+
92
+ func TestCodexExtractCompletedMessageAndReasoning(t *testing.T) {
93
+ payload := []byte(`{
94
+ "type":"response.completed",
95
+ "response":{
96
+ "output":[
97
+ {"type":"reasoning","summary":[{"type":"summary_text","text":"check edge cases"}]},
98
+ {"type":"message","content":[{"type":"output_text","text":"final answer"}]}
99
+ ]
100
+ }
101
+ }`)
102
+ msg, reasoning := codexExtractCompletedMessageAndReasoning(payload)
103
+ if msg != "final answer" {
104
+ t.Fatalf("message = %q, want %q", msg, "final answer")
105
+ }
106
+ if reasoning != "check edge cases" {
107
+ t.Fatalf("reasoning = %q, want %q", reasoning, "check edge cases")
108
+ }
109
+ }
110
+
111
+ func TestCodexAgentPhaseInstructions_IncludeDeepEngineeringStandards(t *testing.T) {
112
+ planner := codexAgentPhaseInstructions("planner")
113
+ if !strings.Contains(planner, "internal planning agent") {
114
+ t.Fatalf("planner instructions missing role text: %q", planner)
115
+ }
116
+ if !strings.Contains(planner, "Override Brevity") || !strings.Contains(planner, "Library & Framework Discipline") {
117
+ t.Fatalf("planner instructions missing shared engineering standards: %q", planner)
118
+ }
119
+ if strings.Contains(planner, "Response format (normal mode)") {
120
+ t.Fatalf("planner instructions should not include final response format block: %q", planner)
121
+ }
122
+
123
+ final := codexAgentPhaseInstructions("final")
124
+ if !strings.Contains(final, "final responder") {
125
+ t.Fatalf("final instructions missing role text: %q", final)
126
+ }
127
+ if !strings.Contains(final, "Response format (normal mode)") {
128
+ t.Fatalf("final instructions missing response format block: %q", final)
129
+ }
130
+ }
internal/runtime/executor/codex_executor_refactored.go CHANGED
@@ -75,6 +75,9 @@ func (e *CodexExecutorRefactored) HttpRequest(ctx context.Context, auth *cliprox
75
 
76
  // Execute performs a non-streaming request to the Codex API.
77
  func (e *CodexExecutorRefactored) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
 
 
 
78
  if opts.Alt == "responses/compact" {
79
  return e.executeCompact(ctx, auth, req, opts)
80
  }
@@ -111,84 +114,35 @@ func (e *CodexExecutorRefactored) executeViaStream(ctx context.Context, auth *cl
111
 
112
  requestedModel := payloadRequestedModel(opts, req.Model)
113
  body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
 
114
  body, err = normalizeCodexRequestBody(body, baseModel, true)
115
  if err != nil {
116
  return resp, err
117
  }
118
 
119
- url := strings.TrimSuffix(baseURL, "/") + "/responses"
120
- httpReq, err := e.cacheHelper(ctx, from, url, req, body)
121
- if err != nil {
122
- return resp, err
123
- }
124
-
125
- provider := NewCodexProvider(e.cfg)
126
- provider.ApplyHeaders(httpReq, auth, apiKey, true)
127
-
128
- var authID, authLabel, authType, authValue string
129
- if auth != nil {
130
- authID = auth.ID
131
- authLabel = auth.Label
132
- authType, authValue = auth.AccountInfo()
133
  }
134
- recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
135
- URL: url,
136
- Method: http.MethodPost,
137
- Headers: httpReq.Header.Clone(),
138
- Body: body,
139
- Provider: e.Identifier(),
140
- AuthID: authID,
141
- AuthLabel: authLabel,
142
- AuthType: authType,
143
- AuthValue: authValue,
144
- })
145
-
146
- httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
147
- httpResp, err := httpClient.Do(httpReq)
148
  if err != nil {
149
- recordAPIResponseError(ctx, e.cfg, err)
150
  return resp, err
151
  }
152
- defer httpResp.Body.Close()
153
-
154
- recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
155
- if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
156
- b, _ := io.ReadAll(httpResp.Body)
157
- appendAPIResponseChunk(ctx, e.cfg, b)
158
- logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
159
- return resp, statusErr{code: httpResp.StatusCode, msg: string(b)}
160
  }
161
 
162
- data, err := io.ReadAll(httpResp.Body)
163
- if err != nil {
164
- recordAPIResponseError(ctx, e.cfg, err)
165
- return resp, err
166
  }
167
- appendAPIResponseChunk(ctx, e.cfg, data)
168
-
169
- // Parse stream to find response.completed event
170
- lines := bytes.Split(data, []byte("\n"))
171
- for _, line := range lines {
172
- if !bytes.HasPrefix(line, dataTag) {
173
- continue
174
- }
175
-
176
- line = bytes.TrimSpace(line[5:])
177
- if gjson.GetBytes(line, "type").String() != "response.completed" {
178
- continue
179
- }
180
-
181
- if detail, ok := parseCodexUsage(line); ok {
182
- reporter.publish(ctx, detail)
183
- }
184
-
185
- var param any
186
- out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, line, &param)
187
- resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()}
188
- return resp, nil
189
- }
190
-
191
- return resp, statusErr{code: 408, msg: "stream error: stream disconnected before completion: stream closed before response.completed"}
192
  }
193
 
194
  // executeCompact handles the /responses/compact endpoint
@@ -288,6 +242,9 @@ func (e *CodexExecutorRefactored) ExecuteStream(ctx context.Context, auth *clipr
288
  if opts.Alt == "responses/compact" {
289
  return nil, statusErr{code: http.StatusBadRequest, msg: "streaming not supported for /responses/compact"}
290
  }
 
 
 
291
 
292
  return e.base.ExecuteStream(ctx, auth, req, opts)
293
  }
 
75
 
76
  // Execute performs a non-streaming request to the Codex API.
77
  func (e *CodexExecutorRefactored) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
78
+ if cfg := parseCodexAgentConfig(req.Payload); cfg.Enabled() && opts.Alt == "responses/compact" {
79
+ return cliproxyexecutor.Response{}, statusErr{code: http.StatusBadRequest, msg: "codex agent_mode is not supported for /responses/compact"}
80
+ }
81
  if opts.Alt == "responses/compact" {
82
  return e.executeCompact(ctx, auth, req, opts)
83
  }
 
114
 
115
  requestedModel := payloadRequestedModel(opts, req.Model)
116
  body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
117
+ agentCfg := parseCodexAgentConfig(body, req.Payload)
118
  body, err = normalizeCodexRequestBody(body, baseModel, true)
119
  if err != nil {
120
  return resp, err
121
  }
122
 
123
+ var pass codexCompletedPassResult
124
+ switch agentCfg.Mode {
125
+ case codexAgentModePlannerReviewer:
126
+ pass, err = e.executeCodexPlannerReviewerPipeline(ctx, auth, apiKey, baseURL, from, req, body)
127
+ default:
128
+ url := strings.TrimSuffix(baseURL, "/") + "/responses"
129
+ pass, err = e.executeCodexCompletedHTTPPass(ctx, auth, apiKey, url, from, req, body, true)
 
 
 
 
 
 
 
130
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  if err != nil {
 
132
  return resp, err
133
  }
134
+ if pass.UsageOK {
135
+ reporter.publish(ctx, pass.Usage)
 
 
 
 
 
 
136
  }
137
 
138
+ reqBodyForTranslation := body
139
+ if len(pass.Request) > 0 {
140
+ reqBodyForTranslation = pass.Request
 
141
  }
142
+ var param any
143
+ out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, reqBodyForTranslation, pass.Completed, &param)
144
+ resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: pass.Headers}
145
+ return resp, nil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  }
147
 
148
  // executeCompact handles the /responses/compact endpoint
 
242
  if opts.Alt == "responses/compact" {
243
  return nil, statusErr{code: http.StatusBadRequest, msg: "streaming not supported for /responses/compact"}
244
  }
245
+ if cfg := parseCodexAgentConfig(req.Payload); cfg.Enabled() {
246
+ return nil, statusErr{code: http.StatusBadRequest, msg: "codex agent_mode is not supported for streaming requests"}
247
+ }
248
 
249
  return e.base.ExecuteStream(ctx, auth, req, opts)
250
  }
internal/runtime/executor/codex_provider.go CHANGED
@@ -140,6 +140,9 @@ func normalizeCodexRequestBody(body []byte, model string, stream bool) ([]byte,
140
  }
141
  }
142
  body, _ = sjson.DeleteBytes(body, "reasoning_effort")
 
 
 
143
 
144
  // Delete Codex-specific fields that shouldn't be sent
145
  body, _ = sjson.DeleteBytes(body, "previous_response_id")
@@ -179,3 +182,123 @@ func codexCompletedEventPayload(data []byte) ([]byte, bool) {
179
  }
180
  return payload, true
181
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
  }
142
  body, _ = sjson.DeleteBytes(body, "reasoning_effort")
143
+ body = applyCodexReasoningProfile(body)
144
+ body, _ = sjson.DeleteBytes(body, "_cliproxy")
145
+ body, _ = sjson.DeleteBytes(body, "agent_mode")
146
 
147
  // Delete Codex-specific fields that shouldn't be sent
148
  body, _ = sjson.DeleteBytes(body, "previous_response_id")
 
182
  }
183
  return payload, true
184
  }
185
+
186
+ // applyCodexReasoningProfile injects a structured analysis scaffold into `instructions`
187
+ // when callers request a local reasoning profile and reasoning is enabled.
188
+ //
189
+ // Local control fields (removed before upstream request):
190
+ // - `_cliproxy.reasoning_profile`: "deep", "deep_engineering"
191
+ // - `_cliproxy.reasoning_prompt`: custom instruction text to append
192
+ // - `_cliproxy.force_reasoning_profile`: bool (inject even if reasoning is disabled)
193
+ //
194
+ // This intentionally requests a visible rationale/analysis structure rather than hidden chain-of-thought.
195
+ func applyCodexReasoningProfile(body []byte) []byte {
196
+ if len(body) == 0 || !gjson.ValidBytes(body) {
197
+ return body
198
+ }
199
+
200
+ profile := strings.TrimSpace(gjson.GetBytes(body, "_cliproxy.reasoning_profile").String())
201
+ custom := strings.TrimSpace(gjson.GetBytes(body, "_cliproxy.reasoning_prompt").String())
202
+ force := gjson.GetBytes(body, "_cliproxy.force_reasoning_profile").Bool()
203
+
204
+ if profile == "" && custom == "" {
205
+ return body
206
+ }
207
+ if !force && !codexReasoningEnabled(body) {
208
+ body, _ = sjson.DeleteBytes(body, "_cliproxy")
209
+ return body
210
+ }
211
+
212
+ snippet := buildCodexReasoningProfilePrompt(profile, custom)
213
+ if strings.TrimSpace(snippet) == "" {
214
+ body, _ = sjson.DeleteBytes(body, "_cliproxy")
215
+ return body
216
+ }
217
+
218
+ currentInstructions := strings.TrimSpace(gjson.GetBytes(body, "instructions").String())
219
+ if currentInstructions == "" {
220
+ body, _ = sjson.SetBytes(body, "instructions", snippet)
221
+ } else {
222
+ body, _ = sjson.SetBytes(body, "instructions", currentInstructions+"\n\n"+snippet)
223
+ }
224
+
225
+ body, _ = sjson.DeleteBytes(body, "_cliproxy")
226
+ return body
227
+ }
228
+
229
+ func codexReasoningEnabled(body []byte) bool {
230
+ for _, path := range []string{"reasoning.effort", "reasoning_effort"} {
231
+ if effort := gjson.GetBytes(body, path); effort.Exists() {
232
+ v := strings.ToLower(strings.TrimSpace(effort.String()))
233
+ if v != "" && v != "none" {
234
+ return true
235
+ }
236
+ }
237
+ }
238
+ return false
239
+ }
240
+
241
+ func buildCodexReasoningProfilePrompt(profile, custom string) string {
242
+ var sections []string
243
+ switch strings.ToLower(strings.TrimSpace(profile)) {
244
+ case "", "none", "off", "disabled":
245
+ // no preset
246
+ case "deep", "deep_engineering", "engineering":
247
+ sections = append(sections, `Response profile:
248
+ - Prefer thoroughness over brevity.
249
+ - Use multi-lens analysis (user intent/cognitive load, technical tradeoffs/performance, accessibility, scalability/maintenance).
250
+ - Avoid shallow conclusions; justify decisions concretely.
251
+ - Provide a visible rationale summary instead of hidden internal reasoning.
252
+ - Structure the answer into:
253
+ 1. Architectural/Design Rationale
254
+ 2. Edge Cases and Failure Prevention
255
+ 3. Production-Ready Implementation`)
256
+ sections = append(sections, codexDeepEngineeringStandardsPrompt())
257
+ sections = append(sections, codexNormalResponseFormatPrompt())
258
+ default:
259
+ // Unknown preset names are ignored; custom text can still be applied.
260
+ }
261
+ if strings.TrimSpace(custom) != "" {
262
+ sections = append(sections, custom)
263
+ }
264
+ return strings.TrimSpace(strings.Join(sections, "\n\n"))
265
+ }
266
+
267
+ func codexDeepEngineeringStandardsPrompt() string {
268
+ return strings.TrimSpace(`Engineering depth policy:
269
+ - Override Brevity: Immediately suspend the "Zero Fluff" rule when rigor is required.
270
+ - Maximum Depth: Engage in exhaustive, deep-level reasoning before writing a single line when implementation is requested.
271
+ - Prohibition: Never use surface-level logic. If the reasoning feels easy, dig deeper until the logic is irrefutable.
272
+
273
+ Multi-Dimensional Analysis (apply when relevant):
274
+ - Architectural: Separation of concerns, modularity, dependency direction, coupling.
275
+ - Performance: Time/space complexity, memory layout, I/O costs, concurrency pitfalls, hot-path optimization.
276
+ - Reliability: Error handling strategy, edge cases, failure modes, defensive programming.
277
+ - Scalability: Will the design hold at 10x load/scope, and what is the long-term maintenance burden.
278
+ - Security: Input validation, injection vectors, privilege boundaries, secrets management.
279
+ - Ecosystem Fit: Prefer solutions native to the language/framework/community.
280
+
281
+ Coding standards (all languages):
282
+ - Library & Framework Discipline (critical): If a library/framework/engine is active in the project, use it. Do not rebuild utilities the ecosystem already provides, and do not introduce redundant overlapping dependencies. Exception: wrappers/extensions are fine when the underlying primitive stays project-native.
283
+ - Language-specific awareness:
284
+ - Python: Type hints, pathlib over os.path, f-strings, dataclasses/Pydantic when appropriate, async for I/O-bound work.
285
+ - Lua: Respect table-driven design, metatables over class emulation unless already used, 1-based indexing, and target runtime differences.
286
+ - JavaScript/TypeScript: Strict TypeScript where possible, framework conventions (React/Vue/Svelte), ESM over CJS.
287
+ - Systems (Rust/Go/C/C++): Ownership/lifetime clarity, avoid unnecessary hot-path allocations, respect the language concurrency model.
288
+ - Shell/Bash: Prefer POSIX when portability matters, use set -euo pipefail, quote variables.
289
+ - SQL: Always parameterize queries unless a documented exception exists.
290
+ - Universal standards:
291
+ - Error Handling: Never swallow errors silently; use the idiomatic error model.
292
+ - Naming: Descriptive and consistent, following language/project conventions.
293
+ - Structure: Logical module organization; no god files; avoid oversized functions.
294
+ - Comments: Explain why, not what.`)
295
+ }
296
+
297
+ func codexNormalResponseFormatPrompt() string {
298
+ return strings.TrimSpace(`Response format (normal mode):
299
+ 1. Rationale: 1-2 sentences on the approach and why.
300
+ 2. The Code: production-ready code using project-native libraries/frameworks.
301
+ 3. Edge Cases: what can fail and how the implementation defends against it.
302
+
303
+ When describing reasoning, provide a visible rationale summary. Do not expose hidden chain-of-thought.`)
304
+ }
internal/runtime/executor/codex_provider_test.go CHANGED
@@ -4,6 +4,7 @@ import (
4
  "context"
5
  "net/http"
6
  "net/http/httptest"
 
7
  "testing"
8
 
9
  "github.com/gin-gonic/gin"
@@ -54,6 +55,92 @@ func TestNormalizeCodexRequestBody_MapsReasoningEffortAlias(t *testing.T) {
54
  }
55
  }
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  func TestCodexProviderApplyHeaders_ForwardsCodexHintHeaders(t *testing.T) {
58
  gin.SetMode(gin.TestMode)
59
  rec := httptest.NewRecorder()
@@ -121,9 +208,25 @@ func TestCodexCompletedEventPayload(t *testing.T) {
121
  }
122
  }
123
 
 
 
 
 
 
 
 
124
  func contextWithGinForTest(c *gin.Context) context.Context {
125
  if c == nil {
126
  return context.Background()
127
  }
128
  return context.WithValue(context.Background(), "gin", c)
129
  }
 
 
 
 
 
 
 
 
 
 
4
  "context"
5
  "net/http"
6
  "net/http/httptest"
7
+ "strings"
8
  "testing"
9
 
10
  "github.com/gin-gonic/gin"
 
55
  }
56
  }
57
 
58
+ func TestNormalizeCodexRequestBody_AutoInjectsReasoningProfile(t *testing.T) {
59
+ input := []byte(`{
60
+ "reasoning_effort":"high",
61
+ "instructions":"Base instructions",
62
+ "_cliproxy":{"reasoning_profile":"deep_engineering"}
63
+ }`)
64
+
65
+ got, err := normalizeCodexRequestBody(input, "gpt-5", true)
66
+ if err != nil {
67
+ t.Fatalf("normalizeCodexRequestBody returned error: %v", err)
68
+ }
69
+
70
+ instructions := gjson.GetBytes(got, "instructions").String()
71
+ if instructions == "" {
72
+ t.Fatal("instructions should not be empty")
73
+ }
74
+ if instructions == "Base instructions" {
75
+ t.Fatalf("expected reasoning profile to append instructions")
76
+ }
77
+ if !containsAll(instructions, "Base instructions", "Architectural/Design Rationale", "Edge Cases and Failure Prevention", "Production-Ready Implementation") {
78
+ t.Fatalf("instructions missing expected scaffold: %q", instructions)
79
+ }
80
+ if !containsAll(instructions, "Override Brevity", "Library & Framework Discipline", "Response format (normal mode)") {
81
+ t.Fatalf("instructions missing deep engineering standards block: %q", instructions)
82
+ }
83
+ if gjson.GetBytes(got, "_cliproxy").Exists() {
84
+ t.Fatalf("expected local _cliproxy controls to be stripped")
85
+ }
86
+ }
87
+
88
+ func TestNormalizeCodexRequestBody_DoesNotInjectReasoningProfileWhenThinkingDisabled(t *testing.T) {
89
+ input := []byte(`{
90
+ "reasoning_effort":"none",
91
+ "instructions":"Base instructions",
92
+ "_cliproxy":{"reasoning_profile":"deep_engineering"}
93
+ }`)
94
+
95
+ got, err := normalizeCodexRequestBody(input, "gpt-5", true)
96
+ if err != nil {
97
+ t.Fatalf("normalizeCodexRequestBody returned error: %v", err)
98
+ }
99
+
100
+ if instructions := gjson.GetBytes(got, "instructions").String(); instructions != "Base instructions" {
101
+ t.Fatalf("instructions = %q, want %q", instructions, "Base instructions")
102
+ }
103
+ if gjson.GetBytes(got, "_cliproxy").Exists() {
104
+ t.Fatalf("expected local _cliproxy controls to be stripped")
105
+ }
106
+ }
107
+
108
+ func TestNormalizeCodexRequestBody_IncludesCustomReasoningPrompt(t *testing.T) {
109
+ input := []byte(`{
110
+ "reasoning":{"effort":"medium"},
111
+ "_cliproxy":{"reasoning_prompt":"Use strict WCAG AAA and include an edge-case checklist."}
112
+ }`)
113
+
114
+ got, err := normalizeCodexRequestBody(input, "gpt-5", true)
115
+ if err != nil {
116
+ t.Fatalf("normalizeCodexRequestBody returned error: %v", err)
117
+ }
118
+
119
+ instructions := gjson.GetBytes(got, "instructions").String()
120
+ if !containsAll(instructions, "strict WCAG AAA", "edge-case checklist") {
121
+ t.Fatalf("custom reasoning prompt not injected: %q", instructions)
122
+ }
123
+ }
124
+
125
+ func TestNormalizeCodexRequestBody_StripsAgentModeControls(t *testing.T) {
126
+ input := []byte(`{
127
+ "agent_mode":"planner-reviewer",
128
+ "_cliproxy":{"agent_mode":"planner-reviewer"},
129
+ "instructions":"hello"
130
+ }`)
131
+
132
+ got, err := normalizeCodexRequestBody(input, "gpt-5", true)
133
+ if err != nil {
134
+ t.Fatalf("normalizeCodexRequestBody returned error: %v", err)
135
+ }
136
+ if gjson.GetBytes(got, "agent_mode").Exists() {
137
+ t.Fatalf("expected agent_mode to be stripped")
138
+ }
139
+ if gjson.GetBytes(got, "_cliproxy").Exists() {
140
+ t.Fatalf("expected _cliproxy to be stripped")
141
+ }
142
+ }
143
+
144
  func TestCodexProviderApplyHeaders_ForwardsCodexHintHeaders(t *testing.T) {
145
  gin.SetMode(gin.TestMode)
146
  rec := httptest.NewRecorder()
 
208
  }
209
  }
210
 
211
+ func TestBuildCodexReasoningProfilePrompt_DeepEngineeringIncludesLanguageStandards(t *testing.T) {
212
+ got := buildCodexReasoningProfilePrompt("deep_engineering", "")
213
+ if !containsAll(got, "Language-specific awareness", "Shell/Bash", "SQL: Always parameterize queries") {
214
+ t.Fatalf("deep engineering prompt missing language-specific standards: %q", got)
215
+ }
216
+ }
217
+
218
  func contextWithGinForTest(c *gin.Context) context.Context {
219
  if c == nil {
220
  return context.Background()
221
  }
222
  return context.WithValue(context.Background(), "gin", c)
223
  }
224
+
225
+ func containsAll(s string, subs ...string) bool {
226
+ for _, sub := range subs {
227
+ if !strings.Contains(s, sub) {
228
+ return false
229
+ }
230
+ }
231
+ return true
232
+ }
internal/runtime/executor/codex_websockets_executor_refactored.go CHANGED
@@ -317,6 +317,11 @@ func (e *CodexWebsocketsExecutorRefactored) Execute(ctx context.Context, auth *c
317
 
318
  requestedModel := payloadRequestedModel(opts, req.Model)
319
  body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
 
 
 
 
 
320
  body, _ = sjson.SetBytes(body, "model", baseModel)
321
  body, _ = sjson.SetBytes(body, "stream", true)
322
  body, _ = sjson.DeleteBytes(body, "previous_response_id")
@@ -445,6 +450,9 @@ func (e *CodexWebsocketsExecutorRefactored) ExecuteStream(ctx context.Context, a
445
  if opts.Alt == "responses/compact" {
446
  return nil, statusErr{code: http.StatusBadRequest, msg: "streaming not supported for /responses/compact"}
447
  }
 
 
 
448
 
449
  baseModel := thinking.ParseSuffix(req.Model).ModelName
450
  apiKey, baseURL := codexCreds(auth)
@@ -466,6 +474,7 @@ func (e *CodexWebsocketsExecutorRefactored) ExecuteStream(ctx context.Context, a
466
 
467
  requestedModel := payloadRequestedModel(opts, req.Model)
468
  body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel)
 
469
 
470
  body, preflight, err := e.prepareCodexWebsocketPreflight(ctx, auth, req, from, apiKey, baseURL, body)
471
  if err != nil {
 
317
 
318
  requestedModel := payloadRequestedModel(opts, req.Model)
319
  body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
320
+ if cfg := parseCodexAgentConfig(body, req.Payload); cfg.Enabled() {
321
+ logWithRequestID(ctx).Debugf("codex websockets executor: agent_mode=%s requested; falling back to HTTP codex executor for non-stream execution", cfg.Mode)
322
+ return e.CodexExecutor.Execute(ctx, auth, req, opts)
323
+ }
324
+ body = applyCodexReasoningProfile(body)
325
  body, _ = sjson.SetBytes(body, "model", baseModel)
326
  body, _ = sjson.SetBytes(body, "stream", true)
327
  body, _ = sjson.DeleteBytes(body, "previous_response_id")
 
450
  if opts.Alt == "responses/compact" {
451
  return nil, statusErr{code: http.StatusBadRequest, msg: "streaming not supported for /responses/compact"}
452
  }
453
+ if cfg := parseCodexAgentConfig(req.Payload); cfg.Enabled() {
454
+ return nil, statusErr{code: http.StatusBadRequest, msg: "codex agent_mode is not supported for streaming requests"}
455
+ }
456
 
457
  baseModel := thinking.ParseSuffix(req.Model).ModelName
458
  apiKey, baseURL := codexCreds(auth)
 
474
 
475
  requestedModel := payloadRequestedModel(opts, req.Model)
476
  body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel)
477
+ body = applyCodexReasoningProfile(body)
478
 
479
  body, preflight, err := e.prepareCodexWebsocketPreflight(ctx, auth, req, from, apiKey, baseURL, body)
480
  if err != nil {