Skip to content

Commit 3a435ad

Browse files
jsong468Copilot
andauthored
FEAT: Allow creation and display of RoundRobinTarget in GUI (#1944)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b9f7c08 commit 3a435ad

19 files changed

Lines changed: 1676 additions & 72 deletions

frontend/src/components/Chat/TargetBadge.styles.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,13 @@ export const useTargetBadgeStyles = makeStyles({
8080
overflowX: 'auto',
8181
boxSizing: 'border-box',
8282
},
83+
/** A single inner target entry in the tooltip's Inner Targets section. */
84+
innerTargetItem: {
85+
display: 'flex',
86+
flexDirection: 'column',
87+
gap: tokens.spacingVerticalXXS,
88+
padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalXS}`,
89+
backgroundColor: tokens.colorNeutralBackground2,
90+
borderRadius: tokens.borderRadiusSmall,
91+
},
8392
})

frontend/src/components/Chat/TargetBadge.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,45 @@ describe('TargetBadge', () => {
7373
).not.toThrow()
7474
expect(screen.getByTestId('target-badge')).toHaveTextContent('TextTarget')
7575
})
76+
77+
it('shows count in display name for RoundRobinTarget with inner targets', () => {
78+
const rrTarget: TargetInstance = {
79+
target_registry_name: 'rr_test',
80+
target_type: 'RoundRobinTarget',
81+
model_name: 'gpt-4o',
82+
inner_targets: [
83+
{ target_registry_name: 'a', target_type: 'OpenAIChatTarget', model_name: 'gpt-4o' },
84+
{ target_registry_name: 'b', target_type: 'OpenAIChatTarget', model_name: 'gpt-4o' },
85+
{ target_registry_name: 'c', target_type: 'OpenAIChatTarget', model_name: 'gpt-4o' },
86+
],
87+
}
88+
render(
89+
<TestWrapper>
90+
<TargetBadge target={rrTarget} />
91+
</TestWrapper>
92+
)
93+
const badge = screen.getByTestId('target-badge')
94+
expect(badge).toHaveTextContent('RoundRobinTarget (gpt-4o ×3)')
95+
})
96+
97+
it('prefers underlying_model_name over model_name for RoundRobinTarget badge', () => {
98+
const rrTarget: TargetInstance = {
99+
target_registry_name: 'rr_mixed',
100+
target_type: 'RoundRobinTarget',
101+
model_name: 'gpt-4o-japan-nilfilter',
102+
underlying_model_name: 'gpt-4o',
103+
inner_targets: [
104+
{ target_registry_name: 'a', target_type: 'OpenAIChatTarget', model_name: 'deploy-1' },
105+
{ target_registry_name: 'b', target_type: 'OpenAIChatTarget', model_name: 'deploy-2' },
106+
],
107+
}
108+
render(
109+
<TestWrapper>
110+
<TargetBadge target={rrTarget} />
111+
</TestWrapper>
112+
)
113+
const badge = screen.getByTestId('target-badge')
114+
// Should show the underlying model, not the deployment name
115+
expect(badge).toHaveTextContent('RoundRobinTarget (gpt-4o ×2)')
116+
})
76117
})

frontend/src/components/Chat/TargetBadge.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,23 @@ function formatParams(params?: Record<string, unknown> | null): string {
3333

3434
export default function TargetBadge({ target }: TargetBadgeProps) {
3535
const styles = useTargetBadgeStyles()
36-
const displayName = target.model_name
37-
? `${target.target_type} (${target.model_name})`
38-
: target.target_type
36+
const innerTargets = target.inner_targets ?? []
37+
const isRoundRobin = innerTargets.length > 0
38+
39+
// For RoundRobinTarget, prefer underlying_model_name because inner targets share
40+
// the same underlying model but may have different deployment names (model_name).
41+
// For regular targets, use model_name as before.
42+
const badgeModel = isRoundRobin
43+
? (target.underlying_model_name ?? target.model_name)
44+
: target.model_name
45+
const displayName = isRoundRobin
46+
? badgeModel
47+
? `${target.target_type} (${badgeModel} ×${innerTargets.length})`
48+
: `${target.target_type}${innerTargets.length})`
49+
: target.model_name
50+
? `${target.target_type} (${target.model_name})`
51+
: target.target_type
52+
3953
const showUnderlying =
4054
target.underlying_model_name &&
4155
target.model_name &&
@@ -47,6 +61,9 @@ export default function TargetBadge({ target }: TargetBadgeProps) {
4761
const outputModalities = target.capabilities?.supported_output_modalities ?? []
4862
const params = formatParams(target.target_specific_params)
4963

64+
// Extract weights from params so we can show them next to each inner target
65+
const weights = target.target_specific_params?.weights as number[] | undefined
66+
5067
const tooltipContent = (
5168
<div className={styles.tooltipBody}>
5269
<div className={styles.tooltipHeader}>
@@ -91,6 +108,28 @@ export default function TargetBadge({ target }: TargetBadgeProps) {
91108
</div>
92109
</div>
93110
)}
111+
{/* Inner targets section — only shown for composite targets like RoundRobinTarget */}
112+
{isRoundRobin && (
113+
<div className={styles.tooltipSection}>
114+
<span className={styles.sectionLabel}>Inner Targets ({innerTargets.length})</span>
115+
{innerTargets.map((inner, idx) => (
116+
<div key={inner.target_registry_name} className={styles.innerTargetItem}>
117+
<Text size={200} weight="semibold">
118+
#{idx + 1}{weights?.[idx] != null ? ` (weight: ${weights[idx]})` : ''}
119+
</Text>
120+
<Text size={200}>
121+
{inner.target_type}
122+
{inner.model_name ? ` — ${inner.model_name}` : ''}
123+
</Text>
124+
{inner.endpoint && (
125+
<Text className={styles.endpointText} size={200}>
126+
{inner.endpoint}
127+
</Text>
128+
)}
129+
</div>
130+
))}
131+
</div>
132+
)}
94133
{params && (
95134
<div className={styles.tooltipSection}>
96135
<span className={styles.sectionLabel}>Parameters</span>

frontend/src/components/Config/CreateTargetDialog.styles.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,19 @@ export const useCreateTargetDialogStyles = makeStyles({
1414
overflowWrap: 'anywhere',
1515
wordBreak: 'break-word',
1616
},
17+
/** Container for the list of selected inner targets in the RoundRobin form. */
18+
selectedTargetsList: {
19+
display: 'flex',
20+
flexDirection: 'column',
21+
gap: tokens.spacingVerticalXS,
22+
},
23+
/** A single row in the selected targets list: target name + weight + remove button. */
24+
selectedTargetRow: {
25+
display: 'flex',
26+
alignItems: 'center',
27+
gap: tokens.spacingHorizontalS,
28+
padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
29+
backgroundColor: tokens.colorNeutralBackground2,
30+
borderRadius: tokens.borderRadiusSmall,
31+
},
1732
})

0 commit comments

Comments
 (0)