Skip to content

Commit ab05f48

Browse files
MarcMcIntoshalashchev17
authored andcommitted
fix: limit the number of tokens a chat can use.
1 parent 0b99b8a commit ab05f48

File tree

4 files changed

+132
-1
lines changed

4 files changed

+132
-1
lines changed

refact-agent/gui/src/components/ChatForm/ChatForm.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
useSendChatRequest,
2020
useCompressChat,
2121
useAutoFocusOnce,
22+
useTotalTokenUsage,
2223
} from "../../hooks";
2324
import { ErrorCallout, Callout } from "../Callout";
2425
import { ComboBox } from "../ComboBox";
@@ -34,6 +35,7 @@ import { useInputValue } from "./useInputValue";
3435
import {
3536
clearInformation,
3637
getInformationMessage,
38+
setInformation,
3739
} from "../../features/Errors/informationSlice";
3840
import { InformationCallout } from "../Callout/Callout";
3941
import { ToolConfirmation } from "./ToolConfirmation";
@@ -96,6 +98,16 @@ export const ChatForm: React.FC<ChatFormProps> = ({
9698
const { compressChat, compressChatRequest } = useCompressChat();
9799
const autoFocus = useAutoFocusOnce();
98100

101+
const { limitReached, tokens, limit } = useTotalTokenUsage();
102+
103+
useEffect(() => {
104+
if (limitReached && !information) {
105+
setInformation(
106+
`Token Limit reached, ${tokens} out of ${limit} used. To continue click the compress button or start a new chat.`,
107+
);
108+
}
109+
}, [tokens, limit, limitReached, information]);
110+
99111
const shouldAgentCapabilitiesBeShown = useMemo(() => {
100112
return threadToolUse === "agent";
101113
}, [threadToolUse]);
@@ -117,6 +129,7 @@ export const ChatForm: React.FC<ChatFormProps> = ({
117129
const disableSend = useMemo(() => {
118130
// TODO: if interrupting chat some errors can occur
119131
if (allDisabled) return true;
132+
if (limitReached) return true;
120133
// if (
121134
// currentThreadMaximumContextTokens &&
122135
// currentThreadUsage?.prompt_tokens &&
@@ -126,7 +139,15 @@ export const ChatForm: React.FC<ChatFormProps> = ({
126139
// if (arePromptTokensBiggerThanContext) return true;
127140
if (messages.length === 0) return false;
128141
return isWaiting || isStreaming || !isOnline || preventSend;
129-
}, [isOnline, isStreaming, isWaiting, preventSend, messages, allDisabled]);
142+
}, [
143+
allDisabled,
144+
limitReached,
145+
messages.length,
146+
isWaiting,
147+
isStreaming,
148+
isOnline,
149+
preventSend,
150+
]);
130151

131152
const { processAndInsertImages } = useAttachedImages();
132153
const handlePastingFile = useCallback(

refact-agent/gui/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ export * from "./useResizeObserver";
3434
export * from "./useCompressChat";
3535
export * from "./useAutoFocusOnce";
3636
export * from "./useHideScroll";
37+
export * from "./useTotalTokenUsage";

refact-agent/gui/src/hooks/useSendChatRequest.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656

5757
import { v4 as uuidv4 } from "uuid";
5858
import { upsertToolCallIntoHistory } from "../features/History/historySlice";
59+
import { useTotalTokenUsage } from "./useTotalTokenUsage";
5960

6061
type SubmitHandlerParams =
6162
| {
@@ -355,6 +356,7 @@ export function useAutoSend() {
355356
const sendImmediately = useAppSelector(selectSendImmediately);
356357
const wasInteracted = useAppSelector(getToolsInteractionStatus); // shows if tool confirmation popup was interacted by user
357358
const areToolsConfirmed = useAppSelector(getToolsConfirmationStatus);
359+
const { limitReached } = useTotalTokenUsage();
358360
const { sendMessages, abort, messagesWithSystemPrompt } =
359361
useSendChatRequest();
360362
// TODO: make a selector for this, or show tool formation
@@ -379,6 +381,7 @@ export function useAutoSend() {
379381
const lastMessage = currentMessages.slice(-1)[0];
380382
// here ish
381383
if (
384+
!limitReached &&
382385
isAssistantMessage(lastMessage) &&
383386
lastMessage.tool_calls &&
384387
lastMessage.tool_calls.length > 0
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { useMemo } from "react";
2+
import { useAppSelector } from "./useAppSelector";
3+
import { isAssistantMessage } from "../events";
4+
import { selectMessages } from "../features/Chat";
5+
import {
6+
CompletionTokenDetails,
7+
PromptTokenDetails,
8+
type Usage,
9+
} from "../services/refact/chat";
10+
import { calculateUsageInputTokens } from "../utils/calculateUsageInputTokens";
11+
12+
const TOKEN_LIMIT = 200_000;
13+
export function useTotalTokenUsage() {
14+
const messages = useAppSelector(selectMessages);
15+
16+
const summedUsages = useMemo(() => {
17+
return messages.reduce<Usage | null>((acc, message) => {
18+
if (acc && isAssistantMessage(message) && message.usage) {
19+
return sumUsages(acc, message.usage);
20+
} else if (isAssistantMessage(message) && message.usage) {
21+
return message.usage;
22+
}
23+
return acc;
24+
}, null);
25+
}, [messages]);
26+
27+
const tokens = useMemo(() => {
28+
if (!summedUsages) return 0;
29+
return calculateUsageInputTokens({
30+
keys: [
31+
"prompt_tokens",
32+
"cache_creation_input_tokens",
33+
"cache_read_input_tokens",
34+
"completion_tokens",
35+
],
36+
usage: summedUsages,
37+
});
38+
}, [summedUsages]);
39+
40+
const limitReached = useMemo(() => {
41+
return tokens >= TOKEN_LIMIT;
42+
}, [tokens]);
43+
44+
return {
45+
summedUsages,
46+
tokens,
47+
limitReached,
48+
limit: TOKEN_LIMIT,
49+
};
50+
}
51+
52+
function addCompletionDetails(
53+
a: CompletionTokenDetails | null,
54+
b: CompletionTokenDetails | null,
55+
): CompletionTokenDetails | null {
56+
if (!a && !b) return null;
57+
if (!a) return b;
58+
if (!b) return a;
59+
60+
return {
61+
accepted_prediction_tokens:
62+
a.accepted_prediction_tokens + b.accepted_prediction_tokens,
63+
audio_tokens: a.audio_tokens + b.audio_tokens,
64+
reasoning_tokens: a.reasoning_tokens + b.reasoning_tokens,
65+
rejected_prediction_tokens:
66+
a.rejected_prediction_tokens + b.rejected_prediction_tokens,
67+
};
68+
}
69+
70+
function addPromptTokenDetails(
71+
a: PromptTokenDetails | null,
72+
b: PromptTokenDetails | null,
73+
): PromptTokenDetails | null {
74+
if (!a && !b) return null;
75+
if (!a) return b;
76+
if (!b) return a;
77+
78+
return {
79+
audio_tokens: a.audio_tokens + b.audio_tokens,
80+
cached_tokens: a.cached_tokens + b.cached_tokens,
81+
};
82+
}
83+
84+
function sumUsages(a: Usage, b: Usage): Usage {
85+
const completionDetails = addCompletionDetails(
86+
a.completion_tokens_details,
87+
b.completion_tokens_details,
88+
);
89+
const promptDetails = addPromptTokenDetails(
90+
a.prompt_tokens_details,
91+
b.prompt_tokens_details,
92+
);
93+
94+
return {
95+
completion_tokens: a.completion_tokens + b.completion_tokens,
96+
prompt_tokens: a.prompt_tokens + b.prompt_tokens,
97+
total_tokens: a.total_tokens + b.total_tokens,
98+
completion_tokens_details: completionDetails,
99+
prompt_tokens_details: promptDetails,
100+
cache_creation_input_tokens:
101+
(a.cache_creation_input_tokens || 0) +
102+
(b.cache_creation_input_tokens || 0),
103+
cache_read_input_tokens:
104+
(a.cache_read_input_tokens || 0) + (b.cache_read_input_tokens || 0),
105+
};
106+
}

0 commit comments

Comments
 (0)