diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index b059192..d735685 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -2649,7 +2649,9 @@ class ApiService { // Add only essential parameters if (conversationId != null) { - data['chat_id'] = conversationId; + if (conversationId != null) { + data['chat_id'] = conversationId; + } } // Add feature flags if enabled @@ -2729,14 +2731,26 @@ class ApiService { debugPrint('DEBUG: session_id value: ${data['session_id']}'); debugPrint('DEBUG: id value: ${data['id']}'); - // If tools are requested, use background task flow to allow server-side execution. - // Open WebUI executes tools and continues the response outside of the - // provider SSE. That path requires background task mode (session_id + id + chat_id). - if (conversationId != null) { + // Decide whether to use background task flow. + // Only enable background task mode when we actually need socket/dynamic-channel + // behavior (e.g., provider-native tools or explicit background tasks with a session). + final bool useBackgroundTasks = ( + // Use background flow only for provider-native tools or explicit tool servers. + // Pure text generation should prefer SSE even if a socket session exists, + // and background_tasks can still be honored at the end of SSE. + (toolIds != null && toolIds.isNotEmpty) || + (toolServers != null && toolServers.isNotEmpty) + ); + + // Use background flow only when required; otherwise prefer SSE even with chat_id. + // SSE must not include session_id/id to avoid server falling back to task mode. + if (useBackgroundTasks) { // Attach identifiers to trigger background task processing on the server data['session_id'] = sessionId; data['id'] = messageId; - data['chat_id'] = conversationId; + if (conversationId != null) { + data['chat_id'] = conversationId; + } // Attach background_tasks if provided if (backgroundTasks != null && backgroundTasks.isNotEmpty) { @@ -2773,7 +2787,10 @@ class ApiService { } }(); } else { - // Use SSE streaming with proper parser + // Ensure we do not leak background-only identifiers into SSE path + data.remove('session_id'); + data.remove('id'); + // Use SSE streaming with proper parser (chat_id allowed) _streamSSE(data, streamController, messageId); } diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index 72cd7d0..a39c0e2 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -1073,7 +1073,13 @@ Future _sendMessageInternal( // In that case, do NOT suppress socket content. // Suppress socket TEXT content when we already have a stream (SSE or polling) // but DO allow tool_call status via socket to surface tiles immediately. - bool suppressSocketContent = (socketSessionId == null); // text-only suppression + // By default we already have an SSE/polling stream for content, + // so suppress socket TEXT chunks to avoid duplicates. We'll still + // surface tool_calls status via socket immediately. If the server + // switches us to a dynamic channel (request:chat:completion), we + // keep suppressing chat-events text but stream from that channel. + bool suppressSocketContent = true; // text-only suppression by default + bool usingDynamicChannel = false; // set true when server provides a channel if (socketService != null) { void chatHandler(Map ev) { try { @@ -1281,6 +1287,7 @@ Future _sendMessageInternal( if (channel is String && channel.isNotEmpty) { // Prefer dynamic channel for streaming content; suppress chat-events text to avoid duplicates suppressSocketContent = true; + usingDynamicChannel = true; if (kSocketVerboseLogging) { DebugLogger.stream('Socket request:chat:completion channel=$channel'); } @@ -1720,7 +1727,8 @@ Future _sendMessageInternal( suppressSocketContent = false; // If this path was SSE-driven (no background socket), finish now. // Otherwise keep streaming state until socket/dynamic channel signals done. - if (socketService == null) { + // We can safely finish on SSE completion when not using a dynamic channel. + if (!usingDynamicChannel) { ref.read(chatMessagesProvider.notifier).finishStreaming(); }