diff --git a/core/index.d.ts b/core/index.d.ts index b5b409ef929..a11f3730e57 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -397,6 +397,8 @@ export interface AssistantChatMessage { content: MessageContent; toolCalls?: ToolCallDelta[]; usage?: Usage; + opcRequestId?: string; + [key: string]: unknown; } export interface SystemChatMessage { diff --git a/core/llm/index.ts b/core/llm/index.ts index 54ea22243f9..b4fe3ff46e6 100644 --- a/core/llm/index.ts +++ b/core/llm/index.ts @@ -890,10 +890,18 @@ export abstract class BaseLLM implements ILLM { options: LLMFullCompletionOptions = {}, ) { let completion = ""; + let opcRequestId: string | undefined = undefined; for await (const message of this.streamChat(messages, signal, options)) { completion += renderChatMessage(message); + if (message.role === "assistant") { + opcRequestId = (message as import("../index").AssistantChatMessage).opcRequestId ?? opcRequestId; + } } - return { role: "assistant" as const, content: completion }; + return { + role: "assistant" as const, + content: completion, + ...(opcRequestId ? { opcRequestId } : {}), + }; } compileChatMessages( diff --git a/core/llm/llms/oca/Oca.ts b/core/llm/llms/oca/Oca.ts index d6a4307e96e..f28bddac0f2 100644 --- a/core/llm/llms/oca/Oca.ts +++ b/core/llm/llms/oca/Oca.ts @@ -16,7 +16,7 @@ import { toChatBody, } from "../../openaiTypeConverters.js"; import { OcaTokenManager } from "./util/ocaTokenManager.js"; -import { createOcaHeaders, generateOpcRequestId } from "./util/utils.js"; +import { createOcaHeaders } from "./util/utils.js"; interface ModelInfoMap { models: { [key: string]: ModelInfo }; @@ -30,7 +30,6 @@ const parsePrice = (price: any) => { }; class Oca extends BaseLLM { private modelMap: ModelInfoMap; - constructor(options: LLMOptions) { super({ ...options, @@ -48,8 +47,8 @@ class Oca extends BaseLLM { static providerName = "oca"; protected useOpenAIAdapterFor: (LlmApiRequestType | "*")[] = [ - "list", - "streamChat", + "list" + // "streamChat" removed so we use OCA's native streaming with custom fields ]; protected _convertModelName(model: string): string { @@ -175,19 +174,43 @@ class Oca extends BaseLLM { signal, }); + let responseOpcRequestId: string | undefined = undefined; + if (typeof response.headers?.get === "function") { + const raw = response.headers.get("opc-request-id"); + console.log( + "[OPC DEBUG] raw response header opc-request-id:", + raw + ); + responseOpcRequestId = raw !== null ? raw : undefined; + console.log( + "[OPC DEBUG] RESPONSE HEADER: opc-request-id (final value):", + responseOpcRequestId + ); + } + // Handle non-streaming response if (body.stream === false) { if (response.status === 499) { return; // Aborted by user } const data = await response.json(); - yield data.choices[0].message; + if (data.choices && data.choices[0] && data.choices[0].message) { + const msg = data.choices[0].message; + if (msg.role === "assistant") { + (msg as any).opcRequestId = responseOpcRequestId; + console.log("[OPC DEBUG] attached opcRequestId to yielded msg:", responseOpcRequestId); + } + console.log("[OPC DEBUG] yielding msg:", JSON.stringify(msg, null, 2)); + yield msg; + } return; } for await (const value of streamSse(response)) { - const chunk = fromChatCompletionChunk(value); + const chunk = fromChatCompletionChunk(value, responseOpcRequestId); if (chunk) { + console.log("[OPC DEBUG] yielding streamed chunk:", JSON.stringify(chunk, null, 2)); + console.log("[OCA _streamChat] yielding chunk", JSON.stringify(chunk, null, 2)); yield chunk; } } diff --git a/core/llm/openaiTypeConverters.ts b/core/llm/openaiTypeConverters.ts index a24af02f7f0..92afed6521d 100644 --- a/core/llm/openaiTypeConverters.ts +++ b/core/llm/openaiTypeConverters.ts @@ -170,13 +170,15 @@ export function fromChatResponse(response: ChatCompletion): ChatMessage { export function fromChatCompletionChunk( chunk: ChatCompletionChunk, + opcRequestId?: string ): ChatMessage | undefined { + console.log("openaiTypeConverters : opcRequestId ", opcRequestId) const delta = chunk.choices?.[0]?.delta; - if (delta?.content) { return { role: "assistant", content: delta.content, + opcRequestId: opcRequestId }; } else if (delta?.tool_calls) { return { @@ -190,6 +192,7 @@ export function fromChatCompletionChunk( arguments: tool_call.function?.arguments, }, })), + opcRequestId: opcRequestId }; } diff --git a/core/llm/streamChat.ts b/core/llm/streamChat.ts index 2692195e226..264c644e139 100644 --- a/core/llm/streamChat.ts +++ b/core/llm/streamChat.ts @@ -105,10 +105,14 @@ export async function* llmStreamChat( break; } if (next.value) { - yield { - role: "assistant", - content: next.value, - }; + if (typeof next.value === "object" && next.value !== null) { + yield next.value; + } else { + yield { + role: "assistant", + content: typeof next.value === "string" ? next.value : String(next.value), + }; + } } next = await gen.next(); } @@ -137,7 +141,7 @@ export async function* llmStreamChat( next = await gen.next(); } if (config.experimental?.readResponseTTS && "completion" in next.value) { - void TTS.read(next.value?.completion); + void TTS.read(typeof next.value?.completion === "string" ? next.value?.completion : String(next.value?.completion)); } void Telemetry.capture( diff --git a/extensions/vscode/src/webviewProtocol.ts b/extensions/vscode/src/webviewProtocol.ts index 324219a516c..59ef09aa1ec 100644 --- a/extensions/vscode/src/webviewProtocol.ts +++ b/extensions/vscode/src/webviewProtocol.ts @@ -70,6 +70,8 @@ export class VsCodeWebviewProtocol ) { let next = await response.next(); while (!next.done) { + console.log("webview_content", next) + console.log("[webviewProtocol] yielding chunk from extension", JSON.stringify(next.value, null, 2)); respond({ done: false, content: next.value, @@ -77,6 +79,7 @@ export class VsCodeWebviewProtocol }); next = await response.next(); } + console.log("webview_content", next) respond({ done: true, content: next.value, diff --git a/gui/src/components/StepContainer/ResponseActions.tsx b/gui/src/components/StepContainer/ResponseActions.tsx index 0684327925c..1a3348d88f5 100644 --- a/gui/src/components/StepContainer/ResponseActions.tsx +++ b/gui/src/components/StepContainer/ResponseActions.tsx @@ -121,6 +121,27 @@ export default function ResponseActions({ checkIconClassName="h-3.5 w-3.5 text-success" /> + {/* Show Copy button for Oracle opcRequestId if present */} + {item.message.role === "assistant" && + !!(item.message as import("core").AssistantChatMessage).opcRequestId && ( + + navigator.clipboard.writeText( + (item.message as import("core").AssistantChatMessage).opcRequestId! + ) + } + > + + + )} + ); diff --git a/gui/src/components/StepContainer/StepContainer.tsx b/gui/src/components/StepContainer/StepContainer.tsx index 84d68193681..452911d0c23 100644 --- a/gui/src/components/StepContainer/StepContainer.tsx +++ b/gui/src/components/StepContainer/StepContainer.tsx @@ -86,6 +86,18 @@ export default function StepContainer(props: StepContainerProps) { "*", ); } + const message = props.item.message; + const opcRequestId = + message.role === "assistant" + ? (message as import("core").AssistantChatMessage).opcRequestId + : undefined; + // console.log( + // "STEP Container: history item at index", + // props.index, + // props.item, + // "assistant.opcRequestId:", + // opcRequestId + // ); return (
@@ -105,6 +117,16 @@ export default function StepContainer(props: StepContainerProps) { source={stripImages(props.item.message.content)} itemIndex={props.index} /> + {/* Show Oracle opcRequestId for OCA assistant messages */} + {message.role === "assistant" && + !!opcRequestId && ( +
+ Oracle opcRequestId:{" "} + + {opcRequestId} + +
+ )} )} {props.isLast && } diff --git a/gui/src/context/IdeMessenger.tsx b/gui/src/context/IdeMessenger.tsx index e30f39ccf09..d6017c4d1a2 100644 --- a/gui/src/context/IdeMessenger.tsx +++ b/gui/src/context/IdeMessenger.tsx @@ -196,6 +196,7 @@ export class IdeMessenger implements IIdeMessenger { }) => { if (event.data.messageId === messageId) { const responseData = event.data.data; + console.log("[Webview RAW CHUNK]", JSON.stringify(responseData, null, 2)); if ("error" in responseData) { error = responseData.error; return; @@ -206,6 +207,7 @@ export class IdeMessenger implements IIdeMessenger { done = true; returnVal = responseData.content; } else { + console.log("[IdeMessenger] buffer.push chunk", JSON.stringify(responseData.content, null, 2)); buffer.push(responseData.content); } } diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index 7f13b87fea5..4554f5da1f3 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -591,12 +591,15 @@ export const sessionSlice = createSlice({ lastMessage.role !== message.role || message.role === "tool" // Tool messages should always create new messages ) { - // Create a new message + // Create a new message, ensuring opcRequestId is preserved for assistant messages const historyItem: ChatHistoryItemWithMessageId = { message: { ...message, content: "", // Start with empty content, let accumulation logic handle it id: uuidv4(), + ...(message.role === "assistant" && (message as any).opcRequestId + ? { opcRequestId: (message as any).opcRequestId } + : {}), }, contextItems: [], }; @@ -606,8 +609,11 @@ export const sessionSlice = createSlice({ lastMessage = lastItem.message; } - // Add to the existing message + // Add to the existing message; also make sure opcRequestId always stays present on assistant messages if (messageContent) { + if (lastMessage.role === "assistant" && (message as any).opcRequestId) { + (lastMessage as any).opcRequestId = (message as any).opcRequestId; + } if (messageContent.includes("")) { lastItem.reasoning = { startAt: Date.now(), diff --git a/gui/src/redux/thunks/streamNormalInput.ts b/gui/src/redux/thunks/streamNormalInput.ts index 6e95191c4d4..2b691fef453 100644 --- a/gui/src/redux/thunks/streamNormalInput.ts +++ b/gui/src/redux/thunks/streamNormalInput.ts @@ -229,6 +229,7 @@ export const streamNormalInput = createAsyncThunk< break; } + console.log("[streamNormalInput] dispatching chunk", JSON.stringify(next.value, null, 2)); dispatch(streamUpdate(next.value)); next = await gen.next(); }