mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-10 10:22:29 +08:00
yes, this finally does set streaming to true
This commit is contained in:
parent
59a358a3ee
commit
b05b88dd76
@ -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`);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user