feat: first pass at adding context editing support for Anthropic Messages API (CAPI) (#2739)

* feat: first pass at adding context editing support for Anthropic Messages API

* feat: implement context editing configuration for Anthropic Messages API

* update context editing configuration for Anthropic Messages API to use team internal settings

* fix: update context editing configuration to use team internal settings

* fix: add missing newline at end of package.json

* fix: disable context editing for Anthropic Messages API

* fix: remove unnecessary context editing settings for Anthropic Messages API

* Update src/platform/networking/common/anthropic.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/platform/configuration/common/configurationService.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Bhavya U
2026-01-08 17:13:05 +00:00
committed by GitHub
co-authored by Copilot
parent 12b7531632
commit 90ce9ca532
9 changed files with 263 additions and 22 deletions
@@ -13,6 +13,7 @@ import { isAnthropicFamily } from '../../../platform/endpoint/common/chatModelCa
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
import { rawPartAsThinkingData } from '../../../platform/endpoint/common/thinkingDataContainer';
import { ILogService } from '../../../platform/log/common/logService';
import { ContextManagementResponse } from '../../../platform/networking/common/anthropic';
import { OpenAiFunctionDef } from '../../../platform/networking/common/fetch';
import { IMakeChatRequestOptions } from '../../../platform/networking/common/networking';
import { IRequestLogger } from '../../../platform/requestLogger/node/requestLogger';
@@ -28,7 +29,7 @@ import { IInstantiationService } from '../../../util/vs/platform/instantiation/c
import { ChatResponsePullRequestPart, LanguageModelDataPart2, LanguageModelPartAudience, LanguageModelToolResult2, MarkdownString } from '../../../vscodeTypes';
import { InteractionOutcomeComputer } from '../../inlineChat/node/promptCraftingTypes';
import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';
import { Conversation, IResultMetadata, ResponseStreamParticipant, TurnStatus } from '../../prompt/common/conversation';
import { ContextEditingMetadata, Conversation, IResultMetadata, ResponseStreamParticipant, TurnStatus } from '../../prompt/common/conversation';
import { IBuildPromptContext, InternalToolReference, IToolCall, IToolCallRound } from '../../prompt/common/intents';
import { cancelText, IToolCallIterationIncrease } from '../../prompt/common/specialRequestTypes';
import { ThinkingDataItem, ToolCallRound } from '../../prompt/common/toolCallRound';
@@ -435,6 +436,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
let statefulMarker: string | undefined;
const toolCalls: IToolCall[] = [];
let thinkingItem: ThinkingDataItem | undefined;
let contextManagementResponse: ContextManagementResponse | undefined;
const endpoint = await this._endpointProvider.getChatEndpoint(this.options.request);
const disableThinking = isContinuation && isAnthropicFamily(endpoint) && !ToolCallingLoop.messagesContainThinking(buildPromptResult.messages);
const fetchResult = await this.fetch({
@@ -454,6 +456,9 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
if (delta.thinking) {
thinkingItem = ThinkingDataItem.createOrUpdate(thinkingItem, delta.thinking);
}
if (delta.contextManagement) {
contextManagementResponse = delta.contextManagement;
}
return stopEarly ? text.length : undefined;
},
@@ -488,6 +493,20 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
this._onDidReceiveResponse.fire({ interactionOutcome: interactionOutcomeComputer, response: fetchResult, toolCalls });
this.turn.setMetadata(interactionOutcomeComputer.interactionOutcome);
// Store context editing metadata if context management cleared tokens
if (contextManagementResponse && contextManagementResponse.applied_edits.length > 0) {
const totalClearedTokens = contextManagementResponse.applied_edits.reduce(
(sum, edit) => sum + (edit.cleared_input_tokens || 0),
0
);
if (totalClearedTokens > 0) {
this.turn.setMetadata(new ContextEditingMetadata(
totalClearedTokens,
contextManagementResponse.applied_edits.length
));
}
}
const toolInputRetry = isToolInputFailure ? (this.toolCallRounds.at(-1)?.toolInputRetry || 0) + 1 : 0;
if (fetchResult.type === ChatFetchResponseType.Success) {
thinkingItem?.updateWithFetchResult(fetchResult);
@@ -387,6 +387,22 @@ export class GlobalContextMessageMetadata {
) { }
}
/**
* Metadata capturing context editing information from Anthropic Messages API.
* When context editing clears tokens on a turn, this metadata is stored so that
* subsequent turns can use the cleared token count to adjust their budget calculation.
*/
export class ContextEditingMetadata extends PromptMetadata {
constructor(
/** Total number of tokens cleared by context editing */
readonly clearedTokens: number,
/** Number of context edits applied */
readonly editCount: number,
) {
super();
}
}
export function getGlobalContextCacheKey(accessor: ServicesAccessor): string {
const workspaceService = accessor.get(IWorkspaceService);
return workspaceService.getWorkspaceFolders().map(folder => folder.toString()).join(',');
@@ -53,6 +53,39 @@ function processDeltasToMessage(deltas: IResponseDelta[]): string {
}).join('\n');
}
// Handle context management
if (d.contextManagement) {
if (i > 0 || text.length > 0) {
text += '\n';
}
const totalClearedTokens = d.contextManagement.applied_edits.reduce(
(sum, edit) => sum + (edit.cleared_input_tokens || 0),
0
);
const totalClearedToolUses = d.contextManagement.applied_edits.reduce(
(sum, edit) => sum + (edit.cleared_tool_uses || 0),
0
);
const totalClearedThinkingTurns = d.contextManagement.applied_edits.reduce(
(sum, edit) => sum + (edit.cleared_thinking_turns || 0),
0
);
const details: string[] = [];
if (totalClearedTokens > 0) {
details.push(`${totalClearedTokens} tokens`);
}
if (totalClearedToolUses > 0) {
details.push(`${totalClearedToolUses} tool uses`);
}
if (totalClearedThinkingTurns > 0) {
details.push(`${totalClearedThinkingTurns} thinking turns`);
}
text += `🧹 Context cleared: ${details.join(', ')}`;
}
return text;
}).join('');
}
@@ -798,6 +798,16 @@ export namespace ConfigKey {
export const AskAgent = defineSetting<boolean>('chat.advanced.enableAskAgent', ConfigType.ExperimentBased, false);
export const RetryNetworkErrors = defineSetting<boolean>('chat.advanced.enableRetryNetworkErrors', ConfigType.ExperimentBased, true);
export const WorkspaceEnableCodeSearchExternalIngest = defineTeamInternalSetting<boolean>('chat.advanced.workspace.codeSearchExternalIngest.enabled', ConfigType.ExperimentBased, false);
/** Context editing for Anthropic Messages API */
export const AnthropicContextEditingEnabled = defineTeamInternalSetting<boolean>('chat.advanced.anthropic.contextEditing.enabled', ConfigType.ExperimentBased, false);
export const AnthropicContextEditingToolResultTriggerType = defineTeamInternalSetting<'input_tokens' | 'tool_uses'>('chat.advanced.anthropic.contextEditing.toolResult.triggerType', ConfigType.ExperimentBased, 'input_tokens');
export const AnthropicContextEditingToolResultTriggerValue = defineTeamInternalSetting<number>('chat.advanced.anthropic.contextEditing.toolResult.triggerValue', ConfigType.ExperimentBased, 100000);
export const AnthropicContextEditingToolResultKeepCount = defineTeamInternalSetting<number>('chat.advanced.anthropic.contextEditing.toolResult.keepCount', ConfigType.ExperimentBased, 3);
export const AnthropicContextEditingToolResultClearAtLeastTokens = defineTeamInternalSetting<number | undefined>('chat.advanced.anthropic.contextEditing.toolResult.clearAtLeastTokens', ConfigType.ExperimentBased, 25000);
export const AnthropicContextEditingToolResultExcludeTools = defineTeamInternalSetting<string[]>('chat.advanced.anthropic.contextEditing.toolResult.excludeTools', ConfigType.Simple, []);
export const AnthropicContextEditingToolResultClearInputs = defineTeamInternalSetting<boolean>('chat.advanced.anthropic.contextEditing.toolResult.clearInputs', ConfigType.ExperimentBased, false);
export const AnthropicContextEditingThinkingKeepTurns = defineTeamInternalSetting<number>('chat.advanced.anthropic.contextEditing.thinking.keepTurns', ConfigType.ExperimentBased, 1);
}
export const Enable = defineSetting<{ [key: string]: boolean }>('enable', ConfigType.Simple, {
@@ -171,8 +171,23 @@ export class ChatEndpoint implements IChatEndpoint {
public getExtraHeaders(): Record<string, string> {
const headers: Record<string, string> = { ...this.modelMetadata.requestHeaders };
if (this.useMessagesApi && this._getThinkingBudget()) {
headers['anthropic-beta'] = 'interleaved-thinking-2025-05-14';
if (this.useMessagesApi) {
const betaFeatures: string[] = [];
// Add thinking beta if enabled
if (this._getThinkingBudget()) {
betaFeatures.push('interleaved-thinking-2025-05-14');
}
// Add context management beta if enabled
const contextEditingEnabled = this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AnthropicContextEditingEnabled, this._expService);
if (contextEditingEnabled) {
betaFeatures.push('context-management-2025-06-27');
}
if (betaFeatures.length > 0) {
headers['anthropic-beta'] = betaFeatures.join(',');
}
}
return headers;
@@ -316,7 +331,7 @@ export class ChatEndpoint implements IChatEndpoint {
if (this.useResponsesApi) {
return processResponseFromChatEndpoint(this._instantiationService, telemetryService, logService, response, expectedNumChoices, finishCallback, telemetryData);
} else if (this.useMessagesApi) {
return processResponseFromMessagesEndpoint(this._instantiationService, telemetryService, logService, response, expectedNumChoices, finishCallback, telemetryData);
return processResponseFromMessagesEndpoint(this._instantiationService, response, finishCallback, telemetryData);
} else if (!this._supportsStreaming) {
return defaultNonStreamChatResponseProcessor(response, finishCallback, telemetryData);
} else {
@@ -12,12 +12,11 @@ import { SSEParser } from '../../../util/vs/base/common/sseParser';
import { generateUuid } from '../../../util/vs/base/common/uuid';
import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';
import { ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';
import { ILogService } from '../../log/common/logService';
import { AnthropicMessagesTool, FinishedCallback, IResponseDelta } from '../../networking/common/fetch';
import { AnthropicMessagesTool, buildContextManagement, ContextManagementResponse } from '../../networking/common/anthropic';
import { FinishedCallback, IResponseDelta } from '../../networking/common/fetch';
import { IChatEndpoint, ICreateEndpointBodyOptions, IEndpointBody } from '../../networking/common/networking';
import { ChatCompletion, FinishedCompletionReason } from '../../networking/common/openai';
import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';
import { ITelemetryService } from '../../telemetry/common/telemetry';
import { TelemetryData } from '../../telemetry/common/telemetryData';
interface AnthropicStreamEvent {
@@ -54,6 +53,7 @@ interface AnthropicStreamEvent {
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
};
context_management?: ContextManagementResponse;
}
export function createMessagesRequestBody(accessor: ServicesAccessor, options: ICreateEndpointBodyOptions, model: string, endpoint: IChatEndpoint): IEndpointBody {
@@ -80,6 +80,19 @@ export function createMessagesRequestBody(accessor: ServicesAccessor, options: I
? Math.min(32000, maxTokens - 1, normalizedBudget)
: undefined;
// Build context management configuration
const contextEditingEnabled = configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AnthropicContextEditingEnabled, experimentationService);
const contextEditingConfig = {
triggerType: configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AnthropicContextEditingToolResultTriggerType, experimentationService) as 'input_tokens' | 'tool_uses',
triggerValue: configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AnthropicContextEditingToolResultTriggerValue, experimentationService),
keepCount: configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AnthropicContextEditingToolResultKeepCount, experimentationService),
clearAtLeastTokens: configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AnthropicContextEditingToolResultClearAtLeastTokens, experimentationService),
excludeTools: configurationService.getConfig(ConfigKey.TeamInternal.AnthropicContextEditingToolResultExcludeTools),
clearInputs: configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AnthropicContextEditingToolResultClearInputs, experimentationService),
thinkingKeepTurns: configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.AnthropicContextEditingThinkingKeepTurns, experimentationService),
};
const contextManagement = contextEditingEnabled ? buildContextManagement(contextEditingConfig, thinkingBudget, endpoint.modelMaxPromptTokens) : undefined;
return {
model,
...rawMessagesToMessagesAPI(options.messages),
@@ -91,6 +104,7 @@ export function createMessagesRequestBody(accessor: ServicesAccessor, options: I
type: 'enabled',
budget_tokens: thinkingBudget,
} : undefined,
context_management: contextManagement,
};
}
@@ -251,10 +265,7 @@ function contentBlockSupportsCacheControl(block: ContentBlockParam): block is Ex
export async function processResponseFromMessagesEndpoint(
instantiationService: IInstantiationService,
telemetryService: ITelemetryService,
logService: ILogService,
response: Response,
expectedNumChoices: number,
finishCallback: FinishedCallback,
telemetryData: TelemetryData
): Promise<AsyncIterableObject<ChatCompletion>> {
@@ -270,7 +281,6 @@ export async function processResponseFromMessagesEndpoint(
return;
}
logService.trace(`SSE: ${trimmed}`);
const parsed = JSON.parse(trimmed) as Partial<AnthropicStreamEvent>;
const type = parsed.type ?? ev.type;
if (!type) {
@@ -303,6 +313,7 @@ export class AnthropicMessagesProcessor {
private inputTokens: number = 0;
private outputTokens: number = 0;
private cachedTokens: number = 0;
private contextManagementResponse?: ContextManagementResponse;
constructor(
private readonly telemetryData: TelemetryData,
@@ -408,8 +419,29 @@ export class AnthropicMessagesProcessor {
if (chunk.usage) {
this.outputTokens = chunk.usage.output_tokens;
}
if (chunk.context_management) {
this.contextManagementResponse = chunk.context_management;
// Report context management via delta so it gets logged to request logger
return onProgress({
text: '',
contextManagement: chunk.context_management
});
}
return;
case 'message_stop':
// Add context management info to telemetry if available
if (this.contextManagementResponse) {
const totalClearedTokens = this.contextManagementResponse.applied_edits.reduce(
(sum, edit) => sum + (edit.cleared_input_tokens || 0),
0
);
this.telemetryData.extendedBy({
contextEditingApplied: 'true',
contextEditingClearedTokens: totalClearedTokens.toString(),
contextEditingEditCount: this.contextManagementResponse.applied_edits.length.toString(),
});
}
return {
blockFinished: true,
choiceIndex: 0,
@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Types for Anthropic Messages API
* Based on https://platform.claude.com/docs/en/api/messages
*/
export interface AnthropicMessagesTool {
name: string;
description?: string;
input_schema: {
type: 'object';
properties?: Record<string, unknown>;
required?: string[];
};
}
/**
* Context management types for Anthropic Messages API
* Based on https://platform.claude.com/docs/en/build-with-claude/context-editing
*/
export type ContextManagementTrigger =
| { type: 'input_tokens'; value: number }
| { type: 'tool_uses'; value: number };
export type ContextManagementKeep =
| { type: 'tool_uses'; value: number }
| { type: 'thinking_turns'; value: number }
| 'all';
export type ContextManagementClearAtLeast = {
type: 'input_tokens';
value: number;
};
export interface ClearToolUsesEdit {
type: 'clear_tool_uses_20250919';
trigger?: ContextManagementTrigger;
keep?: ContextManagementKeep;
clear_at_least?: ContextManagementClearAtLeast;
exclude_tools?: string[];
clear_tool_inputs?: boolean;
}
export interface ClearThinkingEdit {
type: 'clear_thinking_20251015';
keep?: ContextManagementKeep;
}
export type ContextManagementEdit = ClearToolUsesEdit | ClearThinkingEdit;
export interface ContextManagement {
edits: ContextManagementEdit[];
}
export interface AppliedContextEdit {
type: 'clear_thinking_20251015' | 'clear_tool_uses_20250919';
cleared_thinking_turns?: number;
cleared_tool_uses?: number;
cleared_input_tokens?: number;
}
export interface ContextManagementResponse {
applied_edits: AppliedContextEdit[];
}
export interface ContextEditingConfig {
triggerType: 'input_tokens' | 'tool_uses';
triggerValue: number;
keepCount: number;
clearAtLeastTokens: number | undefined;
excludeTools: string[];
clearInputs: boolean;
thinkingKeepTurns: number;
}
/**
* Builds the context_management configuration object for the Messages API request.
* @param config The context editing configuration from individual settings
* @param hasThinking Whether extended thinking is enabled (the thinking budget value)
* @param modelMaxTokens The maximum input tokens supported by the model
* @returns The context_management object to include in the request, or undefined if no edits
*/
export function buildContextManagement(
config: ContextEditingConfig,
hasThinking: number | undefined,
modelMaxTokens: number
): ContextManagement | undefined {
const edits: ContextManagementEdit[] = [];
// Add thinking block clearing if extended thinking is enabled
if (hasThinking) {
const thinkingKeepTurns = config.thinkingKeepTurns;
edits.push({
type: 'clear_thinking_20251015',
keep: { type: 'thinking_turns', value: Math.max(1, thinkingKeepTurns) },
});
}
// Add tool result clearing configuration
const { triggerType, triggerValue, keepCount, clearAtLeastTokens, excludeTools, clearInputs } = config;
// Build trigger based on type - use configured values directly (defaults match Anthropic's recommendations)
const trigger: ContextManagementTrigger = { type: triggerType, value: triggerValue };
const toolEdit: ContextManagementEdit = {
type: 'clear_tool_uses_20250919',
trigger,
keep: { type: 'tool_uses', value: keepCount },
...(clearAtLeastTokens ? { clear_at_least: { type: 'input_tokens' as const, value: clearAtLeastTokens } } : {}),
...(excludeTools.length > 0 ? { exclude_tools: excludeTools } : {}),
...(clearInputs ? { clear_tool_inputs: clearInputs } : {}),
};
edits.push(toolEdit);
return edits.length > 0 ? { edits } : undefined;
}
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { EncryptedThinkingDelta, ThinkingData, ThinkingDelta } from '../../thinking/common/thinking';
import { AnthropicMessagesTool, ContextManagementResponse } from './anthropic';
import { Response } from './fetcherService';
import { ChoiceLogProbs, FilterReason } from './openai';
@@ -135,6 +136,8 @@ export interface IResponseDelta {
retryReason?: FilterReason | 'network_error';
/** Marker for the current response, which should be presented in `IMakeChatRequestOptions` on the next call */
statefulMarker?: string;
/** Context management information from Anthropic Messages API */
contextManagement?: ContextManagementResponse;
}
export const enum ResponsePartKind {
@@ -268,16 +271,6 @@ export function isOpenAiFunctionTool(tool: OpenAiResponsesFunctionTool | OpenAiF
return (tool as OpenAiFunctionTool).function !== undefined;
}
export interface AnthropicMessagesTool {
name: string;
description?: string;
input_schema: {
type: 'object';
properties?: Record<string, any>;
required?: string[];
};
}
/**
* Options for streaming response. Only set this when you set stream: true.
*
@@ -17,7 +17,8 @@ import { CustomModel, EndpointEditToolName } from '../../endpoint/common/endpoin
import { ILogService } from '../../log/common/logService';
import { ITelemetryService, TelemetryProperties } from '../../telemetry/common/telemetry';
import { TelemetryData } from '../../telemetry/common/telemetryData';
import { AnthropicMessagesTool, FinishedCallback, OpenAiFunctionTool, OpenAiResponsesFunctionTool, OptionalChatRequestParams, Prediction } from './fetch';
import { FinishedCallback, OpenAiFunctionTool, OpenAiResponsesFunctionTool, OptionalChatRequestParams, Prediction } from './fetch';
import { AnthropicMessagesTool, ContextManagement } from './anthropic';
import { FetcherId, FetchOptions, IAbortController, IFetcherService, PaginationOptions, Response } from './fetcherService';
import { ChatCompletion, RawMessageConversionCallback, rawMessageToCAPI } from './openai';
@@ -111,6 +112,7 @@ export interface IEndpointBody {
type: 'enabled' | 'disabled';
budget_tokens?: number;
};
context_management?: ContextManagement;
/** ChatCompletions API for Anthropic models */
thinking_budget?: number;