Skip to content

Commit 48b9212

Browse files
committed
feat(FR-1726): Auto-populate ENV variables based on selected Inference Runtime Variant
1 parent f4bf222 commit 48b9212

File tree

2 files changed

+248
-24
lines changed

2 files changed

+248
-24
lines changed

react/src/components/EnvVarFormList.tsx

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
2-
import { Button, Form, FormItemProps, Input, InputRef } from 'antd';
2+
import {
3+
AutoComplete,
4+
Button,
5+
Form,
6+
FormItemProps,
7+
Input,
8+
InputRef,
9+
} from 'antd';
310
import { FormListProps } from 'antd/lib/form';
411
import { BAIFlex } from 'backend.ai-ui';
512
import _ from 'lodash';
613
import React, { useRef } from 'react';
714
import { useTranslation } from 'react-i18next';
815

16+
export interface EnvVarConfig {
17+
variable: string;
18+
placeholder?: string;
19+
required?: boolean;
20+
description?: string;
21+
}
22+
923
interface EnvVarFormListProps extends Omit<FormListProps, 'children'> {
1024
formItemProps?: FormItemProps;
25+
requiredEnvVars?: EnvVarConfig[];
26+
optionalEnvVars?: EnvVarConfig[];
1127
}
1228

1329
export interface EnvVarFormListValue {
@@ -17,11 +33,45 @@ export interface EnvVarFormListValue {
1733
// TODO: validation rule for duplicate variable name
1834
const EnvVarFormList: React.FC<EnvVarFormListProps> = ({
1935
formItemProps,
36+
requiredEnvVars,
37+
optionalEnvVars,
2038
...props
2139
}) => {
2240
const inputRef = useRef<InputRef>(null);
2341
const { t } = useTranslation();
2442
const form = Form.useFormInstance();
43+
44+
const allEnvVars = [
45+
...(requiredEnvVars || []).filter((env) => env && env.variable),
46+
...(optionalEnvVars || []).filter((env) => env && env.variable),
47+
];
48+
49+
const getPlaceholderForVariable = (variable: string) => {
50+
if (!variable || !allEnvVars.length) return 'Value';
51+
const envVarConfig = allEnvVars.find(
52+
(env) => env && env.variable === variable,
53+
);
54+
return envVarConfig?.placeholder || 'Value';
55+
};
56+
57+
const getAutoCompleteOptions = () => {
58+
const currentValues = form.getFieldValue(props.name) || [];
59+
const usedVariables = currentValues
60+
.filter((item: EnvVarFormListValue) => item && item.variable)
61+
.map((item: EnvVarFormListValue) => item.variable);
62+
63+
return (
64+
optionalEnvVars
65+
?.filter(
66+
(env) => env && env.variable && !usedVariables.includes(env.variable),
67+
)
68+
?.map((env) => ({
69+
value: env.variable,
70+
label: env.variable,
71+
})) || []
72+
);
73+
};
74+
2575
return (
2676
<Form.List {...props}>
2777
{(fields, { add, remove }) => {
@@ -71,18 +121,38 @@ const EnvVarFormList: React.FC<EnvVarFormListProps> = ({
71121
]}
72122
{...formItemProps}
73123
>
74-
<Input
75-
ref={index === fields.length - 1 ? inputRef : null}
76-
placeholder="Variable"
77-
onChange={() => {
78-
const fieldNames = fields.map((_field, index) => [
79-
props.name,
80-
index,
81-
'variable',
82-
]);
83-
form.validateFields(fieldNames);
84-
}}
85-
/>
124+
{optionalEnvVars && getAutoCompleteOptions().length > 0 ? (
125+
<AutoComplete
126+
placeholder="Variable"
127+
options={getAutoCompleteOptions()}
128+
onChange={() => {
129+
const fieldNames = fields.map((_field, index) => [
130+
props.name,
131+
index,
132+
'variable',
133+
]);
134+
form.validateFields(fieldNames);
135+
}}
136+
filterOption={(inputValue, option) =>
137+
option?.value
138+
.toLowerCase()
139+
.indexOf(inputValue.toLowerCase()) !== -1
140+
}
141+
/>
142+
) : (
143+
<Input
144+
ref={index === fields.length - 1 ? inputRef : null}
145+
placeholder="Variable"
146+
onChange={() => {
147+
const fieldNames = fields.map((_field, index) => [
148+
props.name,
149+
index,
150+
'variable',
151+
]);
152+
form.validateFields(fieldNames);
153+
}}
154+
/>
155+
)}
86156
</Form.Item>
87157
<Form.Item
88158
{...restField}
@@ -97,9 +167,19 @@ const EnvVarFormList: React.FC<EnvVarFormListProps> = ({
97167
},
98168
]}
99169
validateTrigger={['onChange', 'onBlur']}
170+
dependencies={[[props.name, name, 'variable']]}
100171
>
101172
<Input
102-
placeholder="Value"
173+
placeholder={(() => {
174+
const currentVariable = form.getFieldValue([
175+
props.name,
176+
name,
177+
'variable',
178+
]);
179+
return currentVariable
180+
? getPlaceholderForVariable(currentVariable)
181+
: 'Value';
182+
})()}
103183
// onChange={() => {
104184
// const valueFields = fields.map((field, index) => [
105185
// props.name,

react/src/components/ServiceLauncherPageContent.tsx

Lines changed: 154 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useValidateServiceName } from '../hooks/useValidateServiceName';
2020
import EnvVarFormList, {
2121
sanitizeSensitiveEnv,
2222
EnvVarFormListValue,
23+
EnvVarConfig,
2324
} from './EnvVarFormList';
2425
import ImageEnvironmentSelectFormItems, {
2526
ImageEnvironmentFormInput,
@@ -175,6 +176,126 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
175176

176177
const { getErrorMessage } = useErrorMessageResolver();
177178

179+
// Runtime variant environment variable configurations
180+
const RUNTIME_ENV_VAR_CONFIGS: Record<
181+
string,
182+
{
183+
requiredEnvVars?: EnvVarConfig[];
184+
optionalEnvVars?: EnvVarConfig[];
185+
}
186+
> = {
187+
vllm: {
188+
requiredEnvVars: [
189+
{
190+
variable: 'BACKEND_MODEL_NAME',
191+
placeholder: 'Enter model name (e.g., meta-llama/Llama-2-7b-chat-hf)',
192+
description: "Corresponds to vLLM's --model-name argument",
193+
},
194+
],
195+
optionalEnvVars: [
196+
{
197+
variable: 'VLLM_QUANTIZATION',
198+
placeholder: 'e.g., awq, gptq, fp8',
199+
description: "Corresponds to vLLM's --quantization argument",
200+
},
201+
{
202+
variable: 'VLLM_TP_SIZE',
203+
placeholder: 'Tensor parallel size (e.g., 2, 4, 8)',
204+
description: "Corresponds to vLLM's --tensor-parallel-size argument",
205+
},
206+
{
207+
variable: 'VLLM_PP_SIZE',
208+
placeholder: 'Pipeline parallel size (e.g., 2, 4)',
209+
description:
210+
"Corresponds to vLLM's --pipeline-parallel-size argument",
211+
},
212+
{
213+
variable: 'VLLM_EXTRA_ARGS',
214+
placeholder:
215+
'Additional vLLM arguments (e.g., --tokenizer=/models/custom-tokenizer --dtype=half)',
216+
description:
217+
'Used to pass additional vLLM arguments not mentioned above',
218+
},
219+
],
220+
},
221+
sglang: {
222+
requiredEnvVars: [
223+
{
224+
variable: 'BACKEND_MODEL_NAME',
225+
placeholder:
226+
'Enter model name (e.g., gpt-oss-20b, qwen3-8b-Gemini-3-2.5-Pro-Distill)',
227+
description: 'Model name for sglang',
228+
},
229+
],
230+
optionalEnvVars: [
231+
{
232+
variable: 'SGLANG_QUANTIZATION',
233+
placeholder: 'e.g., awq, awq_marlin, gptq, int4, fp8',
234+
description: 'Quantization method',
235+
},
236+
{
237+
variable: 'SGLANG_TP_SIZE',
238+
placeholder: 'Tensor parallel size (e.g., 1(default), 2, ...)',
239+
description: 'Tensor parallel size',
240+
},
241+
{
242+
variable: 'SGLANG_PP_SIZE',
243+
placeholder: 'Pipeline parallel size (e.g., 1(default), 2, ...)',
244+
description: 'Pipeline parallel size',
245+
},
246+
{
247+
variable: 'SGLANG_EXTRA_ARGS',
248+
placeholder:
249+
'Extra arguments (e.g., --dtype bfloat16 --max-running-requests 100)',
250+
description: 'Extra arguments used for sglang cmd',
251+
},
252+
],
253+
},
254+
nim: {
255+
requiredEnvVars: [
256+
{
257+
variable: 'NGC_API_KEY',
258+
placeholder: 'Enter your NGC API key',
259+
description: 'NGC API key with NIM Model Registry access',
260+
},
261+
],
262+
optionalEnvVars: [],
263+
},
264+
};
265+
266+
// Helper function to set environment variables based on runtime variant
267+
const setEnvironmentVariablesForRuntimeVariant = (runtimeVariant: string) => {
268+
const runtimeConfig = RUNTIME_ENV_VAR_CONFIGS[runtimeVariant];
269+
if (!runtimeConfig) return;
270+
271+
const currentEnvVars = form.getFieldValue('envvars') || [];
272+
const existingVariables = currentEnvVars
273+
.filter((env: EnvVarFormListValue) => env && env.variable)
274+
.map((env: EnvVarFormListValue) => env.variable);
275+
276+
// Add required environment variables that don't exist
277+
const newRequiredEnvVars = (runtimeConfig.requiredEnvVars || [])
278+
.filter((envVar) => !existingVariables.includes(envVar.variable))
279+
.map((envVar) => ({
280+
variable: envVar.variable,
281+
value: '',
282+
}));
283+
284+
if (newRequiredEnvVars.length > 0) {
285+
const updatedEnvVars = [...currentEnvVars, ...newRequiredEnvVars];
286+
form.setFieldValue('envvars', updatedEnvVars);
287+
}
288+
};
289+
290+
// Handler for form values change
291+
const handleFormValuesChange = (
292+
changedValues: Partial<ServiceLauncherInput>,
293+
) => {
294+
if (changedValues.runtimeVariant && !endpoint) {
295+
setEnvironmentVariablesForRuntimeVariant(changedValues.runtimeVariant);
296+
}
297+
};
298+
178299
const endpoint = useFragment(
179300
graphql`
180301
fragment ServiceLauncherPageContentFragment on Endpoint {
@@ -766,6 +887,7 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
766887
layout="vertical"
767888
labelCol={{ span: 12 }}
768889
initialValues={mergedInitialValues}
890+
onValuesChange={handleFormValuesChange}
769891
>
770892
<BAIFlex direction="column" gap={'md'} align="stretch">
771893
<Card>
@@ -780,7 +902,9 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
780902
<Input disabled={!!endpoint} />
781903
</Form.Item>
782904
<Form.Item name="openToPublic" valuePropName="checked">
783-
<Checkbox disabled={!!endpoint}>{t('modelService.OpenToPublic')}</Checkbox>
905+
<Checkbox disabled={!!endpoint}>
906+
{t('modelService.OpenToPublic')}
907+
</Checkbox>
784908
</Form.Item>
785909
{!endpoint ? (
786910
<Form.Item
@@ -1013,15 +1137,35 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
10131137
>
10141138
<ResourceAllocationFormItems enableResourcePresets />
10151139
</div>
1016-
<Form.Item
1017-
label={t('session.launcher.EnvironmentVariable')}
1018-
>
1019-
<EnvVarFormList
1020-
name={'envvars'}
1021-
formItemProps={{
1022-
validateTrigger: ['onChange', 'onBlur'],
1023-
}}
1024-
/>
1140+
<Form.Item dependencies={['runtimeVariant']} noStyle>
1141+
{({ getFieldValue }) => {
1142+
const runtimeVariant =
1143+
getFieldValue('runtimeVariant');
1144+
const runtimeVariantConfig = runtimeVariant
1145+
? RUNTIME_ENV_VAR_CONFIGS[runtimeVariant]
1146+
: null;
1147+
1148+
return (
1149+
<Form.Item
1150+
label={t(
1151+
'session.launcher.EnvironmentVariable',
1152+
)}
1153+
>
1154+
<EnvVarFormList
1155+
name={'envvars'}
1156+
requiredEnvVars={
1157+
runtimeVariantConfig?.requiredEnvVars
1158+
}
1159+
optionalEnvVars={
1160+
runtimeVariantConfig?.optionalEnvVars
1161+
}
1162+
formItemProps={{
1163+
validateTrigger: ['onChange', 'onBlur'],
1164+
}}
1165+
/>
1166+
</Form.Item>
1167+
);
1168+
}}
10251169
</Form.Item>
10261170
</>
10271171
)}

0 commit comments

Comments
 (0)