Skip to content

symfonic.services.models.descriptors

descriptors

The provider capability table โ€” one row per shipped adapter.

Before this module the same facts were spelled out three times: as call-site literals in providers.py (timeout_kwarg="timeout"), as supports_* method bodies, and as family comparisons in the cache layer. A row here is the declaration; tests/services/models/test_provider_contract.py checks each row against the live adapter, and providers.py reads the kwarg names back out of the table so the two cannot drift.

Stdlib-only at module scope on purpose โ€” symfonic.core.providers imports this module, so any import of symfonic.core from here would close a cycle.

ProviderDescriptor dataclass

ProviderDescriptor(label: str, family: ProviderFamily, default_model: str | None, thinking: bool, streaming: bool, thinking_blocks_forced_tool_choice: bool, timeouts: TimeoutBinding, sampling: SamplingSupport)

The static half of a provider's capabilities.

default_model = None is an explicit abstention, not an omission: a gateway adapter (OpenRouter, Bedrock, the OAuth adapters) serves whatever model the caller names, so declaring one would be a guess. The contract checker requires the declaration to exist, which is what turns "nobody thought about it" into a reviewed decision.

cache_dialect_for

cache_dialect_for(family: ProviderFamily) -> CacheDialect

The prompt-cache annotation dialect for a wire family.

Total over the closed family set, and derived rather than assigned: an adapter cannot claim cache_control support without claiming the Anthropic wire family that actually accepts it.

Source code in src/symfonic/services/models/descriptors.py
def cache_dialect_for(family: ProviderFamily) -> CacheDialect:
    """The prompt-cache annotation dialect for a wire family.

    Total over the closed family set, and derived rather than assigned: an
    adapter cannot claim ``cache_control`` support without claiming the
    Anthropic wire family that actually accepts it.
    """
    return _CACHE_DIALECTS.get(family, "none")

descriptor_for_family

descriptor_for_family(family: ProviderFamily) -> ProviderDescriptor

The fallback row for a family. Total over the closed family set.

Source code in src/symfonic/services/models/descriptors.py
def descriptor_for_family(family: ProviderFamily) -> ProviderDescriptor:
    """The fallback row for a family. Total over the closed family set."""
    return PROVIDER_DESCRIPTORS[_FAMILY_FALLBACK.get(family, "unknown")]

descriptor_for_provider

descriptor_for_provider(provider: object, resolved_config: Any = None) -> ProviderDescriptor

Best row for provider, descending routing wrappers like the classifier.

Shipped adapters match by class name; everything else falls back to the row for the family detect_provider_family assigns, so an adopter's own provider still gets a coherent (if generic) capability answer.

Source code in src/symfonic/services/models/descriptors.py
def descriptor_for_provider(
    provider: object, resolved_config: Any = None
) -> ProviderDescriptor:
    """Best row for ``provider``, descending routing wrappers like the classifier.

    Shipped adapters match by class name; everything else falls back to the row
    for the family ``detect_provider_family`` assigns, so an adopter's own
    provider still gets a coherent (if generic) capability answer.
    """
    from symfonic.services.models.family import detect_provider_family, leaf_provider

    leaf = leaf_provider(provider, resolved_config)
    label = PROVIDER_LABELS_BY_CLASS.get(type(leaf).__name__)
    if label is not None:
        return PROVIDER_DESCRIPTORS[label]
    return descriptor_for_family(detect_provider_family(provider, resolved_config))