diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java index 50e643e0..1f0a43d6 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java @@ -2036,9 +2036,26 @@ public class NodeStreamingChatHelper { || 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)"; } + // 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 反序列化错误,提取关键信息 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("rate_limit") || msg.contains("429")) return "Rate limit exceeded, please retry later"; if (msg.contains("timeout") || msg.contains("Timeout")) return "Request timeout, please retry"; diff --git a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java index 8fcb2e61..17c431b8 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/service/ModelDiscoveryService.java @@ -681,8 +681,9 @@ public class ModelDiscoveryService { /** * 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 — - * this is a minimal-token connectivity probe, not a real chat turn — but any + * Probe limits follow the same ModelFamily constraints as runtime chat. + * 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 * {@code chat_template_kwargs} used to disable Qwen thinking mode) is forwarded * verbatim, same as the runtime chat path in @@ -694,14 +695,21 @@ public class ModelDiscoveryService { Map requestBody = new LinkedHashMap<>(ProviderGenerateKwargs.collectPassthroughExtraBody(kwargs)); requestBody.put("model", modelId); requestBody.put("messages", List.of(Map.of("role", "user", "content", "请回复:连接正常"))); - requestBody.put("max_tokens", 10); - Object probeTemperature; - if (ModelFamily.detect(modelId).fixedTemperatureOne()) { - probeTemperature = 1.0d; + ModelFamily family = ModelFamily.detect(modelId); + if (family.useMaxCompletionTokens()) { + requestBody.put("max_completion_tokens", 4096); + // Let OpenAI/Azure use model defaults: reasoning deployments may + // reject explicit sampling options even in a connectivity probe. } 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; } diff --git a/mateclaw-server/src/test/java/vip/mate/agent/graph/ModelParameterErrorHintTest.java b/mateclaw-server/src/test/java/vip/mate/agent/graph/ModelParameterErrorHintTest.java new file mode 100644 index 00000000..dcab41c3 --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/agent/graph/ModelParameterErrorHintTest.java @@ -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")); + } +} diff --git a/mateclaw-server/src/test/java/vip/mate/llm/service/ModelDiscoveryServiceTestPromptTest.java b/mateclaw-server/src/test/java/vip/mate/llm/service/ModelDiscoveryServiceTestPromptTest.java index 18fd98b0..d3e22c59 100644 --- a/mateclaw-server/src/test/java/vip/mate/llm/service/ModelDiscoveryServiceTestPromptTest.java +++ b/mateclaw-server/src/test/java/vip/mate/llm/service/ModelDiscoveryServiceTestPromptTest.java @@ -109,4 +109,26 @@ class ModelDiscoveryServiceTestPromptTest { 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 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 body = ModelDiscoveryService.buildTestPromptRequestBody("gpt-4o", Map.of()); + assertEquals(10, body.get("max_tokens")); + assertEquals(0, body.get("temperature")); + assertFalse(body.containsKey("max_completion_tokens")); + } + }