Files
Pat Sukprasert 8103829ef8 fix(codex-native): honor Max/Ultra reasoning levels instead of coercing to xhigh (#5217)
* fix(codex-native): honor Max/Ultra reasoning levels instead of coercing to xhigh

Codex advertises a per-model reasoning ladder via model/list: Sol reaches
`ultra`, Luna reaches `max`, and a turn at those levels completes (Sol's
`ultra` runs subagents). Omnigent's picker surfaced them, but the codex-native
effort override validated against the xhigh-capped CODEX_EFFORTS ladder, so a
web-picked Max/Ultra was silently coerced to xhigh before the wire — and the
TUI->web effort mirror stored `ultra` as `xhigh`, so the UI showed the wrong
level.

Validate codex-native efforts against the full codex ladder
(CODEX_NATIVE_EFFORTS) and add `ultra` to the session-metadata vocabulary, so a
picked level rides through unchanged and the UI shows the real level. The
SDK/Responses codex path keeps the xhigh cap + ultra/max alias, preserving
OMNI-1694's defensive fold on that backend.

OMNI-4255

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* fix(web): follow the drafted model's effort ladder in the Configure Codex modal

The effort dropdown mapped a static effortLevels prop computed from the
committed model, so switching the model inside the modal (Sol → Luna) still
listed Sol's `ultra`, with the stale level left selected. Recompute the ladder
from the drafted model and drop a picked level the new model doesn't offer, so
the dropdown never shows a rung the model rejects and Save can't submit one.

OMNI-4255

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

* chore(codex): ultra parity in REPL /effort, schema doc, and GLM effort cap

Address Polly's non-blocking notes on #5217, now that `ultra` is a
first-class effort value:
- REPL `/effort` accepts and lists `ultra` (matched EFFORT_VALUES).
- schemas.py reasoning_effort docstring enumerates `ultra`.
- GLM effort cap treats `ultra` as unsupported — every rung above `high` —
  so a pinned `ultra` clamps to medium like `xhigh`/`max`.

OMNI-4255

Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>

---------

Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
2026-08-21 20:52:14 +00:00

209 lines
8.8 KiB
Python

"""Reasoning-effort validation helpers shared across client/runtime paths."""
from __future__ import annotations
from collections.abc import Iterable, Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from omnigent.llms.errors import PermanentLLMError
EFFORT_VALUES = frozenset({"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"})
EFFORT_CLEAR_VALUES = frozenset({"default", "off", "reset"})
# Fold a value to a canonical one, but only where the target ladder lacks it:
# ``validate_effort`` applies an alias only when the raw value is unsupported
# but the canonical one is. On the SDK/Responses codex ladder (``CODEX_EFFORTS``,
# capped at ``xhigh``) the ChatGPT app's ``ultra`` / retired ``max`` fold to
# ``xhigh``; ladders that carry them (codex-native ``CODEX_NATIVE_EFFORTS``,
# Anthropic's ``max``) keep them unchanged.
EFFORT_ALIASES: dict[str, str] = {"ultra": "xhigh", "max": "xhigh"}
OPENAI_EFFORTS = frozenset({"none", "minimal", "low", "medium", "high", "xhigh"})
ANTHROPIC_EFFORTS = frozenset({"low", "medium", "high", "xhigh", "max"})
CLAUDE_EFFORTS = ANTHROPIC_EFFORTS
CODEX_EFFORTS = OPENAI_EFFORTS
# Codex-native drives the real codex process, which is the per-model authority
# on reasoning levels — it advertises them via ``model/list`` and validates the
# pairing itself. Sol reaches ``ultra``; the picker already gates which levels a
# model offers, so accept codex's full ladder here rather than re-clamping a
# valid pick down to ``xhigh``.
CODEX_NATIVE_EFFORTS = frozenset(
{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"}
)
OPENAI_AGENTS_EFFORTS = OPENAI_EFFORTS
GEMINI_EFFORTS = frozenset({"low", "medium", "high"})
ANTIGRAVITY_EFFORTS = GEMINI_EFFORTS
# The GitHub Copilot SDK's ``create_session(reasoning_effort=...)`` accepts
# exactly these levels (``copilot.session.ReasoningEffort`` literal); per-model
# support is gated by the Copilot backend (``list_models()``).
COPILOT_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
def format_supported(values: Iterable[str]) -> str:
"""Return a stable comma-separated supported-values string."""
order = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]
values_set = set(values)
return ", ".join(value for value in order if value in values_set)
def unsupported_effort_message(effort: str, provider: str, supported: Iterable[str]) -> str:
"""Build a clear unsupported-effort error message."""
return (
f"Effort {effort!r} is not supported by {provider}; "
f"supported values: {format_supported(supported)}"
)
# Some models served through a harness reject the harness's full effort ladder.
# GLM on the codex/Responses wire accepts only up to ``high`` (no ``xhigh``), so
# a user default of ``xhigh``/``max`` 400s the turn. Map such a model to the
# effort to use instead of failing — GLM falls back to ``medium``.
#
# These are probed facts about ONE gateway's serving, not properties of the
# effort ladders themselves, so a deployment whose gateway caps other models
# overrides them via ``routing.effort_caps`` (see
# :class:`~omnigent.server.smart_routing.RoutingSettings`). The provider ladders
# above stay frozen: they are the wire APIs' own vocabularies.
_MODEL_EFFORT_FALLBACK: Mapping[str, str] = MappingProxyType({"glm-5-2": "medium"})
# Efforts a fallback model cannot accept, so a pinned high value coerces down.
# GLM tops out at ``high``, so every rung above it (``xhigh``/``max``/``ultra``)
# is unsupported.
_MODEL_EFFORT_UNSUPPORTED: Mapping[str, frozenset[str]] = MappingProxyType(
{"glm-5-2": frozenset({"xhigh", "max", "ultra"})}
)
@dataclass(frozen=True)
class ModelEffortCaps:
"""Per-model effort ceilings a deployment's gateway imposes.
:param fallback: Bare model id → the effort to use when the requested one
is barred. Also the effort a switch onto that model sends when the
caller asked for none.
:param unsupported: Bare model id → the efforts that model's backend
rejects outright.
"""
# default_factory, not default: a mapping is unhashable and dataclasses
# rejects an unhashable default outright.
fallback: Mapping[str, str] = field(default_factory=lambda: _MODEL_EFFORT_FALLBACK)
unsupported: Mapping[str, frozenset[str]] = field(
default_factory=lambda: _MODEL_EFFORT_UNSUPPORTED
)
#: The caps every deployment gets unless its ``routing:`` block overrides them.
DEFAULT_MODEL_EFFORT_CAPS = ModelEffortCaps()
def model_effort_caps(caps: ModelEffortCaps | None = None) -> ModelEffortCaps:
"""Resolve which effort caps apply, defaulting to this deployment's.
``None`` reads :class:`~omnigent.server.smart_routing.RoutingSettings` off
the process caps, so a managed gateway that caps a different model set is
honoured without every caller threading the value. Outside a server process
(the runner holds no routing settings) that read yields the defaults, which
are the frozen tables above — so runner-side clamping is unchanged.
:param caps: Explicit caps, or ``None`` to read the deployment's.
:returns: The caps to clamp with; never ``None``.
"""
if caps is not None:
return caps
try:
from omnigent.server.smart_routing import routing_settings
except ImportError: # pragma: no cover — a build without the server extra
return DEFAULT_MODEL_EFFORT_CAPS
return routing_settings().model_effort_caps or DEFAULT_MODEL_EFFORT_CAPS
def _bare_model(model: str) -> str:
"""Strip a catalog/gateway prefix and fold to the comparison spelling."""
bare = model.rsplit("/", 1)[-1]
for prefix in ("databricks-", "system.ai."):
if bare.startswith(prefix):
bare = bare[len(prefix) :]
return bare.replace(".", "-").lower()
def clamp_effort_for_model(
effort: str | None,
model: str | None,
*,
caps: ModelEffortCaps | None = None,
) -> str | None:
"""Coerce *effort* to one *model* accepts, keeping the user's pick otherwise.
A model whose backend rejects a high effort (e.g. GLM has no ``xhigh``)
falls back to a supported value rather than 400-ing the turn. Any other
model, or an already-accepted effort, is returned unchanged.
:param caps: Effort ceilings to clamp against; ``None`` uses this
deployment's (see :func:`model_effort_caps`).
"""
if effort is None or model is None:
return effort
resolved = model_effort_caps(caps)
key = _bare_model(model)
unsupported = resolved.unsupported.get(key)
if unsupported is not None and effort in unsupported:
return resolved.fallback.get(key, effort)
return effort
def effort_for_model_switch(
effort: str | None,
model: str | None,
*,
caps: ModelEffortCaps | None = None,
) -> str | None:
"""Effort to send when switching to *model*, guarding a rejected default.
Like :func:`clamp_effort_for_model` for an explicit *effort*. When *effort*
is ``None`` (no effort requested), a model that caps its ladder still needs
guarding, because the switched-to thread inherits the config's default
(which may be too high): return that model's fallback so the live turn does
not 400. A model with no cap and no requested effort returns ``None``.
:param caps: Effort ceilings to clamp against; ``None`` uses this
deployment's (see :func:`model_effort_caps`).
"""
if effort is not None:
return clamp_effort_for_model(effort, model, caps=caps)
if model is None:
return None
return model_effort_caps(caps).fallback.get(_bare_model(model))
def validate_effort(effort: object, provider: str, supported: Iterable[str]) -> str | None:
"""Validate *effort* against *supported*, returning a string or None.
A deprecated alias (see :data:`EFFORT_ALIASES`) is coerced to its
canonical value when the raw value is unsupported but the canonical one
is — e.g. the ChatGPT app's ``ultra`` becomes ``xhigh`` for codex, while
``max`` stays ``max`` for providers that still support it (Anthropic).
"""
if effort is None or effort == "":
return None
effort_str = str(effort)
supported_set = set(supported)
if effort_str not in supported_set:
alias = EFFORT_ALIASES.get(effort_str)
if alias is not None and alias in supported_set:
return alias
raise ValueError(unsupported_effort_message(effort_str, provider, supported_set))
return effort_str
def validate_effort_or_llm_error(
effort: object,
provider: str,
supported: Iterable[str],
) -> str | None:
"""Validate for native LLM paths, raising non-retryable PermanentLLMError."""
try:
return validate_effort(effort, provider, supported)
except ValueError as exc:
raise PermanentLLMError(str(exc), code="unsupported_reasoning_effort") from exc