Skip to content

symfonic.core.testing.deterministic

deterministic

Deterministic provider adapters, one per supported wire family (T3.1.4).

MockModelProvider answers "what does the agent do when the model replies?". It cannot answer "what does the agent do when the model is on the Anthropic wire?" — it declares no family, so every test written against it exercises the unknown path, which is the one path where the framework has no cache dialect, no native content-block encoding and no forced-tool-choice constraint.

That gap is not academic. Extended thinking blocking a forced tool choice, cache_control placement, and the top_k sampling knob are all per-family behaviour, and none of it was reachable from a test that did not hold a real credential.

:class:DeterministicProvider closes it. It is a real adapter — it satisfies the provider contract, it appears in the capability matrix alongside the shipped adapters — that serves a scripted response over a declared wire family, with no credential, no network and no optional extra.

Every family-dependent answer is read from the descriptor table rather than restated here. A double that hard-codes top_k=True for Anthropic is a second source of truth and the first thing to go stale when the real row changes; reading the row means a change to the framework's claim about a family shows up in the double on the same commit.

DeterministicChatModel

Bases: MockChatModel

A scripted chat model that records what actually reached it.

Two things MockChatModel cannot do, both of which a parity matrix needs:

  • Report usage. Its AIMessage carries no usage_metadata, so the whole cost path (extract -> typed breakdown -> USD) is unreachable from a test.
  • Refuse. Its bind_tools returns self and swallows tool_choice, so every binding looks successful and the degrade paths the framework documents — no bind_tools at all, no tool_choice keyword — cannot be exercised.

Both are opt-in here: the defaults behave exactly like MockChatModel, and a test that wants a refusing provider says so.

DeterministicProvider

DeterministicProvider(family: str = 'unknown', *, response: str = 'Mock response', tool_calls: list[dict[str, Any]] | None = None, structured_response: dict[str, Any] | None = None, usage: dict[str, Any] | None = None, bind_tools_supported: bool = True, tool_choice_supported: bool = True)

A contract-conformant adapter for one declared wire family.

The ClassVars are the declaration the contract checker requires; the instance attributes set in __init__ are what classification and resolution actually read, which is the same duck-typed pattern AWSBedrockProvider uses when it reclassifies itself from the model it is serving.

Source code in src/symfonic/core/testing/deterministic.py
def __init__(
    self,
    family: str = "unknown",
    *,
    response: str = "Mock response",
    tool_calls: list[dict[str, Any]] | None = None,
    structured_response: dict[str, Any] | None = None,
    usage: dict[str, Any] | None = None,
    bind_tools_supported: bool = True,
    tool_choice_supported: bool = True,
) -> None:
    if family not in PROVIDER_FAMILIES:
        raise ValueError(
            f"unsupported provider family {family!r}; expected one of "
            f"{sorted(PROVIDER_FAMILIES)}"
        )
    self._symfonic_provider_family = family
    self._symfonic_default_model = _FAMILY_DEFAULT_MODEL[family]
    self._response = response
    self._tool_calls = tool_calls
    self._structured_response = structured_response
    self._usage = usage
    self._bind_tools_supported = bind_tools_supported
    self._tool_choice_supported = tool_choice_supported

refusal_reason

refusal_reason(config: ModelConfig) -> str | None

A refusal an operator cannot explain is a refusal they cannot fix.

Source code in src/symfonic/core/testing/deterministic.py
def refusal_reason(self, config: ModelConfig) -> str | None:
    """A refusal an operator cannot explain is a refusal they cannot fix."""
    if self.supports_forced_tool_choice(config):
        return None
    return (
        f"the {self._symfonic_provider_family} wire rejects a forced "
        "tool_choice while extended thinking is enabled (HTTP 400 per the "
        "published constraint)"
    )

supports_forced_tool_choice

supports_forced_tool_choice(config: ModelConfig) -> bool

Refuse exactly where the declared family's wire refuses.

Pure and derived: the constraint is a property of the wire, so a family whose row does not carry it must not acquire it here.

Source code in src/symfonic/core/testing/deterministic.py
def supports_forced_tool_choice(self, config: ModelConfig) -> bool:
    """Refuse exactly where the declared family's wire refuses.

    Pure and derived: the constraint is a property of the wire, so a
    family whose row does not carry it must not acquire it here.
    """
    if not self._row().thinking_blocks_forced_tool_choice:
        return True
    return not bool(getattr(config, "thinking", None))