Skip to content

symfonic.kernel.contracts.forcing

forcing

Whether this provider will honour a required tool call, asked once.

ModelProvider has carried supports_forced_tool_choice(config) since v7.8.5, and it exists to prevent one thing: an extended-thinking model refuses a hard tool_choice, so binding one anyway produces a vendor error at the API boundary, after the request is paid for. The legacy engine asked. The kernel's forced-choice path did not -- it bound the force and let the provider find out -- which is the invariant unenforced rather than absent.

Asked once, at compile time, because both answers it needs are fixed there: the provider is the one the agent was composed with and the ModelConfig is the one the plan resolved. Asking per turn would re-run an adopter's introspection for an answer that cannot have changed.

ForcingSupport dataclass

ForcingSupport(supported: bool = True, reason: str = '')

The provider's answer, and its own words for a refusal.

forcing_support

forcing_support(provider: Any, config: Any) -> ForcingSupport

Ask the provider, defaulting to supported when it does not answer.

A provider that does not implement the probe is not refusing -- the method is optional and most providers honour a forced choice. Defaulting to refusal would disable the lever for every custom provider that never heard of the probe, which is a larger break than the one this prevents.

A probe that raises is a different matter and is treated as a refusal: a provider whose introspection is broken cannot be relied on to honour the force either, and a required call that silently becomes optional is the failure this whole path exists to remove.

Source code in src/symfonic/kernel/contracts/forcing.py
def forcing_support(provider: Any, config: Any) -> ForcingSupport:
    """Ask the provider, defaulting to *supported* when it does not answer.

    A provider that does not implement the probe is not refusing -- the method
    is optional and most providers honour a forced choice. Defaulting to
    refusal would disable the lever for every custom provider that never heard
    of the probe, which is a larger break than the one this prevents.

    A probe that *raises* is a different matter and is treated as a refusal: a
    provider whose introspection is broken cannot be relied on to honour the
    force either, and a required call that silently becomes optional is the
    failure this whole path exists to remove.
    """
    probe = getattr(provider, "supports_forced_tool_choice", None)
    if not callable(probe):
        return ForcingSupport()
    try:
        if probe(config):
            return ForcingSupport()
    except Exception as exc:  # noqa: BLE001 - a broken probe is a refusal
        return ForcingSupport(
            supported=False,
            reason=(
                f"the provider's supports_forced_tool_choice raised "
                f"{type(exc).__name__}"
            ),
        )
    return ForcingSupport(supported=False, reason=_reason(provider, config))