From 57f1b6cedcfd75bcd47156da7a0239e0e8441203 Mon Sep 17 00:00:00 2001 From: iiusky Date: Tue, 16 Jun 2026 18:52:02 +0800 Subject: [PATCH] fix(ai): avoid empty Claude system field rejected by Anthropic Anthropic rejects requests whose system field is an empty string with "system: text content blocks must be non-empty". The connection test hardcoded an empty system, and regular completion/stream requests sent an empty system_prompt when none was configured. Add a claude_system_prompt() helper that substitutes a non-empty default for empty or whitespace-only prompts, and apply it to all Claude request sites. Covered by a unit test. --- crates/dbx-core/src/ai.rs | 42 +++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/dbx-core/src/ai.rs b/crates/dbx-core/src/ai.rs index 82dd3ae526..cd43c00a2b 100644 --- a/crates/dbx-core/src/ai.rs +++ b/crates/dbx-core/src/ai.rs @@ -612,7 +612,7 @@ pub async fn call_claude(client: &reqwest::Client, request: AiCompletionRequest) "model": request.config.model, "max_tokens": request.max_tokens.unwrap_or(2048), "temperature": request.temperature.unwrap_or(0.2), - "system": request.system_prompt, + "system": claude_system_prompt(&request.system_prompt), "messages": request.messages, }); @@ -800,6 +800,23 @@ async fn measure_first_stream_chunk( const TEST_PROMPT: &str = "Who are you?"; +/// Fallback system prompt for the Anthropic (Claude) API. +/// +/// Anthropic rejects requests whose `system` field is an empty string with +/// `system: text content blocks must be non-empty`. When the caller has no +/// system prompt we send this minimal placeholder so the request stays valid. +const CLAUDE_DEFAULT_SYSTEM: &str = "You are a helpful assistant."; + +/// Returns a non-empty system prompt for Claude requests, substituting a +/// default when the provided prompt is empty or whitespace-only. +fn claude_system_prompt(system_prompt: &str) -> &str { + if system_prompt.trim().is_empty() { + CLAUDE_DEFAULT_SYSTEM + } else { + system_prompt + } +} + pub async fn test_connection_core(config: &AiConfig) -> Result { validate_config(config)?; @@ -817,7 +834,7 @@ pub async fn test_connection_core(config: &AiConfig) -> Result Result, String> { #[cfg(test)] mod tests { use super::{ - build_ai_http_client, claude_headers, gemini_text, openai_response_text, openai_stream_text, + build_ai_http_client, claude_headers, claude_system_prompt, gemini_text, openai_response_text, openai_stream_text, parse_model_list_response, resolve_endpoint, resolve_model_list_endpoint, responses_max_output_tokens, responses_text, supports_temperature, validate_config, AiApiStyle, AiAuthMethod, AiConfig, AiModelInfo, - AiProvider, AUTHORIZATION, + AiProvider, AUTHORIZATION, CLAUDE_DEFAULT_SYSTEM, }; #[test] @@ -2205,6 +2222,19 @@ mod tests { assert!(bearer_headers.get("x-api-key").is_none()); } + #[test] + fn claude_system_prompt_substitutes_default_when_empty() { + // Empty or whitespace-only prompts must fall back to a non-empty value, + // otherwise Anthropic rejects the request with + // "system: text content blocks must be non-empty". + assert_eq!(claude_system_prompt(""), CLAUDE_DEFAULT_SYSTEM); + assert_eq!(claude_system_prompt(" \n\t"), CLAUDE_DEFAULT_SYSTEM); + assert!(!CLAUDE_DEFAULT_SYSTEM.is_empty()); + + // Real prompts pass through unchanged. + assert_eq!(claude_system_prompt("Be concise."), "Be concise."); + } + #[test] fn parses_openai_and_claude_model_list_items() { let data = serde_json::json!({