Skip to content

Commit ad4fcd4

Browse files
committed
feat(FR-1726): Auto-populate ENV variables based on selected Inference Runtime Variant
1 parent 7486f48 commit ad4fcd4

File tree

24 files changed

+489
-45
lines changed

24 files changed

+489
-45
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: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
convertToBinaryUnit,
99
useBaiSignedRequestWithPromise,
1010
} from '../helper';
11+
import { getRuntimeEnvVarConfigs } from '../helper/runtimeVariantConfigs';
1112
import {
1213
useCurrentDomainValue,
1314
useSuspendedBackendaiClient,
@@ -175,6 +176,40 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
175176

176177
const { getErrorMessage } = useErrorMessageResolver();
177178

179+
// Helper function to set environment variables based on runtime variant
180+
const setEnvironmentVariablesForRuntimeVariant = (runtimeVariant: string) => {
181+
const RUNTIME_ENV_VAR_CONFIGS = getRuntimeEnvVarConfigs(t);
182+
const runtimeConfig = RUNTIME_ENV_VAR_CONFIGS[runtimeVariant];
183+
if (!runtimeConfig) return;
184+
185+
const currentEnvVars = form.getFieldValue('envvars') || [];
186+
const existingVariables = currentEnvVars
187+
.filter((env: EnvVarFormListValue) => env && env.variable)
188+
.map((env: EnvVarFormListValue) => env.variable);
189+
190+
// Add required environment variables that don't exist
191+
const newRequiredEnvVars = (runtimeConfig.requiredEnvVars || [])
192+
.filter((envVar) => !existingVariables.includes(envVar.variable))
193+
.map((envVar) => ({
194+
variable: envVar.variable,
195+
value: '',
196+
}));
197+
198+
if (newRequiredEnvVars.length > 0) {
199+
const updatedEnvVars = [...currentEnvVars, ...newRequiredEnvVars];
200+
form.setFieldValue('envvars', updatedEnvVars);
201+
}
202+
};
203+
204+
// Handler for form values change
205+
const handleFormValuesChange = (
206+
changedValues: Partial<ServiceLauncherInput>,
207+
) => {
208+
if (changedValues.runtimeVariant && !endpoint) {
209+
setEnvironmentVariablesForRuntimeVariant(changedValues.runtimeVariant);
210+
}
211+
};
212+
178213
const endpoint = useFragment(
179214
graphql`
180215
fragment ServiceLauncherPageContentFragment on Endpoint {
@@ -766,6 +801,7 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
766801
layout="vertical"
767802
labelCol={{ span: 12 }}
768803
initialValues={mergedInitialValues}
804+
onValuesChange={handleFormValuesChange}
769805
>
770806
<BAIFlex direction="column" gap={'md'} align="stretch">
771807
<Card>
@@ -780,7 +816,9 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
780816
<Input disabled={!!endpoint} />
781817
</Form.Item>
782818
<Form.Item name="openToPublic" valuePropName="checked">
783-
<Checkbox disabled={!!endpoint}>{t('modelService.OpenToPublic')}</Checkbox>
819+
<Checkbox disabled={!!endpoint}>
820+
{t('modelService.OpenToPublic')}
821+
</Checkbox>
784822
</Form.Item>
785823
{!endpoint ? (
786824
<Form.Item
@@ -1013,15 +1051,37 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
10131051
>
10141052
<ResourceAllocationFormItems enableResourcePresets />
10151053
</div>
1016-
<Form.Item
1017-
label={t('session.launcher.EnvironmentVariable')}
1018-
>
1019-
<EnvVarFormList
1020-
name={'envvars'}
1021-
formItemProps={{
1022-
validateTrigger: ['onChange', 'onBlur'],
1023-
}}
1024-
/>
1054+
<Form.Item dependencies={['runtimeVariant']} noStyle>
1055+
{({ getFieldValue }) => {
1056+
const runtimeVariant =
1057+
getFieldValue('runtimeVariant');
1058+
const RUNTIME_ENV_VAR_CONFIGS =
1059+
getRuntimeEnvVarConfigs(t);
1060+
const runtimeVariantConfig = runtimeVariant
1061+
? RUNTIME_ENV_VAR_CONFIGS[runtimeVariant]
1062+
: null;
1063+
1064+
return (
1065+
<Form.Item
1066+
label={t(
1067+
'session.launcher.EnvironmentVariable',
1068+
)}
1069+
>
1070+
<EnvVarFormList
1071+
name={'envvars'}
1072+
requiredEnvVars={
1073+
runtimeVariantConfig?.requiredEnvVars
1074+
}
1075+
optionalEnvVars={
1076+
runtimeVariantConfig?.optionalEnvVars
1077+
}
1078+
formItemProps={{
1079+
validateTrigger: ['onChange', 'onBlur'],
1080+
}}
1081+
/>
1082+
</Form.Item>
1083+
);
1084+
}}
10251085
</Form.Item>
10261086
</>
10271087
)}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { EnvVarConfig } from '../components/EnvVarFormList';
2+
import { TFunction } from 'i18next';
3+
4+
export interface RuntimeVariantConfig {
5+
requiredEnvVars?: EnvVarConfig[];
6+
optionalEnvVars?: EnvVarConfig[];
7+
}
8+
9+
export const getRuntimeEnvVarConfigs = (
10+
t: TFunction,
11+
): Record<string, RuntimeVariantConfig> => ({
12+
vllm: {
13+
requiredEnvVars: [
14+
{
15+
variable: 'BACKEND_MODEL_NAME',
16+
placeholder: t('modelService.VllmModelName'),
17+
},
18+
],
19+
optionalEnvVars: [
20+
{
21+
variable: 'VLLM_QUANTIZATION',
22+
placeholder: t('modelService.VllmQuantization'),
23+
},
24+
{
25+
variable: 'VLLM_TP_SIZE',
26+
placeholder: t('modelService.VllmTpSize'),
27+
},
28+
{
29+
variable: 'VLLM_PP_SIZE',
30+
placeholder: t('modelService.VllmPpSize'),
31+
},
32+
{
33+
variable: 'VLLM_EXTRA_ARGS',
34+
placeholder: t('modelService.VllmExtraArgs'),
35+
},
36+
],
37+
},
38+
sglang: {
39+
requiredEnvVars: [
40+
{
41+
variable: 'BACKEND_MODEL_NAME',
42+
placeholder: t('modelService.SglangModelName'),
43+
},
44+
],
45+
optionalEnvVars: [
46+
{
47+
variable: 'SGLANG_QUANTIZATION',
48+
placeholder: t('modelService.SglangQuantization'),
49+
},
50+
{
51+
variable: 'SGLANG_TP_SIZE',
52+
placeholder: t('modelService.SglangTpSize'),
53+
},
54+
{
55+
variable: 'SGLANG_PP_SIZE',
56+
placeholder: t('modelService.SglangPpSize'),
57+
},
58+
{
59+
variable: 'SGLANG_EXTRA_ARGS',
60+
placeholder: t('modelService.SglangExtraArgs'),
61+
},
62+
],
63+
},
64+
nim: {
65+
requiredEnvVars: [
66+
{
67+
variable: 'NGC_API_KEY',
68+
placeholder: t('modelService.NimApiKey'),
69+
},
70+
],
71+
optionalEnvVars: [],
72+
},
73+
});

resources/i18n/de.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,7 @@
981981
"Image": "Bild",
982982
"ModelDefinitionPath": "Pfad der Modelldefinitionsdatei",
983983
"ModelMountDestination": "Mount-Ziel für Modellordner",
984+
"NimApiKey": "Ihr NGC-API-Schlüssel",
984985
"NoExtraMounts": "Keine zusätzlichen Halterungen",
985986
"NoServiceEndpoint": "Kein Dienst-Endpunkt",
986987
"NumberOfReplicas": "Anzahl der Replikate",
@@ -1020,6 +1021,11 @@
10201021
"ServingRouteErrorModalTitle": "Serving Route Fehler",
10211022
"SessionId": "Sitzungs-ID",
10221023
"SessionOwner": "Besitzer der Sitzung",
1024+
"SglangExtraArgs": "Zusätzliche Argumente (z. B. --dtype bfloat16 --max-running-requests 100)",
1025+
"SglangModelName": "Modellname (z. B. gpt-oss-20b, qwen3-8b-Gemini-3-2.5-Pro-Distill)",
1026+
"SglangPpSize": "Pipeline-Parallelgröße (z. B. 1 (Standard), 2, ...)",
1027+
"SglangQuantization": "z. B., awq, awq_marlin, gptq, int4, fp8",
1028+
"SglangTpSize": "Tensor-Parallelgröße (z. B. 1 (Standard), 2, ...)",
10231029
"StartNewService": "Neuen Dienst starten",
10241030
"StartNewServing": "Neue Portion starten",
10251031
"StartService": "Dienst starten",
@@ -1037,7 +1043,12 @@
10371043
"TrafficRatio": "Verkehrsdichte",
10381044
"UpdateService": "Update-Service",
10391045
"Validate": "Bestätigen",
1040-
"ValidationInfo": "Validierungsinformationen"
1046+
"ValidationInfo": "Validierungsinformationen",
1047+
"VllmExtraArgs": "Zusätzliche vLLM-Argumente (z. B. --tokenizer=/models/custom-tokenizer --dtype=half)",
1048+
"VllmModelName": "Modellnamen (z. B. meta-llama/Llama-2-7b-chat-hf)",
1049+
"VllmPpSize": "Pipeline-Parallelgröße (z. B. 2, 4)",
1050+
"VllmQuantization": "z. B. awq, gptq, fp8",
1051+
"VllmTpSize": "Tensor-Parallelgröße (z. B. 2, 4, 8)"
10411052
},
10421053
"modelStore": {
10431054
"Author": "Autor",

0 commit comments

Comments
 (0)