diff --git a/src/renderer/src/aiCore/clients/AihubmixAPIClient.ts b/src/renderer/src/aiCore/clients/AihubmixAPIClient.ts index 3aa0bc4263..b24eea2a2d 100644 --- a/src/renderer/src/aiCore/clients/AihubmixAPIClient.ts +++ b/src/renderer/src/aiCore/clients/AihubmixAPIClient.ts @@ -20,6 +20,7 @@ import { SdkToolCall } from '@renderer/types/sdk' +import { CompletionsContext } from '../middleware/types' import { AnthropicAPIClient } from './anthropic/AnthropicAPIClient' import { BaseApiClient } from './BaseApiClient' import { GeminiAPIClient } from './gemini/GeminiAPIClient' @@ -163,8 +164,8 @@ export class AihubmixAPIClient extends BaseApiClient { return this.currentClient.getRequestTransformer() } - getResponseChunkTransformer(): ResponseChunkTransformer { - return this.currentClient.getResponseChunkTransformer() + getResponseChunkTransformer(ctx: CompletionsContext): ResponseChunkTransformer { + return this.currentClient.getResponseChunkTransformer(ctx) } convertMcpToolsToSdkTools(mcpTools: MCPTool[]): SdkTool[] { diff --git a/src/renderer/src/aiCore/clients/BaseApiClient.ts b/src/renderer/src/aiCore/clients/BaseApiClient.ts index 5daa073f38..4e39e9464d 100644 --- a/src/renderer/src/aiCore/clients/BaseApiClient.ts +++ b/src/renderer/src/aiCore/clients/BaseApiClient.ts @@ -42,7 +42,8 @@ import { defaultTimeout } from '@shared/config/constant' import Logger from 'electron-log/renderer' import { isEmpty } from 'lodash' -import { ApiClient, RawStreamListener, RequestTransformer, ResponseChunkTransformer } from './types' +import { CompletionsContext } from '../middleware/types' +import { ApiClient, RequestTransformer, ResponseChunkTransformer } from './types' /** * Abstract base class for API clients. @@ -95,7 +96,7 @@ export abstract class BaseApiClient< // 在 CoreRequestToSdkParamsMiddleware中使用 abstract getRequestTransformer(): RequestTransformer // 在RawSdkChunkToGenericChunkMiddleware中使用 - abstract getResponseChunkTransformer(): ResponseChunkTransformer + abstract getResponseChunkTransformer(ctx: CompletionsContext): ResponseChunkTransformer /** * 工具转换 @@ -129,17 +130,6 @@ export abstract class BaseApiClient< */ abstract extractMessagesFromSdkPayload(sdkPayload: TSdkParams): TMessageParam[] - /** - * 附加原始流监听器 - */ - public attachRawStreamListener>( - rawOutput: TRawOutput, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _listener: TListener - ): TRawOutput { - return rawOutput - } - /** * 通用函数 **/ diff --git a/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts b/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts index 480cb17b3d..864f2fff30 100644 --- a/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts +++ b/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts @@ -367,7 +367,7 @@ export class AnthropicAPIClient extends BaseApiClient< * Anthropic专用的原始流监听器 * 处理MessageStream对象的特定事件 */ - override attachRawStreamListener( + attachRawStreamListener( rawOutput: AnthropicSdkRawOutput, listener: RawStreamListener ): AnthropicSdkRawOutput { diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts index 9168e5129a..a53247c1f7 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts @@ -494,7 +494,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient< } // 在RawSdkChunkToGenericChunkMiddleware中使用 - getResponseChunkTransformer = (): ResponseChunkTransformer => { + getResponseChunkTransformer(): ResponseChunkTransformer { let hasBeenCollectedWebSearch = false const collectWebSearchData = ( chunk: OpenAISdkRawChunk, diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts index c36b084cde..71b809c338 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts @@ -1,4 +1,5 @@ import { GenericChunk } from '@renderer/aiCore/middleware/schemas' +import { CompletionsContext } from '@renderer/aiCore/middleware/types' import { isOpenAIChatCompletionOnlyModel, isSupportedReasoningEffortOpenAIModel, @@ -38,6 +39,7 @@ import { buildSystemPrompt } from '@renderer/utils/prompt' import { MB } from '@shared/config/constant' import { isEmpty } from 'lodash' import OpenAI from 'openai' +import { ResponseInput } from 'openai/resources/responses/responses' import { RequestTransformer, ResponseChunkTransformer } from '../types' import { OpenAIAPIClient } from './OpenAIApiClient' @@ -225,9 +227,15 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< return } + private convertResponseToMessageContent(response: OpenAI.Responses.Response): ResponseInput { + const content: OpenAI.Responses.ResponseInput = [] + content.push(...response.output) + return content + } + public buildSdkMessages( currentReqMessages: OpenAIResponseSdkMessageParam[], - output: string | undefined, + output: OpenAI.Responses.Response | undefined, toolResults: OpenAIResponseSdkMessageParam[], toolCalls: OpenAIResponseSdkToolCall[] ): OpenAIResponseSdkMessageParam[] { @@ -239,11 +247,9 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< return [...currentReqMessages, ...(toolCalls || []), ...(toolResults || [])] } - const assistantMessage: OpenAIResponseSdkMessageParam = { - role: 'assistant', - content: [{ type: 'input_text', text: output }] - } - const newReqMessages = [...currentReqMessages, assistantMessage, ...(toolCalls || []), ...(toolResults || [])] + const content = this.convertResponseToMessageContent(output) + + const newReqMessages = [...currentReqMessages, ...content, ...(toolResults || [])] return newReqMessages } @@ -415,13 +421,17 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< } } - getResponseChunkTransformer(): ResponseChunkTransformer { + getResponseChunkTransformer(ctx: CompletionsContext): ResponseChunkTransformer { const toolCalls: OpenAIResponseSdkToolCall[] = [] const outputItems: OpenAI.Responses.ResponseOutputItem[] = [] + let hasBeenCollectedToolCalls = false return () => ({ async transform(chunk: OpenAIResponseSdkRawChunk, controller: TransformStreamDefaultController) { // 处理chunk if ('output' in chunk) { + if (ctx._internal?.toolProcessingState) { + ctx._internal.toolProcessingState.output = chunk + } for (const output of chunk.output) { switch (output.type) { case 'message': @@ -463,6 +473,22 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< }) } } + if (toolCalls.length > 0) { + controller.enqueue({ + type: ChunkType.MCP_TOOL_CREATED, + tool_calls: toolCalls + }) + } + controller.enqueue({ + type: ChunkType.LLM_RESPONSE_COMPLETE, + response: { + usage: { + prompt_tokens: chunk.usage?.input_tokens || 0, + completion_tokens: chunk.usage?.output_tokens || 0, + total_tokens: chunk.usage?.total_tokens || 0 + } + } + }) } else { switch (chunk.type) { case 'response.output_item.added': @@ -510,7 +536,8 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< if (outputItem.type === 'function_call') { toolCalls.push({ ...outputItem, - arguments: chunk.arguments + arguments: chunk.arguments, + status: 'completed' }) } } @@ -526,15 +553,26 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient< } }) } - if (toolCalls.length > 0) { + if (toolCalls.length > 0 && !hasBeenCollectedToolCalls) { controller.enqueue({ type: ChunkType.MCP_TOOL_CREATED, tool_calls: toolCalls }) + hasBeenCollectedToolCalls = true } break } case 'response.completed': { + if (ctx._internal?.toolProcessingState) { + ctx._internal.toolProcessingState.output = chunk.response + } + if (toolCalls.length > 0 && !hasBeenCollectedToolCalls) { + controller.enqueue({ + type: ChunkType.MCP_TOOL_CREATED, + tool_calls: toolCalls + }) + hasBeenCollectedToolCalls = true + } const completion_tokens = chunk.response.usage?.output_tokens || 0 const total_tokens = chunk.response.usage?.total_tokens || 0 controller.enqueue({ diff --git a/src/renderer/src/aiCore/clients/types.ts b/src/renderer/src/aiCore/clients/types.ts index 84562a13e9..ff03f10d38 100644 --- a/src/renderer/src/aiCore/clients/types.ts +++ b/src/renderer/src/aiCore/clients/types.ts @@ -3,6 +3,8 @@ import { Assistant, MCPTool, MCPToolResponse, Model, ToolCallResponse } from '@r import { Provider } from '@renderer/types' import { AnthropicSdkRawChunk, + OpenAIResponseSdkRawChunk, + OpenAIResponseSdkRawOutput, OpenAISdkRawChunk, SdkMessageParam, SdkParams, @@ -14,6 +16,7 @@ import { import OpenAI from 'openai' import { CompletionsParams, GenericChunk } from '../middleware/schemas' +import { CompletionsContext } from '../middleware/types' /** * 原始流监听器接口 @@ -33,6 +36,14 @@ export interface OpenAIStreamListener extends RawStreamListener void } +/** + * OpenAI Response 专用的流监听器 + */ +export interface OpenAIResponseStreamListener + extends RawStreamListener { + onMessage?: (response: OpenAIResponseSdkRawOutput) => void +} + /** * Anthropic 专用的流监听器 */ @@ -101,7 +112,7 @@ export interface ApiClient< // SDK相关方法 getSdkInstance(): Promise | TSdkInstance getRequestTransformer(): RequestTransformer - getResponseChunkTransformer(): ResponseChunkTransformer + getResponseChunkTransformer(ctx: CompletionsContext): ResponseChunkTransformer // 原始流监听方法 attachRawStreamListener?(rawOutput: TRawOutput, listener: RawStreamListener): TRawOutput diff --git a/src/renderer/src/aiCore/index.ts b/src/renderer/src/aiCore/index.ts index bcf360b7ae..5b1bb5e181 100644 --- a/src/renderer/src/aiCore/index.ts +++ b/src/renderer/src/aiCore/index.ts @@ -76,7 +76,7 @@ export default class AiProvider { if (!(this.apiClient instanceof OpenAIAPIClient)) { builder.remove(ThinkingTagExtractionMiddlewareName) } - if (!(this.apiClient instanceof AnthropicAPIClient)) { + if (!(this.apiClient instanceof AnthropicAPIClient) && !(this.apiClient instanceof OpenAIResponseAPIClient)) { builder.remove(RawStreamListenerMiddlewareName) } if (!params.enableWebSearch) { diff --git a/src/renderer/src/aiCore/middleware/core/RawStreamListenerMiddleware.ts b/src/renderer/src/aiCore/middleware/core/RawStreamListenerMiddleware.ts index 36c1693b3a..3c5df05b28 100644 --- a/src/renderer/src/aiCore/middleware/core/RawStreamListenerMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/core/RawStreamListenerMiddleware.ts @@ -15,8 +15,6 @@ export const RawStreamListenerMiddleware: CompletionsMiddleware = // 在这里可以监听到从SDK返回的最原始流 if (result.rawOutput) { - console.log(`[${MIDDLEWARE_NAME}] 检测到原始SDK输出,准备附加监听器`) - const providerType = ctx.apiClientInstance.provider.type // TODO: 后面下放到AnthropicAPIClient if (providerType === 'anthropic') { diff --git a/src/renderer/src/aiCore/middleware/core/ResponseTransformMiddleware.ts b/src/renderer/src/aiCore/middleware/core/ResponseTransformMiddleware.ts index fbb3bac198..eccbe86bdd 100644 --- a/src/renderer/src/aiCore/middleware/core/ResponseTransformMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/core/ResponseTransformMiddleware.ts @@ -37,7 +37,7 @@ export const ResponseTransformMiddleware: CompletionsMiddleware = } // 获取响应转换器 - const responseChunkTransformer = apiClient.getResponseChunkTransformer?.() + const responseChunkTransformer = apiClient.getResponseChunkTransformer(ctx) if (!responseChunkTransformer) { Logger.warn(`[${MIDDLEWARE_NAME}] No ResponseChunkTransformer available, skipping transformation`) return result diff --git a/src/renderer/src/aiCore/middleware/core/StreamAdapterMiddleware.ts b/src/renderer/src/aiCore/middleware/core/StreamAdapterMiddleware.ts index 118d96e035..893f891c06 100644 --- a/src/renderer/src/aiCore/middleware/core/StreamAdapterMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/core/StreamAdapterMiddleware.ts @@ -25,7 +25,6 @@ export const StreamAdapterMiddleware: CompletionsMiddleware = // 但是这个中间件的职责是流适配,是否在这调用优待商榷 // 调用下游中间件 const result = await next(ctx, params) - if ( result.rawOutput && !(result.rawOutput instanceof ReadableStream) && diff --git a/src/renderer/src/aiCore/middleware/core/TransformCoreToSdkParamsMiddleware.ts b/src/renderer/src/aiCore/middleware/core/TransformCoreToSdkParamsMiddleware.ts index 12c7a27acd..0ff536d418 100644 --- a/src/renderer/src/aiCore/middleware/core/TransformCoreToSdkParamsMiddleware.ts +++ b/src/renderer/src/aiCore/middleware/core/TransformCoreToSdkParamsMiddleware.ts @@ -14,8 +14,6 @@ export const TransformCoreToSdkParamsMiddleware: CompletionsMiddleware = () => (next) => async (ctx: CompletionsContext, params: CompletionsParams): Promise => { - Logger.debug(`🔄 [${MIDDLEWARE_NAME}] Starting core to SDK params transformation:`, ctx) - const internal = ctx._internal // 🔧 检测递归调用:检查 params 中是否携带了预处理的 SDK 消息