Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@ export interface AssistantChatMessage {
content: MessageContent;
toolCalls?: ToolCallDelta[];
usage?: Usage;
opcRequestId?: string;
[key: string]: unknown;
}

export interface SystemChatMessage {
Expand Down
10 changes: 9 additions & 1 deletion core/llm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
35 changes: 29 additions & 6 deletions core/llm/llms/oca/Oca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -30,7 +30,6 @@ const parsePrice = (price: any) => {
};
class Oca extends BaseLLM {
private modelMap: ModelInfoMap;

constructor(options: LLMOptions) {
super({
...options,
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
}
Expand Down
5 changes: 4 additions & 1 deletion core/llm/openaiTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -190,6 +192,7 @@ export function fromChatCompletionChunk(
arguments: tool_call.function?.arguments,
},
})),
opcRequestId: opcRequestId
};
}

Expand Down
14 changes: 9 additions & 5 deletions core/llm/streamChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions extensions/vscode/src/webviewProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ 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,
status: "success",
});
next = await response.next();
}
console.log("webview_content", next)
respond({
done: true,
content: next.value,
Expand Down
21 changes: 21 additions & 0 deletions gui/src/components/StepContainer/ResponseActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
<HeaderButtonWithToolTip
text="Copy Oracle opcRequestId"
tabIndex={-1}
onClick={() =>
navigator.clipboard.writeText(
(item.message as import("core").AssistantChatMessage).opcRequestId!
)
}
>
<CopyIconButton
tabIndex={-1}
text={(item.message as import("core").AssistantChatMessage).opcRequestId!}
clipboardIconClassName="h-3.5 w-3.5 text-blue-500"
checkIconClassName="h-3.5 w-3.5 text-success"
/>
</HeaderButtonWithToolTip>
)}

<FeedbackButtons item={item} />
</div>
);
Expand Down
22 changes: 22 additions & 0 deletions gui/src/components/StepContainer/StepContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
Expand All @@ -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 && (
<div className="text-xs text-gray-400 mt-1">
Oracle opcRequestId:{" "}
<span style={{ wordBreak: "break-all" }}>
{opcRequestId}
</span>
</div>
)}
</>
)}
{props.isLast && <ThinkingIndicator historyItem={props.item} />}
Expand Down
2 changes: 2 additions & 0 deletions gui/src/context/IdeMessenger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
Expand Down
10 changes: 8 additions & 2 deletions gui/src/redux/slices/sessionSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
};
Expand All @@ -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("<think>")) {
lastItem.reasoning = {
startAt: Date.now(),
Expand Down
1 change: 1 addition & 0 deletions gui/src/redux/thunks/streamNormalInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down