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 +488 -0
- internal/runtime/executor/codex_agent_pipeline_test.go +130 -0
- internal/runtime/executor/codex_executor_refactored.go +23 -66
- internal/runtime/executor/codex_provider.go +123 -0
- internal/runtime/executor/codex_provider_test.go +103 -0
- internal/runtime/executor/codex_websockets_executor_refactored.go +9 -0
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 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 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 |
-
|
| 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 |
-
|
| 163 |
-
if
|
| 164 |
-
|
| 165 |
-
return resp, err
|
| 166 |
}
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 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, ¶m)
|
| 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, ¶m)
|
| 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 {
|