Files
continue/core/llm/streamChat.ts
2025-11-03 23:44:05 -08:00

182 lines
4.7 KiB
TypeScript

import { fetchwithRequestOptions } from "@continuedev/fetch";
import { ChatMessage, IDE, PromptLog } from "..";
import { ConfigHandler } from "../config/ConfigHandler";
import { usesCreditsBasedApiKey } from "../config/usesFreeTrialApiKey";
import { FromCoreProtocol, ToCoreProtocol } from "../protocol";
import { IMessenger, Message } from "../protocol/messenger";
import { Telemetry } from "../util/posthog";
import { TTS } from "../util/tts";
import { isOutOfStarterCredits } from "./utils/starterCredits";
export async function* llmStreamChat(
configHandler: ConfigHandler,
abortController: AbortController,
msg: Message<ToCoreProtocol["llm/streamChat"][0]>,
ide: IDE,
messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>,
): AsyncGenerator<ChatMessage, PromptLog> {
const { config } = await configHandler.loadConfig();
if (!config) {
throw new Error("Config not loaded");
}
// Stop TTS on new StreamChat
if (config.experimental?.readResponseTTS) {
void TTS.kill();
}
const {
legacySlashCommandData,
completionOptions,
messages,
messageOptions,
} = msg.data;
const model = config.selectedModelByRole.chat;
if (!model) {
throw new Error("No chat model selected");
}
// Log to return in case of error
const errorPromptLog = {
modelTitle: model?.title ?? model?.model,
modelProvider: model?.underlyingProviderName ?? "unknown",
completion: "",
prompt: "",
completionOptions: {
...msg.data.completionOptions,
model: model?.model,
},
};
try {
if (legacySlashCommandData) {
const { command, contextItems, historyIndex, input, selectedCode } =
legacySlashCommandData;
const slashCommand = config.slashCommands?.find(
(sc) => sc.name === command.name,
);
if (!slashCommand) {
throw new Error(`Unknown slash command ${command.name}`);
}
if (!slashCommand.run) {
console.error(
`Slash command ${command.name} (${command.source}) has no run function`,
);
throw new Error(`Slash command not found`);
}
const gen = slashCommand.run({
input,
history: messages,
llm: model,
contextItems,
params: command.params,
ide,
addContextItem: (item) => {
void messenger.request("addContextItem", {
item,
historyIndex,
});
},
selectedCode,
config,
fetch: (url, init) =>
fetchwithRequestOptions(
url,
{
...init,
signal: abortController.signal,
},
model.requestOptions,
),
completionOptions,
abortController,
});
let next = await gen.next();
while (!next.done) {
if (abortController.signal.aborted) {
next = await gen.return(errorPromptLog);
break;
}
if (next.value) {
yield {
role: "assistant",
content: next.value,
};
}
next = await gen.next();
}
if (!next.done) {
throw new Error("Will never happen");
}
return next.value;
} else {
const gen = model.streamChat(
messages,
abortController.signal,
completionOptions,
messageOptions,
);
let next = await gen.next();
while (!next.done) {
if (abortController.signal.aborted) {
next = await gen.return(errorPromptLog);
break;
}
const chunk = next.value;
yield chunk;
next = await gen.next();
}
if (config.experimental?.readResponseTTS && "completion" in next.value) {
void TTS.read(next.value?.completion);
}
void Telemetry.capture(
"chat",
{
model: model.model,
provider: model.providerName,
},
true,
);
void checkForOutOfStarterCredits(configHandler, messenger);
if (!next.done) {
throw new Error("Will never happen");
}
return next.value;
}
} catch (error) {
// Moved error handling that was here to GUI, keeping try/catch for clean diff
throw error;
}
}
async function checkForOutOfStarterCredits(
configHandler: ConfigHandler,
messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>,
) {
try {
const { config } = await configHandler.getSerializedConfig();
const creditStatus =
await configHandler.controlPlaneClient.getCreditStatus();
if (
config &&
creditStatus &&
isOutOfStarterCredits(usesCreditsBasedApiKey(config), creditStatus)
) {
void messenger.request("freeTrialExceeded", undefined);
}
} catch (error) {
console.error("Error checking free trial status:", error);
}
}