Add Codex response defaults to OAuth settings

Expose reasoning effort, reasoning summary, and answer verbosity in the Codex OAuth provider card, defaulting effort to high.\n\nNormalize provider settings and apply them to Responses requests without overriding explicit request values.

Alessandro committed Jul 28, 2026 at 13:19 UTC 4884e26a15fdca3af28e6d40b7a74f0960baa9d7
9 files changed +186 -4
plugins/_oauth/AGENTS.md
+1
@@ -45,6 +45,7 @@
45 - Local proxy routes must remain loopback or token protected and must not add broad CORS access.
46 - Codex Responses proxy requests must include Codex client metadata and compatibility headers such as `client_metadata`, `x-codex-installation-id`, `originator`, `session-id`, and `thread-id`, and must forward `input` as a list for upstream Codex compatibility.
47 - Codex Responses proxy requests must translate the legacy top-level `reasoning_effort` field to `reasoning.effort`; an explicit native `reasoning` field takes precedence.
48 +- Codex Responses proxy defaults for reasoning effort, reasoning summary, and text verbosity come from the `codex` plugin config; explicit native request values take precedence.
49
50 ## Work Guidance
51
plugins/_oauth/README.md
+1
@@ -18,6 +18,7 @@ OAuth-backed model providers do not require users to enter API keys. Agent Zero
18 - Writes Codex-compatible credentials to an Agent Zero-owned `auth.json` file.
19 - Refreshes local tokens when needed.
20 - Exposes the local OpenAI-compatible wrapper at `/oauth/codex/v1`.
21 +- Lets users choose default reasoning effort, visible reasoning summaries, and answer verbosity while preserving explicit per-request settings.
22
23 ### GitHub Copilot (`github_copilot_oauth`)
24
plugins/_oauth/default_config.yaml
+5
@@ -25,6 +25,11 @@ codex:
25 models: []
26 request_timeout_seconds: 120
27
28 + # Defaults for Codex Responses requests. Explicit model/request values win.
29 + reasoning_effort: "high"
30 + reasoning_summary: "auto"
31 + text_verbosity: "medium"
32 +
33 # The OpenAI-compatible wrapper is mounted at /oauth/codex/v1.
34 # It is loopback-only by default and does not emit CORS headers.
35 proxy_base_path: "/oauth/codex"
plugins/_oauth/helpers/codex.py
+31 -2
@@ -666,9 +666,38 @@ def fetch_models() -> list[str]:
666
667 def prepare_responses_body(body: dict[str, Any], *, force_stream: bool) -> dict[str, Any]:
668 normalized = dict(body)
669 + settings = codex_config()
670 reasoning_effort = normalized.pop("reasoning_effort", None)
670 - if reasoning_effort is not None and "reasoning" not in normalized:
671 - normalized["reasoning"] = {"effort": reasoning_effort}
671 + reasoning = normalized.get("reasoning")
672 + if isinstance(reasoning, dict):
673 + reasoning = dict(reasoning)
674 + elif "reasoning" not in normalized:
675 + reasoning = {}
676 + effort = reasoning_effort or settings.get("reasoning_effort", "high")
677 + if effort != "default":
678 + reasoning["effort"] = effort
679 + else:
680 + reasoning = None
681 + if reasoning is not None:
682 + summary = settings.get("reasoning_summary", "auto")
683 + if summary != "off":
684 + reasoning.setdefault("summary", summary)
685 + if reasoning:
686 + normalized["reasoning"] = reasoning
687 + else:
688 + normalized.pop("reasoning", None)
689 +
690 + verbosity = normalized.pop("verbosity", None) or settings.get(
691 + "text_verbosity", "medium"
692 + )
693 + text_config = normalized.get("text")
694 + if isinstance(text_config, dict):
695 + text_config = dict(text_config)
696 + if verbosity != "default":
697 + text_config.setdefault("verbosity", verbosity)
698 + normalized["text"] = text_config
699 + elif "text" not in normalized and verbosity != "default":
700 + normalized["text"] = {"verbosity": verbosity}
701 input_value = normalized.get("input")
702 if isinstance(input_value, str):
703 normalized["input"] = (
plugins/_oauth/helpers/config.py
+17
@@ -19,6 +19,9 @@ DEFAULT_CODEX_SCOPES = [
19 "api.connectors.read",
20 "api.connectors.invoke",
21 ]
22 +CODEX_REASONING_EFFORTS = {"default", "minimal", "low", "medium", "high", "xhigh"}
23 +CODEX_REASONING_SUMMARIES = {"off", "auto", "concise", "detailed"}
24 +CODEX_TEXT_VERBOSITIES = {"default", "low", "medium", "high"}
25 DEFAULT_GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai"
26 DEFAULT_GEMINI_API_SCOPES = [
27 "openid",
@@ -52,6 +55,15 @@ def codex_config(config: dict[str, Any] | None = None) -> dict[str, Any]:
55 "codex_version": _as_str(raw.get("codex_version")),
56 "models": _as_str_list(raw.get("models")),
57 "request_timeout_seconds": _as_int(raw.get("request_timeout_seconds"), 120),
58 + "reasoning_effort": _as_choice(
59 + raw.get("reasoning_effort"), CODEX_REASONING_EFFORTS, "high"
60 + ),
61 + "reasoning_summary": _as_choice(
62 + raw.get("reasoning_summary"), CODEX_REASONING_SUMMARIES, "auto"
63 + ),
64 + "text_verbosity": _as_choice(
65 + raw.get("text_verbosity"), CODEX_TEXT_VERBOSITIES, "medium"
66 + ),
67 "proxy_base_path": _normalize_base_path(raw.get("proxy_base_path"), "/oauth/codex"),
68 "callback_path": _normalize_base_path(raw.get("callback_path"), "/auth/callback"),
69 "require_proxy_token": _as_bool(raw.get("require_proxy_token"), False),
@@ -104,6 +116,11 @@ def _as_bool(value: Any, default: bool) -> bool:
116 return default
117
118
119 +def _as_choice(value: Any, choices: set[str], default: str) -> str:
120 + normalized = _as_str(value).lower()
121 + return normalized if normalized in choices else default
122 +
123 +
124 def _as_str_list(value: Any) -> list[str]:
125 if value is None:
126 return []
plugins/_oauth/webui/config.html
+68
@@ -124,6 +124,43 @@
124 </label>
125 </div>
126
127 + <div class="oauth-response-defaults" x-show="card.provider_id === 'codex_oauth'">
128 + <div class="oauth-response-defaults-head">
129 + <strong>Codex response defaults</strong>
130 + <span>Applied when an individual model request does not override them.</span>
131 + </div>
132 + <label class="oauth-provider-input">
133 + <span>Reasoning effort</span>
134 + <select x-model="$store.oauthConfig.codex().reasoning_effort" name="codex_reasoning_effort">
135 + <option value="default">Model default</option>
136 + <option value="minimal">Minimal</option>
137 + <option value="low">Low</option>
138 + <option value="medium">Medium</option>
139 + <option value="high">High</option>
140 + <option value="xhigh">Extra high</option>
141 + </select>
142 + </label>
143 + <label class="oauth-provider-input">
144 + <span>Reasoning summary</span>
145 + <select x-model="$store.oauthConfig.codex().reasoning_summary" name="codex_reasoning_summary">
146 + <option value="auto">Auto</option>
147 + <option value="concise">Concise</option>
148 + <option value="detailed">Detailed</option>
149 + <option value="off">Off</option>
150 + </select>
151 + </label>
152 + <label class="oauth-provider-input">
153 + <span>Answer verbosity</span>
154 + <select x-model="$store.oauthConfig.codex().text_verbosity" name="codex_text_verbosity">
155 + <option value="default">Model default</option>
156 + <option value="low">Low</option>
157 + <option value="medium">Medium</option>
158 + <option value="high">High</option>
159 + </select>
160 + </label>
161 + <p class="oauth-provider-note">Effort controls reasoning depth, summaries appear while the agent works, and verbosity shapes the final answer.</p>
162 + </div>
163 +
164 <div class="oauth-device" x-show="$store.oauthConfig.providerDevice(card.provider_id)?.user_code">
165 <span>Enter this code</span>
166 <strong x-text="$store.oauthConfig.providerDevice(card.provider_id)?.user_code"></strong>
@@ -588,6 +625,35 @@
625 gap: 8px;
626 }
627
628 + .oauth-response-defaults {
629 + display: grid;
630 + grid-template-columns: repeat(3, minmax(0, 1fr));
631 + gap: 10px;
632 + padding: 12px;
633 + border: 1px solid color-mix(in srgb, var(--color-border) 74%, transparent);
634 + border-radius: 8px;
635 + background: color-mix(in srgb, var(--color-panel) 82%, transparent);
636 + }
637 +
638 + .oauth-response-defaults-head,
639 + .oauth-response-defaults .oauth-provider-note {
640 + grid-column: 1 / -1;
641 + }
642 +
643 + .oauth-response-defaults-head {
644 + display: grid;
645 + gap: 2px;
646 + }
647 +
648 + .oauth-response-defaults-head strong {
649 + font-size: 0.84rem;
650 + }
651 +
652 + .oauth-response-defaults-head span {
653 + color: var(--color-text-secondary);
654 + font-size: 0.76rem;
655 + }
656 +
657 .oauth-manual-callback {
658 grid-template-columns: minmax(0, 1fr) auto auto;
659 align-items: center;
@@ -611,6 +677,7 @@
677 }
678
679 .oauth-provider-input input,
680 + .oauth-provider-input select,
681 .oauth-manual-callback input {
682 width: 100%;
683 min-width: 0;
@@ -1162,6 +1229,7 @@
1229
1230 @media (max-width: 720px) {
1231 .oauth-device,
1232 + .oauth-response-defaults,
1233 .oauth-provider-row-detail,
1234 .oauth-provider-usage,
1235 .oauth-status-row,
plugins/_oauth/webui/oauth-config-store.js
+3
@@ -51,6 +51,9 @@ function ensureConfig(config) {
51 codex.proxy_token = String(codex.proxy_token || "");
52 codex.codex_version = String(codex.codex_version || "");
53 codex.models = Array.isArray(codex.models) ? codex.models : [];
54 + codex.reasoning_effort = String(codex.reasoning_effort || "high");
55 + codex.reasoning_summary = String(codex.reasoning_summary || "auto");
56 + codex.text_verbosity = String(codex.text_verbosity || "medium");
57
58 config.gemini_api = config.gemini_api && typeof config.gemini_api === "object" ? config.gemini_api : {};
59 const geminiApi = config.gemini_api;
tests/test_oauth_codex.py
+56 -2
@@ -204,16 +204,21 @@ def test_prepare_responses_body_adds_codex_client_metadata(monkeypatch):
204 }
205 assert body["input"] == [{"role": "user", "content": "hello"}]
206 assert body["stream"] is True
207 + assert body["reasoning"] == {"effort": "medium", "summary": "auto"}
208 assert body["include"] == ["output_text", "reasoning.encrypted_content"]
209
210
211 @pytest.mark.parametrize(
212 ("request_reasoning", "expected"),
213 [
213 - ({"reasoning_effort": "xhigh"}, {"effort": "xhigh"}),
214 + ({"reasoning_effort": "xhigh"}, {"effort": "xhigh", "summary": "auto"}),
215 (
216 {"reasoning": {"effort": "medium"}, "reasoning_effort": "xhigh"},
216 - {"effort": "medium"},
217 + {"effort": "medium", "summary": "auto"},
218 + ),
219 + (
220 + {"reasoning": {"effort": "medium", "summary": "detailed"}},
221 + {"effort": "medium", "summary": "detailed"},
222 ),
223 ],
224 )
@@ -231,6 +236,55 @@ def test_prepare_responses_body_normalizes_reasoning_effort(
236 assert "reasoning_effort" not in body
237
238
239 +def test_prepare_responses_body_applies_codex_response_defaults(monkeypatch):
240 + monkeypatch.setattr(codex, "build_client_metadata", lambda: {})
241 + monkeypatch.setattr(
242 + codex,
243 + "codex_config",
244 + lambda: {
245 + "reasoning_effort": "high",
246 + "reasoning_summary": "concise",
247 + "text_verbosity": "low",
248 + },
249 + )
250 +
251 + defaults = codex.prepare_responses_body(
252 + {"model": "gpt-5.5", "input": "hello"}, force_stream=True
253 + )
254 + overrides = codex.prepare_responses_body(
255 + {
256 + "model": "gpt-5.5",
257 + "input": "hello",
258 + "reasoning": {"effort": "low", "summary": "detailed"},
259 + "text": {"verbosity": "high"},
260 + },
261 + force_stream=True,
262 + )
263 +
264 + assert defaults["reasoning"] == {"effort": "high", "summary": "concise"}
265 + assert defaults["text"] == {"verbosity": "low"}
266 + assert overrides["reasoning"] == {"effort": "low", "summary": "detailed"}
267 + assert overrides["text"] == {"verbosity": "high"}
268 +
269 +
270 +def test_codex_config_validates_response_defaults():
271 + from plugins._oauth.helpers.config import codex_config
272 +
273 + config = codex_config(
274 + {
275 + "codex": {
276 + "reasoning_effort": "invalid",
277 + "reasoning_summary": "DETAILED",
278 + "text_verbosity": "low",
279 + }
280 + }
281 + )
282 +
283 + assert config["reasoning_effort"] == "high"
284 + assert config["reasoning_summary"] == "detailed"
285 + assert config["text_verbosity"] == "low"
286 +
287 +
288 def test_prepare_responses_body_sends_empty_continuation_input_as_list(monkeypatch):
289 monkeypatch.setattr(codex, "build_client_metadata", lambda: {})
290
tests/test_oauth_static.py
+4
@@ -61,6 +61,10 @@ def test_oauth_settings_exposes_provider_specific_controls_and_generic_copy():
61 assert "supports_quota_project" in config_html + store_js
62 assert "OAuth client ID" in config_html
63 assert "quota_project_id" in config_html + store_js
64 + assert "Codex response defaults" in config_html
65 + assert "$store.oauthConfig.codex().reasoning_effort" in config_html
66 + assert "$store.oauthConfig.codex().reasoning_summary" in config_html
67 + assert "$store.oauthConfig.codex().text_verbosity" in config_html
68 assert "providerDetailOpen(card.provider_id)" in config_html + store_js
69 assert "providerDevice(card.provider_id)?.user_code" in config_html
70 assert "submitManualCallback(card.provider_id)" in config_html