symfonic.services.models.resolution¶
resolution ¶
One precedence chain for "which model serves this call?".
Three independent answers shipped before T3.1.1:
SymfonicAgent.resolve_model_config(role)read the staticrole_modelstable and nothing else._AgentModelResolver.resolve(ctx)layered per-dispatch policies and the adopter hook over anAgentConfig.modelsnapshot, and could not see roles.agent.backend.model.resolve_model_config(provider)asked the provider for its declared default, and could not see either of the above — which is why the simple facade could not honour a role at all.
Each was correct about its own layer and blind to the others, so "why did this
call use that model?" had no single answer. :class:ModelResolutionService
composes all five sources in one ordered chain and records which one won.
ModelResolutionService ¶
ModelResolutionService(provider: Any, *, default_config: ModelConfig | None = None, role_models: Mapping[str, ModelConfig] | None = None, dispatch_resolver: Any = None)
Resolve a role (and optionally one dispatch) to a model and its capabilities.
Precedence, first match wins:
dispatch_resolver(ctx)— consulted only when a dispatch context is supplied, so a service constructed with a resolver still answers static questions statically.role_models[role]— the static per-role table.default_config— the caller's explicit snapshot default.- the provider's own declaration (
default_model_config/_symfonic_default_model), followed through routing wrappers. ModelConfig()— the framework default.
Source code in src/symfonic/services/models/resolution.py
role_models
property
¶
Read-only view of the role table this service was built with.
resolve ¶
Resolve role — and, when a context is given, this dispatch.
Source code in src/symfonic/services/models/resolution.py
resolve_config ¶
The config alone, for call sites that need nothing else.
ResolvedModel
dataclass
¶
ResolvedModel(config: ModelConfig, role: str, source: ResolutionSource, family: ProviderFamily, capabilities: ProviderCapabilities)
One resolution, with the reason it came out that way.
source is the load-bearing field. A resolution that cannot say why it
chose a model is indistinguishable from a resolution that ignored the
caller's configuration, which is the class of bug this service exists to
make impossible to ship silently.
provider_default_config ¶
The ModelConfig a provider declares for callers that name none.
Resolution order, first match wins:
provider.default_model_config— aModelConfigor a zero-argument callable returning one. The full seam: model, temperature and token budget pinned in one place.provider._symfonic_default_model— a model-name string. The cheap seam, and the one every shipped adapter declares.ModelConfig()— the framework default.
Routing wrappers are followed through _default / default, so a
router wrapping one provider inherits that provider's declaration.
This is the function symfonic.agent.backend.model.resolve_model_config
now is: the simple Agent facade has no model= parameter, so
without it Agent(OpenAIProvider()) would ask OpenAI for
claude-sonnet-4-5 and fail at request time with model_not_found.