Skip to content

Commit ed307ed

Browse files
committed
Merge branch 'main' into whoisthey/language-model-input-modalities
2 parents 507d758 + 33e24c6 commit ed307ed

13 files changed

Lines changed: 480 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111
- [EE] Improved Ask Sourcebot prompt caching by splitting static and dynamic prompt sections and advancing cache breakpoints after every agent step instead of only after each message. [#1366](https://github.com/sourcebot-dev/sourcebot/pull/1366)
12+
- Refactored Ask Sourcebot user message text extraction into a shared helper that robustly handles non-text message parts. [#1371](https://github.com/sourcebot-dev/sourcebot/pull/1371)
1213

1314
### Added
1415
- Added per-step token cost tracking and estimated tool call token usage to Ask Sourcebot chat history. [#1353](https://github.com/sourcebot-dev/sourcebot/pull/1353)
16+
- [EE] Added a context-window usage gauge to the Ask Sourcebot chat details, showing how much of the selected model's context window each turn occupies. Window sizes are resolved from the models.dev catalog. [#1370](https://github.com/sourcebot-dev/sourcebot/pull/1370)
1517
- Added optional `inputModalities` and `supportedDocumentTypes` configuration for language models, exposing model input-modality and document capabilities (defaults to text-only, no documents). [#1372](https://github.com/sourcebot-dev/sourcebot/pull/1372)
1618

1719
### Fixed

packages/web/src/app/(app)/chat/[id]/page.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { __unsafePrisma } from '@/prisma';
1414
import { ChatVisibility } from '@sourcebot/db';
1515
import { Metadata } from 'next';
1616
import { SBChatMessage } from '@/features/chat/types';
17+
import { getUserMessageText } from '@/features/chat/utils';
1718
import { env } from '@sourcebot/shared';
1819
import { hasEntitlement } from '@/lib/entitlements';
1920
import { ChatEntitlementMessage } from '@/features/chat/components/chatEntitlementMessage';
@@ -54,11 +55,11 @@ export const generateMetadata = async ({ params }: PageProps): Promise<Metadata>
5455

5556
let description = 'A chat on Sourcebot';
5657
if (firstUserMessage) {
57-
const textPart = firstUserMessage.parts.find(p => p.type === 'text');
58-
if (textPart && textPart.type === 'text') {
59-
description = textPart.text.length > 160
60-
? textPart.text.substring(0, 160).trim() + '...'
61-
: textPart.text;
58+
const text = getUserMessageText(firstUserMessage);
59+
if (text) {
60+
description = text.length > 160
61+
? text.substring(0, 160).trim() + '...'
62+
: text;
6263
}
6364
}
6465

packages/web/src/app/api/(server)/ee/chat/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { additionalChatRequestParamsSchema } from "@/features/chat/types";
66
import { getLanguageModelKey } from "@/features/chat/utils";
77
import { checkAskEntitlement, getConfiguredLanguageModels, isOwnerOfChat, updateChatMessages } from "@/features/chat/utils.server";
88
import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server";
9+
import { resolveContextWindow } from "@/features/chat/modelContextWindow.server";
910
import { apiHandler } from "@/lib/apiHandler";
1011
import { ErrorCode } from "@/lib/errorCodes";
1112
import { captureEvent } from "@/lib/posthog";
@@ -89,6 +90,11 @@ export const POST = apiHandler(async (req: NextRequest) => {
8990

9091
const { model, providerOptions, temperature } = await getAISDKLanguageModelAndOptions(languageModelConfig);
9192

93+
// Total context window for the selected model, used as the
94+
// denominator for the UI's context-usage gauge. Undefined when
95+
// unknown (e.g. self-hosted models).
96+
const contextWindow = await resolveContextWindow(languageModelConfig);
97+
9298
// No-op for non-Anthropic providers / when caching is disabled, so
9399
// it never perturbs other providers' requests.
94100
const promptCacheStrategy = getPromptCacheStrategy(
@@ -139,6 +145,7 @@ export const POST = apiHandler(async (req: NextRequest) => {
139145
disabledMcpServerIds,
140146
model,
141147
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
148+
contextWindow,
142149
promptCacheStrategy,
143150
modelProviderOptions: providerOptions,
144151
modelTemperature: temperature,

packages/web/src/ee/features/chat/agent.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { randomUUID } from "crypto";
2222
import _dedent from "dedent";
2323
import { ANSWER_TAG, FILE_REFERENCE_PREFIX } from "@/features/chat/constants";
2424
import { Source } from "@/features/chat/types";
25-
import { addLineNumbers, fileReferenceToString, getAnswerPartFromAssistantMessage, getTurnProgressState } from "@/features/chat/utils";
25+
import { addLineNumbers, fileReferenceToString, getAnswerPartFromAssistantMessage, getTurnProgressState, getUserMessageText } from "@/features/chat/utils";
2626
import { createTools } from "./tools";
2727
import { getConnectedMcpClients } from "@/ee/features/chat/mcp/mcpClientFactory";
2828
import { getMcpTools, McpToolsResult } from "@/ee/features/chat/mcp/mcpToolSets";
@@ -54,6 +54,7 @@ interface CreateMessageStreamResponseProps {
5454
disabledMcpServerIds?: string[];
5555
model: AISDKLanguageModelV3;
5656
modelName: string;
57+
contextWindow?: number;
5758
promptCacheStrategy: PromptCacheStrategy;
5859
onFinish: UIMessageStreamOnFinishCallback<SBChatMessage>;
5960
onError: (error: unknown) => string;
@@ -73,6 +74,7 @@ export const createMessageStream = async ({
7374
disabledMcpServerIds,
7475
model,
7576
modelName,
77+
contextWindow,
7678
promptCacheStrategy,
7779
modelProviderOptions,
7880
modelTemperature,
@@ -105,7 +107,7 @@ export const createMessageStream = async ({
105107
if (message.role === 'user') {
106108
return {
107109
role: 'user',
108-
content: message.parts[0].type === 'text' ? message.parts[0].text : '',
110+
content: getUserMessageText(message),
109111
};
110112
}
111113

@@ -279,6 +281,7 @@ export const createMessageStream = async ({
279281
// phases so earlier phases' steps are preserved in order.
280282
stepTokenUsage: [...(priorMetadata?.stepTokenUsage ?? []), ...stepTokenUsage],
281283
modelName,
284+
contextWindow,
282285
traceId,
283286
}
284287
});

packages/web/src/ee/features/chat/components/chatThread/chatThread.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
55
import { Separator } from '@/components/ui/separator';
66
import { CustomSlateEditor } from '@/features/chat/customSlateEditor';
77
import { AdditionalChatRequestParams, CustomEditor, LanguageModelInfo, SBChatMessage, SearchScope, Source } from '@/features/chat/types';
8-
import { createUIMessage, getAllMentionElements, getTurnProgressState, resetEditor, slateContentToString } from '@/features/chat/utils';
8+
import { createUIMessage, getAllMentionElements, getTurnProgressState, getUserMessageText, resetEditor, slateContentToString } from '@/features/chat/utils';
99
import { useChat } from '@ai-sdk/react';
1010
import { CreateUIMessage, DefaultChatTransport, lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai';
1111
import { ArrowDownIcon, CopyIcon } from 'lucide-react';
@@ -204,16 +204,16 @@ export const ChatThread = ({
204204
} satisfies AdditionalChatRequestParams,
205205
});
206206

207+
const userMessageText = getUserMessageText(message);
207208
if (
208209
messages.length === 0 &&
209-
message.parts.length > 0 &&
210-
message.parts[0].type === 'text'
210+
userMessageText.length > 0
211211
) {
212212
generateAndUpdateChatNameFromMessage(
213213
{
214214
chatId,
215215
languageModelId: selectedLanguageModel.model,
216-
message: message.parts[0].text,
216+
message: userMessageText,
217217
},
218218
).then((response) => {
219219
if (isServiceError(response)) {

packages/web/src/ee/features/chat/components/chatThread/chatThreadListItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { CSSProperties, forwardRef, memo, useCallback, useEffect, useMemo, useRe
88
import scrollIntoView from 'scroll-into-view-if-needed';
99
import { Reference, referenceSchema, SBChatMessage, Source } from "@/features/chat/types";
1010
import { useExtractReferences } from '../../useExtractReferences';
11-
import { getAnswerPartFromAssistantMessage, getLastStepParts, groupMessageIntoSteps, isSBChatToolPart, repairReferences, tryResolveFileReference } from '@/features/chat/utils';
11+
import { getAnswerPartFromAssistantMessage, getLastStepParts, getUserMessageText, groupMessageIntoSteps, isSBChatToolPart, repairReferences, tryResolveFileReference } from '@/features/chat/utils';
1212
import { AnswerCard } from './answerCard';
1313
import { DetailsCard } from './detailsCard';
1414
import { ApprovalRequestedToolPart, ToolApprovalBanner } from './toolApprovalBanner';
@@ -49,7 +49,7 @@ const ChatThreadListItemComponent = forwardRef<HTMLDivElement, ChatThreadListIte
4949
const userHasManuallyExpanded = useRef(false);
5050

5151
const userQuestion = useMemo(() => {
52-
return userMessage.parts.length > 0 && userMessage.parts[0].type === 'text' ? userMessage.parts[0].text : '';
52+
return getUserMessageText(userMessage);
5353
}, [userMessage]);
5454

5555
// Take the assistant message and repair any references that are not properly formatted.

packages/web/src/ee/features/chat/components/chatThread/detailsCard.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ const DetailsCardComponent = ({
8686
? Math.round((cacheReadTokens / inputTokens) * 100)
8787
: 0;
8888

89+
// Context-window usage gauge. "In use" is the input the model saw on its
90+
// most recent step — i.e. the full accumulated prompt occupying the window
91+
// right now — not the cumulative totalInputTokens.
92+
const stepTokenUsage = metadata?.stepTokenUsage;
93+
const currentContextTokens = stepTokenUsage && stepTokenUsage.length > 0
94+
? stepTokenUsage[stepTokenUsage.length - 1].inputTokens
95+
: undefined;
96+
const contextWindow = metadata?.contextWindow;
97+
const contextUsagePercent = currentContextTokens !== undefined && contextWindow !== undefined && contextWindow > 0
98+
? Math.min(100, Math.round((currentContextTokens / contextWindow) * 100))
99+
: undefined;
100+
89101
const handleExpandedChanged = useCallback((next: boolean) => {
90102
captureEvent('wa_chat_details_card_toggled', { chatId, isExpanded: next });
91103
onExpandedChanged(next);
@@ -193,6 +205,23 @@ const DetailsCardComponent = ({
193205
)}
194206
</div>
195207
)}
208+
{contextUsagePercent !== undefined && currentContextTokens !== undefined && contextWindow !== undefined && (
209+
<Tooltip>
210+
<TooltipTrigger asChild>
211+
<div className="cursor-help">
212+
<ContextWindowGauge
213+
total={contextWindow}
214+
percent={contextUsagePercent}
215+
/>
216+
</div>
217+
</TooltipTrigger>
218+
<TooltipContent side="bottom">
219+
<div className="max-w-xs text-xs">
220+
The most recent step&apos;s prompt used {currentContextTokens.toLocaleString()} of the model&apos;s {contextWindow.toLocaleString()}-token context window ({contextUsagePercent}%).
221+
</div>
222+
</TooltipContent>
223+
</Tooltip>
224+
)}
196225
{metadata?.totalResponseTimeMs && (
197226
<div className="flex items-center text-xs">
198227
<Clock className="w-3 h-3 mr-1 flex-shrink-0" />
@@ -367,6 +396,61 @@ const StepTokenUsage = ({ usage, label = 'step' }: { usage: StepTokenUsageEntry,
367396
);
368397
}
369398

399+
400+
const CONTEXT_USAGE_YELLOW_PERCENT = 50;
401+
const CONTEXT_USAGE_RED_PERCENT = 80;
402+
403+
const getContextUsageColorClass = (percent: number): string => {
404+
if (percent >= CONTEXT_USAGE_RED_PERCENT) {
405+
return "text-red-500";
406+
}
407+
if (percent >= CONTEXT_USAGE_YELLOW_PERCENT) {
408+
return "text-yellow-500";
409+
}
410+
return "text-[#6cb38f]";
411+
};
412+
413+
const ContextWindowGauge = ({ total, percent }: { total: number, percent: number }) => {
414+
const size = 14;
415+
const strokeWidth = 2;
416+
const radius = (size - strokeWidth) / 2;
417+
const circumference = 2 * Math.PI * radius;
418+
const dashOffset = circumference * (1 - Math.min(100, percent) / 100);
419+
const colorClass = getContextUsageColorClass(percent);
420+
421+
return (
422+
<div className="flex items-center gap-1.5 text-xs whitespace-nowrap">
423+
<svg width={size} height={size} className="-rotate-90 flex-shrink-0">
424+
{/* Neutral gray track. */}
425+
<circle
426+
cx={size / 2}
427+
cy={size / 2}
428+
r={radius}
429+
fill="none"
430+
stroke="currentColor"
431+
strokeWidth={strokeWidth}
432+
className="text-zinc-500"
433+
/>
434+
{/* Progress arc. */}
435+
<circle
436+
cx={size / 2}
437+
cy={size / 2}
438+
r={radius}
439+
fill="none"
440+
stroke="currentColor"
441+
strokeWidth={strokeWidth}
442+
strokeLinecap="round"
443+
strokeDasharray={circumference}
444+
strokeDashoffset={dashOffset}
445+
className={cn("transition-all duration-300", colorClass)}
446+
/>
447+
</svg>
448+
<span className={cn("font-semibold", colorClass)}>{percent}%</span>
449+
<span className="text-muted-foreground">of {getShortenedNumberDisplayString(total, 0).toUpperCase()}</span>
450+
</div>
451+
);
452+
}
453+
370454
type GuardedToolType =
371455
| 'tool-read_file'
372456
| 'tool-grep'

packages/web/src/ee/features/mcp/askCodebase.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { sew } from "@/middleware/sew";
22
import { getConfiguredLanguageModels, updateChatMessages, checkAskEntitlement } from "@/features/chat/utils.server";
33
import { generateChatNameFromMessage } from "@/ee/features/chat/llm.server";
44
import { getAISDKLanguageModelAndOptions } from "@/features/chat/llm.server";
5+
import { resolveContextWindow } from "@/features/chat/modelContextWindow.server";
56
import { LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types";
67
import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils";
78
import { resolveModelInputModalities, resolveModelSupportedDocumentTypes } from "@/features/chat/modelCapabilities";
@@ -85,6 +86,7 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul
8586

8687
const { model, providerOptions, temperature } = await getAISDKLanguageModelAndOptions(languageModelConfig);
8788
const modelName = languageModelConfig.displayName ?? languageModelConfig.model;
89+
const contextWindow = await resolveContextWindow(languageModelConfig);
8890

8991
// No-op for non-Anthropic providers / when caching is disabled.
9092
const promptCacheStrategy = getPromptCacheStrategy(
@@ -183,6 +185,7 @@ export const askCodebase = (params: AskCodebaseParams): Promise<AskCodebaseResul
183185
prisma,
184186
model,
185187
modelName,
188+
contextWindow,
186189
promptCacheStrategy,
187190
modelProviderOptions: providerOptions,
188191
modelTemperature: temperature,

0 commit comments

Comments
 (0)