fix(llm): adapt reasoning model probes and parameter errors (#640)

This commit is contained in:
matevip 2026-09-17 23:00:04 -04:00
parent 0a7dbef619
commit 8337d13403
4 changed files with 96 additions and 9 deletions

View File

@ -2036,9 +2036,26 @@ public class NodeStreamingChatHelper {
|| combined.contains("model not found") || combined.contains("not_found_error")) { || combined.contains("model not found") || combined.contains("not_found_error")) {
return "Model name not available on this provider — verify the model exists and is supported (Settings → Models)"; return "Model name not available on this provider — verify the model exists and is supported (Settings → Models)";
} }
// Do not mislabel unsupported model parameters as image errors (#640).
// Only emit known parameter names and fixed guidance, never raw response
// bodies, which may contain credentials or echoed conversation content.
String lowerError = combined.toLowerCase(java.util.Locale.ROOT);
if (lowerError.contains("unsupported parameter") || lowerError.contains("unsupported_parameter")
|| lowerError.contains("unsupported value") || lowerError.contains("unsupported_value")) {
if (lowerError.contains("max_tokens") && lowerError.contains("max_completion_tokens")) {
return "模型不支持 max_tokens,请改用 max_completion_tokens。请更新 MateClaw,并检查模型及提供商生成参数配置。";
}
for (String parameter : java.util.List.of("max_completion_tokens", "max_tokens", "temperature", "top_p",
"reasoning_effort", "stream_options", "parallel_tool_calls", "tool_choice", "response_format")) {
if (lowerError.contains(parameter)) {
return "模型不支持生成参数或参数值:" + parameter + "。请检查模型及提供商生成参数配置。";
}
}
return "模型不支持当前生成参数或参数值,请检查模型及提供商生成参数配置。";
}
// 对 Jackson 反序列化错误,提取关键信息 // 对 Jackson 反序列化错误,提取关键信息
if (msg.contains("engine_overloaded")) return "Model service overloaded, please retry later"; if (msg.contains("engine_overloaded")) return "Model service overloaded, please retry later";
if (msg.contains("unsupported image format") || msg.contains("unsupported")) return "Unsupported file format (e.g. SVG), use PNG/JPG instead"; if (lowerError.contains("unsupported image format")) return "Unsupported file format (e.g. SVG), use PNG/JPG instead";
if (msg.contains("invalid_request_error") || msg.contains("400 Bad Request")) return "Bad request, please check input"; if (msg.contains("invalid_request_error") || msg.contains("400 Bad Request")) return "Bad request, please check input";
if (msg.contains("rate_limit") || msg.contains("429")) return "Rate limit exceeded, please retry later"; if (msg.contains("rate_limit") || msg.contains("429")) return "Rate limit exceeded, please retry later";
if (msg.contains("timeout") || msg.contains("Timeout")) return "Request timeout, please retry"; if (msg.contains("timeout") || msg.contains("Timeout")) return "Request timeout, please retry";

View File

@ -681,8 +681,9 @@ public class ModelDiscoveryService {
/** /**
* Build the smoke-test request body for the OpenAI-compatible test-prompt path. * Build the smoke-test request body for the OpenAI-compatible test-prompt path.
* The core fields (model/messages/max_tokens/temperature) are fixed by design — * Probe limits follow the same ModelFamily constraints as runtime chat.
* this is a minimal-token connectivity probe, not a real chat turn — but any * Reasoning models need a completion budget that includes reasoning tokens;
* the standard ten-token probe is insufficient for those models. Any
* unrecognized top-level {@code generateKwargs} key (e.g. vLLM's * unrecognized top-level {@code generateKwargs} key (e.g. vLLM's
* {@code chat_template_kwargs} used to disable Qwen thinking mode) is forwarded * {@code chat_template_kwargs} used to disable Qwen thinking mode) is forwarded
* verbatim, same as the runtime chat path in * verbatim, same as the runtime chat path in
@ -694,14 +695,21 @@ public class ModelDiscoveryService {
Map<String, Object> requestBody = new LinkedHashMap<>(ProviderGenerateKwargs.collectPassthroughExtraBody(kwargs)); Map<String, Object> requestBody = new LinkedHashMap<>(ProviderGenerateKwargs.collectPassthroughExtraBody(kwargs));
requestBody.put("model", modelId); requestBody.put("model", modelId);
requestBody.put("messages", List.of(Map.of("role", "user", "content", "请回复:连接正常"))); requestBody.put("messages", List.of(Map.of("role", "user", "content", "请回复:连接正常")));
requestBody.put("max_tokens", 10); ModelFamily family = ModelFamily.detect(modelId);
Object probeTemperature; if (family.useMaxCompletionTokens()) {
if (ModelFamily.detect(modelId).fixedTemperatureOne()) { requestBody.put("max_completion_tokens", 4096);
probeTemperature = 1.0d; // Let OpenAI/Azure use model defaults: reasoning deployments may
// reject explicit sampling options even in a connectivity probe.
} else { } else {
probeTemperature = 0; requestBody.put("max_tokens", 10);
Object probeTemperature;
if (family.fixedTemperatureOne()) {
probeTemperature = 1.0d;
} else {
probeTemperature = 0;
}
requestBody.put("temperature", probeTemperature);
} }
requestBody.put("temperature", probeTemperature);
return requestBody; return requestBody;
} }

View File

@ -0,0 +1,40 @@
package vip.mate.agent.graph;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.http.HttpHeaders;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
class ModelParameterErrorHintTest {
@Test
void unsupportedTokenParameterHasActionableHintWithoutEchoingSecrets() {
String body = "{\"error\":{\"message\":\"Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead. token=private-secret\"}}";
var error = WebClientResponseException.create(400, "Bad Request", HttpHeaders.EMPTY,
body.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
String hint = ReflectionTestUtils.invokeMethod(NodeStreamingChatHelper.class,
"extractUserFriendlyError", new IllegalStateException("400 Bad Request", error));
assertNotNull(hint);
assertTrue(hint.contains("max_completion_tokens"));
assertFalse(hint.contains("private-secret"));
assertFalse(hint.contains("file format"));
}
@Test
void unsupportedSamplingParameterIsNotAnImageError() {
String hint = ReflectionTestUtils.invokeMethod(NodeStreamingChatHelper.class,
"extractUserFriendlyError", new IllegalStateException("400 Bad Request: unsupported parameter: 'temperature'"));
assertNotNull(hint);
assertTrue(hint.contains("temperature"));
assertFalse(hint.contains("file format"));
}
@Test
void actualUnsupportedImageFormatKeepsExistingHint() {
String hint = ReflectionTestUtils.invokeMethod(NodeStreamingChatHelper.class,
"extractUserFriendlyError", new IllegalStateException("unsupported image format"));
assertNotNull(hint);
assertTrue(hint.contains("PNG/JPG"));
}
}

View File

@ -109,4 +109,26 @@ class ModelDiscoveryServiceTestPromptTest {
assertEquals(1.0d, requestBody.get("temperature")); assertEquals(1.0d, requestBody.get("temperature"));
} }
@Test
@DisplayName("OpenAI reasoning probes use completion-token budgets and omit unsupported sampling parameters (#640)")
void reasoningProbeUsesCompletionTokens() {
for (String model : java.util.List.of("gpt-5.5", "gpt-5-mini", "o1", "o3", "o4-mini")) {
Map<String, Object> body = ModelDiscoveryService.buildTestPromptRequestBody(model,
Map.of("max_tokens", 10, "temperature", 0.2, "top_p", 0.9));
assertFalse(body.containsKey("max_tokens"), model);
assertEquals(4096, body.get("max_completion_tokens"), model);
assertFalse(body.containsKey("temperature"), model);
assertFalse(body.containsKey("top_p"), model);
}
}
@Test
@DisplayName("GPT-4o probes retain their supported standard parameters")
void gpt4oProbeRemainsStandard() {
Map<String, Object> body = ModelDiscoveryService.buildTestPromptRequestBody("gpt-4o", Map.of());
assertEquals(10, body.get("max_tokens"));
assertEquals(0, body.get("temperature"));
assertFalse(body.containsKey("max_completion_tokens"));
}
} }