From b1695ed758959564e0af7eae733c312445d9c772 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Fri, 30 Oct 2026 23:04:25 +0800 Subject: [PATCH] Set the minimum generation speed when creating a provider The create-provider dialog gets the same row as the edit dialog: "Stop when generation is slower than [ ] tokens/s for [20] s", checked as typed, a blank speed for off. The row is one component shared by both dialogs; the create request always carries both fields. Co-Authored-By: Claude Opus 5.5 --- .../routes/gateway/provider-dialogs.test.tsx | 32 ++++++ web/src/routes/gateway/provider-dialogs.tsx | 101 ++++++++++++------ 2 files changed, 102 insertions(+), 31 deletions(-) diff --git a/web/src/routes/gateway/provider-dialogs.test.tsx b/web/src/routes/gateway/provider-dialogs.test.tsx index 8b255375..eaba99f4 100644 --- a/web/src/routes/gateway/provider-dialogs.test.tsx +++ b/web/src/routes/gateway/provider-dialogs.test.tsx @@ -78,9 +78,41 @@ describe('CreateProviderDialog', () => { provider_type: 'bedrock', base_url: 'us-east-1', headers: [{ key: 'Authorization', value: 'Bearer ABSK-test' }], + min_speed_tokens_per_sec: null, + min_speed_window_secs: 20, }) }) + it('creates a provider with the slowest generation its streams may keep up', async () => { + const user = userEvent.setup() + renderCreate() + + await user.type(screen.getByLabelText('Name'), 'local-vllm') + const rate = screen.getByLabelText('Generation speed (tokens/s)') + const window = screen.getByLabelText('Window (seconds)') + expect(rate).toHaveValue('') + expect(window).toHaveValue('20') + await user.type(rate, '15') + await user.clear(window) + await user.type(window, '30') + await user.click(screen.getByRole('button', { name: 'Create Provider' })) + + expect(apiPost).toHaveBeenCalledWith( + '/api/admin/providers', + expect.objectContaining({ name: 'local-vllm', min_speed_tokens_per_sec: 15, min_speed_window_secs: 30 }), + ) + }) + + it('will not create a provider with a speed or window out of range', async () => { + const user = userEvent.setup() + renderCreate() + + await user.clear(screen.getByLabelText('Window (seconds)')) + await user.type(screen.getByLabelText('Window (seconds)'), '121') + expect(screen.getByText(/whole number from 1 to 1000/)).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Create Provider' })).toBeDisabled() + }) + it('tests a Bedrock provider with the access keys typed into the form', async () => { const user = userEvent.setup() renderCreate() diff --git a/web/src/routes/gateway/provider-dialogs.tsx b/web/src/routes/gateway/provider-dialogs.tsx index 64f32def..da770d84 100644 --- a/web/src/routes/gateway/provider-dialogs.tsx +++ b/web/src/routes/gateway/provider-dialogs.tsx @@ -179,6 +179,53 @@ const headerPresets = (t: (key: string) => string) => [ // Create Provider Dialog // --------------------------------------------------------------------------- +/** + * "Stop when generation is slower than [ ] tokens/s for [20] s": the + * provider's minimum generation speed, checked as it is typed. + */ +function MinSpeedRow({ id, rate, window, onRateChange, onWindowChange }: { + id: string; + rate: string; + window: string; + onRateChange: (v: string) => void; + onWindowChange: (v: string) => void; +}) { + const { t } = useTranslation(); + const valid = minSpeedFields(rate, window) !== null; + return ( +
+ +
+ {t('providers.minSpeedBefore')} + onRateChange(e.target.value)} + /> + {t('providers.minSpeedMiddle')} + onWindowChange(e.target.value)} + /> + {t('providers.minSpeedAfter')} +
+ {valid ? ( +

{t('providers.minSpeedHint')}

+ ) : ( +

{t('providers.minSpeedInvalid')}

+ )} +
+ ); +} + interface CreateProviderDialogProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -196,6 +243,8 @@ export function CreateProviderDialog({ open, onOpenChange, onSuccess }: CreatePr const [bedrockAuthMode, setBedrockAuthMode] = useState('aksk'); const [awsAccessKeyId, setAwsAccessKeyId] = useState(''); const [awsSecretKey, setAwsSecretKey] = useState(''); + const [minSpeedRate, setMinSpeedRate] = useState(''); + const [minSpeedWindow, setMinSpeedWindow] = useState(String(DEFAULT_MIN_SPEED_WINDOW_SECS)); const [formError, setFormError] = useState(''); const [submitting, setSubmitting] = useState(false); const [testing, setTesting] = useState(false); @@ -231,12 +280,17 @@ export function CreateProviderDialog({ open, onOpenChange, onSuccess }: CreatePr setBedrockAuthMode('aksk'); setAwsAccessKeyId(''); setAwsSecretKey(''); + setMinSpeedRate(''); + setMinSpeedWindow(String(DEFAULT_MIN_SPEED_WINDOW_SECS)); setFormError(''); setTestResult(null); }; + const minSpeed = minSpeedFields(minSpeedRate, minSpeedWindow); + const handleCreate = async (e: FormEvent) => { e.preventDefault(); + if (!minSpeed) return; setFormError(''); setSubmitting(true); try { @@ -247,6 +301,7 @@ export function CreateProviderDialog({ open, onOpenChange, onSuccess }: CreatePr base_url: effectiveBaseUrl, headers: headers.filter(([k]) => k.trim()).map(([k, v]) => ({ key: k, value: v })), ...(awsKeys ? { config: awsKeys } : {}), + ...minSpeed, }); onOpenChange(false); resetForm(); @@ -372,6 +427,13 @@ export function CreateProviderDialog({ open, onOpenChange, onSuccess }: CreatePr presets={headerPresets(t)} /> + {formError && ( @@ -389,7 +451,7 @@ export function CreateProviderDialog({ open, onOpenChange, onSuccess }: CreatePr setTestResult, )} /> -