fix(auxiliary): treat explicit model:auto sentinel, not just cfg_model

'auto' is a sentinel meaning "inherit from main runtime / auto-detect",
not a literal model id -- already handled for cfg_model (config-derived)
in _resolve_task_provider_model, but not for the explicit `model` kwarg.

MoA reference/aggregator slots (agent/moa_loop.py's _slot_runtime) forward
a preset's `model:` field as this explicit argument rather than through
auxiliary.<task> config, so a MoA preset configured with `model: auto`
(a natural thing to try given the existing auxiliary.*.model: auto
convention) reached this function as the explicit `model` arg and took
the `model or cfg_model` branch, bypassing the cfg_model-only sentinel
check entirely -- sending the literal string "auto" to the wire as a
model id.

Normalize both the explicit `model` and `cfg_model` the same way, fixing
this at the single chokepoint every caller (MoA included) already goes
through, rather than patching moa_loop.py separately.
This commit is contained in:
srojk34 2026-07-01 14:01:34 +03:00 committed by Teknium
parent 0dbf639bc8
commit 2962ba2b7b
2 changed files with 58 additions and 0 deletions

View file

@ -6369,6 +6369,15 @@ def _resolve_task_provider_model(
# which downstream consumers like ContextCompressor accept as the task output.
# The provider-side 'auto' is handled in _resolve_auto() via main_runtime
# fallback, so dropping cfg_model to None here lets that path do its job.
#
# The explicit `model` kwarg needs the identical normalization: MoA slots
# (agent/moa_loop.py's _slot_runtime) forward a preset's `model:` field as
# this explicit argument rather than through auxiliary.<task> config, so a
# user-configured `model: auto` on a MoA reference/aggregator slot reaches
# this function here, not as cfg_model. Only normalizing cfg_model let that
# literal "auto" slip through via `model or cfg_model` below.
if model and model.lower() == "auto":
model = None
if cfg_model and cfg_model.lower() == "auto":
cfg_model = None

View file

@ -394,6 +394,55 @@ class TestResolveTaskProviderModel:
assert resolved_provider == "anthropic"
assert model == "claude-haiku-4-5-20251001"
def test_explicit_model_auto_sentinel_is_normalized(self):
"""MoA slots (agent/moa_loop.py's _slot_runtime) forward a preset's
`model:` field as the explicit `model` kwarg here, not through
auxiliary.<task> config. Only cfg_model was normalized before, so a
MoA reference/aggregator slot configured with `model: auto` sent the
literal string "auto" to the wire as a model id."""
resolved_provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
provider="anthropic",
model="auto",
)
assert resolved_provider == "anthropic"
assert model is None
def test_explicit_model_auto_sentinel_case_insensitive(self):
resolved_provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
provider="anthropic",
model="AUTO",
)
assert model is None
def test_explicit_model_auto_falls_back_to_cfg_model(self, monkeypatch):
"""When the explicit model is the "auto" sentinel, it must not shadow
a real configured task model the `model or cfg_model` fallback
chain should still reach cfg_model exactly as if model had been
omitted entirely."""
monkeypatch.setattr(
"agent.auxiliary_client._get_auxiliary_task_config",
lambda _task: {"provider": "openai", "model": "gpt-real-model"},
)
resolved_provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
task="moa_reference",
model="auto",
)
assert model == "gpt-real-model"
def test_non_auto_model_is_unaffected(self):
"""Regression guard: a real model name must not be touched by the
sentinel normalization."""
resolved_provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
provider="openai",
model="gpt-4o-mini",
)
assert model == "gpt-4o-mini"
class TestMoaAggregatorSharedResolution:
"""The shared MoA→aggregator helper and the layers that consume it.