Skip to content

Commit c7e90e9

Browse files
authored
feat: usage counter stories and basic UI (#516)
* feat: usage counter stories and basic UI * chore: fixed prettier check * feat: saving usage to assistant message on stream & reasoning mode selection fix * fix: stubs for usage counters, fixed stories and refactored UsageCounters * chore: removed console.log * feat: storing usage object for chat thread instead of for each assistant message & showing suggestion to open new chat, if prompt tokens are exceeding 30k limit boundary * chore: better place for calculateInputTokens utility function * refactoring(middleware): simplified chat response middleware * chore: remove unused imports * refactoring: remove duplicate code * feat: calculating recommended maximum input limit based on model n_ctx * fix: fix tests * fix: moved UsageCounter to separate component modlet out of ChatContent scope, removed comments
1 parent ec92d56 commit c7e90e9

File tree

20 files changed

+347
-44
lines changed

20 files changed

+347
-44
lines changed

refact-agent/gui/src/app/middleware.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
restoreChat,
1212
newIntegrationChat,
1313
chatResponse,
14+
setThreadUsage,
1415
} from "../features/Chat/Thread";
1516
import { statisticsApi } from "../services/refact/statistics";
1617
import { integrationsApi } from "../services/refact/integrations";
@@ -32,14 +33,11 @@ import { nextTip } from "../features/TipOfTheDay";
3233
import { telemetryApi } from "../services/refact/telemetry";
3334
import { CONFIG_PATH_URL, FULL_PATH_URL } from "../services/refact/consts";
3435
import { resetConfirmationInteractedState } from "../features/ToolConfirmation/confirmationSlice";
35-
import {
36-
getAgentUsageCounter,
37-
getMaxFreeAgentUsage,
38-
} from "../features/Chat/Thread/utils";
3936
import {
4037
updateAgentUsage,
4138
updateMaxAgentUsageAmount,
4239
} from "../features/AgentUsage/agentUsageSlice";
40+
import { isChatResponseChoice } from "../services/refact";
4341

4442
const AUTH_ERROR_MESSAGE =
4543
"There is an issue with your API key. Check out your API Key or re-login";
@@ -97,15 +95,22 @@ startListening({
9795
// saving to store agent_usage counter from the backend, only one chunk has this field.
9896
const { payload } = action;
9997

100-
if ("refact_agent_request_available" in payload) {
101-
const agentUsageCounter = getAgentUsageCounter(payload);
102-
103-
dispatch(updateAgentUsage(agentUsageCounter ?? null));
104-
}
105-
106-
if ("refact_agent_max_request_num" in payload) {
107-
const maxFreeAgentUsage = getMaxFreeAgentUsage(payload);
108-
dispatch(updateMaxAgentUsageAmount(maxFreeAgentUsage));
98+
if (isChatResponseChoice(payload)) {
99+
const {
100+
usage,
101+
refact_agent_max_request_num,
102+
refact_agent_request_available,
103+
} = payload;
104+
const actions = [
105+
updateAgentUsage(refact_agent_request_available),
106+
updateMaxAgentUsageAmount(refact_agent_max_request_num),
107+
];
108+
109+
actions.forEach((action) => dispatch(action));
110+
111+
if (usage) {
112+
dispatch(setThreadUsage({ chatId: payload.id, usage }));
113+
}
109114
}
110115
},
111116
});

refact-agent/gui/src/components/ChatContent/AssistantInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const AssistantInput: React.FC<ChatInputProps> = ({
2121
}) => {
2222
const [sendTelemetryEvent] =
2323
telemetryApi.useLazySendTelemetryChatEventQuery();
24+
2425
const handleCopy = useCallback(
2526
(text: string) => {
2627
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
selectIsWaiting,
2525
selectMessages,
2626
selectThread,
27+
selectThreadUsage,
2728
} from "../../features/Chat/Thread/selectors";
2829
import { takeWhile } from "../../utils";
2930
import { GroupedDiffs } from "./DiffContent";
@@ -32,6 +33,7 @@ import { popBackTo } from "../../features/Pages/pagesSlice";
3233
import { ChatLinks, UncommittedChangesWarning } from "../ChatLinks";
3334
import { telemetryApi } from "../../services/refact/telemetry";
3435
import { PlaceHolderText } from "./PlaceHolderText";
36+
import { UsageCounter } from "../UsageCounter";
3537

3638
export type ChatContentProps = {
3739
onRetry: (index: number, question: UserMessage["content"]) => void;
@@ -47,6 +49,7 @@ export const ChatContent: React.FC<ChatContentProps> = ({
4749
const messages = useAppSelector(selectMessages);
4850
const isStreaming = useAppSelector(selectIsStreaming);
4951
const thread = useAppSelector(selectThread);
52+
const threadUsage = useAppSelector(selectThreadUsage);
5053
const isConfig = thread.mode === "CONFIGURE";
5154
const isWaiting = useAppSelector(selectIsWaiting);
5255
const [sendTelemetryEvent] =
@@ -113,6 +116,7 @@ export const ChatContent: React.FC<ChatContentProps> = ({
113116
{messages.length === 0 && <PlaceHolderText />}
114117
{renderMessages(messages, onRetryWrapper)}
115118
<UncommittedChangesWarning />
119+
{threadUsage && <UsageCounter usage={threadUsage} />}
116120

117121
<Container py="4">
118122
<Spinner spinning={isWaiting} />

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const SuggestNewChat = ({
9696
usage limits faster.
9797
</Text>
9898
<Flex align="center" gap="3" flexShrink="0">
99-
<Link size="1" onClick={onCreateNewChat}>
99+
<Link size="1" onClick={onCreateNewChat} color="indigo">
100100
Start a new chat
101101
</Link>
102102
<IconButton
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Usage } from "../../services/refact";
2+
3+
export const USAGE_COUNTER_STUB_GPT: Usage = {
4+
completion_tokens: 30,
5+
prompt_tokens: 3391,
6+
total_tokens: 3421,
7+
completion_tokens_details: {
8+
accepted_prediction_tokens: 0,
9+
audio_tokens: 0,
10+
reasoning_tokens: 0,
11+
rejected_prediction_tokens: 0,
12+
},
13+
prompt_tokens_details: {
14+
audio_tokens: 0,
15+
cached_tokens: 3328,
16+
},
17+
};
18+
19+
export const USAGE_COUNTER_STUB_ANTHROPIC: Usage = {
20+
completion_tokens: 142,
21+
prompt_tokens: 5,
22+
total_tokens: 147,
23+
completion_tokens_details: null,
24+
prompt_tokens_details: null,
25+
cache_creation_input_tokens: 3291,
26+
cache_read_input_tokens: 3608,
27+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.usageCounterContainer {
2+
/* position: absolute; */
3+
/* top: 0;
4+
right: 0; */
5+
margin-left: auto;
6+
display: flex;
7+
align-items: center;
8+
padding: var(--space-2) var(--space-3);
9+
gap: 8px;
10+
max-width: max-content;
11+
opacity: 0.7;
12+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from "react";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
import { Provider } from "react-redux";
4+
5+
import { setUpStore } from "../../app/store";
6+
import { Theme } from "../Theme";
7+
import { AbortControllerProvider } from "../../contexts/AbortControllers";
8+
9+
import { UsageCounter } from ".";
10+
import { Usage } from "../../services/refact";
11+
import {
12+
USAGE_COUNTER_STUB_ANTHROPIC,
13+
USAGE_COUNTER_STUB_GPT,
14+
} from "./UsageCounter.fixtures";
15+
16+
const MockedStore: React.FC<{ usage: Usage }> = ({ usage }) => {
17+
const store = setUpStore({
18+
config: {
19+
themeProps: {
20+
appearance: "dark",
21+
},
22+
host: "web",
23+
lspPort: 8001,
24+
},
25+
});
26+
27+
return (
28+
<Provider store={store}>
29+
<AbortControllerProvider>
30+
<Theme accentColor="gray">
31+
<UsageCounter usage={usage} />
32+
</Theme>
33+
</AbortControllerProvider>
34+
</Provider>
35+
);
36+
};
37+
38+
const meta: Meta<typeof MockedStore> = {
39+
title: "UsageCounter",
40+
component: MockedStore,
41+
args: {
42+
usage: USAGE_COUNTER_STUB_GPT,
43+
},
44+
};
45+
46+
export default meta;
47+
48+
export const GPTUsageCounter: StoryObj<typeof UsageCounter> = {
49+
args: {
50+
usage: USAGE_COUNTER_STUB_GPT,
51+
},
52+
};
53+
export const AnthropicUsageCounter: StoryObj<typeof UsageCounter> = {
54+
args: {
55+
usage: USAGE_COUNTER_STUB_ANTHROPIC,
56+
},
57+
};
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React from "react";
2+
import { Card, Flex, HoverCard, Text } from "@radix-ui/themes";
3+
import { ArrowDownIcon, ArrowUpIcon } from "@radix-ui/react-icons";
4+
5+
import { ScrollArea } from "../ScrollArea";
6+
import { calculateUsageInputTokens } from "../../utils/calculateUsageInputTokens";
7+
import type { Usage } from "../../services/refact";
8+
9+
import styles from "./UsageCounter.module.css";
10+
11+
type UsageCounterProps = {
12+
usage: Usage;
13+
};
14+
15+
function formatNumber(num: number): string {
16+
return num >= 1_000_000
17+
? (num / 1_000_000).toFixed(1) + "M"
18+
: num >= 1_000
19+
? (num / 1_000).toFixed(2) + "k"
20+
: num.toString();
21+
}
22+
23+
const TokenDisplay: React.FC<{ label: string; value: number }> = ({
24+
label,
25+
value,
26+
}) => (
27+
<Flex align="center" justify="between" width="100%">
28+
<Text size="1" weight="bold">
29+
{label}
30+
</Text>
31+
<Text size="1">{value}</Text>
32+
</Flex>
33+
);
34+
35+
export const UsageCounter: React.FC<UsageCounterProps> = ({ usage }) => {
36+
const inputTokens = calculateUsageInputTokens(usage, [
37+
"prompt_tokens",
38+
"cache_creation_input_tokens",
39+
"cache_read_input_tokens",
40+
]);
41+
const outputTokens = calculateUsageInputTokens(usage, ["completion_tokens"]);
42+
43+
return (
44+
<HoverCard.Root>
45+
<HoverCard.Trigger>
46+
<Card className={styles.usageCounterContainer}>
47+
<Flex align="center">
48+
<ArrowUpIcon width="12" height="12" />
49+
<Text size="1">{formatNumber(inputTokens)}</Text>
50+
</Flex>
51+
<Flex align="center">
52+
<ArrowDownIcon width="12" height="12" />
53+
<Text size="1">{outputTokens}</Text>
54+
</Flex>
55+
</Card>
56+
</HoverCard.Trigger>
57+
<ScrollArea scrollbars="both" asChild>
58+
<HoverCard.Content
59+
size="1"
60+
maxHeight="50vh"
61+
maxWidth="90vw"
62+
minWidth="300px"
63+
avoidCollisions
64+
align="end"
65+
side="top"
66+
>
67+
<Flex direction="column" align="start" gap="2">
68+
<Text size="2" mb="2">
69+
Tokens spent per message:
70+
</Text>
71+
<TokenDisplay
72+
label="Input tokens (in total):"
73+
value={inputTokens}
74+
/>
75+
{usage.cache_read_input_tokens !== undefined && (
76+
<TokenDisplay
77+
label="Cache read input tokens:"
78+
value={usage.cache_read_input_tokens}
79+
/>
80+
)}
81+
{usage.cache_creation_input_tokens !== undefined && (
82+
<TokenDisplay
83+
label="Cache creation input tokens:"
84+
value={usage.cache_creation_input_tokens}
85+
/>
86+
)}
87+
<TokenDisplay label="Completion tokens:" value={outputTokens} />
88+
{usage.completion_tokens_details && (
89+
<TokenDisplay
90+
label="Reasoning tokens:"
91+
value={usage.completion_tokens_details.reasoning_tokens}
92+
/>
93+
)}
94+
</Flex>
95+
</HoverCard.Content>
96+
</ScrollArea>
97+
</HoverCard.Root>
98+
);
99+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { UsageCounter } from "./UsageCounter";

refact-agent/gui/src/features/Chat/Thread/actions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
LspChatMode,
99
PayloadWithChatAndMessageId,
1010
PayloadWithChatAndBoolean,
11+
PayloadWithChatAndUsage,
12+
PayloadWithChatAndNumber,
1113
} from "./types";
1214
import {
1315
isAssistantDelta,
@@ -58,10 +60,19 @@ export const setLastUserMessageId = createAction<PayloadWithChatAndMessageId>(
5860
"chatThread/setLastUserMessageId",
5961
);
6062

63+
export const updateMaximumContextTokens =
64+
createAction<PayloadWithChatAndNumber>(
65+
"chatThread/updateMaximumContextTokens",
66+
);
67+
6168
export const setIsNewChatSuggested = createAction<PayloadWithChatAndBoolean>(
6269
"chatThread/setIsNewChatSuggested",
6370
);
6471

72+
export const setThreadUsage = createAction<PayloadWithChatAndUsage>(
73+
"chatThread/setThreadUsage",
74+
);
75+
6576
export const setIsNewChatSuggestionRejected =
6677
createAction<PayloadWithChatAndBoolean>(
6778
"chatThread/setIsNewChatSuggestionRejected",

0 commit comments

Comments
 (0)