mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	yes, this finally does set streaming to true
This commit is contained in:
		| @@ -410,8 +410,14 @@ export default class LlmChatPanel extends BasicWidget { | |||||||
|      */ |      */ | ||||||
|     private async handleDirectResponse(messageParams: any): Promise<boolean> { |     private async handleDirectResponse(messageParams: any): Promise<boolean> { | ||||||
|         try { |         try { | ||||||
|             // Send the message via POST request |             // Add format parameter to maintain consistency with the streaming GET request | ||||||
|             const postResponse = await server.post<any>(`llm/sessions/${this.sessionId}/messages`, messageParams); |             const postParams = { | ||||||
|  |                 ...messageParams, | ||||||
|  |                 format: 'stream'  // Match the format parameter used in the GET streaming request | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             // Send the message via POST request with the updated params | ||||||
|  |             const postResponse = await server.post<any>(`llm/sessions/${this.sessionId}/messages`, postParams); | ||||||
|  |  | ||||||
|             // If the POST request returned content directly, display it |             // If the POST request returned content directly, display it | ||||||
|             if (postResponse && postResponse.content) { |             if (postResponse && postResponse.content) { | ||||||
| @@ -460,8 +466,8 @@ export default class LlmChatPanel extends BasicWidget { | |||||||
|         const useAdvancedContext = messageParams.useAdvancedContext; |         const useAdvancedContext = messageParams.useAdvancedContext; | ||||||
|         const showThinking = messageParams.showThinking; |         const showThinking = messageParams.showThinking; | ||||||
|  |  | ||||||
|         // Set up streaming via EventSource |         // Set up streaming via EventSource - explicitly add stream=true parameter to ensure consistency | ||||||
|         const streamUrl = `./api/llm/sessions/${this.sessionId}/messages?format=stream&useAdvancedContext=${useAdvancedContext}&showThinking=${showThinking}`; |         const streamUrl = `./api/llm/sessions/${this.sessionId}/messages?format=stream&stream=true&useAdvancedContext=${useAdvancedContext}&showThinking=${showThinking}`; | ||||||
|  |  | ||||||
|         return new Promise((resolve, reject) => { |         return new Promise((resolve, reject) => { | ||||||
|             const source = new EventSource(streamUrl); |             const source = new EventSource(streamUrl); | ||||||
|   | |||||||
| @@ -20,6 +20,18 @@ export interface StreamChunk { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Options for chat completion requests | ||||||
|  |  *  | ||||||
|  |  * Key properties: | ||||||
|  |  * - stream: If true, the response will be streamed | ||||||
|  |  * - model: Model name to use | ||||||
|  |  * - provider: Provider to use (openai, anthropic, ollama, etc.) | ||||||
|  |  * - enableTools: If true, enables tool support | ||||||
|  |  *  | ||||||
|  |  * The stream option is particularly important and should be consistently handled | ||||||
|  |  * throughout the pipeline. It should be explicitly set to true or false. | ||||||
|  |  */ | ||||||
| export interface ChatCompletionOptions { | export interface ChatCompletionOptions { | ||||||
|     model?: string; |     model?: string; | ||||||
|     temperature?: number; |     temperature?: number; | ||||||
|   | |||||||
| @@ -197,6 +197,13 @@ export class AIServiceManager implements IAIServiceManager { | |||||||
|     async generateChatCompletion(messages: Message[], options: ChatCompletionOptions = {}): Promise<ChatResponse> { |     async generateChatCompletion(messages: Message[], options: ChatCompletionOptions = {}): Promise<ChatResponse> { | ||||||
|         this.ensureInitialized(); |         this.ensureInitialized(); | ||||||
|  |  | ||||||
|  |         log.info(`[AIServiceManager] generateChatCompletion called with options: ${JSON.stringify({ | ||||||
|  |             model: options.model, | ||||||
|  |             stream: options.stream, | ||||||
|  |             enableTools: options.enableTools | ||||||
|  |         })}`); | ||||||
|  |         log.info(`[AIServiceManager] Stream option type: ${typeof options.stream}`); | ||||||
|  |  | ||||||
|         if (!messages || messages.length === 0) { |         if (!messages || messages.length === 0) { | ||||||
|             throw new Error('No messages provided for chat completion'); |             throw new Error('No messages provided for chat completion'); | ||||||
|         } |         } | ||||||
| @@ -219,6 +226,7 @@ export class AIServiceManager implements IAIServiceManager { | |||||||
|             if (availableProviders.includes(providerName as ServiceProviders)) { |             if (availableProviders.includes(providerName as ServiceProviders)) { | ||||||
|                 try { |                 try { | ||||||
|                     const modifiedOptions = { ...options, model: modelName }; |                     const modifiedOptions = { ...options, model: modelName }; | ||||||
|  |                     log.info(`[AIServiceManager] Using provider ${providerName} from model prefix with modifiedOptions.stream: ${modifiedOptions.stream}`); | ||||||
|                     return await this.services[providerName as ServiceProviders].generateChatCompletion(messages, modifiedOptions); |                     return await this.services[providerName as ServiceProviders].generateChatCompletion(messages, modifiedOptions); | ||||||
|                 } catch (error) { |                 } catch (error) { | ||||||
|                     log.error(`Error with specified provider ${providerName}: ${error}`); |                     log.error(`Error with specified provider ${providerName}: ${error}`); | ||||||
| @@ -232,6 +240,7 @@ export class AIServiceManager implements IAIServiceManager { | |||||||
|  |  | ||||||
|         for (const provider of sortedProviders) { |         for (const provider of sortedProviders) { | ||||||
|             try { |             try { | ||||||
|  |                 log.info(`[AIServiceManager] Trying provider ${provider} with options.stream: ${options.stream}`); | ||||||
|                 return await this.services[provider].generateChatCompletion(messages, options); |                 return await this.services[provider].generateChatCompletion(messages, options); | ||||||
|             } catch (error) { |             } catch (error) { | ||||||
|                 log.error(`Error with provider ${provider}: ${error}`); |                 log.error(`Error with provider ${provider}: ${error}`); | ||||||
|   | |||||||
| @@ -227,14 +227,36 @@ export class ChatPipeline { | |||||||
|             log.info(`Prepared ${preparedMessages.messages.length} messages for LLM, tools enabled: ${useTools}`); |             log.info(`Prepared ${preparedMessages.messages.length} messages for LLM, tools enabled: ${useTools}`); | ||||||
|  |  | ||||||
|             // Setup streaming handler if streaming is enabled and callback provided |             // Setup streaming handler if streaming is enabled and callback provided | ||||||
|             const enableStreaming = this.config.enableStreaming && |             // Check if streaming should be enabled based on several conditions | ||||||
|                                   modelSelection.options.stream !== false && |             const streamEnabledInConfig = this.config.enableStreaming; | ||||||
|                                   typeof streamCallback === 'function'; |             const streamFormatRequested = input.format === 'stream'; | ||||||
|  |             const streamRequestedInOptions = modelSelection.options.stream === true; | ||||||
|             if (enableStreaming) { |             const streamCallbackAvailable = typeof streamCallback === 'function'; | ||||||
|                 // Make sure stream is enabled in options |              | ||||||
|  |             log.info(`[ChatPipeline] Request type info - Format: ${input.format || 'not specified'}, Options from pipelineInput: ${JSON.stringify({stream: input.options?.stream})}`); | ||||||
|  |             log.info(`[ChatPipeline] Stream settings - config.enableStreaming: ${streamEnabledInConfig}, format parameter: ${input.format}, modelSelection.options.stream: ${modelSelection.options.stream}, streamCallback available: ${streamCallbackAvailable}`); | ||||||
|  |              | ||||||
|  |             // IMPORTANT: Different behavior for GET vs POST requests: | ||||||
|  |             // - For GET requests with streamCallback available: Always enable streaming | ||||||
|  |             // - For POST requests: Use streaming options but don't actually stream (since we can't stream back to client) | ||||||
|  |             if (streamCallbackAvailable) { | ||||||
|  |                 // If a stream callback is available (GET requests), we can stream the response | ||||||
|                 modelSelection.options.stream = true; |                 modelSelection.options.stream = true; | ||||||
|  |                 log.info(`[ChatPipeline] Stream callback available, setting stream=true for real-time streaming`); | ||||||
|  |             } else { | ||||||
|  |                 // For POST requests, preserve the stream flag as-is from input options | ||||||
|  |                 // This ensures LLM request format is consistent across both GET and POST | ||||||
|  |                 if (streamRequestedInOptions) { | ||||||
|  |                     log.info(`[ChatPipeline] No stream callback but stream requested in options, preserving stream=true`); | ||||||
|  |                 } else { | ||||||
|  |                     log.info(`[ChatPipeline] No stream callback and no stream in options, setting stream=false`); | ||||||
|  |                     modelSelection.options.stream = false; | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|  |              | ||||||
|  |             log.info(`[ChatPipeline] Final modelSelection.options.stream = ${modelSelection.options.stream}`); | ||||||
|  |             log.info(`[ChatPipeline] Will actual streaming occur? ${streamCallbackAvailable && modelSelection.options.stream}`); | ||||||
|  |              | ||||||
|  |  | ||||||
|             // STAGE 5 & 6: Handle LLM completion and tool execution loop |             // STAGE 5 & 6: Handle LLM completion and tool execution loop | ||||||
|             log.info(`========== STAGE 5: LLM COMPLETION ==========`); |             log.info(`========== STAGE 5: LLM COMPLETION ==========`); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { BasePipelineStage } from '../pipeline_stage.js'; | import { BasePipelineStage } from '../pipeline_stage.js'; | ||||||
| import type { LLMCompletionInput } from '../interfaces.js'; | import type { LLMCompletionInput } from '../interfaces.js'; | ||||||
| import type { ChatResponse } from '../../ai_interface.js'; | import type { ChatCompletionOptions, ChatResponse } from '../../ai_interface.js'; | ||||||
| import aiServiceManager from '../../ai_service_manager.js'; | import aiServiceManager from '../../ai_service_manager.js'; | ||||||
| import toolRegistry from '../../tools/tool_registry.js'; | import toolRegistry from '../../tools/tool_registry.js'; | ||||||
| import log from '../../../log.js'; | import log from '../../../log.js'; | ||||||
| @@ -19,8 +19,35 @@ export class LLMCompletionStage extends BasePipelineStage<LLMCompletionInput, { | |||||||
|     protected async process(input: LLMCompletionInput): Promise<{ response: ChatResponse }> { |     protected async process(input: LLMCompletionInput): Promise<{ response: ChatResponse }> { | ||||||
|         const { messages, options, provider } = input; |         const { messages, options, provider } = input; | ||||||
|  |  | ||||||
|         // Create a copy of options to avoid modifying the original |         // Log input options, particularly focusing on the stream option | ||||||
|         const updatedOptions = { ...options }; |         log.info(`[LLMCompletionStage] Input options: ${JSON.stringify({ | ||||||
|  |             model: options.model, | ||||||
|  |             provider, | ||||||
|  |             stream: options.stream, | ||||||
|  |             enableTools: options.enableTools | ||||||
|  |         })}`); | ||||||
|  |         log.info(`[LLMCompletionStage] Stream option in input: ${options.stream}, type: ${typeof options.stream}`); | ||||||
|  |  | ||||||
|  |         // Create a deep copy of options to avoid modifying the original | ||||||
|  |         const updatedOptions: ChatCompletionOptions = JSON.parse(JSON.stringify(options)); | ||||||
|  |  | ||||||
|  |         // IMPORTANT: Ensure stream property is explicitly set to a boolean value | ||||||
|  |         // This is critical to ensure consistent behavior across all providers | ||||||
|  |         updatedOptions.stream = options.stream === true; | ||||||
|  |  | ||||||
|  |         log.info(`[LLMCompletionStage] Explicitly set stream option to boolean: ${updatedOptions.stream}`); | ||||||
|  |  | ||||||
|  |         // If this is a direct (non-stream) call to Ollama but has the stream flag, | ||||||
|  |         // ensure we set additional metadata to maintain proper state | ||||||
|  |         if (updatedOptions.stream && !provider && updatedOptions.providerMetadata?.provider === 'ollama') { | ||||||
|  |             log.info(`[LLMCompletionStage] This is an Ollama request with stream=true, ensuring provider config is consistent`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         log.info(`[LLMCompletionStage] Copied options: ${JSON.stringify({ | ||||||
|  |             model: updatedOptions.model, | ||||||
|  |             stream: updatedOptions.stream, | ||||||
|  |             enableTools: updatedOptions.enableTools | ||||||
|  |         })}`); | ||||||
|  |  | ||||||
|         // Check if tools should be enabled |         // Check if tools should be enabled | ||||||
|         if (updatedOptions.enableTools !== false) { |         if (updatedOptions.enableTools !== false) { | ||||||
| @@ -48,15 +75,22 @@ export class LLMCompletionStage extends BasePipelineStage<LLMCompletionInput, { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         log.info(`Generating LLM completion, provider: ${selectedProvider || 'auto'}, model: ${updatedOptions?.model || 'default'}`); |         log.info(`Generating LLM completion, provider: ${selectedProvider || 'auto'}, model: ${updatedOptions?.model || 'default'}`); | ||||||
|  |         log.info(`[LLMCompletionStage] Options before service call: ${JSON.stringify({ | ||||||
|  |             model: updatedOptions.model, | ||||||
|  |             stream: updatedOptions.stream, | ||||||
|  |             enableTools: updatedOptions.enableTools | ||||||
|  |         })}`); | ||||||
|  |  | ||||||
|         // If provider is specified (either explicit or from metadata), use that specific provider |         // If provider is specified (either explicit or from metadata), use that specific provider | ||||||
|         if (selectedProvider && aiServiceManager.isProviderAvailable(selectedProvider)) { |         if (selectedProvider && aiServiceManager.isProviderAvailable(selectedProvider)) { | ||||||
|             const service = aiServiceManager.getService(selectedProvider); |             const service = aiServiceManager.getService(selectedProvider); | ||||||
|  |             log.info(`[LLMCompletionStage] Using specific service for ${selectedProvider}, stream option: ${updatedOptions.stream}`); | ||||||
|             const response = await service.generateChatCompletion(messages, updatedOptions); |             const response = await service.generateChatCompletion(messages, updatedOptions); | ||||||
|             return { response }; |             return { response }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Otherwise use the service manager to select an available provider |         // Otherwise use the service manager to select an available provider | ||||||
|  |         log.info(`[LLMCompletionStage] Using auto-selected service, stream option: ${updatedOptions.stream}`); | ||||||
|         const response = await aiServiceManager.generateChatCompletion(messages, updatedOptions); |         const response = await aiServiceManager.generateChatCompletion(messages, updatedOptions); | ||||||
|         return { response }; |         return { response }; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -18,8 +18,20 @@ export class ModelSelectionStage extends BasePipelineStage<ModelSelectionInput, | |||||||
|     protected async process(input: ModelSelectionInput): Promise<{ options: ChatCompletionOptions }> { |     protected async process(input: ModelSelectionInput): Promise<{ options: ChatCompletionOptions }> { | ||||||
|         const { options: inputOptions, query, contentLength } = input; |         const { options: inputOptions, query, contentLength } = input; | ||||||
|  |  | ||||||
|  |         // Log input options | ||||||
|  |         log.info(`[ModelSelectionStage] Input options: ${JSON.stringify({ | ||||||
|  |             model: inputOptions?.model, | ||||||
|  |             stream: inputOptions?.stream, | ||||||
|  |             enableTools: inputOptions?.enableTools | ||||||
|  |         })}`); | ||||||
|  |         log.info(`[ModelSelectionStage] Stream option in input: ${inputOptions?.stream}, type: ${typeof inputOptions?.stream}`); | ||||||
|  |  | ||||||
|         // Start with provided options or create a new object |         // Start with provided options or create a new object | ||||||
|         const updatedOptions: ChatCompletionOptions = { ...(inputOptions || {}) }; |         const updatedOptions: ChatCompletionOptions = { ...(inputOptions || {}) }; | ||||||
|  |          | ||||||
|  |         // Preserve the stream option exactly as it was provided, including undefined state | ||||||
|  |         // This is critical for ensuring the stream option propagates correctly down the pipeline | ||||||
|  |         log.info(`[ModelSelectionStage] After copy, stream: ${updatedOptions.stream}, type: ${typeof updatedOptions.stream}`); | ||||||
|  |  | ||||||
|         // If model already specified, don't override it |         // If model already specified, don't override it | ||||||
|         if (updatedOptions.model) { |         if (updatedOptions.model) { | ||||||
| @@ -36,6 +48,7 @@ export class ModelSelectionStage extends BasePipelineStage<ModelSelectionInput, | |||||||
|                 log.info(`Using explicitly specified model: ${updatedOptions.model}`); |                 log.info(`Using explicitly specified model: ${updatedOptions.model}`); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             log.info(`[ModelSelectionStage] Returning early with stream: ${updatedOptions.stream}`); | ||||||
|             return { options: updatedOptions }; |             return { options: updatedOptions }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -151,6 +164,13 @@ export class ModelSelectionStage extends BasePipelineStage<ModelSelectionInput, | |||||||
|         this.addProviderMetadata(updatedOptions, defaultProvider, defaultModelName); |         this.addProviderMetadata(updatedOptions, defaultProvider, defaultModelName); | ||||||
|  |  | ||||||
|         log.info(`Selected model: ${defaultModelName} from provider: ${defaultProvider} for query complexity: ${queryComplexity}`); |         log.info(`Selected model: ${defaultModelName} from provider: ${defaultProvider} for query complexity: ${queryComplexity}`); | ||||||
|  |         log.info(`[ModelSelectionStage] Final options: ${JSON.stringify({ | ||||||
|  |             model: updatedOptions.model, | ||||||
|  |             stream: updatedOptions.stream, | ||||||
|  |             provider: defaultProvider, | ||||||
|  |             enableTools: updatedOptions.enableTools | ||||||
|  |         })}`); | ||||||
|  |  | ||||||
|         return { options: updatedOptions }; |         return { options: updatedOptions }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -114,11 +114,21 @@ export class OllamaService extends BaseAIService { | |||||||
|                 messages: messagesToSend |                 messages: messagesToSend | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|  |             // Debug logging for stream option | ||||||
|  |             log.info(`Stream option in providerOptions: ${providerOptions.stream}`); | ||||||
|  |             log.info(`Stream option type: ${typeof providerOptions.stream}`); | ||||||
|  |  | ||||||
|             log.info(`Stream: ${providerOptions.stream}`); |             // Stream is a top-level option - ALWAYS set it explicitly to ensure consistency | ||||||
|             // Stream is a top-level option |             // This is critical for ensuring streaming works properly | ||||||
|             if (providerOptions.stream !== undefined) { |             requestBody.stream = providerOptions.stream === true; | ||||||
|                 requestBody.stream = providerOptions.stream; |             log.info(`Set requestBody.stream to boolean: ${requestBody.stream}`); | ||||||
|  |              | ||||||
|  |             // Log additional information about the streaming context | ||||||
|  |             log.info(`Streaming context: Will stream to client: ${typeof opts.streamCallback === 'function'}`); | ||||||
|  |              | ||||||
|  |             // If we have a streaming callback but the stream flag isn't set for some reason, warn about it | ||||||
|  |             if (typeof opts.streamCallback === 'function' && !requestBody.stream) { | ||||||
|  |                 log.warn(`WARNING: Stream callback provided but stream=false in request. This may cause streaming issues.`); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Add options object if provided |             // Add options object if provided | ||||||
|   | |||||||
| @@ -542,7 +542,7 @@ export async function getOllamaOptions( | |||||||
|  |  | ||||||
|             // API parameters |             // API parameters | ||||||
|             model: modelName,  // Clean model name without provider prefix |             model: modelName,  // Clean model name without provider prefix | ||||||
|             stream: opts.stream, |             stream: opts.stream !== undefined ? opts.stream : true, // Default to true if not specified | ||||||
|             options: { |             options: { | ||||||
|                 temperature: opts.temperature, |                 temperature: opts.temperature, | ||||||
|                 num_ctx: modelContextWindow, |                 num_ctx: modelContextWindow, | ||||||
|   | |||||||
| @@ -458,7 +458,9 @@ class RestChatService { | |||||||
|                     temperature: session.metadata.temperature, |                     temperature: session.metadata.temperature, | ||||||
|                     maxTokens: session.metadata.maxTokens, |                     maxTokens: session.metadata.maxTokens, | ||||||
|                     model: session.metadata.model, |                     model: session.metadata.model, | ||||||
|                     stream: req.method === 'GET' ? true : undefined  // Explicitly set stream: true for GET requests |                     // Always set stream to true for all request types to ensure consistency | ||||||
|  |                     // This ensures the pipeline always knows streaming is supported, even for POST requests | ||||||
|  |                     stream: true | ||||||
|                 }, |                 }, | ||||||
|                 streamCallback: req.method === 'GET' ? (data, done) => { |                 streamCallback: req.method === 'GET' ? (data, done) => { | ||||||
|                     res.write(`data: ${JSON.stringify({ content: data, done })}\n\n`); |                     res.write(`data: ${JSON.stringify({ content: data, done })}\n\n`); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user