package vip.mate.llm.service; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** * Regression coverage for {@link ModelDiscoveryService#buildTestPromptRequestBody}. * *

Prior to this fix, the Model Management "Test Model" / "Test Connection" button * (backed by {@code sendOpenAiTestPrompt}) built its outbound request body from a * hard-coded {@code Map.of(model, messages, max_tokens, temperature)} and never * consulted {@code generateKwargs} at all (beyond {@code completionsPath} and * {@code customHeaders}, applied separately). So an admin-configured passthrough * key like vLLM's {@code chat_template_kwargs} (to disable Qwen thinking mode) was * silently dropped on the test path even after the runtime chat path * ({@code OpenAiCompatibleChatModelBuilder#buildOpenAiOptions}) started forwarding it * — "I configured disable-thinking but the UI test still shows thinking enabled". */ class ModelDiscoveryServiceTestPromptTest { @Test @DisplayName("Unrecognized top-level key (chat_template_kwargs) is forwarded into the test request body") void unknownKey_chatTemplateKwargs_forwardedToRequestBody() { Map chatTemplateKwargs = Map.of("enable_thinking", false); Map kwargs = Map.of("chat_template_kwargs", chatTemplateKwargs); Map requestBody = ModelDiscoveryService.buildTestPromptRequestBody("qwen3-32b", kwargs); assertEquals(chatTemplateKwargs, requestBody.get("chat_template_kwargs"), "the nested map must be forwarded verbatim, not flattened or re-wrapped"); assertEquals("qwen3-32b", requestBody.get("model")); assertEquals(10, requestBody.get("max_tokens")); assertEquals(0, requestBody.get("temperature")); } @Test @DisplayName("Reserved key (temperature) in generateKwargs does not override the fixed smoke-test values") void reservedKey_doesNotOverrideFixedProbeFields() { Map kwargs = new LinkedHashMap<>(); kwargs.put("temperature", 0.9); kwargs.put("maxTokens", 4096); kwargs.put("chat_template_kwargs", Map.of("enable_thinking", false)); Map requestBody = ModelDiscoveryService.buildTestPromptRequestBody("qwen3-32b", kwargs); assertEquals(0, requestBody.get("temperature"), "the probe's fixed temperature=0 must win over a reserved generateKwargs key"); assertEquals(10, requestBody.get("max_tokens"), "the probe's fixed max_tokens=10 must win over a reserved generateKwargs key"); assertFalse(requestBody.containsKey("maxTokens"), "reserved keys (even in their original casing) must not leak into the body verbatim"); assertTrue(requestBody.containsKey("chat_template_kwargs"), "the unrecognized key must still be forwarded alongside the fixed probe fields"); } @Test @DisplayName("customHeaders is reserved (consumed as real HTTP headers) and must not leak into the JSON body") void customHeaders_doesNotLeakIntoRequestBody() { Map kwargs = Map.of("customHeaders", Map.of("X-Foo", "bar")); Map requestBody = ModelDiscoveryService.buildTestPromptRequestBody("qwen3-32b", kwargs); assertFalse(requestBody.containsKey("customHeaders"), "customHeaders is applied via applyCustomHeaders() as real HTTP headers, not as a body field"); } @Test @DisplayName("modelsPath is reserved (consumed by model discovery) and must not leak into the JSON body") void modelsPath_doesNotLeakIntoRequestBody() { Map kwargs = new LinkedHashMap<>(); kwargs.put("modelsPath", "/openai/v1/models"); kwargs.put("chat_template_kwargs", Map.of("enable_thinking", false)); Map requestBody = ModelDiscoveryService.buildTestPromptRequestBody("qwen3-32b", kwargs); assertFalse(requestBody.containsKey("modelsPath"), "modelsPath configures the list-models endpoint and is not a chat completion body field"); assertTrue(requestBody.containsKey("chat_template_kwargs"), "unrecognized passthrough keys must still be forwarded"); } @Test @DisplayName("Empty or null generateKwargs: request body contains only the fixed probe fields") void emptyOrNullGenerateKwargs_onlyFixedFields() { Map requestBody = ModelDiscoveryService.buildTestPromptRequestBody("gpt-4-turbo", Map.of()); assertEquals(Set.of("model", "messages", "max_tokens", "temperature"), requestBody.keySet()); assertEquals("gpt-4-turbo", requestBody.get("model")); assertEquals(10, requestBody.get("max_tokens")); assertEquals(0, requestBody.get("temperature")); Map requestBodyFromNull = ModelDiscoveryService.buildTestPromptRequestBody("gpt-4-turbo", null); assertEquals(requestBody, requestBodyFromNull); } @Test @DisplayName("Kimi smoke test uses the same fixed temperature required by runtime") void kimiForCoding_usesTemperatureOne() { Map requestBody = ModelDiscoveryService.buildTestPromptRequestBody( "kimi-for-coding", Map.of("temperature", 0.2)); assertEquals(1.0d, requestBody.get("temperature")); } }