Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit 6969f93

Browse files
bhavyausCopilot
andauthored
Update token usage tracking for Anthropic models and implement summarization triggers based on token limits (#2779)
* Enhance token usage tracking for Anthropic models and implement summarization triggers based on token limits * Update configuration and checks * Refactor Anthropic context editing check and streamline token usage tracking * Add experimental context editing option for Anthropic models * Update src/extension/intents/node/agentIntent.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/extension/intents/node/agentIntent.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor token usage tracking to use AnthropicTokenUsageMetadata for improved context handling --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 5670c34 commit 6969f93

15 files changed

Lines changed: 197 additions & 115 deletions

File tree

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3266,6 +3266,15 @@
32663266
"onExp"
32673267
]
32683268
},
3269+
"github.copilot.chat.anthropic.contextEditing.enabled": {
3270+
"type": "boolean",
3271+
"default": false,
3272+
"markdownDescription": "%github.copilot.config.anthropic.contextEditing.enabled%",
3273+
"tags": [
3274+
"experimental",
3275+
"onExp"
3276+
]
3277+
},
32693278
"github.copilot.chat.useResponsesApi": {
32703279
"type": "boolean",
32713280
"default": true,

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@
304304
"copilot.toolSet.search.description": "Search files in your workspace",
305305
"copilot.toolSet.web.description": "Fetch information from the web",
306306
"github.copilot.config.useMessagesApi": "Use the Messages API instead of the Chat Completions API when supported.\n\n**Note**: This is an experimental feature that is not yet activated for all users.",
307+
"github.copilot.config.anthropic.contextEditing.enabled": "Enable context editing for Anthropic models. Automatically manages conversation context as it grows, helping optimize costs and stay within context window limits.\n\n**Note**: This is an experimental feature. Context editing may cause additional cache rewrites. Enable with caution.",
307308
"github.copilot.config.useResponsesApi": "Use the Responses API instead of the Chat Completions API when supported. Enables reasoning and reasoning summaries.\n\n**Note**: This is an experimental feature that is not yet activated for all users.\n\n**Important**: URL API path resolution for custom OpenAI-compatible and Azure models is independent of this setting and fully determined by `url` property of `#github.copilot.chat.customOAIModels#` or `#github.copilot.chat.azureModels#` respectively.",
308309
"github.copilot.config.responsesApiReasoningEffort": "Sets the reasoning effort used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
309310
"github.copilot.config.responsesApiReasoningSummary": "Sets the reasoning summary style used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",

src/extension/intents/node/agentIntent.ts

Lines changed: 103 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import { BudgetExceededError } from '@vscode/prompt-tsx/dist/base/materialized';
1010
import type * as vscode from 'vscode';
1111
import { ChatLocation, ChatResponse } from '../../../platform/chat/common/commonTypes';
1212
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
13-
import { modelCanUseApplyPatchExclusively, modelCanUseReplaceStringExclusively, modelSupportsApplyPatch, modelSupportsMultiReplaceString, modelSupportsReplaceString, modelSupportsSimplifiedApplyPatchInstructions } from '../../../platform/endpoint/common/chatModelCapabilities';
13+
import { isAnthropicFamily, modelCanUseApplyPatchExclusively, modelCanUseReplaceStringExclusively, modelSupportsApplyPatch, modelSupportsMultiReplaceString, modelSupportsReplaceString, modelSupportsSimplifiedApplyPatchInstructions } from '../../../platform/endpoint/common/chatModelCapabilities';
1414
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
1515
import { IEnvService } from '../../../platform/env/common/envService';
1616
import { ILogService } from '../../../platform/log/common/logService';
1717
import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService';
18+
import { isAnthropicContextEditingEnabled } from '../../../platform/networking/common/anthropic';
1819
import { IChatEndpoint } from '../../../platform/networking/common/networking';
1920
import { INotebookService } from '../../../platform/notebook/common/notebookService';
2021
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
@@ -28,7 +29,7 @@ import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platfo
2829
import { ICommandService } from '../../commands/node/commandService';
2930
import { Intent } from '../../common/constants';
3031
import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';
31-
import { RenderedUserMessageMetadata } from '../../prompt/common/conversation';
32+
import { AnthropicTokenUsageMetadata, RenderedUserMessageMetadata } from '../../prompt/common/conversation';
3233
import { IBuildPromptContext } from '../../prompt/common/intents';
3334
import { getRequestedToolCallIterationLimit, IContinueOnErrorConfirmation } from '../../prompt/common/specialRequestTypes';
3435
import { IDefaultIntentRequestHandlerOptions } from '../../prompt/node/defaultIntentRequestHandler';
@@ -180,6 +181,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
180181
@ITelemetryService telemetryService: ITelemetryService,
181182
@INotebookService notebookService: INotebookService,
182183
@ILogService private readonly logService: ILogService,
184+
@IExperimentationService private readonly expService: IExperimentationService,
183185
) {
184186
super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService);
185187
}
@@ -218,11 +220,47 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
218220
this.endpoint.modelMaxPromptTokens
219221
);
220222
const useTruncation = this.configurationService.getConfig(ConfigKey.Advanced.UseResponsesApiTruncation);
221-
const safeBudget = useTruncation ?
222-
Number.MAX_SAFE_INTEGER :
223-
Math.floor((baseBudget - toolTokens) * 0.85);
224-
const endpoint = toolTokens > 0 ? this.endpoint.cloneWithTokenOverride(safeBudget) : this.endpoint;
225223
const summarizationEnabled = this.configurationService.getConfig(ConfigKey.SummarizeAgentConversationHistory) && this.prompt === AgentPrompt;
224+
225+
// For Anthropic models with context editing, check previous turn's token usage to determine budget
226+
// 1. No token usage info (no prev turn) -> use normal safeBudget and let prompt rendering handle BudgetExceededError
227+
// 2. Token usage + current turn > threshold -> throw BudgetExceededError to trigger summarization
228+
// 3. Token usage + current turn < threshold -> use MAX_SAFE_INTEGER (no summarization needed)
229+
let safeBudget: number = -1;
230+
let shouldTriggerSummarize = false;
231+
const budgetThreshold = Math.floor((baseBudget - toolTokens) * 0.85);
232+
233+
const anthropicContextEditingEnabled = isAnthropicContextEditingEnabled(this.configurationService, this.expService);
234+
if (summarizationEnabled && isAnthropicFamily(this.endpoint) && anthropicContextEditingEnabled) {
235+
// First check current turn for token usage (from tool calling loop), then fall back to previous turn's result metadata
236+
const currentTurn = promptContext.conversation?.getLatestTurn();
237+
const currentTurnTokenUsage = currentTurn?.getMetadata(AnthropicTokenUsageMetadata);
238+
const previousTurn = promptContext.history?.at(-1);
239+
240+
const promptTokens = currentTurnTokenUsage?.promptTokens ?? previousTurn?.resultMetadata?.promptTokens;
241+
const outputTokens = currentTurnTokenUsage?.outputTokens ?? previousTurn?.resultMetadata?.outputTokens;
242+
243+
if (promptTokens !== undefined && outputTokens !== undefined) {
244+
// Estimate total tokens from the last completed turn (prompt + output) and add a 15% buffer to anticipate growth in the upcoming turn/tool call
245+
const totalEstimatedTokens = (promptTokens + outputTokens) * 1.15;
246+
247+
if (totalEstimatedTokens > this.endpoint.modelMaxPromptTokens) {
248+
// Will exceed budget - trigger summarization
249+
shouldTriggerSummarize = true;
250+
safeBudget = budgetThreshold; // Use normal budget for the summarization render
251+
this.logService.debug(`AgentIntent: token usage exceeds threshold, will trigger summarization (promptTokens=${promptTokens}, outputTokens=${outputTokens}, total=${totalEstimatedTokens}, threshold=${budgetThreshold})`);
252+
} else {
253+
// Under budget - no summarization needed, use unlimited budget
254+
safeBudget = Number.MAX_SAFE_INTEGER;
255+
this.logService.debug(`AgentIntent: token usage under threshold, skipping summarization (promptTokens=${promptTokens}, outputTokens=${outputTokens}, total=${totalEstimatedTokens}, threshold=${budgetThreshold})`);
256+
}
257+
}
258+
}
259+
if (safeBudget < 0) {
260+
safeBudget = useTruncation ? Number.MAX_SAFE_INTEGER : budgetThreshold;
261+
}
262+
const endpoint = toolTokens > 0 ? this.endpoint.cloneWithTokenOverride(safeBudget) : this.endpoint;
263+
226264
this.logService.debug(`AgentIntent: rendering with budget=${safeBudget} (baseBudget: ${baseBudget}, toolTokens: ${toolTokens}), summarizationEnabled=${summarizationEnabled}`);
227265
let result: RenderPromptResult;
228266
const props: AgentPromptProps = {
@@ -239,59 +277,70 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I
239277
...this.extraPromptProps,
240278
customizations: this._resolvedCustomizations
241279
};
242-
try {
243-
const renderer = PromptRenderer.create(this.instantiationService, endpoint, this.prompt, props);
244-
result = await renderer.render(progress, token);
245-
} catch (e) {
246-
if (e instanceof BudgetExceededError && summarizationEnabled) {
247-
this.logService.debug(`[Agent] budget exceeded, triggering summarization (${e.message})`);
248-
if (!promptContext.toolCallResults) {
249-
promptContext = {
250-
...promptContext,
251-
toolCallResults: {}
252-
};
253-
}
254-
e.metadata.getAll(ToolResultMetadata).forEach((metadata) => {
255-
promptContext.toolCallResults![metadata.toolCallId] = metadata.result;
280+
281+
// Helper function for summarization flow with fallbacks
282+
const renderWithSummarization = async (reason: string, renderProps: AgentPromptProps = props): Promise<RenderPromptResult> => {
283+
this.logService.debug(`[Agent] ${reason}, triggering summarization`);
284+
try {
285+
const renderer = PromptRenderer.create(this.instantiationService, endpoint, this.prompt, {
286+
...renderProps,
287+
triggerSummarize: true,
288+
});
289+
return await renderer.render(progress, token);
290+
} catch (e) {
291+
this.logService.error(e, `[Agent] summarization failed`);
292+
const errorKind = e instanceof BudgetExceededError ? 'budgetExceeded' : 'error';
293+
/* __GDPR__
294+
"triggerSummarizeFailed" : {
295+
"owner": "roblourens",
296+
"comment": "Tracks when triggering summarization failed - for example, a summary was created but not applied successfully.",
297+
"errorKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The success state or failure reason of the summarization." },
298+
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model ID used for the summarization." }
299+
}
300+
*/
301+
this.telemetryService.sendMSFTTelemetryEvent('triggerSummarizeFailed', { errorKind, model: renderProps.endpoint.model });
302+
303+
// Something else went wrong, eg summarization failed, so render the prompt with no cache breakpoints, summarization, endpoint not reduced in size for tools or safety buffer
304+
const renderer = PromptRenderer.create(this.instantiationService, this.endpoint, this.prompt, {
305+
...renderProps,
306+
endpoint: this.endpoint,
307+
enableCacheBreakpoints: false
256308
});
257309
try {
258-
const renderer = PromptRenderer.create(this.instantiationService, endpoint, this.prompt, {
259-
...props,
260-
triggerSummarize: true,
261-
});
262-
result = await renderer.render(progress, token);
310+
return await renderer.render(progress, token);
263311
} catch (e) {
264-
this.logService.error(e, `[Agent] summarization failed`);
265-
const errorKind = e instanceof BudgetExceededError ? 'budgetExceeded' : 'error';
266-
/* __GDPR__
267-
"triggerSummarizeFailed" : {
268-
"owner": "roblourens",
269-
"comment": "Tracks when triggering summarization failed - for example, a summary was created but not applied successfully.",
270-
"errorKind": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The success state or failure reason of the summarization." },
271-
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model ID used for the summarization." }
272-
}
273-
*/
274-
this.telemetryService.sendMSFTTelemetryEvent('triggerSummarizeFailed', { errorKind, model: props.endpoint.model });
275-
276-
// Something else went wrong, eg summarization failed, so render the prompt with no cache breakpoints, summarization, endpoint not reduced in size for tools or safety buffer
277-
const renderer = PromptRenderer.create(this.instantiationService, this.endpoint, this.prompt, {
278-
...props,
279-
endpoint: this.endpoint,
280-
enableCacheBreakpoints: false
281-
});
282-
try {
283-
result = await renderer.render(progress, token);
284-
} catch (e) {
285-
if (e instanceof BudgetExceededError) {
286-
this.logService.error(e, `[Agent] final render fallback failed due to budget exceeded`);
287-
const maxTokens = this.endpoint.modelMaxPromptTokens;
288-
throw new Error(`Unable to build prompt, modelMaxPromptTokens=${maxTokens} (${e.message})`);
289-
}
290-
throw e;
312+
if (e instanceof BudgetExceededError) {
313+
this.logService.error(e, `[Agent] final render fallback failed due to budget exceeded`);
314+
const maxTokens = this.endpoint.modelMaxPromptTokens;
315+
throw new Error(`Unable to build prompt, modelMaxPromptTokens = ${maxTokens} (${e.message})`);
316+
}
317+
throw e;
318+
}
319+
}
320+
};
321+
322+
if (shouldTriggerSummarize) {
323+
// Token usage from previous turn indicates we'll exceed budget - go directly to summarization flow
324+
result = await renderWithSummarization('token usage from previous turn exceeds budget threshold');
325+
} else {
326+
try {
327+
const renderer = PromptRenderer.create(this.instantiationService, endpoint, this.prompt, props);
328+
result = await renderer.render(progress, token);
329+
} catch (e) {
330+
if (e instanceof BudgetExceededError && summarizationEnabled) {
331+
if (!promptContext.toolCallResults) {
332+
promptContext = {
333+
...promptContext,
334+
toolCallResults: {}
335+
};
291336
}
337+
e.metadata.getAll(ToolResultMetadata).forEach((metadata) => {
338+
promptContext.toolCallResults![metadata.toolCallId] = metadata.result;
339+
});
340+
result = await renderWithSummarization(`budget exceeded(${e.message})`);
341+
} else {
342+
throw e;
292343
}
293-
} else {
294-
throw e;
295344
}
296345
}
297346

src/extension/intents/node/askAgentIntent.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogS
1313
import { IChatEndpoint } from '../../../platform/networking/common/networking';
1414
import { INotebookService } from '../../../platform/notebook/common/notebookService';
1515
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
16+
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
1617
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
1718
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
1819
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
@@ -123,8 +124,9 @@ export class AskAgentIntentInvocation extends AgentIntentInvocation {
123124
@ITelemetryService telemetryService: ITelemetryService,
124125
@INotebookService notebookService: INotebookService,
125126
@ILogService logService: ILogService,
127+
@IExperimentationService expService: IExperimentationService,
126128
) {
127-
super(intent, location, endpoint, request, { processCodeblocks: true }, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService);
129+
super(intent, location, endpoint, request, { processCodeblocks: true }, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService);
128130
}
129131

130132
public override async getAvailableTools(): Promise<vscode.LanguageModelToolInformation[]> {

src/extension/intents/node/editCodeIntent2.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ export class EditCode2IntentInvocation extends AgentIntentInvocation {
115115
@ITelemetryService telemetryService: ITelemetryService,
116116
@INotebookService notebookService: INotebookService,
117117
@ILogService logService: ILogService,
118+
@IExperimentationService expService: IExperimentationService,
118119
) {
119-
super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService);
120+
super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService);
120121
}
121122

122123
public override async getAvailableTools(): Promise<vscode.LanguageModelToolInformation[]> {

src/extension/intents/node/notebookEditorIntent.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ export class NotebookEditorIntentInvocation extends EditCode2IntentInvocation {
102102
@ITelemetryService telemetryService: ITelemetryService,
103103
@INotebookService notebookService: INotebookService,
104104
@ILogService logService: ILogService,
105+
@IExperimentationService expService: IExperimentationService,
105106
) {
106-
super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService);
107+
super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService);
107108
}
108109

109110
protected override prompt = NotebookInlinePrompt;

0 commit comments

Comments
 (0)