Path: blob/main/extensions/copilot/src/platform/endpoint/node/imageLimits.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as l10n from '@vscode/l10n';5import { Raw } from '@vscode/prompt-tsx';67/**8* Model-facing placeholder substituted for dropped history images.9* Intentionally not localized — this text is sent to the model, not the user.10*/11const IMAGE_PLACEHOLDER_TEXT = '[Image omitted from conversation history due to model limit.]';1213/**14* Silently drops the oldest images from history when the total number of images15* in the conversation exceeds `maxImages`. Images belonging to the current turn16* (the last user message and anything after it, e.g. recent tool results) are17* always preserved.18*19* If the current turn alone exceeds the limit, throws a localized error rather20* than sending a request we know will be rejected with an opaque server error.21*22* @returns A (possibly filtered) copy of messages. The original array is never mutated.23*/24export function filterHistoryImages(messages: Raw.ChatMessage[], maxImages: number): Raw.ChatMessage[] {25// Anchor the current turn at the last user message; anything at or after this26// index is treated as "current turn" and its images are never filtered.27let lastUserIdx = -1;28for (let i = messages.length - 1; i >= 0; i--) {29if (messages[i].role === Raw.ChatRole.User) {30lastUserIdx = i;31break;32}33}3435// Corner case: no user message at all (e.g. system-only history). Treat the36// last message as the current turn so we still filter earlier images.37if (lastUserIdx === -1 && messages.length > 0) {38lastUserIdx = messages.length - 1;39}4041// Count images in the current turn (the last user message and anything after it).42let currentTurnImages = 0;43for (let i = Math.max(lastUserIdx, 0); i < messages.length; i++) {44const content = messages[i].content;45if (!Array.isArray(content)) {46continue;47}48for (const part of content) {49if (part.type === Raw.ChatCompletionContentPartKind.Image) {50currentTurnImages++;51}52}53}5455// Count total images across all messages56let totalImages = 0;57for (const message of messages) {58if (Array.isArray(message.content)) {59for (const part of message.content) {60if (part.type === Raw.ChatCompletionContentPartKind.Image) {61totalImages++;62}63}64}65}6667// No filtering needed if total is within the limit68if (totalImages <= maxImages) {69return messages;70}7172// Fail fast with a clear, localized error when the current turn alone exceeds73// the limit — otherwise we'd send a request the server will reject with an74// opaque error. Silent history filtering is only safe when dropping history75// images can bring the total down to the limit.76if (currentTurnImages > maxImages) {77throw new Error(l10n.t('Too many images in request: {0} images provided, but the model supports a maximum of {1} images.', currentTurnImages, maxImages));78}7980// Walk backward through history (before the current turn), keeping the81// most recent images and replacing the oldest with placeholders.82let historyBudget = maxImages - currentTurnImages;8384// Collect keep/drop decisions by walking backward through history85const historyImageDecisions = new Map<string, boolean>(); // "msgIdx:partIdx" -> keep86for (let i = lastUserIdx - 1; i >= 0; i--) {87if (!Array.isArray(messages[i].content)) {88continue;89}90for (let j = messages[i].content.length - 1; j >= 0; j--) {91if (messages[i].content[j].type === Raw.ChatCompletionContentPartKind.Image) {92const key = `${i}:${j}`;93if (historyBudget > 0) {94historyImageDecisions.set(key, true);95historyBudget--;96} else {97historyImageDecisions.set(key, false);98}99}100}101}102103// Build filtered messages, replacing dropped images with text placeholders104return messages.map((message, msgIdx) => {105if (msgIdx >= lastUserIdx) {106return message;107}108if (!Array.isArray(message.content)) {109return message;110}111if (!message.content.some(p => p.type === Raw.ChatCompletionContentPartKind.Image)) {112return message;113}114return {115...message,116content: message.content.map((part, partIdx) => {117if (part.type !== Raw.ChatCompletionContentPartKind.Image) {118return part;119}120if (historyImageDecisions.get(`${msgIdx}:${partIdx}`)) {121return part;122}123return { type: Raw.ChatCompletionContentPartKind.Text, text: IMAGE_PLACEHOLDER_TEXT };124})125};126});127}128129130