From 3b26672c96c766caffea4c2e4e738757d8c45e00 Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Thu, 23 Oct 2025 19:40:43 -0700 Subject: [PATCH] Revert "feat: Integrate OpenAI Responses API to enable GPT-5 features (#7891)" This reverts commit 5eb9f4a9bf14cb9e0e4ce53b63ea11ef3fddb47f. --- core/index.d.ts | 14 - core/llm/index.ts | 291 +++-------- core/llm/llms/Deepseek.ts | 2 - core/llm/llms/OpenAI.ts | 288 +--------- core/llm/llms/OpenRouter.ts | 2 - core/llm/openaiTypeConverters.ts | 756 +-------------------------- gui/src/redux/slices/sessionSlice.ts | 24 - 7 files changed, 106 insertions(+), 1271 deletions(-) diff --git a/core/index.d.ts b/core/index.d.ts index ed6237f7aac..c5e106b4239 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -359,15 +359,11 @@ export interface ToolResultChatMessage { role: "tool"; content: string; toolCallId: string; - /** Arbitrary per-message metadata (IDs, provider-specific info, etc.) */ - metadata?: Record; } export interface UserChatMessage { role: "user"; content: MessageContent; - /** Arbitrary per-message metadata (IDs, provider-specific info, etc.) */ - metadata?: Record; } export interface ThinkingChatMessage { @@ -376,12 +372,6 @@ export interface ThinkingChatMessage { signature?: string; redactedThinking?: string; toolCalls?: ToolCallDelta[]; - reasoning_details?: { - signature?: string; - [key: string]: any; - }[]; - /** Arbitrary per-message metadata (IDs, provider-specific info, etc.) */ - metadata?: Record; } /** @@ -410,15 +400,11 @@ export interface AssistantChatMessage { content: MessageContent; toolCalls?: ToolCallDelta[]; usage?: Usage; - /** Arbitrary per-message metadata (IDs, provider-specific info, etc.) */ - metadata?: Record; } export interface SystemChatMessage { role: "system"; content: string; - /** Arbitrary per-message metadata (IDs, provider-specific info, etc.) */ - metadata?: Record; } export type ChatMessage = diff --git a/core/llm/index.ts b/core/llm/index.ts index 76a222b6114..39b6b127b89 100644 --- a/core/llm/index.ts +++ b/core/llm/index.ts @@ -87,9 +87,6 @@ type InteractionStatus = "in_progress" | "success" | "error" | "cancelled"; export abstract class BaseLLM implements ILLM { static providerName: string; static defaultOptions: Partial | undefined = undefined; - // Provider capabilities (overridable by subclasses) - protected supportsReasoningField: boolean = false; - protected supportsReasoningDetailsField: boolean = false; get providerName(): string { return (this.constructor as typeof BaseLLM).providerName; @@ -996,114 +993,6 @@ export abstract class BaseLLM implements ILLM { return completionOptions; } - // Update the processChatChunk method: - private processChatChunk( - chunk: ChatMessage, - interaction: ILLMInteractionLog | undefined, - ): { - completion: string[]; - thinking: string[]; - usage: Usage | null; - chunk: ChatMessage; - } { - const completion: string[] = []; - const thinking: string[] = []; - let usage: Usage | null = null; - - if (chunk.role === "assistant") { - completion.push(this._formatChatMessage(chunk)); - } else if (chunk.role === "thinking" && typeof chunk.content === "string") { - thinking.push(chunk.content); - } - - interaction?.logItem({ - kind: "message", - message: chunk, - }); - - if (chunk.role === "assistant" && chunk.usage) { - usage = chunk.usage; - } - - return { - completion, - thinking, - usage, - chunk, - }; - } - - private canUseOpenAIResponses(options: CompletionOptions): boolean { - return ( - this.providerName === "openai" && - typeof (this as any)._streamResponses === "function" && - (this as any).isOSeriesOrGpt5Model(options.model) - ); - } - - private async *openAIAdapterStream( - body: ChatCompletionCreateParams, - signal: AbortSignal, - onCitations: (c: string[]) => void, - ): AsyncGenerator { - const stream = this.openaiAdapter!.chatCompletionStream( - { ...body, stream: true }, - signal, - ); - for await (const chunk of stream) { - if (!this.lastRequestId && typeof (chunk as any).id === "string") { - this.lastRequestId = (chunk as any).id; - } - const chatChunk = fromChatCompletionChunk(chunk as any); - if (chatChunk) { - yield chatChunk; - } - if ((chunk as any).citations && Array.isArray((chunk as any).citations)) { - onCitations((chunk as any).citations); - } - } - } - - private async *openAIAdapterNonStream( - body: ChatCompletionCreateParams, - signal: AbortSignal, - ): AsyncGenerator { - const response = await this.openaiAdapter!.chatCompletionNonStream( - { ...body, stream: false }, - signal, - ); - this.lastRequestId = response.id ?? this.lastRequestId; - const messages = fromChatResponse(response as any); - for (const msg of messages) { - yield msg; - } - } - - private async *responsesStream( - messages: ChatMessage[], - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - const g = (this as any)._streamResponses( - messages, - signal, - options, - ) as AsyncGenerator; - for await (const m of g) { - yield m; - } - } - - private async *responsesNonStream( - messages: ChatMessage[], - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - const msg = await (this as any)._responses(messages, signal, options); - yield msg as ChatMessage; - } - - // Update the streamChat method: async *streamChat( _messages: ChatMessage[], signal: AbortSignal, @@ -1136,12 +1025,9 @@ export abstract class BaseLLM implements ILLM { messages = compiledChatMessages; } - const messagesCopy = [...messages]; // templateMessages may modify messages. - const prompt = this.templateMessages - ? this.templateMessages(messagesCopy) - : this._formatChatMessages(messagesCopy); - + ? this.templateMessages(messages) + : this._formatChatMessages(messages); if (logEnabled) { interaction?.logItem({ kind: "startChat", @@ -1154,111 +1040,100 @@ export abstract class BaseLLM implements ILLM { } } - // Performance optimization: Use arrays instead of string concatenation. - // String concatenation in loops creates new string objects for each operation, - // which is O(n²) for n chunks. Arrays with push() are O(1) per operation, - // making the total O(n). We join() only once at the end. - const thinking: string[] = []; - const completion: string[] = []; + let thinking = ""; + let completion = ""; let usage: Usage | undefined = undefined; - let citations: null | string[] = null; try { - { - if (this.shouldUseOpenAIAdapter("streamChat") && this.openaiAdapter) { - let body = toChatBody(messages, completionOptions, { - includeReasoningField: this.supportsReasoningField, - includeReasoningDetailsField: this.supportsReasoningDetailsField, + if (this.templateMessages) { + for await (const chunk of this._streamComplete( + prompt, + signal, + completionOptions, + )) { + completion += chunk; + interaction?.logItem({ + kind: "chunk", + chunk: chunk, }); + yield { role: "assistant", content: chunk }; + } + } else { + if (this.shouldUseOpenAIAdapter("streamChat") && this.openaiAdapter) { + let body = toChatBody(messages, completionOptions); body = this.modifyChatBody(body); - if (logEnabled) { + if (completionOptions.stream === false) { + // Stream false + const response = await this.openaiAdapter.chatCompletionNonStream( + { ...body, stream: false }, + signal, + ); + this.lastRequestId = response.id ?? this.lastRequestId; + const msg = fromChatResponse(response); + yield msg; + completion = this._formatChatMessage(msg); interaction?.logItem({ - kind: "startChat", - messages, - options: { - ...completionOptions, - requestBody: body, - } as CompletionOptions, - provider: this.providerName, + kind: "message", + message: msg, }); - if (this.llmRequestHook) { - this.llmRequestHook(completionOptions.model, prompt); - } - } - - const canUseResponses = this.canUseOpenAIResponses(completionOptions); - const useStream = completionOptions.stream !== false; - - let iterable: AsyncIterable; - if (canUseResponses) { - iterable = useStream - ? this.responsesStream(messages, signal, completionOptions) - : this.responsesNonStream(messages, signal, completionOptions); } else { - iterable = useStream - ? this.openAIAdapterStream(body, signal, (c) => { - if (!citations) { - citations = c; - } - }) - : this.openAIAdapterNonStream(body, signal); - } - - for await (const chunk of iterable) { - const result = this.processChatChunk(chunk, interaction); - completion.push(...result.completion); - thinking.push(...result.thinking); - if (result.usage !== null) { - usage = result.usage; + // Stream true + const stream = this.openaiAdapter.chatCompletionStream( + { + ...body, + stream: true, + }, + signal, + ); + for await (const chunk of stream) { + if ( + !this.lastRequestId && + typeof (chunk as any).id === "string" + ) { + this.lastRequestId = (chunk as any).id; + } + const result = fromChatCompletionChunk(chunk); + if (result) { + completion += this._formatChatMessage(result); + interaction?.logItem({ + kind: "message", + message: result, + }); + yield result; + } } - yield result.chunk; } } else { - if (logEnabled) { - interaction?.logItem({ - kind: "startChat", - messages, - options: completionOptions, - provider: this.providerName, - }); - if (this.llmRequestHook) { - this.llmRequestHook(completionOptions.model, prompt); - } - } - for await (const chunk of this._streamChat( messages, signal, completionOptions, )) { - const result = this.processChatChunk(chunk, interaction); - completion.push(...result.completion); - thinking.push(...result.thinking); - if (result.usage !== null) { - usage = result.usage; + if (chunk.role === "assistant") { + completion += this._formatChatMessage(chunk); + } else if (chunk.role === "thinking") { + thinking += chunk.content; + } + + interaction?.logItem({ + kind: "message", + message: chunk, + }); + + if (chunk.role === "assistant" && chunk.usage) { + usage = chunk.usage; } - yield result.chunk; + + yield chunk; } } } - - if (citations) { - const cits = citations as string[]; - interaction?.logItem({ - kind: "message", - message: { - role: "assistant", - content: `\n\nCitations:\n${cits.map((c: string, i: number) => `${i + 1}: ${c}`).join("\n")}\n\n`, - }, - }); - } - status = this._logEnd( completionOptions.model, prompt, - completion.join(""), - thinking.join(""), + completion, + thinking, interaction, usage, ); @@ -1276,8 +1151,8 @@ export abstract class BaseLLM implements ILLM { status = this._logEnd( completionOptions.model, prompt, - completion.join(""), - thinking.join(""), + completion, + thinking, interaction, usage, e, @@ -1288,7 +1163,7 @@ export abstract class BaseLLM implements ILLM { this._logEnd( completionOptions.model, prompt, - completion.join(""), + completion, undefined, interaction, usage, @@ -1297,20 +1172,20 @@ export abstract class BaseLLM implements ILLM { } } /* - TODO: According to: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking - During tool use, you must pass thinking and redacted_thinking blocks back to the API, - and you must include the complete unmodified block back to the API. This is critical - for maintaining the model's reasoning flow and conversation integrity. + TODO: According to: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking + During tool use, you must pass thinking and redacted_thinking blocks back to the API, + and you must include the complete unmodified block back to the API. This is critical + for maintaining the model's reasoning flow and conversation integrity. - On the other hand, adding thinking and redacted_thinking blocks are ignored on subsequent - requests when not using tools, so it's the simplest option to always add to history. - */ + On the other hand, adding thinking and redacted_thinking blocks are ignored on subsequent + requests when not using tools, so it's the simplest option to always add to history. + */ return { modelTitle: this.title ?? completionOptions.model, modelProvider: this.underlyingProviderName, prompt, - completion: completion.join(""), + completion, }; } diff --git a/core/llm/llms/Deepseek.ts b/core/llm/llms/Deepseek.ts index 01d16ec3d50..305d177e751 100644 --- a/core/llm/llms/Deepseek.ts +++ b/core/llm/llms/Deepseek.ts @@ -6,8 +6,6 @@ import OpenAI from "./OpenAI.js"; class Deepseek extends OpenAI { static providerName = "deepseek"; - protected supportsReasoningField = true; - protected supportsReasoningDetailsField = false; static defaultOptions: Partial = { apiBase: "https://api.deepseek.com/", model: "deepseek-coder", diff --git a/core/llm/llms/OpenAI.ts b/core/llm/llms/OpenAI.ts index 90512ecfbce..83166b6a2dc 100644 --- a/core/llm/llms/OpenAI.ts +++ b/core/llm/llms/OpenAI.ts @@ -14,18 +14,9 @@ import { renderChatMessage } from "../../util/messageContent.js"; import { BaseLLM } from "../index.js"; import { fromChatCompletionChunk, - fromResponsesChunk, LlmApiRequestType, toChatBody, - toResponsesInput, } from "../openaiTypeConverters.js"; -import { - ResponseInput, - ResponseInputItem, - ResponseInputMessageContentList, - ResponseCreateParamsBase, - Tool as ResponsesTool, -} from "openai/resources/responses/responses.mjs"; const NON_CHAT_MODELS = [ "text-davinci-002", @@ -58,139 +49,6 @@ const formatMessageForO1OrGpt5 = (messages: ChatCompletionMessageParam[]) => { }); }; -const formatMessageForO1OrGpt5ForResponses = ( - messages: ChatCompletionMessageParam[], -): ResponseInputItem[] => { - const input: ResponseInputItem[] = []; - - const pushMessage = ( - role: "user" | "assistant" | "system" | "developer", - content: string | ResponseInputMessageContentList, - ) => { - // o-series / gpt-5 use `developer` instead of `system` - const normalizedRole: "user" | "assistant" | "system" | "developer" = - role === "system" ? "developer" : role; - - input.push({ role: normalizedRole, content }); - }; - - for (const message of messages) { - switch (message.role) { - case "system": - case "developer": { - const content = message.content; - if (typeof content === "string") { - pushMessage("developer", content); - } else if (Array.isArray(content)) { - const parts: ResponseInputMessageContentList = content - .filter( - (p): p is { type: "text"; text: string } => p.type === "text", - ) - .map((p) => ({ type: "input_text" as const, text: p.text })); - pushMessage("developer", parts.length ? parts : ""); - } - break; - } - - case "user": { - const content = message.content; - if (typeof content === "string") { - pushMessage("user", content); - } else if (Array.isArray(content)) { - const parts: ResponseInputMessageContentList = []; - for (const part of content) { - if (part.type === "text") { - parts.push({ type: "input_text", text: part.text }); - } else if (part.type === "image_url") { - parts.push({ - type: "input_image", - image_url: part.image_url.url, - detail: part.image_url.detail ?? "auto", - }); - } else if (part.type === "file") { - parts.push({ - type: "input_file", - file_data: part.file.file_data, - file_id: part.file.file_id ?? undefined, - filename: part.file.filename, - }); - } - } - if (parts.length) { - pushMessage("user", parts); - } - } - break; - } - - case "assistant": { - const content = message.content; - if (typeof content === "string") { - if (content.length) pushMessage("assistant", content); - } else if (Array.isArray(content)) { - const text = content - .filter( - (p): p is { type: "text"; text: string } => p.type === "text", - ) - .map((p) => p.text) - .join(""); - if (text.length) pushMessage("assistant", text); - } - - if (Array.isArray(message.tool_calls)) { - for (const tc of message.tool_calls) { - if (tc.type === "function") { - input.push({ - type: "function_call", - name: tc.function.name, - arguments: tc.function.arguments, - call_id: tc.id, - }); - } else if (tc.type === "custom") { - input.push({ - type: "custom_tool_call", - name: tc.custom.name, - input: tc.custom.input, - call_id: tc.id, - }); - } - } - } - break; - } - - case "tool": { - const content = message.content; - const output = - typeof content === "string" - ? content - : content - .filter( - (p): p is { type: "text"; text: string } => p.type === "text", - ) - .map((p) => p.text) - .join(""); - input.push({ - type: "function_call_output", - call_id: message.tool_call_id, - output, - }); - break; - } - - case "function": { - // Deprecated in Chat Completions; no safe mapping into Responses input - break; - } - - default: - break; - } - } - - return input; -}; - class OpenAI extends BaseLLM { public useLegacyCompletionsEndpoint: boolean | undefined = undefined; @@ -276,10 +134,7 @@ class OpenAI extends BaseLLM { options: CompletionOptions, messages: ChatMessage[], ): ChatCompletionCreateParams { - const finalOptions = toChatBody(messages, options, { - includeReasoningField: this.supportsReasoningField, - includeReasoningDetailsField: this.supportsReasoningDetailsField, - }); + const finalOptions = toChatBody(messages, options); finalOptions.stop = options.stop?.slice(0, this.getMaxStopWords()); @@ -316,60 +171,6 @@ class OpenAI extends BaseLLM { return finalOptions; } - protected _convertArgsResponses( - options: CompletionOptions, - messages: ChatMessage[], - ): ResponseCreateParamsBase { - // Specialized conversion for Responses API (strongly typed body) - const model = options.model; - - const input = toResponsesInput(messages); - - const body: ResponseCreateParamsBase = { - model, - input, - temperature: options.temperature ?? null, - top_p: options.topP ?? null, - reasoning: { - effort: "medium", - summary: "auto", - }, - include: ["reasoning.encrypted_content"], - }; - - // Tools support for Responses API (schema differs from Chat Completions) - if (options.tools?.length) { - body.tools = options.tools - .filter((t) => !t.type || t.type === "function") - .map( - (t) => - ({ - type: "function", - name: t.function.name, - description: t.function.description ?? undefined, - parameters: t.function.parameters ?? undefined, - strict: t.function.strict ?? undefined, - }) as ResponsesTool, - ); - } - if (options.toolChoice) { - body.tool_choice = { - type: "function", - name: options.toolChoice.function.name, - } as ResponseCreateParamsBase["tool_choice"]; - } - - if (typeof options.maxTokens === "number") { - body.max_output_tokens = options.maxTokens; - } - - if (model === "o1") { - body.stream = false; - } - - return body; - } - protected _getHeaders() { return { "Content-Type": "application/json", @@ -396,7 +197,7 @@ class OpenAI extends BaseLLM { } protected _getEndpoint( - endpoint: "chat/completions" | "completions" | "models" | "responses", + endpoint: "chat/completions" | "completions" | "models", ) { if (!this.apiBase) { throw new Error( @@ -567,91 +368,6 @@ class OpenAI extends BaseLLM { } } - // Minimal draft: Responses API support for select models - protected async *_streamResponses( - messages: ChatMessage[], - signal: AbortSignal, - options: CompletionOptions, - ): AsyncGenerator { - if (!this.isOSeriesOrGpt5Model(options.model)) { - return; - } - - const body: any = this._convertArgsResponses(options, messages); - - // o1 does not support streaming - if (body.model === "o1") { - const res = await this._responses(messages, signal, options); - if (Array.isArray(res)) { - for (const m of res) { - if (m) yield m; - } - } else if (res) { - yield res; - } - return; - } - - const response = await this.fetch(this._getEndpoint("responses"), { - method: "POST", - headers: this._getHeaders(), - body: JSON.stringify({ - ...body, - stream: true, - ...this.extraBodyProperties(), - }), - signal, - }); - - for await (const evt of streamSse(response)) { - try { - const msg = fromResponsesChunk(evt); - if (Array.isArray(msg)) { - for (const m of msg) { - if (m) yield m; - } - } else if (msg) { - yield msg; - } - } catch { - // ignore malformed chunks - } - } - } - - protected async _responses( - messages: ChatMessage[], - signal: AbortSignal, - options: CompletionOptions, - ): Promise { - if (!this.isOSeriesOrGpt5Model(options.model)) { - // Minimal draft: only handle supported models for now - return { role: "assistant", content: "" }; - } - - const body: any = this._convertArgsResponses(options, messages); - - const response = await this.fetch(this._getEndpoint("responses"), { - method: "POST", - headers: this._getHeaders(), - body: JSON.stringify({ - ...body, - stream: false, - ...this.extraBodyProperties(), - }), - signal, - }); - - if ((response as any).status === 499) { - return { role: "assistant", content: "" }; - } - - const data: any = await response.json().catch(() => ({})); - const msg = fromResponsesChunk(data); - if (msg) return msg; - return { role: "assistant", content: "" }; - } - protected async *_streamFim( prefix: string, suffix: string, diff --git a/core/llm/llms/OpenRouter.ts b/core/llm/llms/OpenRouter.ts index 4672a465897..f2ef6bde25f 100644 --- a/core/llm/llms/OpenRouter.ts +++ b/core/llm/llms/OpenRouter.ts @@ -7,8 +7,6 @@ import OpenAI from "./OpenAI.js"; class OpenRouter extends OpenAI { static providerName = "openrouter"; - protected supportsReasoningField = true; - protected supportsReasoningDetailsField = true; static defaultOptions: Partial = { apiBase: "https://openrouter.ai/api/v1/", model: "gpt-4o-mini", diff --git a/core/llm/openaiTypeConverters.ts b/core/llm/openaiTypeConverters.ts index b01e45bc6f9..75ea7ee911d 100644 --- a/core/llm/openaiTypeConverters.ts +++ b/core/llm/openaiTypeConverters.ts @@ -4,102 +4,15 @@ import { ChatCompletionAssistantMessageParam, ChatCompletionChunk, ChatCompletionCreateParams, - ChatCompletionMessage, ChatCompletionMessageParam, CompletionCreateParams, } from "openai/resources/index"; -import type { - Response as OpenAIResponse, - ResponseStreamEvent, - ResponseTextDeltaEvent, - ResponseTextDoneEvent, - ResponseReasoningSummaryTextDeltaEvent, - ResponseReasoningSummaryTextDoneEvent, - ResponseReasoningTextDeltaEvent, - ResponseReasoningTextDoneEvent, - ResponseInput, - ResponseInputItem, - ResponseInputMessageContentList, - ResponseOutputMessage, - ResponseOutputText, - ResponseFunctionToolCall, - EasyInputMessage, - ResponseReasoningItem, -} from "openai/resources/responses/responses.mjs"; -import { - ChatMessage, - CompletionOptions, - TextMessagePart, - ThinkingChatMessage, - ToolCallDelta, - AssistantChatMessage, - UserChatMessage, - SystemChatMessage, - ToolResultChatMessage, - MessageContent, - ImageMessagePart, - MessagePart, -} from ".."; - -function appendReasoningFieldsIfSupported( - msg: ChatCompletionAssistantMessageParam & { - reasoning?: string; - reasoning_details?: any[]; - }, - options: CompletionOptions, - prevMessage?: ChatMessage, - providerFlags?: { - includeReasoningField?: boolean; - includeReasoningDetailsField?: boolean; - }, -) { - if (!prevMessage || prevMessage.role !== "thinking") return; - - const includeReasoning = !!providerFlags?.includeReasoningField; - const includeReasoningDetails = !!providerFlags?.includeReasoningDetailsField; - if (!includeReasoning && !includeReasoningDetails) return; - - const reasoningDetailsValue = - prevMessage.reasoning_details || - (prevMessage.signature - ? [{ signature: prevMessage.signature }] - : undefined); - - // Claude-specific safeguard: prevent errors when switching to Claude after another model. - // Claude requires a signed reasoning_details block; if missing, we must omit both fields. - // This check is done before adding any fields to avoid deletes. - if ( - includeReasoningDetails && - options.model.includes("claude") && - !( - Array.isArray(reasoningDetailsValue) && - reasoningDetailsValue.some((d) => d && d.signature) - ) - ) { - console.warn( - "Omitting reasoning fields for Claude: no signature present in reasoning_details", - ); - return; - } - - if (includeReasoningDetails && reasoningDetailsValue) { - msg.reasoning_details = reasoningDetailsValue || []; - } - if (includeReasoning) { - msg.reasoning = prevMessage.content as string; - } -} +import { ChatMessage, CompletionOptions, TextMessagePart } from ".."; export function toChatMessage( message: ChatMessage, - options: CompletionOptions, - prevMessage?: ChatMessage, - providerFlags?: { - includeReasoningField?: boolean; - includeReasoningDetailsField?: boolean; - }, -): ChatCompletionMessageParam | null { +): ChatCompletionMessageParam { if (message.role === "tool") { return { role: "tool", @@ -113,30 +26,18 @@ export function toChatMessage( content: message.content, }; } - if (message.role === "thinking") { - // Return null - thinking messages are merged into following assistant messages - return null; - } if (message.role === "assistant") { - // Base assistant message - const msg: ChatCompletionAssistantMessageParam & { - reasoning?: string; - reasoning_details?: { - [key: string]: any; - signature?: string | undefined; - }[]; - } = { + const msg: ChatCompletionAssistantMessageParam = { role: "assistant", content: typeof message.content === "string" ? message.content || " " // LM Studio (and other providers) don't accept empty content : message.content .filter((part) => part.type === "text") - .map((part) => part as TextMessagePart), + .map((part) => part as TextMessagePart), // can remove with newer typescript version }; - // Add tool calls if present if (message.toolCalls) { msg.tool_calls = message.toolCalls.map((toolCall) => ({ id: toolCall.id!, @@ -147,16 +48,7 @@ export function toChatMessage( }, })); } - - // Preserving reasoning blocks - appendReasoningFieldsIfSupported( - msg as any, - options, - prevMessage, - providerFlags, - ); - - return msg as ChatCompletionMessageParam; + return msg; } else { if (typeof message.content === "string") { return { @@ -170,8 +62,11 @@ export function toChatMessage( // that don't support multi-media format return { role: "user", - content: message.content.some((item) => item.type !== "text") - ? message.content.map((part) => { + content: !message.content.some((item) => item.type !== "text") + ? message.content + .map((item) => (item as TextMessagePart).text) + .join("") || " " + : message.content.map((part) => { if (part.type === "imageUrl") { return { type: "image_url" as const, @@ -182,10 +77,7 @@ export function toChatMessage( }; } return part; - }) - : message.content - .map((item) => (item as TextMessagePart).text) - .join("") || " ", + }), }; } } @@ -193,17 +85,9 @@ export function toChatMessage( export function toChatBody( messages: ChatMessage[], options: CompletionOptions, - providerFlags?: { - includeReasoningField?: boolean; - includeReasoningDetailsField?: boolean; - }, ): ChatCompletionCreateParams { const params: ChatCompletionCreateParams = { - messages: messages - .map((m, index) => - toChatMessage(m, options, messages[index - 1], providerFlags), - ) - .filter((m) => m !== null) as ChatCompletionMessageParam[], + messages: messages.map(toChatMessage), model: options.model, max_tokens: options.maxTokens, temperature: options.temperature, @@ -269,40 +153,11 @@ export function toFimBody( } as any; } -export function fromChatResponse(response: ChatCompletion): ChatMessage[] { - const messages: ChatMessage[] = []; - const message = response.choices[0].message as ChatCompletionMessage & { - reasoning?: string; - reasoning_content?: string; - reasoning_details?: { - signature?: string; - [key: string]: any; - }[]; - }; - - // Check for reasoning content first (similar to fromChatCompletionChunk) - if (message.reasoning_content || message.reasoning) { - const thinkingMessage: ChatMessage = { - role: "thinking", - content: (message as any).reasoning_content || (message as any).reasoning, - }; - - // Preserve reasoning_details if present - if (message.reasoning_details) { - thinkingMessage.reasoning_details = message.reasoning_details; - // Extract signature from reasoning_details if available - if (message.reasoning_details[0]?.signature) { - thinkingMessage.signature = message.reasoning_details[0].signature; - } - } - - messages.push(thinkingMessage); - } - - // Then add the assistant message +export function fromChatResponse(response: ChatCompletion): ChatMessage { + const message = response.choices[0].message; const toolCall = message.tool_calls?.[0]; if (toolCall) { - messages.push({ + return { role: "assistant", content: "", toolCalls: message.tool_calls @@ -315,31 +170,19 @@ export function fromChatResponse(response: ChatCompletion): ChatMessage[] { arguments: (tc as any).function?.arguments, }, })), - }); - } else { - messages.push({ - role: "assistant", - content: message.content ?? "", - }); + }; } - return messages; + return { + role: "assistant", + content: message.content ?? "", + }; } export function fromChatCompletionChunk( chunk: ChatCompletionChunk, ): ChatMessage | undefined { - console.log("chunk", chunk); - - const delta = chunk.choices?.[0]?.delta as - | (ChatCompletionChunk.Choice.Delta & { - reasoning?: string; - reasoning_content?: string; - reasoning_details?: { - signature?: string; - }[]; - }) - | undefined; + const delta = chunk.choices?.[0]?.delta; if (delta?.content) { return { @@ -365,568 +208,11 @@ export function fromChatCompletionChunk( toolCalls, }; } - } else if ( - delta?.reasoning_content || - delta?.reasoning || - delta?.reasoning_details?.length - ) { - const message: ThinkingChatMessage = { - role: "thinking", - content: delta.reasoning_content || delta.reasoning || "", - signature: delta?.reasoning_details?.[0]?.signature, - reasoning_details: delta?.reasoning_details as any[], - }; - return message; - } - - return undefined; -} - -function handleTextDeltaEvent( - e: ResponseTextDeltaEvent, -): ChatMessage | undefined { - return e.delta ? { role: "assistant", content: e.delta } : undefined; -} - -function handleFunctionCallArgsDelta(e: any): ChatMessage | undefined { - const ev: any = e as any; - const item = ev.item || {}; - const name = item && typeof item.name === "string" ? item.name : undefined; - const argDelta = - typeof ev.delta === "string" - ? ev.delta - : (ev.delta?.arguments ?? ev.arguments); - if (typeof argDelta === "string" && argDelta.length > 0) { - const call_id = - (item?.call_id as string | undefined) || - (item?.id as string | undefined) || - ""; - const toolCalls: ToolCallDelta[] = [ - { - id: call_id, - type: "function", - function: { name: name || "", arguments: argDelta }, - }, - ]; - const assistant: AssistantChatMessage = { - role: "assistant", - content: "", - toolCalls, - }; - return assistant; - } - return undefined; -} - -function handleOutputItemAdded( - e: ResponseStreamEvent, -): ChatMessage | undefined { - const item = (e as any).item as { - type?: string; - id?: string; - name?: string; - arguments?: string; - call_id?: string; - summary?: Array<{ type: string; text: string }>; - encrypted_content?: string; - }; - if (!item || !item.type) return undefined; - if (item.type === "reasoning") { - const details: Array<{ [k: string]: unknown }> = []; - if (item.id) details.push({ type: "reasoning_id", id: item.id }); - if (typeof item.encrypted_content === "string" && item.encrypted_content) { - details.push({ - type: "encrypted_content", - encrypted_content: item.encrypted_content, - }); - } - if (Array.isArray(item.summary)) { - for (const part of item.summary) { - if (part?.type === "summary_text" && typeof part.text === "string") { - details.push({ type: "summary_text", text: part.text }); - } - } - } - const thinking: ThinkingChatMessage = { - role: "thinking", - content: "", - reasoning_details: details, - metadata: { - reasoningId: item.id as string, - encrypted_content: item.encrypted_content as string | undefined, - }, - }; - return thinking; - } - if (item.type === "message" && typeof item.id === "string") { - return { - role: "assistant", - content: "", - metadata: { responsesOutputItemId: item.id }, - }; - } - if (item.type === "function_call" && typeof item.id === "string") { - const name = item.name as string | undefined; - const args = typeof item.arguments === "string" ? item.arguments : ""; - const call_id = item.call_id as string | undefined; - const toolCalls: ToolCallDelta[] = name - ? [ - { - id: call_id || (item.id as string), - type: "function", - function: { name, arguments: args }, - }, - ] - : []; - const assistant: AssistantChatMessage = { - role: "assistant", - content: "", - toolCalls, - metadata: { responsesOutputItemId: item.id as string }, - }; - return assistant; - } - return undefined; -} - -function handleReasoningSummaryDelta( - e: ResponseReasoningSummaryTextDeltaEvent, -): ChatMessage | undefined { - const details: Array<{ [k: string]: unknown }> = [ - { type: "summary_text", text: e.delta }, - ]; - if ((e as any).item_id) - details.push({ type: "reasoning_id", id: (e as any).item_id }); - const thinking: ThinkingChatMessage = { - role: "thinking", - content: e.delta, - reasoning_details: details, - }; - return thinking; -} - -function handleReasoningSummaryDone( - e: ResponseReasoningSummaryTextDoneEvent, -): ChatMessage | undefined { - const details: Array<{ [k: string]: unknown }> = []; - if (e.text) details.push({ type: "summary_text", text: e.text }); - if ((e as any).item_id) - details.push({ type: "reasoning_id", id: (e as any).item_id }); - const thinking: ThinkingChatMessage = { - role: "thinking", - content: e.text, - reasoning_details: details, - }; - return thinking; -} - -function handleReasoningTextDelta( - e: ResponseReasoningTextDeltaEvent, -): ChatMessage | undefined { - const details: Array<{ [k: string]: unknown }> = [ - { type: "reasoning_text", text: e.delta }, - ]; - if ((e as any).item_id) - details.push({ type: "reasoning_id", id: (e as any).item_id }); - const thinking: ThinkingChatMessage = { - role: "thinking", - content: e.delta, - reasoning_details: details, - }; - return thinking; -} - -function handleReasoningTextDone( - e: ResponseReasoningTextDoneEvent, -): ChatMessage | undefined { - const details: Array<{ [k: string]: unknown }> = []; - if (e.text) details.push({ type: "reasoning_text", text: e.text }); - if ((e as any).item_id) - details.push({ type: "reasoning_id", id: (e as any).item_id }); - const thinking: ThinkingChatMessage = { - role: "thinking", - content: e.text, - reasoning_details: details, - }; - return thinking; -} - -function handleResponsesStreamEvent( - e: ResponseStreamEvent, -): ChatMessage | undefined { - const t = (e as any).type as string; - if (t === "response.output_text.delta") { - return handleTextDeltaEvent(e as ResponseTextDeltaEvent); - } - if (t === "response.output_text.done") { - return undefined; // avoid duplicate final text - } - if (t === "response.function_call_arguments.delta") { - return handleFunctionCallArgsDelta(e); - } - if (t === "response.function_call_arguments.done") { - return undefined; - } - if (t === "response.output_item.added") { - return handleOutputItemAdded(e); - } - if (t === "response.reasoning_summary_text.delta") { - return handleReasoningSummaryDelta( - e as ResponseReasoningSummaryTextDeltaEvent, - ); - } - if (t === "response.reasoning_summary_text.done") { - return handleReasoningSummaryDone( - e as ResponseReasoningSummaryTextDoneEvent, - ); - } - if (t === "response.reasoning_text.delta") { - return handleReasoningTextDelta(e as ResponseReasoningTextDeltaEvent); - } - if (t === "response.reasoning_text.done") { - return handleReasoningTextDone(e as ResponseReasoningTextDoneEvent); - } - return undefined; -} - -function handleResponsesFinal( - resp: OpenAIResponse, -): ChatMessage | ChatMessage[] | undefined { - // Prefer structured output items when present - if (Array.isArray(resp.output) && resp.output.length > 0) { - const result: ChatMessage[] = []; - for (const raw of resp.output as any[]) { - const item = raw as any; - if (!item || typeof item !== "object") continue; - if (item.type === "reasoning") { - const details: Array<{ [k: string]: unknown }> = []; - if (typeof item.id === "string") { - details.push({ type: "reasoning_id", id: item.id }); - } - if (Array.isArray(item.summary)) { - for (const s of item.summary) { - if (s?.type === "summary_text" && typeof s.text === "string") { - details.push({ type: "summary_text", text: s.text }); - } - } - } - if (Array.isArray(item.content)) { - for (const c of item.content) { - if (c?.type === "reasoning_text" && typeof c.text === "string") { - details.push({ type: "reasoning_text", text: c.text }); - } - } - } - if ( - typeof item.encrypted_content === "string" && - item.encrypted_content - ) { - details.push({ - type: "encrypted_content", - encrypted_content: item.encrypted_content, - }); - } - const thinking: ThinkingChatMessage = { - role: "thinking", - content: "", - reasoning_details: details, - metadata: { - reasoningId: item.id as string, - encrypted_content: item.encrypted_content as string | undefined, - }, - }; - result.push(thinking); - continue; - } - if (item.type === "message") { - let text = ""; - if (Array.isArray(item.content)) { - text = (item.content as any[]) - .map((c) => (typeof c?.text === "string" ? c.text : "")) - .join(""); - } else if (typeof item.content === "string") { - text = item.content; - } - const assistant: AssistantChatMessage = { - role: "assistant", - content: text || "", - metadata: - typeof item.id === "string" - ? { responsesOutputItemId: item.id } - : undefined, - }; - result.push(assistant); - continue; - } - if (item.type === "function_call") { - const name = item.name as string | undefined; - const args = - typeof item.arguments === "string" - ? item.arguments - : JSON.stringify(item.arguments ?? ""); - const call_id = - (item.call_id as string | undefined) || - (item.id as string | undefined) || - ""; - const toolCalls: ToolCallDelta[] = name - ? [ - { - id: call_id, - type: "function", - function: { name, arguments: args || "" }, - }, - ] - : []; - const assistant: AssistantChatMessage = { - role: "assistant", - content: "", - toolCalls, - metadata: - typeof item.id === "string" - ? { responsesOutputItemId: item.id } - : undefined, - }; - result.push(assistant); - continue; - } - } - if (result.length > 0) return result; - } - - // Fallback to output_text when no structured output is present - if (typeof resp.output_text === "string" && resp.output_text.length > 0) { - return { role: "assistant", content: resp.output_text }; } return undefined; } -export function fromResponsesChunk( - event: ResponseStreamEvent | OpenAIResponse, -): ChatMessage | ChatMessage[] | undefined { - if (typeof (event as any).type === "string") { - return handleResponsesStreamEvent(event as ResponseStreamEvent); - } - return handleResponsesFinal(event as OpenAIResponse); -} - -export function mergeReasoningDetails( - existing: any[] | undefined, - delta: any[] | undefined, -): any[] | undefined { - if (!delta) return existing; - if (!existing) return delta; - - const result = [...existing]; - - for (const deltaItem of delta) { - // Skip items without a type - if (!deltaItem.type) { - continue; - } - - // Find existing item with the same type - const existingIndex = result.findIndex( - (item) => item.type === deltaItem.type, - ); - - if (existingIndex === -1) { - // No existing item with this type, add new item - result.push({ ...deltaItem }); - } else { - // Merge with existing item of the same type - const existingItem = result[existingIndex]; - - for (const [key, value] of Object.entries(deltaItem)) { - if (value === null || value === undefined) continue; - - if (key === "text" || key === "signature" || key === "summary") { - // Concatenate text and signature fields - existingItem[key] = (existingItem[key] || "") + value; - } else if (key !== "type") { - // Don't overwrite type - // Overwrite other fields - existingItem[key] = value; - } - } - } - } - - return result; -} - -function getTextFromMessageContent(content: MessageContent): string { - if (typeof content === "string") return content; - return content - .filter((p): p is TextMessagePart => p.type === "text") - .map((p) => p.text) - .join(""); -} - -function toResponseInputContentList( - parts: MessagePart[], -): ResponseInputMessageContentList { - const list: ResponseInputMessageContentList = []; - for (const part of parts) { - if (part.type === "text") { - list.push({ type: "input_text", text: part.text }); - } else if (part.type === "imageUrl") { - list.push({ - type: "input_image", - image_url: part.imageUrl.url, - detail: "auto", - }); - } - } - return list; -} - -export function toResponsesInput(messages: ChatMessage[]): ResponseInput { - const input: ResponseInput = []; - - const pushMessage = ( - role: "user" | "assistant" | "system" | "developer", - content: string | ResponseInputMessageContentList, - ) => { - const normalizedRole: "user" | "assistant" | "system" | "developer" = - role === "system" ? "developer" : role; - const easyMsg: EasyInputMessage = { - role: normalizedRole, - content, - type: "message", - }; - input.push(easyMsg as ResponseInputItem); - }; - - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]; - switch (msg.role) { - case "system": { - const content = getTextFromMessageContent(msg.content); - pushMessage("developer", content || ""); - break; - } - case "user": { - if (typeof msg.content === "string") { - pushMessage("user", msg.content); - } else if (Array.isArray(msg.content)) { - const parts = toResponseInputContentList( - msg.content as MessagePart[], - ); - pushMessage("user", parts.length ? parts : ""); - } - break; - } - case "assistant": { - const text = getTextFromMessageContent(msg.content); - - const respId = msg.metadata?.responsesOutputItemId as - | string - | undefined; - const toolCalls = msg.toolCalls as ToolCallDelta[] | undefined; - - if (respId && Array.isArray(toolCalls) && toolCalls.length > 0) { - // Emit full function_call output item - const tc = toolCalls[0]; - const name = tc?.function?.name as string | undefined; - const args = tc?.function?.arguments as string | undefined; - const call_id = tc?.id as string | undefined; - const functionCallItem: ResponseFunctionToolCall = { - id: respId, - type: "function_call", - name: name || "", - arguments: typeof args === "string" ? args : "{}", - call_id: call_id || respId, - }; - input.push(functionCallItem); - } else if (respId) { - // Emit full assistant output message item - const outputMessageItem: ResponseOutputMessage = { - id: respId, - role: "assistant", - type: "message", - status: "completed", - content: [ - { - type: "output_text", - text: text || "", - annotations: [], - } satisfies ResponseOutputText, - ], - }; - input.push(outputMessageItem); - } else { - // Fallback to EasyInput assistant message - pushMessage("assistant", text || ""); - } - break; - } - case "tool": { - const call_id = msg.toolCallId; - const output = - typeof msg.content === "string" - ? msg.content - : JSON.stringify(msg.content); - const functionCallOutput: ResponseInputItem = { - type: "function_call_output", - call_id, - output, - } as ResponseInputItem; - input.push(functionCallOutput); - break; - } - case "thinking": { - const details = (msg as ThinkingChatMessage).reasoning_details ?? []; - if (details.length) { - let id: string | undefined; - let summaryText = ""; - let encrypted: string | undefined; - let reasoningText = ""; - for (const raw of details as Array>) { - const d = raw as { - type?: string; - id?: string; - text?: string; - encrypted_content?: string; - }; - if (d.type === "reasoning_id" && d.id) id = d.id; - else if (d.type === "encrypted_content" && d.encrypted_content) - encrypted = d.encrypted_content; - else if (d.type === "summary_text" && typeof d.text === "string") - summaryText += d.text; - else if (d.type === "reasoning_text" && typeof d.text === "string") - reasoningText += d.text; - } - if (id) { - const reasoningItem: ResponseReasoningItem = { - id, - type: "reasoning", - summary: [], - } as ResponseReasoningItem; - if (summaryText) { - reasoningItem.summary = [ - { type: "summary_text", text: summaryText }, - ]; - } - if (reasoningText) { - reasoningItem.content = [ - { type: "reasoning_text", text: reasoningText }, - ]; - } - if (encrypted) { - reasoningItem.encrypted_content = encrypted; - } - input.push(reasoningItem as ResponseInputItem); - } - } - break; - } - } - } - - return input; -} - export type LlmApiRequestType = | "chat" | "streamChat" diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index 6e723340c15..d4ebe48686d 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -24,7 +24,6 @@ import { ToolCallDelta, ToolCallState, } from "core"; -import { mergeReasoningDetails } from "core/llm/openaiTypeConverters"; import type { RemoteSessionMetadata } from "core/control-plane/client"; import { NEW_SESSION_TITLE } from "core/util/constants"; import { @@ -650,29 +649,6 @@ export const sessionSlice = createSlice({ ) { handleStreamingToolCallUpdates(message, lastItem); } - - // Attach Responses API output item id to the current assistant message if present - // fromResponsesChunk sets message.metadata.responsesOutputItemId when it sees output_item.added for messages - if ( - message.role === "assistant" && - lastMessage.role === "assistant" && - message.metadata?.responsesOutputItemId - ) { - lastMessage.metadata = lastMessage.metadata || {}; - lastMessage.metadata.responsesOutputItemId = message.metadata - .responsesOutputItemId as string; - } - - if ( - message.role === "thinking" && - message.reasoning_details && - lastMessage.role === "thinking" - ) { - lastMessage.reasoning_details = mergeReasoningDetails( - lastMessage.reasoning_details, - message.reasoning_details, - ); - } } } },