FEAT: Allow creation and display of RoundRobinTarget in GUI#1944
FEAT: Allow creation and display of RoundRobinTarget in GUI#1944jsong468 wants to merge 5 commits into
RoundRobinTarget in GUI#1944Conversation
… target creation and display in gui
| weights: list[int] | None = params.get("weights") or None | ||
|
|
||
| # The constructor validates same-class, same-config, behavioral consistency, etc. | ||
| return RoundRobinTarget(targets=resolved_targets, weights=weights) |
There was a problem hiding this comment.
The dedup logic you added to _auto_group_targets (skipping members with the same ComponentIdentifier.hash) should apply here too. Right now the user-driven flow happily accepts duplicates — easy to repro in the GUI:
- Pick RoundRobinTarget.
- Add
azure_openai_gpt4oandopenai_chat— bothOpenAIChatTarget (gpt-4o-japan-nilfilter)pointing at the same endpoint.
The created RR's inner_targets come back with identical unique_names (OpenAIChatTarget::5bf9656e ×2), so the "round-robin" just hits the same target twice. The picker doesn't dedupe by identifier hash either, which makes this very easy to do by accident with the airt init (which often registers the same target under multiple aliases).
Cheapest fix: before constructing the RR, dedupe resolved_targets by target.get_identifier().hash and raise if < 2 distinct entries remain. Same shape as the seen_hashes block in the initializer.
There was a problem hiding this comment.
implemented in the most recent commit. but, I think the better solution might be to not show identical targets when the first target is chosen (following the pattern of not allowing non-compatible targets to be selected). We would have to use identifier_hash though and expose it on TargetInstance.
| style={{ width: '60px' }} | ||
| onChange={(_, data) => { | ||
| const w = parseInt(data.value, 10) | ||
| if (!isNaN(w) && w > 0) updateInnerTargetWeight(sel.registryName, w) |
There was a problem hiding this comment.
Minor: parseInt(data.value, 10) silently truncates decimal input (2.5 becomes 2, 1e10 becomes 1) and there's no upper bound, so a typo like 99999999999 is accepted and forwarded to the backend. The HTML min="1" is also advisory only — 0 and negatives are filtered in JS, which is fine, but a user typing 0 sees the value visually revert with no feedback.
Adding step="1" + max="1000" (or whatever sane cap) and showing a small validation message when the parse fails would make the weight field behave the way the UI implies.
Mirrors the dedup logic in TargetInitializer._auto_group_targets so the GUI/API flow can't produce a round-robin that hits the same underlying endpoint twice. Weights for deduped entries are dropped alongside them. If fewer than 2 distinct targets remain after dedup, raise a clear error listing the skipped duplicate names. Addresses review comment on PR microsoft#1944. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, fix isCompatible fallback
Addresses Roman's review comment on isCompatible() in CreateTargetDialog:
1. Hash-based dedup in the picker (Option A from review discussion).
- Surfaces ComponentIdentifier.hash as a new optional identifier_hash field
on the TargetInstance DTO.
- target_object_to_instance now populates it; the existing recursive
_build_inner_targets call propagates it to inner targets for free.
- eligibleTargets in CreateTargetDialog filters out candidates whose
identifier_hash matches any already-selected target's hash. Targets with
null/undefined hash are NOT collapsed together (safe handling).
- This plugs the last gap left by isCompatible: behavioral-param mismatches
are already pre-filtered, but two registry entries that resolve to the
same backend config (same hash) were still pickable and would hit the
service-layer dedup error from the previous commit.
2. Fix isCompatible() fallback bug (Roman's specific repro).
- Adds effectiveUnderlyingModel(t) helper using || (catches both null AND
empty string, matching the backend's �alue is None or value == "" rule
in RoundRobinTarget._resolve_param).
- isCompatible() now compares effective underlying models so that, for
example, picking azure_foundry_deepseek (DeepSeek-R1, underlying=None)
no longer shows azure_foundry_mistral_large / google_gemini / ollama
as compatible just because all four have underlying_model_name=None.
3. Surface RFC 7807 backend detail on failed creation.
- Replaces the catch block's err.message with toApiError(err).detail so the
dialog shows the actual validation message (e.g. "Behavioral parameter
'underlying_model_name' differs...") instead of the generic axios
"Request failed with status code 400".
- Updates one pre-existing test ("non-Error exceptions") that expected a
hard-coded "Failed to create target" string — toApiError now surfaces the
thrown string verbatim, which is strictly more informative.
4. Extend the frontend/backend drift guard.
- Adds test_target_eval_param_fallbacks_match_frontend asserting
TARGET_EVAL_PARAM_FALLBACKS == {'underlying_model_name': 'model_name'}
so future fallback additions surface a clear failure pointing at
effectiveUnderlyingModel() in CreateTargetDialog.tsx.
Tests added:
- backend: test_target_eval_param_fallbacks_match_frontend
- backend: extended test_maps_target_with_identifier to cover identifier_hash
- frontend: filters duplicate-by-identifier-hash targets out of the picker
- frontend: applies underlying_model_name -> model_name fallback when filtering
- frontend: surfaces the backend error detail when target creation fails
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Roman's review comment #2: parseInt("2.5") silently truncates to 2, parseInt("1e10") returns 1, and 99999999999 was accepted with no upper bound. Typing 0 silently reverted. Changes: - Extract parseWeight + MAX_WEIGHT=1000 to weightValidation.ts - Strict regex-based parser rejects empty, non-integers (2.5, 1e10, -3), values < 1, and values > MAX_WEIGHT - Refactor SelectedInnerTarget to single source of truth (weightInput: string), avoiding silent revert - Add step='1', min='1', max='1000', aria-invalid, aria-label to Input; render alert below row on invalid - Disable Create when any weight is invalid; re-validate in handleSubmit to defend against Enter-key bypass Tests: 9 parseWeight unit tests + 2 dialog integration tests (alert+disabled, end-to-end submit with parsed ints). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Improves the GUI experience for
RoundRobinTargetacross three surfaces: the Target Configuration table, the Create Target dialog, and the Chat window target badge. Also fixes duplicate inner targets inTargetsInitializercaused by overlapping.enventries.Target Configuration Table
model_nameonly when all inner targets have the same deployment name; otherwise shows "—" to avoid misleading display.InnerTargetRowscomponent: Sub-row rendering is shared between the active summary and main table to eliminate duplication.Create Target Dialog
RoundRobinTargetto the target type dropdown. When selected, the endpoint/model/auth fields are replaced with a target picker that lets users select existing targets from the registry.target_type,underlying_model_name,temperature,top_p). Backend performs definitive validation on submit..pyrit_confauto-populate hint is hidden for RoundRobinTarget since users are picking existing targets, not configuring new ones.Chat Window Badge & Tooltip
RoundRobinTarget (gpt-4o ×3)usingunderlying_model_name(the shared model) rather than a potentially inconsistent deployment name.Backend Changes
TargetInstancemodel: Added optionalinner_targets: list[TargetInstance]field for composite target support.model_nameonly when all inner targets agree; hoistsunderlying_model_namewhen uniform.create_target_asynchandlesRoundRobinTargetby resolvingtarget_registry_namesfrom the registry and delegating all validation to theRoundRobinTargetconstructor._auto_group_targetsnow deduplicates inner targets byComponentIdentifier.hashbefore creating round-robin groups. This prevents duplicate entries when.envfiles have overlapping configurations that resolve to identical targets (e.g., two env var blocks pointing to the same endpoint + model + key). Previously, these duplicates wasted rotation slots in the round-robin without adding any load-balancing benefit.Drift Guard
TestFrontendBackendCompatibilitySync— a backend test that assertsTARGET_EVAL_PARAMSmatches the expected set. If someone adds a behavioral param, this test fails with a message directing them to update the frontendisCompatible()function.Tests and Documentation
Backend Tests
test_mappers.py): 4 tests — inner target population, model name hoisting when uniform, model name omission when divergent, null for non-composite targets.test_target_service.py): 3 tests — registry name resolution with mocked constructor, <2 names error, unknown name error.test_targets_initializer.py): 1 test — deduplication of identical targets in auto-grouping verifies that a duplicate target (same endpoint + model + key registered under a different name) is excluded from the round-robin group.test_target_service.py): 1 test —TARGET_EVAL_PARAMSmatches frontendisCompatible()fields.Frontend Tests
TargetTable.test.tsx): 2 tests — expand button renders only for RoundRobinTarget, click expands sub-rows with inner target details.TargetBadge.test.tsx): 2 tests —×Ncount in display name,underlying_model_namepreferred overmodel_name.CreateTargetDialog.test.tsx): 2 tests — RoundRobinTarget shows target picker (hides endpoint fields), Create button disabled with <2 selections.Manual Testing