Skip to content

Commit d7bbe29

Browse files
feat!: pass args and command as string (#64)
1 parent 4dfea9c commit d7bbe29

15 files changed

+402
-13
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ dependencies {
8585
implementation 'commons-io:commons-io:2.18.0'
8686
implementation 'org.apache.commons:commons-lang3:3.18.0'
8787
implementation 'org.apache.commons:commons-collections4:4.5.0-M3'
88+
implementation 'org.apache.commons:commons-exec:1.4.0'
8889
implementation 'org.springframework.retry:spring-retry'
8990

9091
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'

src/main/java/com/epam/aidial/deployment/manager/service/manifest/InferenceManifestGenerator.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import com.epam.aidial.deployment.manager.model.SimpleEnvVar;
88
import com.epam.aidial.deployment.manager.utils.K8sNamingUtils;
99
import com.epam.aidial.deployment.manager.utils.mapping.InferenceMappers;
10+
import com.epam.aidial.deployment.manager.utils.mapping.MappingChain;
1011
import io.fabric8.kubernetes.api.model.IntOrString;
1112
import io.kserve.serving.v1beta1.InferenceService;
13+
import io.kserve.serving.v1beta1.inferenceservicespec.predictor.Model;
1214
import io.kserve.serving.v1beta1.inferenceservicespec.predictor.model.Env;
1315
import io.kserve.serving.v1beta1.inferenceservicespec.predictor.model.Ports;
1416
import io.kserve.serving.v1beta1.inferenceservicespec.predictor.model.env.ValueFrom;
@@ -77,7 +79,7 @@ public InferenceService serviceConfig(
7779
// Explicitly set model name to ensure the model uses the intended name.
7880
// If omitted, the inference service will default to the Kubernetes service name,
7981
// which may differ from the actual model name due to naming transformations.
80-
modelChain.get(InferenceMappers.MODEL_ARGS_FIELD).data().addAll(List.of(MODEL_NAME_ARGUMENT_NAME, name));
82+
setModelNameIfNotSet(name, modelChain);
8183

8284
var envListMapper = modelChain
8385
.getList(InferenceMappers.MODEL_ENV_FIELD, InferenceMappers.ENV_VAR_NAME);
@@ -101,6 +103,26 @@ public InferenceService serviceConfig(
101103
return config.data();
102104
}
103105

106+
private void setModelNameIfNotSet(String modelName, MappingChain<Model> modelChain) {
107+
var command = modelChain.getNullable(InferenceMappers.MODEL_COMMAND_FIELD).data();
108+
var args = modelChain.getNullable(InferenceMappers.MODEL_ARGS_FIELD).data();
109+
110+
if (isArgPresent(MODEL_NAME_ARGUMENT_NAME, command) || isArgPresent(MODEL_NAME_ARGUMENT_NAME, args)) {
111+
log.info("Argument {} is already set for model '{}', skipping.", MODEL_NAME_ARGUMENT_NAME, modelName);
112+
return;
113+
}
114+
modelChain.get(InferenceMappers.MODEL_ARGS_FIELD).data().addAll(List.of(MODEL_NAME_ARGUMENT_NAME, modelName));
115+
}
116+
117+
private boolean isArgPresent(String targetArg, List<String> existingArgs) {
118+
if (existingArgs == null) {
119+
return false;
120+
}
121+
var targetArgWithEquals = targetArg + "=";
122+
return existingArgs.stream()
123+
.anyMatch(arg -> arg.equals(targetArg) || arg.startsWith(targetArgWithEquals));
124+
}
125+
104126
private ValueFrom buildInferenceServiceSecretRef(SensitiveEnvVar env) {
105127
var secretKeyRef = new SecretKeyRef();
106128
secretKeyRef.setKey(env.getK8sSecretKey());

src/main/java/com/epam/aidial/deployment/manager/utils/mapping/MappingChain.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ public <Y> MappingChain<Y> get(FieldMapper<T, Y> fieldMapper) {
77
return new MappingChain<>(fieldMapper.getOrSet(data));
88
}
99

10+
public <Y> MappingChain<Y> getNullable(FieldMapper<T, Y> fieldMapper) {
11+
return new MappingChain<>(fieldMapper.getter().apply(data));
12+
}
13+
1014
public <Y> ListMapper<Y> getList(FieldMapper<T, List<Y>> fieldMapper, NamedItemMapper<Y> itemMapper) {
1115
return new ListMapper<>(fieldMapper.getOrSet(data), itemMapper);
1216
}

src/main/java/com/epam/aidial/deployment/manager/web/dto/deployment/CreateInferenceDeploymentRequestDto.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
import lombok.ToString;
1010
import org.jetbrains.annotations.Nullable;
1111

12-
import java.util.List;
13-
1412
@Data
1513
@NoArgsConstructor
1614
@AllArgsConstructor
@@ -23,8 +21,8 @@ public class CreateInferenceDeploymentRequestDto extends CreateDeploymentRequest
2321
@NotNull @Valid
2422
private InferenceDeploymentSourceDto source;
2523
@Nullable
26-
private List<String> command;
24+
private String command;
2725
@Nullable
28-
private List<String> args;
26+
private String args;
2927

3028
}

src/main/java/com/epam/aidial/deployment/manager/web/dto/deployment/InferenceDeploymentDto.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
import lombok.ToString;
1010
import org.jetbrains.annotations.Nullable;
1111

12-
import java.util.List;
13-
1412
@Data
1513
@NoArgsConstructor
1614
@AllArgsConstructor
@@ -22,7 +20,7 @@ public class InferenceDeploymentDto extends DeploymentDto {
2220
@NotNull @Valid
2321
private InferenceDeploymentSourceDto source;
2422
@Nullable
25-
private List<String> command;
23+
private String command;
2624
@Nullable
27-
private List<String> args;
25+
private String args;
2826
}

src/main/java/com/epam/aidial/deployment/manager/web/mapper/DeploymentDtoMapper.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.epam.aidial.deployment.manager.model.deployment.NimDeployment;
2424
import com.epam.aidial.deployment.manager.model.deployment.NimDeploymentNgcRegistrySource;
2525
import com.epam.aidial.deployment.manager.model.deployment.NimDeploymentSource;
26-
import com.epam.aidial.deployment.manager.service.ImageDefinitionService;
2726
import com.epam.aidial.deployment.manager.service.McpEndpointPathResolver;
2827
import com.epam.aidial.deployment.manager.web.dto.DeploymentInfoDto;
2928
import com.epam.aidial.deployment.manager.web.dto.DeploymentMetadataDto;
@@ -57,8 +56,12 @@
5756
import com.epam.aidial.deployment.manager.web.dto.internal.McpDeploymentInternalDto;
5857
import com.epam.aidial.deployment.manager.web.dto.internal.NimDeploymentInternalDto;
5958
import com.epam.aidial.deployment.manager.web.dto.value.EnvVarValueDto;
59+
import lombok.extern.slf4j.Slf4j;
60+
import org.apache.commons.collections4.CollectionUtils;
6061
import org.apache.commons.collections4.ListUtils;
6162
import org.apache.commons.collections4.MapUtils;
63+
import org.apache.commons.exec.CommandLine;
64+
import org.apache.commons.lang3.StringUtils;
6265
import org.mapstruct.AfterMapping;
6366
import org.mapstruct.Mapper;
6467
import org.mapstruct.Mapping;
@@ -69,10 +72,12 @@
6972
import org.springframework.beans.factory.annotation.Autowired;
7073

7174
import java.util.ArrayList;
75+
import java.util.Collections;
7276
import java.util.List;
7377
import java.util.Map;
7478
import java.util.stream.Collectors;
7579

80+
@Slf4j
7681
@Mapper(
7782
componentModel = "spring",
7883
uses = {EnvVarValueDtoMapper.class},
@@ -83,8 +88,6 @@ public abstract class DeploymentDtoMapper {
8388
@Autowired
8489
private EnvVarValueDtoMapper envVarValueDtoMapper;
8590
@Autowired
86-
private ImageDefinitionService imageDefinitionService;
87-
@Autowired
8891
private McpEndpointPathResolver mcpEndpointPathResolver;
8992

9093
@Mapping(target = "id", source = "name")
@@ -243,4 +246,46 @@ protected McpTransportDto toMcpTransportDtoWithDefault(McpTransport transport) {
243246
};
244247
}
245248

249+
protected List<String> stringToList(String str) {
250+
if (StringUtils.isBlank(str)) {
251+
return null;
252+
}
253+
254+
try {
255+
// Parse the string; this automatically handles quoted tokens (e.g., "foo bar")
256+
CommandLine commandLine = CommandLine.parse(str);
257+
258+
List<String> result = new ArrayList<>();
259+
260+
// 1. Add the executable (the first token)
261+
result.add(commandLine.getExecutable());
262+
// 2. Add the arguments (remaining tokens)
263+
Collections.addAll(result, commandLine.getArguments());
264+
265+
return result;
266+
} catch (IllegalArgumentException e) {
267+
var errorMessage = "Cannot parse command/arguments: '%s'".formatted(str);
268+
log.warn(errorMessage, e);
269+
throw new IllegalArgumentException(errorMessage, e);
270+
}
271+
}
272+
273+
protected String listToString(List<String> list) {
274+
if (CollectionUtils.isEmpty(list)) {
275+
return null;
276+
}
277+
278+
// 1. Create CommandLine using the first element as the executable
279+
CommandLine commandLine = new CommandLine(list.get(0));
280+
281+
// 2. Add the rest of the list as arguments
282+
if (list.size() > 1) {
283+
String[] args = list.subList(1, list.size()).toArray(new String[0]);
284+
commandLine.addArguments(args);
285+
}
286+
287+
// 3. toString() automatically quotes the executable and arguments as needed
288+
return commandLine.toString();
289+
}
290+
246291
}

src/test/java/com/epam/aidial/deployment/manager/service/manifest/InferenceManifestGeneratorTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.List;
2424
import java.util.Map;
2525

26+
import static org.assertj.core.api.Assertions.assertThat;
2627
import static org.mockito.Mockito.when;
2728

2829
@ExtendWith(MockitoExtension.class)
@@ -155,6 +156,65 @@ void testServiceConfig_withArgs() throws JsonProcessingException, JSONException
155156
JSONAssert.assertEquals(expected, jsonOutput, true);
156157
}
157158

159+
@Test
160+
void testServiceConfig_doesNotOverrideExplicitModelNameArg_valueForm() {
161+
// Given
162+
var deploymentName = "explicit-model-name-app";
163+
var storageUri = "s3://my-bucket/model";
164+
var args = List.of("--arg1", "value1", "--model_name", "my-explicit-name", "--arg2", "value2");
165+
166+
// When
167+
var generatedService = manifestGenerator.serviceConfig(
168+
deploymentName, MODEL_FORMAT, storageUri, Collections.emptyList(), Collections.emptyList(), new Resources(),
169+
null, null, null, args, null
170+
);
171+
172+
// Then
173+
var actualArgs = generatedService.getSpec().getPredictor().getModel().getArgs();
174+
assertThat(actualArgs).containsSubsequence("--model_name", "my-explicit-name");
175+
assertThat(actualArgs).doesNotContain("--model_name=" + deploymentName);
176+
assertThat(actualArgs).doesNotContainSubsequence("--model_name", deploymentName);
177+
}
178+
179+
@Test
180+
void testServiceConfig_doesNotOverrideExplicitModelNameArg_equalsForm() {
181+
// Given
182+
var deploymentName = "explicit-equals-model-name-app";
183+
var storageUri = "s3://my-bucket/model";
184+
var args = List.of("--arg1", "value1", "--model_name=my-explicit-name", "--arg2", "value2");
185+
186+
// When
187+
var generatedService = manifestGenerator.serviceConfig(
188+
deploymentName, MODEL_FORMAT, storageUri, Collections.emptyList(), Collections.emptyList(), new Resources(),
189+
null, null, null, args, null
190+
);
191+
192+
// Then
193+
var actualArgs = generatedService.getSpec().getPredictor().getModel().getArgs();
194+
assertThat(actualArgs).contains("--model_name=my-explicit-name");
195+
assertThat(actualArgs).doesNotContain("--model_name=" + deploymentName);
196+
assertThat(actualArgs).doesNotContainSubsequence("--model_name", deploymentName);
197+
}
198+
199+
@Test
200+
void testServiceConfig_doesNotAddModelNameWhenPresentInCommand() {
201+
// Given
202+
var deploymentName = "command-model-name-app";
203+
var storageUri = "s3://my-bucket/model";
204+
var command = List.of("--model_name=" + deploymentName, "--serve");
205+
206+
// When
207+
var generatedService = manifestGenerator.serviceConfig(
208+
deploymentName, MODEL_FORMAT, storageUri, Collections.emptyList(), Collections.emptyList(), new Resources(),
209+
null, null, command, Collections.emptyList(), null
210+
);
211+
212+
// Then
213+
var model = generatedService.getSpec().getPredictor().getModel();
214+
assertThat(model.getCommand()).containsExactlyElementsOf(command);
215+
assertThat(model.getArgs()).isEmpty();
216+
}
217+
158218
private String serialize(Object obj) throws JsonProcessingException {
159219
return objectMapper.writeValueAsString(obj);
160220
}

0 commit comments

Comments
 (0)