Skip to content

Commit 5799c3b

Browse files
committed
feat(effort-cap): hard reasoning-effort ceiling for main and sub-agent turns
Add effortCap and subagentEffortCap config options that enforce a per-request hard ceiling on reasoning effort at the proxy layer. This closes the gap where codex-rs inherits the parent's ultra-tier default on bare spawns regardless of injection-side advisory effort. - src/server/effort-policy.ts: sub-agent request detection from codex-rs spawn markers (x-openai-subagent, x-codex-turn-metadata), cap resolution against the model's supported effort ladder, dual-shape rewrite (parsed options + raw body) that runs before the native effort clamp - src/server/responses.ts: effort cap enforcement hook before mock-max clamp - src/server/management-api.ts: GET/PUT /api/effort-caps endpoints - src/types.ts: effortCap, subagentEffortCap on OcxConfig - src/reasoning-effort.ts: codexEffortRank helper - src/adapters/openai-responses.ts: forward x-openai-subagent header - gui: dashboard effort cap selectors with i18n (en/de/ko/zh) - docs: configuration reference (en/ko/zh) - tests: 32 tests covering classification, resolution, rewrite, and API
1 parent 755fac5 commit 5799c3b

15 files changed

Lines changed: 646 additions & 0 deletions

File tree

docs-site/src/content/docs/ko/reference/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ opencodex는 `~/.opencodex/config.json`에서 설정을 읽습니다. `ocx init`
2020
| `subagentModels?` | `string[]` | `gpt-5.5`, GPT-5.6 3종, `gpt-5.4-mini` | Codex 서브에이전트 선택기 앞쪽에 표시할 네이티브 slug 또는 `provider/model` id. 최대 5개이며, 명시적인 빈 배열도 그대로 보존합니다. v2 위임 안내에는 사용 가능한 모델 로스터로도 주입되며, 각 항목이 카탈로그에 광고하는 effort 사다리가 함께 표기됩니다. |
2121
| `injectionModel?` | `string` || 주입되는 multi-agent 안내(v2 표면)에 들어갈 네이티브/라우팅 모델. 위임 안내에서 이 모델을 `fork_turns: "none"`과 함께 `spawn_agent`에 넘기게 합니다. |
2222
| `injectionEffort?` | `string` || 선호하는 `spawn_agent` reasoning effort(`low`부터 `ultra`). `injectionModel`과 함께 쓸 때만 의미가 있습니다. |
23+
| `effortCap?` | `string` || 프록시를 거치는 모든 턴(메인 에이전트와 서브에이전트)의 reasoning effort에 요청별로 적용하는 강제 상한입니다. `low`부터 `ultra`까지 허용하며 값을 높이지 않고 낮추기만 합니다. 상한 이하에서 모델이 지원하는 가장 높은 단계로 내립니다. 모델이 effort 제어를 노출하지 않거나 상한 이하에 지원 단계가 없으면 effort 필드를 제거하고 프로바이더 기본값을 적용합니다. `max``ultra`도 허용하지만 더 낮은 rank 상한을 만들지는 않습니다(클라이언트가 `ultra``max`로 변환하므로 요청은 `low`부터 `max`로 들어옵니다). 단, 알려진 모델 effort 사다리에 따라 단계가 내려가거나 필드가 제거될 수 있습니다. 대시보드 선택기는 `low`부터 `xhigh`까지 제공합니다. `GET /api/effort-caps``PUT /api/effort-caps`로 관리합니다. |
24+
| `subagentEffortCap?` | `string` | — | codex-rs가 생성한 자식 요청에 붙이는 표식, 즉 `x-openai-subagent` 헤더나 `x-codex-turn-metadata`의 `subagent_kind`로 식별된 서브에이전트 턴에만 같은 강제 상한을 적용합니다. `low`부터 `ultra`까지 허용하며 두 상한이 모두 설정되면 더 낮은 값이 적용되고, 값을 높이지 않고 낮추기만 합니다. 상한 이하에서 모델이 지원하는 가장 높은 단계로 내립니다. 모델이 effort 제어를 노출하지 않거나 상한 이하에 지원 단계가 없으면 effort 필드를 제거하고 프로바이더 기본값을 적용합니다. `max`와 `ultra`도 허용하지만 더 낮은 rank 상한을 만들지는 않습니다(클라이언트가 `ultra`를 `max`로 변환하므로 요청은 `low`부터 `max`로 들어옵니다). 단, 알려진 모델 effort 사다리에 따라 단계가 내려가거나 필드가 제거될 수 있습니다. 대시보드 선택기는 `low`부터 `xhigh`까지 제공합니다. `GET /api/effort-caps`와 `PUT /api/effort-caps`로 관리합니다. |
2325
| `injectionPrompt?` | `string` || 주입되는 v2 안내 본문을 통째로 교체하는 커스텀 텍스트. `{{model}}`, `{{effort}}`, `{{roster}}` 플레이스홀더가 치환되며 발화 조건은 그대로입니다. `PUT /api/injection-model``prompt` 키로도 설정할 수 있습니다. |
2426
| `disabledModels?` | `string[]` || Codex에서 숨길 모델. 라우팅된 `provider/model` id는 카탈로그와 `/v1/models`에서 제외합니다. `gpt-5.4` 같은 일반 네이티브 GPT slug는 카탈로그 항목을 `visibility: "hide"`로 바꾸고 일반 `/v1/models` 목록에서 뺍니다. 대시보드 Models 페이지에서 모델별로 전환할 수 있습니다. |
2527
| `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | 3단계 multi-agent surface override. `"v1"`은 업스트림 pin보다 우선해 모든 모델을 v1로, `"default"`는 업스트림 model pin(sol/terra=v2, luna=v1)을 따르고, `"v2"`는 모두 v2로 강제합니다. 대시보드 Models 페이지나 `ocx v2 mode`에서 설정합니다. |

docs-site/src/content/docs/reference/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ console warning, and starts with defaults. Missing files also fall back to a def
2121
| `subagentModels?` | `string[]` | `gpt-5.5`, GPT-5.6 trio, `gpt-5.4-mini` | Up to 5 native slugs or `provider/model` ids featured first in Codex's subagent picker. Also injected into v2 delegation guidance as the available-model roster, annotated with the effort ladder each entry advertises in the catalog. An explicit empty list is preserved. |
2222
| `injectionModel?` | `string` || Preferred native or routed model named in the injected multi-agent guidance (v2 surface); delegation is told to pass this exact model to `spawn_agent` with `fork_turns: "none"`. |
2323
| `injectionEffort?` | `string` || Preferred `spawn_agent` reasoning effort (`low` through `ultra`). Only meaningful with `injectionModel`. |
24+
| `effortCap?` | `string` || Hard per-request ceiling for the reasoning effort of every proxied turn (main agent and sub-agents). Accepts `low` through `ultra`; caps only lower, never raise. Snaps down to the highest supported rung at or below the cap. If the model exposes no effort control, or no supported rung fits under the cap, the effort field is removed and the provider default applies. `max` and `ultra` are accepted but do not impose a lower rank ceiling (requests arrive as `low` through `max` after the client's `ultra``max` conversion), though known model ladders may still cause snap-down or strip. The Dashboard picker offers `low` through `xhigh`. Managed via `GET /api/effort-caps` and `PUT /api/effort-caps`. |
25+
| `subagentEffortCap?` | `string` || The same hard ceiling, applied only to sub-agent turns identified by codex-rs spawned-child markers: the `x-openai-subagent` header or `subagent_kind` in `x-codex-turn-metadata`. Accepts `low` through `ultra`; when both caps are set, the lower one wins, and caps only lower, never raise. Snaps down to the highest supported rung at or below the cap. If the model exposes no effort control, or no supported rung fits under the cap, the effort field is removed and the provider default applies. `max` and `ultra` are accepted but do not impose a lower rank ceiling (requests arrive as `low` through `max` after the client's `ultra``max` conversion), though known model ladders may still cause snap-down or strip. The Dashboard picker offers `low` through `xhigh`. Managed via `GET /api/effort-caps` and `PUT /api/effort-caps`. |
2426
| `injectionPrompt?` | `string` || Custom override for the injected v2 guidance body. Replaces the built-in text; `{{model}}`, `{{effort}}`, and `{{roster}}` placeholders are substituted. Firing gates are unchanged. Settable via `PUT /api/injection-model` (`prompt` key). |
2527
| `disabledModels?` | `string[]` || Models hidden from Codex. Routed `provider/model` ids are excluded from the catalog and `/v1/models`; bare native GPT slugs (e.g. `gpt-5.4`) flip their catalog entry to `visibility: "hide"` and drop from the bare `/v1/models` list. Toggleable per model from the dashboard Models page. |
2628
| `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | 3-state multi-agent surface override. `"v1"` forces all models to the v1 surface (overrides upstream pins); `"default"` respects upstream model pins (sol/terra=v2, luna=v1); `"v2"` forces all models to v2. Settable from the dashboard Models page or `ocx v2 mode`. |

docs-site/src/content/docs/zh-cn/reference/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ opencodex 使用 `~/.opencodex/config.json` 配置。`ocx init` 和仪表盘会
2020
| `subagentModels?` | `string[]` | `gpt-5.5`、三款 GPT-5.6、`gpt-5.4-mini` | 最多 5 个原生 slug 或 `provider/model` id,优先显示在 Codex subagent picker 中。显式空数组会被保留。 也会作为可用模型清单注入 v2 委派指南,并标注各模型在目录中公布的 effort 阶梯。 |
2121
| `injectionModel?` | `string` || 注入 multi-agent 指南(v2 界面)的首选原生或路由模型;委派指南会要求把该模型连同 `fork_turns: "none"` 一起传给 `spawn_agent`|
2222
| `injectionEffort?` | `string` || 首选 `spawn_agent` reasoning effort(`low``ultra`)。只有与 `injectionModel` 一起使用才有意义。 |
23+
| `effortCap?` | `string` || 为经过代理的每个轮次(主代理和子代理)的 reasoning effort 设置逐请求硬上限。接受 `low``ultra`;只会降低 effort,绝不会提高。会降至不高于上限的最高受支持档位。若模型不提供 effort 控制,或上限之下没有可用档位,则移除 effort 字段并采用 provider 默认值。`max``ultra` 均可使用,但不会形成更低的等级上限(客户端会将 `ultra` 转换为 `max`,因此请求以 `low``max` 的范围到达);不过,已知的模型 effort 阶梯仍可能触发降档或移除字段。仪表盘选择器提供 `low``xhigh`。通过 `GET /api/effort-caps``PUT /api/effort-caps` 管理。 |
24+
| `subagentEffortCap?` | `string` || 同样的硬上限,但只用于带有 codex-rs 派生子级标记的子代理轮次:`x-openai-subagent` 请求头,或 `x-codex-turn-metadata` 中的 `subagent_kind`。接受 `low``ultra`;两个上限同时设置时取较低者,且只会降低 effort,绝不会提高。会降至不高于上限的最高受支持档位。若模型不提供 effort 控制,或上限之下没有可用档位,则移除 effort 字段并采用 provider 默认值。`max``ultra` 均可使用,但不会形成更低的等级上限(客户端会将 `ultra` 转换为 `max`,因此请求以 `low``max` 的范围到达);不过,已知的模型 effort 阶梯仍可能触发降档或移除字段。仪表盘选择器提供 `low``xhigh`。通过 `GET /api/effort-caps``PUT /api/effort-caps` 管理。 |
2325
| `injectionPrompt?` | `string` || 整体替换注入的 v2 指南正文的自定义文本。`{{model}}``{{effort}}``{{roster}}` 占位符会被替换,触发条件保持不变。也可通过 `PUT /api/injection-model``prompt` 键设置。 |
2426
| `disabledModels?` | `string[]` || 从 Codex 隐藏的模型。路由 `provider/model` id 会从目录和 `/v1/models` 排除;bare 原生 GPT slug(如 `gpt-5.4`)的目录条目会改成 `visibility: "hide"`,并从 bare `/v1/models` 列表移除。可在仪表盘 Models 页面按模型切换。 |
2527
| `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | 三态 multi-agent surface override。`"v1"` 覆盖 upstream pin,强制全部模型使用 v1;`"default"` 遵循 upstream model pin(sol/terra=v2,luna=v1);`"v2"` 强制全部模型使用 v2。可在仪表盘 Models 页面或 `ocx v2 mode` 中设置。 |

gui/src/i18n/de.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export const de = {
5454
"dash.injectionNone": "Keine",
5555
"dash.injectionEffortLabel": "Reasoning-Aufwand",
5656
"dash.injectionEffortNone": "Modell-Standard",
57+
"dash.effortCapLabel": "Ultra Effort-Limit",
58+
"dash.subagentEffortCapLabel": "Effort-Limit (Sub-Agents)",
59+
"dash.effortCapNone": "Kein Limit",
5760
"dash.maintenance": "Wartung",
5861
"dash.maintenanceHint": "Aktualisiere Codex’ Modellkatalog oder installiere eine neuere opencodex-Version.",
5962
"dash.syncModels": "Modelle synchronisieren",

gui/src/i18n/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export const en = {
5858
"dash.injectionNone": "None",
5959
"dash.injectionEffortLabel": "Reasoning effort",
6060
"dash.injectionEffortNone": "Model default",
61+
"dash.effortCapLabel": "Ultra reasoning effort limit",
62+
"dash.subagentEffortCapLabel": "Effort cap (sub-agents)",
63+
"dash.effortCapNone": "No cap",
6164
"dash.maintenance": "Maintenance",
6265
"dash.maintenanceHint": "Refresh Codex's model catalog or install a newer opencodex release.",
6366
"dash.syncModels": "Sync models",

gui/src/i18n/ko.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export const ko: Record<TKey, string> = {
5858
"dash.injectionNone": "없음",
5959
"dash.injectionEffortLabel": "추론 강도",
6060
"dash.injectionEffortNone": "모델 기본값",
61+
"dash.effortCapLabel": "ultra 추론 강도 제한",
62+
"dash.subagentEffortCapLabel": "추론 강도 상한 (서브에이전트)",
63+
"dash.effortCapNone": "상한 없음",
6164
"dash.maintenance": "유지보수",
6265
"dash.maintenanceHint": "Codex 모델 카탈로그를 새로고침하거나 최신 opencodex 릴리스를 설치합니다.",
6366
"dash.syncModels": "모델 동기화",

gui/src/i18n/zh.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export const zh: Record<TKey, string> = {
5858
"dash.injectionNone": "无",
5959
"dash.injectionEffortLabel": "推理强度",
6060
"dash.injectionEffortNone": "模型默认",
61+
"dash.effortCapLabel": "Ultra 推理强度限制",
62+
"dash.subagentEffortCapLabel": "推理强度上限(子代理)",
63+
"dash.effortCapNone": "无上限",
6164
"dash.maintenance": "维护",
6265
"dash.maintenanceHint": "刷新 Codex 模型目录,或安装新的 opencodex 版本。",
6366
"dash.syncModels": "同步模型",

gui/src/pages/Dashboard.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { modelLabel } from "../model-display";
6666
const SEARCH_SIDECAR_MODELS = ["gpt-5.6-luna", "gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.6-sol", "gpt-5.6-terra"];
6767
const VISION_SIDECAR_MODELS = ["gpt-5.6-luna", "gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra"];
6868
const REASONING_LEVELS = ["low", "medium", "high"];
69+
const EFFORT_CAP_LEVELS = ["low", "medium", "high", "xhigh"];
6970
const UPDATE_CHECK_MAX_AUTO_RETRIES = 2;
7071
const UPDATE_CHECK_RETRY_BASE_MS = 800;
7172

@@ -102,6 +103,9 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
102103
const [injectionEfforts, setInjectionEfforts] = useState<string[]>([]);
103104
const [injectionAvailable, setInjectionAvailable] = useState<Array<{ provider: string; model: string; namespaced: string }>>([]);
104105
const [injectionSaving, setInjectionSaving] = useState(false);
106+
const [effortCap, setEffortCap] = useState<string>("");
107+
const [subagentEffortCap, setSubagentEffortCap] = useState<string>("");
108+
const [effortCapSaving, setEffortCapSaving] = useState(false);
105109
const [syncResult, setSyncResult] = useState<SyncResult | null>(null);
106110
const [syncError, setSyncError] = useState<string | null>(null);
107111
const [projectConfigWarnings, setProjectConfigWarnings] = useState<ProjectCodexConfigGroup[]>([]);
@@ -161,6 +165,14 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
161165
setInjectionAvailable(imData.available ?? []);
162166
}
163167
} catch { /* old server */ }
168+
try {
169+
const ecRes = await fetch(`${apiBase}/api/effort-caps`);
170+
if (ecRes.ok) {
171+
const ecData = await ecRes.json() as { effortCap?: string | null; subagentEffortCap?: string | null; efforts?: string[] };
172+
setEffortCap(ecData.effortCap ?? "");
173+
setSubagentEffortCap(ecData.subagentEffortCap ?? "");
174+
}
175+
} catch { /* old server */ }
164176
} catch {
165177
setError(true);
166178
}
@@ -496,6 +508,64 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
496508
</div>
497509
)}
498510

511+
<div className="panel" style={{ marginBottom: 24 }}>
512+
<div className="injection-head">
513+
<span className="injection-label">{t("dash.effortCapLabel")}</span>
514+
<Select
515+
value={effortCap}
516+
options={[
517+
{ value: "", label: t("dash.effortCapNone") },
518+
...EFFORT_CAP_LEVELS.map(e => ({ value: e, label: e })),
519+
]}
520+
onChange={async (v) => {
521+
if (effortCapSaving) return;
522+
setEffortCapSaving(true);
523+
try {
524+
const res = await fetch(`${apiBase}/api/effort-caps`, {
525+
method: "PUT",
526+
headers: { "Content-Type": "application/json" },
527+
body: JSON.stringify({ effortCap: v || null }),
528+
});
529+
if (res.ok) {
530+
const data = await res.json() as { ok: boolean; effortCap?: string | null; subagentEffortCap?: string | null };
531+
setEffortCap(data.effortCap ?? "");
532+
setSubagentEffortCap(data.subagentEffortCap ?? "");
533+
}
534+
} catch { /* ignore */ }
535+
finally { setEffortCapSaving(false); }
536+
}}
537+
disabled={effortCapSaving}
538+
label={t("dash.effortCapLabel")}
539+
/>
540+
<Select
541+
value={subagentEffortCap}
542+
options={[
543+
{ value: "", label: t("dash.effortCapNone") },
544+
...EFFORT_CAP_LEVELS.map(e => ({ value: e, label: e })),
545+
]}
546+
onChange={async (v) => {
547+
if (effortCapSaving) return;
548+
setEffortCapSaving(true);
549+
try {
550+
const res = await fetch(`${apiBase}/api/effort-caps`, {
551+
method: "PUT",
552+
headers: { "Content-Type": "application/json" },
553+
body: JSON.stringify({ subagentEffortCap: v || null }),
554+
});
555+
if (res.ok) {
556+
const data = await res.json() as { ok: boolean; effortCap?: string | null; subagentEffortCap?: string | null };
557+
setEffortCap(data.effortCap ?? "");
558+
setSubagentEffortCap(data.subagentEffortCap ?? "");
559+
}
560+
} catch { /* ignore */ }
561+
finally { setEffortCapSaving(false); }
562+
}}
563+
disabled={effortCapSaving}
564+
label={t("dash.subagentEffortCapLabel")}
565+
/>
566+
</div>
567+
</div>
568+
499569
<div className="panel" style={{ marginBottom: 24 }}>
500570
<div className="injection-head">
501571
<span className="injection-label">{t("dash.injectionLabel")}</span>

src/adapters/openai-responses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const FORWARD_HEADERS = [
2121
"x-codex-turn-state",
2222
"x-codex-window-id",
2323
"x-oai-attestation",
24+
"x-openai-subagent",
2425
"x-responsesapi-include-timing-metrics",
2526
];
2627

src/reasoning-effort.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export function isCodexReasoningEffort(effort: string): boolean {
1919
return CODEX_REASONING_SET.has(effort);
2020
}
2121

22+
/** Position of `effort` in the Codex ladder (low=0 .. ultra=5), or -1 when not a ladder member. */
23+
export function codexEffortRank(effort: string): number {
24+
return CODEX_REASONING_ORDER.indexOf(effort);
25+
}
26+
2227
export function modelRecordValue<T>(record: Record<string, T> | undefined, modelId: string): T | undefined {
2328
if (!record) return undefined;
2429
if (Object.prototype.hasOwnProperty.call(record, modelId)) return record[modelId];

0 commit comments

Comments
 (0)