diff --git a/docs-site/src/content/docs/fr/reference/configuration/agents.md b/docs-site/src/content/docs/fr/reference/configuration/agents.md index 03e289e3e9..8e6d4522a2 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/fr/reference/configuration/agents.md @@ -119,4 +119,6 @@ Les plafonds s’appliquent uniquement à la fonctionnalité de collaboration v2 Un plafond ne peut que réduire l’effort. Le niveau retenu est le niveau annoncé le plus élevé qui ne dépasse pas le plafond. Si le modèle ne propose aucun contrôle d’effort ou si aucun niveau pris en charge ne convient, opencodex supprime le paramètre d’effort et laisse le fournisseur appliquer sa valeur par défaut. `max` et `ultra` sont acceptés, tandis que le tableau de bord propose les niveaux de `low` à `xhigh`. +Les plafonds configurés s’appliquent aussi aux tours Chat Completions natifs admissibles sans effort épinglé pour le modèle. La conversion vers la valeur du fournisseur intervient lorsqu’un épinglage est appliqué ou qu’un plafond modifie la valeur ; sinon, la valeur de l’appelant natif conserve sa forme d’origine. + Pour une présentation destinée aux débutants des comportements v1, default et v2, consultez [Surfaces de sous-agents](/fr/guides/sub-agent-surface/). diff --git a/docs-site/src/content/docs/ja/reference/configuration/agents.md b/docs-site/src/content/docs/ja/reference/configuration/agents.md index def092132c..3ab50e22fc 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ja/reference/configuration/agents.md @@ -87,4 +87,6 @@ opencodex は、無効、ルーティング不能、異常、冷却期間、ま キャップは労力を軽減するだけです。これらは、キャップまたはキャップの下で宣伝されている最も高い段にスナップします。モデルにエフォート制御がない場合、またはサポートされているラングフィットがない場合、opencodex はエフォートを削除し、プロバイダーのデフォルトを適用します。 `max` および `ultra` が受け入れられますが、ダッシュボードでは `low` から `xhigh` が提供されます。 +モデルの effort pin がなくても、対象のネイティブ Chat Completions ターンには設定された上限が適用されます。pin を適用した場合、または上限で値を変更した場合にプロバイダーの送信値へ変換し、どちらも起きない呼び出し元の値は元の表記を維持します。 + v1、デフォルト、および v2 の動作に関する初心者向けの説明については、「[サブエージェントサーフェス](/guides/sub-agent-surface/)」を参照してください。 diff --git a/docs-site/src/content/docs/ko/reference/configuration/agents.md b/docs-site/src/content/docs/ko/reference/configuration/agents.md index 5393052849..58383e63a4 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ko/reference/configuration/agents.md @@ -87,4 +87,6 @@ opencodex는 비활성, 라우팅 불가, 비정상, 쿨다운 중, 또는 할 상한은 노력만 낮춥니다. 모델이 광고한 단계 중 상한 이하에서 가장 높은 단계로 맞춥니다. 모델에 노력 제어가 없거나 맞는 지원 단계가 없으면, opencodex는 노력을 제거하고 제공자 기본값을 적용합니다. `max`와 `ultra`는 허용되며, 대시보드는 `low`부터 `xhigh`까지 제공합니다. +모델 effort pin이 없어도 적용 대상 native Chat Completions 요청에는 설정된 상한이 적용됩니다. pin을 적용하거나 상한이 값을 바꾼 경우에 제공자 전송 값으로 매핑하며, 둘 다 없으면 호출자 값은 원래 표기를 유지합니다. + v1, default, v2 동작에 대한 초보자용 설명은 [Sub-agent surfaces](/guides/sub-agent-surface/)를 참고하세요. diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index 24dd9dec16..a113813b40 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -253,6 +253,8 @@ Caps only lower effort. They snap to the highest advertised rung at or below the no effort control or no supported rung fits, opencodex removes the effort and lets the provider default apply. `max` and `ultra` are accepted, while the dashboard offers `low` through `xhigh`. +Configured caps also apply to eligible native Chat Completions turns that carry no model effort pin. Provider wire mapping runs when a pin is applied or when a cap changes the value; a native caller value keeps its original wire spelling when neither happens. + For a beginner-oriented explanation of v1, default, and v2 behavior, see [Sub-agent surfaces](/guides/sub-agent-surface/). diff --git a/docs-site/src/content/docs/ru/reference/configuration/agents.md b/docs-site/src/content/docs/ru/reference/configuration/agents.md index 831f49bcf9..62a5723508 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ru/reference/configuration/agents.md @@ -136,5 +136,7 @@ Cap'ы умеют только понижать effort. Они опускают помещается под cap, opencodex убирает поле effort и позволяет провайдеру применить собственный дефолт. `max` и `ultra` принимаются, хотя дашборд предлагает только `low`–`xhigh`. +Настроенные потолки применяются и к подходящим нативным запросам Chat Completions без закреплённого effort модели. Преобразование в значение провайдера выполняется, когда применяется закрепление или потолок меняет значение; иначе значение нативного вызывающего клиента сохраняет исходное написание. + Если нужен объясняющий вариант для начинающих о поведении v1, default и v2, см. [Поверхность подагентов](/guides/sub-agent-surface/). diff --git a/docs-site/src/content/docs/tr/reference/configuration/agents.md b/docs-site/src/content/docs/tr/reference/configuration/agents.md index 128ac3920a..6320e18f08 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/tr/reference/configuration/agents.md @@ -259,5 +259,7 @@ hiçbir basamak uymuyorsa opencodex çabayı kaldırır ve sağlayıcı varsayı uygulanmasına izin verir. `max` ve `ultra` kabul edilirken kontrol paneli `low` ile `xhigh` arasını sunar. +Yapılandırılmış sınırlar, model effort sabitlemesi olmayan uygun yerel Chat Completions turlarına da uygulanır. Sağlayıcının iletim değerine eşleme yalnızca sabitleme uygulandığında veya sınır değeri değiştirdiğinde yapılır; aksi hâlde yerel çağıran değeri özgün yazımını korur. + v1, varsayılan ve v2 davranışının yeni başlayanlara yönelik açıklaması için [Alt ajan yüzeyleri](/tr/guides/sub-agent-surface/) sayfasına bakın. diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md index 094e6df720..fcc84ab87f 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md @@ -86,4 +86,6 @@ opencodex 会跳过已禁用、不可路由、不健康、处于冷却中,或 上限只会降低 effort。它们会向下贴合到不高于上限、且模型公开的最高档位。如果模型没有 effort 控制,或者没有任何受支持的档位可用,opencodex 会移除 effort,让提供方默认值生效。`max` 和 `ultra` 都可接受,而仪表板提供 `low` 到 `xhigh`。 +即使没有设置模型 effort pin,符合条件的原生 Chat Completions 轮次也会应用配置的上限。仅在应用 pin 或上限改变值时才映射为提供方的传输值;两者都未发生时,原生调用方值保留原始写法。 + 关于 v1、default 和 v2 行为的面向初学者说明,请参阅 [Sub-agent surfaces](/guides/sub-agent-surface/)。 diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md b/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md index d3fdf7059b..6f3ad3c84e 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md @@ -71,4 +71,6 @@ opencodex 會跳過已停用、不可路由、不健康、冷卻中或達到配 上限僅會降低 effort。它們吸附到上限或以下的最高宣告級別。若模型沒有 effort 控制或沒有支援的級別符合,opencodex 會移除 effort 並讓供應商預設值套用。`max` 與 `ultra` 被接受,而儀表板提供 `low` 到 `xhigh`。 +即使沒有設定模型 effort pin,符合條件的原生 Chat Completions 回合也會套用設定的上限。套用 pin 或上限改變值時才會對應為供應商的傳輸值;兩者皆未發生時,原生呼叫端值保留原始寫法。 + 關於 v1、default 與 v2 行為的入門導向說明,請見[子代理介面](/zh-tw/guides/sub-agent-surface/)。 diff --git a/src/server/chat-native.ts b/src/server/chat-native.ts index 7f3c97c10e..9fb24e143a 100644 --- a/src/server/chat-native.ts +++ b/src/server/chat-native.ts @@ -105,19 +105,27 @@ function normalizePinnedChatEffort(options: HandleNativeChatOptions): void { const from = typeof chatBody.reasoning_effort === "string" ? chatBody.reasoning_effort : undefined; logCtx.requestedEffort = from; // Compaction is normally excluded by native-route eligibility; preserve that boundary here too. - const pinned = chatBody.compaction_trigger === undefined + const compaction = chatBody.compaction_trigger !== undefined; + const pinned = !compaction ? resolvePinnedEffort(route, selector, config) : undefined; + let normalizeForWire = false; if (pinned !== undefined) { logCtx.requestedEffort = from ? `${from}->${pinned}` : pinned; if (pinned === "none") delete chatBody.reasoning_effort; else chatBody.reasoning_effort = pinned; - // The native lane historically passes caller effort through, including with caps set. - // Only a newly operator-pinned value enters the cap and provider-mapping pipeline. - if (effortCapAppliesTo(chatCollabSurface(chatBody), req.headers, config)) { - const capped = applyChatEffortCap(chatBody, req.headers, config, supportedLadderFor(route)); - if (capped) logCtx.requestedEffort = `${logCtx.requestedEffort}->${capped.to}`; + normalizeForWire = true; + } + // A qualifying turn's ceiling is independent of whether an operator pin resolved. + if (effortCapAppliesTo(chatCollabSurface(chatBody), req.headers, config, compaction)) { + const capped = applyChatEffortCap(chatBody, req.headers, config, supportedLadderFor(route)); + if (capped) { + logCtx.requestedEffort = `${logCtx.requestedEffort ?? capped.from}->${capped.to}`; + normalizeForWire = true; } + } + // Normalize operator-pinned values and cap rewrites; otherwise preserve caller spelling. + if (normalizeForWire) { const effort = typeof chatBody.reasoning_effort === "string" ? chatBody.reasoning_effort : undefined; const wireEffort = mapReasoningEffort(route.provider, route.modelId, effort); if (wireEffort === undefined) delete chatBody.reasoning_effort; diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 9e869c4f7a..7da15cd16a 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -4,6 +4,10 @@ Runtime adapter construction has one authority: `src/adapters/registry.ts`. +Native Chat admission in `src/server/chat-native.ts` applies the shared +[effort-cap and wire-mapping policy](../catalog.md#ultra-reasoning-level) before the selected +adapter builds its request, including when no model effort pin resolves. + `src/server/adapter-resolve.ts` may resolve a provider/model onto an adapter id, but it does not maintain a second adapter factory inventory. The selected persisted/configured adapter id remains an untrusted string until the registry lookup succeeds. Unknown ids fail with the existing `Unknown adapter: ` error instead of widening configuration types around a closed compile-time union. ## Semantic inheritance is not constructor inheritance diff --git a/structure/catalog.md b/structure/catalog.md index 0ba4acca3e..eda6f26e80 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -251,8 +251,10 @@ Provider model pins precede provider-wide pins, then global selector/destination A pin can raise the effective caller effort; the later cap can still lower or omit it. `none` means explicit-effort omission (provider default), not guaranteed reasoning disablement. Compaction maintenance is exempt. Pins are user overlays and do not alter registry seeds, -model discovery or advertised ladders. Native Chat normalizes newly pinned values through -provider wire mapping; unpinned native requests retain their existing pass-through contract. +model discovery or advertised ladders. Native Chat applies qualifying caps even without a +pin, and normalizes pinned values and values rewritten by a cap through provider wire +mapping. Without a pin or a cap rewrite, native caller values retain their original wire +spelling; the V1 and compaction cap exemptions are preserved. > Decision record: [ADR-0023](decisions/ADR-0023-ultra-reasoning-level.md) diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 2914823958..9caccaeb4c 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -1,5 +1,9 @@ # Claude Desktop Integration +Client profile discovery and persistence do not determine per-turn effort-cap admission. +For native Chat requests, `src/server/chat-native.ts` owns the +[shared cap contract](../catalog.md#ultra-reasoning-level), including eligible unpinned turns. + ## Connected Claude Desktop profiles Connected `ocx claude desktop apply` reads the hub's Desktop snapshot and writes the hub origin diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 25646c7de4..affe7696a8 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -6,6 +6,9 @@ Codex's local `image_gen.imagegen` tool makes a second Images request after the `POST /v1/images/generations` for generation or `POST /v1/images/edits` for reference-image edits. These are standalone Images API routes, not the hosted Responses `image_generation` tool. +The [native Chat effort-cap contract](../catalog.md#ultra-reasoning-level) belongs to +`src/server/chat-native.ts`; the standalone Images relay does not apply that Chat request policy. + `src/server/images.ts` uses the existing ChatGPT/OpenAI fallback unless `images.provider` explicitly selects a custom API-key `openai-responses` provider. Explicit selection fails closed when the provider is missing, disabled, registry-managed, incompatible, or lacks a usable key; it never diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 2d17c11875..b51e9e9e7c 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -22,6 +22,12 @@ parallel tools (or pinned false by the existing provider opt-out contract). Combo/policy routes and requests that need Responses-only hosted tools, continuation, background, or storage semantics retain the existing Chat -> Responses -> Chat bridge. +`src/server/chat-native.ts` applies the shared +[effort-cap contract](../catalog.md#ultra-reasoning-level) independently of model pins. +Provider wire mapping runs for an applied pin or a cap rewrite; otherwise the caller's +effort spelling is preserved. Same-destination retries reuse the normalized value and +annotation; a destination change starts again from the original caller effort. + The direct SSE relay accepts CRLF and arbitrary transport chunk boundaries while retaining at most one bounded event. EOF with an unterminated event and an event above the translator limit are typed upstream failures, never successful partial completions. Provider-controlled structured error diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 73090d646e..5d202f1ba0 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -1,5 +1,9 @@ # GUI And Management API +Configured effort ceilings are enforced per request by the +[shared effort policy](catalog.md#ultra-reasoning-level). The native Chat implementation in +`src/server/chat-native.ts` applies eligible caps without requiring a model-pin setting. + ## Dashboard serving The bundled React dashboard is built into `gui/dist` and served by the same Bun proxy. `ocx gui` diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 39dc9a82da..6f6fb67cda 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -1,5 +1,9 @@ # Background Service And Sidecars +Effort ceilings are request policy rather than service or sidecar startup policy. +`src/server/chat-native.ts` applies the [shared cap contract](../catalog.md#ultra-reasoning-level) +to eligible native Chat turns with or without a model pin before dispatch. + ## Background service command selection A bare `ocx service` is an idempotent install-or-repair command. Argument validation happens before diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 5b149ac6a2..1d322824f9 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -1,5 +1,9 @@ # xAI Grok Provider +For routes admitted to `src/server/chat-native.ts`, request effort follows the shared +[cap and provider-mapping contract](../catalog.md#ultra-reasoning-level), including eligible +requests without a pin. Provider-specific Grok transport behavior is described below. + ## xAI Grok hardening (official Grok Build contract parity) Grounded in the open-sourced official client (xai-org/grok-build); unit + evidence: diff --git a/structure/runtime.md b/structure/runtime.md index 2f5ce70e21..0aa10d09ad 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -1,5 +1,9 @@ # Runtime +Native Chat request preparation in `src/server/chat-native.ts` applies the shared +[effort-cap contract](catalog.md#ultra-reasoning-level) before provider dispatch, including +eligible requests without an operator effort pin. + ## Entrypoints | Path | Responsibility | diff --git a/structure/subagents.md b/structure/subagents.md index f190aab084..27d8d2f396 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -133,6 +133,10 @@ availability on a bounded interval (default 60 s, `src/codex/subagent-model-fall the requested model id only; effort remains owned by the caps described under [Ultra reasoning level](catalog.md#ultra-reasoning-level). +Native Chat uses that same cap policy in `src/server/chat-native.ts` for qualifying v2 main +turns and exactly marked spawned children, even without a model effort pin. Leaf children +do not need collaboration tools to qualify; forced v1 and compaction remain exempt. + `injectionModel` and `injectionEffort` are shared selections with two independent consumers. `multiAgentGuidanceEnabled` controls only OpenCodex-authored delegation guidance. `syncCodexSubagentDefaults` is a separate, default-off opt-in that applies the selected values to diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index b2fc3b3fae..94cc33062b 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -14,7 +14,7 @@ surface is listed here so a maintainer can find the owner without grepping: | Adapter execution support | `src/adapters/run-turn-queue.ts`, `src/adapters/tool-catalog-nudge.ts`, `src/adapters/identity.ts`, `src/adapters/image.ts`, `src/adapters/upstream-http-error.ts` | Shared machinery: turn ordering, tool-catalog nudging, client fingerprinting, image conversion, upstream error normalization. | | Cursor (beyond the sections above) | `src/adapters/cursor/live-transport.ts`, `src/adapters/cursor/http1-bidi.ts`, `src/adapters/cursor/live-models.ts`, `src/adapters/cursor/transport-retry.ts`, `src/adapters/cursor/mcp-manager.ts`, `src/adapters/cursor/thread-continuity.ts`, `src/adapters/cursor/checkpoint-store.ts` | Thread continuity is the point: a retry must not start a new Cursor thread, and a validated checkpoint must not rebuild the full root history. HTTP/2 remains the default; an explicit `http1.1`/`h1` pin maps the bidi run onto Cursor's `RunSSE` receive stream plus sequenced `BidiAppend` sends, and applies to live discovery too. | | Claude Messages | `src/server/claude-messages.ts` | Routed translation, a native Anthropic passthrough branch, and `count_tokens`. | -| Chat Completions inbound | `src/server/chat-completions.ts`, `src/chat/` | Inbound translation onto the same routing pipeline. The content mapper preserves image URLs and supported detail, including screenshot-bearing tool results; target adapters own image placement on their wire. Image-free tool results stay strings. | +| Chat Completions inbound | `src/server/chat-completions.ts`, `src/server/chat-native.ts`, `src/chat/` | Eligible native requests retain their Chat wire source and apply the shared [effort-cap policy](../catalog.md#ultra-reasoning-level), including without a pin. Other requests translate onto the same routing pipeline. The content mapper preserves image URLs and supported detail, including screenshot-bearing tool results; target adapters own image placement on their wire. Image-free tool results stay strings. | | Hosted search relay | `src/server/search.ts` | Direct relay; distinct from the web-search sidecar loop below. | | Image/video generation loop | `src/images/loop.ts`, `src/images/plan.ts`, `src/images/fulfill.ts`, `src/images/xai-client.ts`, `src/images/xai-video-client.ts`, `src/images/artifacts.ts` | A provider-returned image URL is downloaded into a local artifact once, then served locally; warnings stay URL-free because provider CDN URLs may embed credentials. | | GitHub Copilot | `src/providers/xai-transport.ts` (`resolveProviderTransport`), `src/providers/github-copilot-transport.ts` | `resolveProviderTransport` selects the Copilot transport when the routed provider name is `github-copilot`; the Copilot module then resolves its headers and base URL, and the registry seeds the provider row and model fallback. | diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 2321d78dd6..8f18c8c9a6 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -6,6 +6,10 @@ provider, lets the selected adapter speak the upstream protocol, then bridges adapter events back to Responses-compatible streaming output. +The native Chat lane applies the same [effort-cap contract](../catalog.md#ultra-reasoning-level) +in `src/server/chat-native.ts` before dispatch, so eligible unpinned Chat requests do not +need a Responses projection to receive their configured ceiling. + ### Credential-bearing HTTP redirects Credential/body-bearing HTTP sends use `redirect: "manual"` at the final executor boundary, diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index 68093843ea..43d237f70e 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -1,5 +1,9 @@ # Streaming Health And WebSocket +Native Chat effort normalization occurs before the upstream send in `src/server/chat-native.ts`. +Its [cap and wire-mapping policy](../catalog.md#ultra-reasoning-level) also covers eligible +unpinned requests; the SSE relay and its cancellation/deadline ownership follow the resulting send. + ## Heartbeat and stall deadline The HTTP/SSE bridge emits an SSE comment-line keep-alive (`: opencodex heartbeat`) during upstream diff --git a/tests/codex-integration/model-pinned-effort.test.ts b/tests/codex-integration/model-pinned-effort.test.ts index 7c7b15401c..a808b86767 100644 --- a/tests/codex-integration/model-pinned-effort.test.ts +++ b/tests/codex-integration/model-pinned-effort.test.ts @@ -428,12 +428,74 @@ describe("operator pins on the actual request wire", () => { expect(wire.reasoning).toEqual({ effort: "max", summary: "auto" }); }); - test("native Chat without pins preserves caller wire spelling and existing cap behavior", async () => { + test("native Chat without pins caps spawned children and preserves unqualified caller spelling", async () => { const c = config({ reasoningEfforts: ["low"], reasoningEffortMap: { max: "enabled" } }, { effortCap: "low", subagentEffortCap: "low" }); - expect((await request(c, "chat", { reasoning_effort: "ultra" }, { "x-openai-subagent": "collab_spawn" })).reasoning_effort).toBe("ultra"); + expect((await request(c, "chat", { reasoning_effort: "ultra" }, { "x-openai-subagent": "collab_spawn" })).reasoning_effort).toBe("low"); + expect((await request(c, "chat", { reasoning_effort: "ultra" })).reasoning_effort).toBe("ultra"); expect(Object.hasOwn(await request(c, "chat", { reasoning_effort: undefined }), "reasoning_effort")).toBe(false); }); + test("native Chat without pins caps v2 main turns and preserves forced v1", async () => { + const c = config({}, { effortCap: "medium", subagentEffortCap: "low" }); + const tools = [{ type: "function", function: { name: "spawn_agent", parameters: { type: "object", properties: {} } } }]; + expect((await request(c, "chat", { tools, reasoning_effort: "ultra" })).reasoning_effort).toBe("medium"); + expect((await request(c, "chat", { tools, reasoning_effort: "ultra" }, { "x-openai-subagent": "collab_spawn" })).reasoning_effort).toBe("low"); + c.multiAgentMode = "v1"; + expect((await request(c, "chat", { tools, reasoning_effort: "ultra" }, { "x-openai-subagent": "collab_spawn" })).reasoning_effort).toBe("ultra"); + }); + + test("native Chat maps newly capped values and preserves lower, non-ladder and absent efforts", async () => { + const c = config({ reasoningEffortMap: { medium: "enabled", low: "disabled" } }, { subagentEffortCap: "medium" }); + const headers = { "x-codex-turn-metadata": JSON.stringify({ subagent_kind: "thread_spawn" }) }; + expect((await request(c, "chat", { reasoning_effort: "ultra" }, headers)).reasoning_effort).toBe("enabled"); + expect((await request(c, "chat", { reasoning_effort: "low" }, headers)).reasoning_effort).toBe("low"); + expect((await request(c, "chat", { reasoning_effort: "enabled" }, headers)).reasoning_effort).toBe("enabled"); + expect(Object.hasOwn(await request(c, "chat", { reasoning_effort: undefined }, headers), "reasoning_effort")).toBe(false); + c.providers.fixture!.pinnedReasoningEffort = "medium"; + expect((await request(c, "chat", { reasoning_effort: "medium" })).reasoning_effort).toBe("enabled"); + }); + + test("native Chat without pins omits effort when no supported rung fits the cap", async () => { + for (const reasoningEfforts of [[], ["high", "max"]]) { + const c = config({ reasoningEfforts }, { subagentEffortCap: "low" }); + const wire = await request(c, "chat", { reasoning_effort: "max", temperature: 0.4 }, { "x-openai-subagent": "collab_spawn" }); + expect(Object.hasOwn(wire, "reasoning_effort")).toBe(false); + expect(wire.temperature).toBe(0.4); + } + }); + + test("direct native Chat compaction keeps its pin and cap exemption", async () => { + const c = config({ pinnedReasoningEffort: "high" }, { subagentEffortCap: "low" }); + const chatBody = { model: "fixture/pin-model", messages: [{ role: "user", content: "summarize" }], + reasoning_effort: "ultra", compaction_trigger: {} }; + const req = new Request("http://localhost/v1/chat/completions", { method: "POST", headers: { "x-openai-subagent": "collab_spawn" } }); + const response = await handleNativeChatCompletions({ req, config: c, logCtx: { model: "", provider: "" }, + route: routeModel(c, chatBody.model), chatBody, requestedModel: chatBody.model, + requestedStream: false, translatorBudget: createTestTranslatorBudget() }); + expect(response.status, await response.text()).toBe(200); + expect(captured.at(-1)!.body.reasoning_effort).toBe("ultra"); + }); + + test("native cap retries retain their annotation and restore original effort for a new destination", async () => { + const c = config({}, { defaultProvider: "first", subagentEffortCap: "high", providers: { + first: provider({ reasoningEfforts: ["low", "max"] }), + second: provider({ reasoningEfforts: ["medium", "high"] }), + } }); + const chatBody = { model: "first/pin-model", messages: [{ role: "user", content: "hello" }], reasoning_effort: "ultra" }; + const req = new Request("http://localhost/v1/chat/completions", { method: "POST", headers: { "x-openai-subagent": "collab_spawn" } }); + const annotations: Array = []; + for (const name of ["first", "first", "second"]) { + const logCtx = { model: "", provider: "", requestedEffort: undefined as string | undefined }; + const response = await handleNativeChatCompletions({ req, config: c, logCtx, + route: routeModel(c, `${name}/pin-model`), chatBody, requestedModel: `${name}/pin-model`, + requestedStream: false, translatorBudget: createTestTranslatorBudget() }); + expect(response.status, await response.text()).toBe(200); + annotations.push(logCtx.requestedEffort); + } + expect(captured.map(({ body }) => body.reasoning_effort)).toEqual(["low", "low", "high"]); + expect(annotations).toEqual(["ultra->low", "ultra->low", "ultra->high"]); + }); + test("unpinned Responses keeps its existing applicable cap", async () => { expect((await request(config({}, { subagentEffortCap: "medium" }), "responses", { reasoning: { effort: "max", summary: "auto" } }, { "x-openai-subagent": "collab_spawn" })).reasoning_effort).toBe("medium");