Summary
In the V2 chat composer, the Model, Agent and Reasoning pickers are all independently live. Selecting an agent leaves a model showing as selected and a reasoning level selectable, even though an agent can act on neither.
The visible inconsistency is the smaller half. The request also sends all three fields together, and the server reads a model identity sent alongside agent_info as a deliberate override — so an agent could answer through a different model than the one it is configured with, silently.
Steps to reproduce
- Open the V2 chat page (
/v2/chat).
- Pick a model from the Model picker.
- Pick an agent from the Agent picker.
- Observe that the model still shows as selected and the Reasoning picker is still offered.
- Send a message.
Expected: the agent answers using its own configured deployment, and the composer does not offer choices that cannot apply.
Actual: the composer presents a model and a reasoning level as if they were in force, and the request carries them, which suppresses the agent's own model resolution.
Root cause
Two independent halves.
The controls were never told about each other
Composer.tsx rendered the model picker on gating.showModelPicker (false only during image generation) and the reasoning picker on model support alone. Neither consulted the agent selection.
They are not independent:
- An agent answers with its own deployment —
deployment = agent.get("azure_openai_gpt_deployment") in semantic_kernel_loader.py.
reasoning_effort only ever reaches the direct-model call parameters, via _resolve_reasoning_effort_for_model into api_params / stream_params in route_backend_chats.py.
The request sent both halves, which the server reads as an override
chatStore.sendMessage assigned the model identity unconditionally, then appended agent_info and reasoning_effort. The route only lets an agent request choose its own model when no model identity was sent:
# route_backend_chats.py
should_use_default_model = (
_has_chat_agent_selection(request_agent_info)
and settings.get('enable_multi_model_endpoints', False)
and not data.get('model_id')
and not data.get('model_endpoint_id')
)
Because V2 always sent model_id and model_endpoint_id, that branch never fired. The route has agent-without-a-model handling for every configuration — the multi-endpoint default, the first APIM deployment, and the configured default model — and V2 reached none of it.
Impact
- Correctness: an agent could answer through the wrong model, with no error and nothing in the UI to indicate it.
- Usability: two controls appeared to offer choices that had no effect.
Affects the V2 interface only. The classic interface hides the model picker and the reasoning button when agents are enabled, so the confusing presentation is V2-specific.
Note on the classic client
V1 has a related asymmetry worth recording: getCurrentAgentSelection checks that agent mode is active, but getCurrentModelSelection reads the model select without checking that agent mode has hidden it — so V1 posts a model alongside an agent too. Its UI hides the picker, so it is not user-visible there, but the request shape has the same defect.
Resolution
Fixed in v0.261.034 by #1391.
- Selecting an agent renders the model picker as its plain
Model placeholder in muted styling, with a tooltip naming the agent. The picker stays clickable, and its menu still marks the retained model.
- Choosing a model clears the agent; choosing an agent retains the model rather than clearing it.
- The reasoning picker is hidden while an agent is selected, and also during image generation, matching
updateReasoningButtonVisibility in the classic client.
- With an agent selected, the request carries
agent_info and nothing else.
The rule lives in one module, buildSelectionFields, read by both the toolbar and the request builder — the original defect arose because each decided separately.
Documented in docs/explanation/fixes/V2_AGENT_MODEL_EXCLUSIVITY_FIX.md. Covered by functional_tests/test_v2_agent_model_exclusivity.py and functional_tests/test_v2_agent_model_exclusivity_logic.ts.
Known gaps left open
Two related pre-existing inconsistencies were found while tracing this and deliberately left out of the fix:
- Image generation hides the model picker but leaves the agent picker visible, and the classic client forces
image_generation = false when an agent is explicitly tagged.
retryMessage is invoked with no options from MessageActions.tsx, so a retry uses server defaults rather than the composer's current selection.
Summary
In the V2 chat composer, the Model, Agent and Reasoning pickers are all independently live. Selecting an agent leaves a model showing as selected and a reasoning level selectable, even though an agent can act on neither.
The visible inconsistency is the smaller half. The request also sends all three fields together, and the server reads a model identity sent alongside
agent_infoas a deliberate override — so an agent could answer through a different model than the one it is configured with, silently.Steps to reproduce
/v2/chat).Expected: the agent answers using its own configured deployment, and the composer does not offer choices that cannot apply.
Actual: the composer presents a model and a reasoning level as if they were in force, and the request carries them, which suppresses the agent's own model resolution.
Root cause
Two independent halves.
The controls were never told about each other
Composer.tsxrendered the model picker ongating.showModelPicker(false only during image generation) and the reasoning picker on model support alone. Neither consulted the agent selection.They are not independent:
deployment = agent.get("azure_openai_gpt_deployment")insemantic_kernel_loader.py.reasoning_effortonly ever reaches the direct-model call parameters, via_resolve_reasoning_effort_for_modelintoapi_params/stream_paramsinroute_backend_chats.py.The request sent both halves, which the server reads as an override
chatStore.sendMessageassigned the model identity unconditionally, then appendedagent_infoandreasoning_effort. The route only lets an agent request choose its own model when no model identity was sent:Because V2 always sent
model_idandmodel_endpoint_id, that branch never fired. The route has agent-without-a-model handling for every configuration — the multi-endpoint default, the first APIM deployment, and the configured default model — and V2 reached none of it.Impact
Affects the V2 interface only. The classic interface hides the model picker and the reasoning button when agents are enabled, so the confusing presentation is V2-specific.
Note on the classic client
V1 has a related asymmetry worth recording:
getCurrentAgentSelectionchecks that agent mode is active, butgetCurrentModelSelectionreads the model select without checking that agent mode has hidden it — so V1 posts a model alongside an agent too. Its UI hides the picker, so it is not user-visible there, but the request shape has the same defect.Resolution
Fixed in v0.261.034 by #1391.
Modelplaceholder in muted styling, with a tooltip naming the agent. The picker stays clickable, and its menu still marks the retained model.updateReasoningButtonVisibilityin the classic client.agent_infoand nothing else.The rule lives in one module,
buildSelectionFields, read by both the toolbar and the request builder — the original defect arose because each decided separately.Documented in
docs/explanation/fixes/V2_AGENT_MODEL_EXCLUSIVITY_FIX.md. Covered byfunctional_tests/test_v2_agent_model_exclusivity.pyandfunctional_tests/test_v2_agent_model_exclusivity_logic.ts.Known gaps left open
Two related pre-existing inconsistencies were found while tracing this and deliberately left out of the fix:
image_generation = falsewhen an agent is explicitly tagged.retryMessageis invoked with no options fromMessageActions.tsx, so a retry uses server defaults rather than the composer's current selection.