mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Show information about anthropic calls when clicking usage (#34)
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { memo } from 'react';
|
||||
import { Markdown } from './Markdown';
|
||||
import type { JSONValue } from 'ai';
|
||||
import type { ChatAnthropicInfo, AnthropicCall } from '~/lib/.server/llm/chat-anthropic';
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages/messages.mjs';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
interface AssistantMessageProps {
|
||||
content: string;
|
||||
@@ -13,6 +16,7 @@ export function getAnnotationsTokensUsage(annotations: JSONValue[] | undefined)
|
||||
) || []) as { type: string; value: any }[];
|
||||
|
||||
const usage: {
|
||||
chatInfo: ChatAnthropicInfo;
|
||||
completionTokens: number;
|
||||
promptTokens: number;
|
||||
totalTokens: number;
|
||||
@@ -21,13 +25,83 @@ export function getAnnotationsTokensUsage(annotations: JSONValue[] | undefined)
|
||||
return usage;
|
||||
}
|
||||
|
||||
function flatMessageContent(content: string | ContentBlockParam[]): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
let result = "";
|
||||
for (const elem of content) {
|
||||
if (elem.type === "text") {
|
||||
result += elem.text;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
console.log("AnthropicUnknownContent", JSON.stringify(content, null, 2));
|
||||
return "AnthropicUnknownContent";
|
||||
}
|
||||
|
||||
function describeChatInfo(chatInfo: ChatAnthropicInfo) {
|
||||
let text = "";
|
||||
|
||||
function appendCall(call: AnthropicCall) {
|
||||
text += "************************************************\n";
|
||||
text += "AnthropicMessageSend\n";
|
||||
text += "Message system:\n";
|
||||
text += call.systemPrompt;
|
||||
for (const message of call.messages) {
|
||||
text += `Message ${message.role}:\n`;
|
||||
text += flatMessageContent(message.content);
|
||||
}
|
||||
text += "Response:\n";
|
||||
text += call.responseText;
|
||||
text += "\n";
|
||||
text += `Tokens ${call.completionTokens + call.promptTokens}\n`;
|
||||
text += "************************************************\n";
|
||||
}
|
||||
|
||||
appendCall(chatInfo.mainCall);
|
||||
for (const call of chatInfo.restoreCalls) {
|
||||
appendCall(call);
|
||||
}
|
||||
|
||||
for (const info of chatInfo.infos) {
|
||||
text += info;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
|
||||
const usage = getAnnotationsTokensUsage(annotations);
|
||||
|
||||
const onUsageClicked = () => {
|
||||
if (!usage.chatInfo) {
|
||||
toast.error("No chat info found");
|
||||
return;
|
||||
}
|
||||
const text = describeChatInfo(usage.chatInfo);
|
||||
|
||||
// Create a blob with the text content
|
||||
const blob = new Blob([text], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Open the blob URL in a new window
|
||||
window.open(url);
|
||||
|
||||
// Clean up the blob URL after a short delay
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden w-full">
|
||||
{usage && (
|
||||
<div className="text-sm text-bolt-elements-textSecondary mb-2">
|
||||
<div
|
||||
className="text-sm text-bolt-elements-textSecondary mb-2 cursor-pointer hover:underline"
|
||||
onClick={onUsageClicked}
|
||||
title="View call information"
|
||||
>
|
||||
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -24,23 +24,6 @@ function convertContentToAnthropic(content: any): ContentBlockParam[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function flatMessageContent(content: string | ContentBlockParam[]): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
let result = "";
|
||||
for (const elem of content) {
|
||||
if (elem.type === "text") {
|
||||
result += elem.text;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
console.log("AnthropicUnknownContent", JSON.stringify(content, null, 2));
|
||||
return "AnthropicUnknownContent";
|
||||
}
|
||||
|
||||
export interface AnthropicApiKey {
|
||||
key: string;
|
||||
isUser: boolean;
|
||||
@@ -73,15 +56,7 @@ const callAnthropic = wrapWithSpan(
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: apiKey.key });
|
||||
|
||||
console.log("************************************************");
|
||||
console.log("AnthropicMessageSend");
|
||||
console.log("Message system:");
|
||||
console.log(systemPrompt);
|
||||
for (const message of messages) {
|
||||
console.log(`Message ${message.role}:`);
|
||||
console.log(flatMessageContent(message.content));
|
||||
}
|
||||
console.log("************************************************");
|
||||
|
||||
const response = await anthropic.messages.create({
|
||||
model: Model,
|
||||
@@ -111,11 +86,7 @@ const callAnthropic = wrapWithSpan(
|
||||
});
|
||||
|
||||
|
||||
console.log("************************************************");
|
||||
console.log("AnthropicMessageResponse:");
|
||||
console.log(responseText);
|
||||
console.log("AnthropicTokens", completionTokens + promptTokens);
|
||||
console.log("************************************************");
|
||||
console.log("AnthropicMessageResponse");
|
||||
|
||||
return {
|
||||
systemPrompt,
|
||||
@@ -142,7 +113,13 @@ function shouldRestorePartialFile(existingContent: string, newContent: string):
|
||||
return existingContent.length > newContent.length;
|
||||
}
|
||||
|
||||
interface ChatState {
|
||||
// Info about how the chat was processed which will be conveyed back to the client.
|
||||
infos: string[];
|
||||
}
|
||||
|
||||
async function restorePartialFile(
|
||||
state: ChatState,
|
||||
existingContent: string,
|
||||
newContent: string,
|
||||
apiKey: AnthropicApiKey,
|
||||
@@ -203,7 +180,7 @@ ${responseDescription}
|
||||
const closeTag = restoreCall.responseText.indexOf(CloseTag);
|
||||
|
||||
if (openTag === -1 || closeTag === -1) {
|
||||
console.error("Invalid restored content", restoreCall.responseText);
|
||||
state.infos.push(`Error: Invalid restored content: ${restoreCall.responseText}`);
|
||||
return { restoreCall, restoredContent: newContent };
|
||||
}
|
||||
|
||||
@@ -212,7 +189,7 @@ ${responseDescription}
|
||||
// Sometimes the model ignores its instructions and doesn't return the content if it hasn't
|
||||
// made any modifications. In this case we use the unmodified new content.
|
||||
if (restoredContent.length < existingContent.length && restoredContent.length < newContent.length) {
|
||||
console.error("Restored content too short", restoredContent);
|
||||
state.infos.push(`Error: Restored content too short: ${restoreCall.responseText}`);
|
||||
return { restoreCall, restoredContent: newContent };
|
||||
}
|
||||
|
||||
@@ -242,7 +219,7 @@ function getMessageDescription(responseText: string): string {
|
||||
return responseText;
|
||||
}
|
||||
|
||||
async function getLatestPackageVersion(packageName: string) {
|
||||
async function getLatestPackageVersion(state: ChatState, packageName: string) {
|
||||
try {
|
||||
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
||||
const data = await response.json() as any;
|
||||
@@ -250,7 +227,7 @@ async function getLatestPackageVersion(packageName: string) {
|
||||
return data.version;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error getting latest package version", packageName, e);
|
||||
state.infos.push(`Error getting latest package version: ${packageName}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -262,12 +239,12 @@ function ignorePackageUpgrade(packageName: string) {
|
||||
|
||||
// Upgrade dependencies in package.json to the latest version, instead of the random
|
||||
// and sometimes ancient versions that the AI picks.
|
||||
async function upgradePackageJSON(content: string) {
|
||||
async function upgradePackageJSON(state: ChatState, content: string) {
|
||||
try {
|
||||
const packageJSON = JSON.parse(content);
|
||||
for (const key of Object.keys(packageJSON.dependencies)) {
|
||||
if (!ignorePackageUpgrade(key)) {
|
||||
const version = await getLatestPackageVersion(key);
|
||||
const version = await getLatestPackageVersion(state, key);
|
||||
if (version) {
|
||||
packageJSON.dependencies[key] = version;
|
||||
}
|
||||
@@ -275,12 +252,12 @@ async function upgradePackageJSON(content: string) {
|
||||
}
|
||||
return JSON.stringify(packageJSON, null, 2);
|
||||
} catch (e) {
|
||||
console.error("Error upgrading package.json", e);
|
||||
state.infos.push(`Error upgrading package.json: ${e}`);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFileContents(responseText: string, oldContent: string, newContent: string) {
|
||||
function replaceFileContents(state: ChatState, responseText: string, oldContent: string, newContent: string) {
|
||||
let contentIndex = responseText.indexOf(oldContent);
|
||||
|
||||
if (contentIndex === -1) {
|
||||
@@ -288,8 +265,10 @@ function replaceFileContents(responseText: string, oldContent: string, newConten
|
||||
oldContent = oldContent.trim();
|
||||
contentIndex = responseText.indexOf(oldContent);
|
||||
|
||||
console.error("Old content not found in response", JSON.stringify({ responseText, oldContent }));
|
||||
return responseText;
|
||||
if (contentIndex == -1) {
|
||||
state.infos.push(`Error: Old content not found in response: ${JSON.stringify({ responseText, oldContent })}`);
|
||||
return responseText;
|
||||
}
|
||||
}
|
||||
|
||||
return responseText.substring(0, contentIndex) +
|
||||
@@ -302,7 +281,7 @@ interface FileContents {
|
||||
content: string;
|
||||
}
|
||||
|
||||
async function fixupResponseFiles(files: FileMap, apiKey: AnthropicApiKey, responseText: string) {
|
||||
async function fixupResponseFiles(state: ChatState, files: FileMap, apiKey: AnthropicApiKey, responseText: string) {
|
||||
const fileContents: FileContents[] = [];
|
||||
|
||||
const messageParser = new StreamingMessageParser({
|
||||
@@ -328,24 +307,31 @@ async function fixupResponseFiles(files: FileMap, apiKey: AnthropicApiKey, respo
|
||||
|
||||
if (shouldRestorePartialFile(existingContent, newContent)) {
|
||||
const { restoreCall, restoredContent } = await restorePartialFile(
|
||||
state,
|
||||
existingContent,
|
||||
newContent,
|
||||
apiKey,
|
||||
responseDescription
|
||||
);
|
||||
restoreCalls.push(restoreCall);
|
||||
responseText = replaceFileContents(responseText, newContent, restoredContent);
|
||||
responseText = replaceFileContents(state, responseText, newContent, restoredContent);
|
||||
}
|
||||
|
||||
if (filePath.includes("package.json")) {
|
||||
const newPackageJSON = await upgradePackageJSON(newContent);
|
||||
responseText = replaceFileContents(responseText, newContent, newPackageJSON);
|
||||
const newPackageJSON = await upgradePackageJSON(state, newContent);
|
||||
responseText = replaceFileContents(state, responseText, newContent, newPackageJSON);
|
||||
}
|
||||
}
|
||||
|
||||
return { responseText, restoreCalls };
|
||||
}
|
||||
|
||||
export type ChatAnthropicInfo = {
|
||||
mainCall: AnthropicCall;
|
||||
restoreCalls: AnthropicCall[];
|
||||
infos: string[];
|
||||
}
|
||||
|
||||
export async function chatAnthropic(chatController: ChatStreamController, files: FileMap, apiKey: AnthropicApiKey, systemPrompt: string, messages: CoreMessage[]) {
|
||||
const messageParams: MessageParam[] = [];
|
||||
|
||||
@@ -360,18 +346,22 @@ export async function chatAnthropic(chatController: ChatStreamController, files:
|
||||
|
||||
const mainCall = await callAnthropic(apiKey, systemPrompt, messageParams);
|
||||
|
||||
const { responseText, restoreCalls } = await fixupResponseFiles(files, apiKey, mainCall.responseText);
|
||||
const state: ChatState = {
|
||||
infos: [],
|
||||
};
|
||||
|
||||
const { responseText, restoreCalls } = await fixupResponseFiles(state, files, apiKey, mainCall.responseText);
|
||||
|
||||
chatController.writeText(responseText);
|
||||
|
||||
const callInfos = [mainCall, ...restoreCalls];
|
||||
const chatInfo: ChatAnthropicInfo = { mainCall, restoreCalls, infos: state.infos };
|
||||
|
||||
let completionTokens = 0;
|
||||
let promptTokens = 0;
|
||||
for (const callInfo of callInfos) {
|
||||
for (const callInfo of [mainCall, ...restoreCalls]) {
|
||||
completionTokens += callInfo.completionTokens;
|
||||
promptTokens += callInfo.promptTokens;
|
||||
}
|
||||
|
||||
chatController.writeUsage({ callInfos, completionTokens, promptTokens });
|
||||
chatController.writeUsage({ chatInfo, completionTokens, promptTokens });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// to be functionality exported from the associated packages to do this so
|
||||
// for now we do it manually after reverse engineering the protocol.
|
||||
|
||||
import type { AnthropicCall } from "~/lib/.server/llm/chat-anthropic";
|
||||
import type { ChatAnthropicInfo } from "~/lib/.server/llm/chat-anthropic";
|
||||
|
||||
export interface ChatFileChange {
|
||||
filePath: string;
|
||||
@@ -38,7 +38,7 @@ export class ChatStreamController {
|
||||
this.controller.enqueue(data);
|
||||
}
|
||||
|
||||
writeUsage({ callInfos, completionTokens, promptTokens }: { callInfos: AnthropicCall[], completionTokens: number, promptTokens: number }) {
|
||||
this.writeAnnotation("usage", { callInfos, completionTokens, promptTokens, totalTokens: completionTokens + promptTokens });
|
||||
writeUsage({ chatInfo, completionTokens, promptTokens }: { chatInfo: ChatAnthropicInfo, completionTokens: number, promptTokens: number }) {
|
||||
this.writeAnnotation("usage", { chatInfo, completionTokens, promptTokens, totalTokens: completionTokens + promptTokens });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user