Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export const globalSettingsSchema = z.object({
openRouterImageApiKey: z.string().optional(),
openRouterImageGenerationSelectedModel: z.string().optional(),

condensingApiConfigOverride: z.boolean().optional(),
condensingApiConfigId: z.string().optional(),
customCondensingPrompt: z.string().optional(),

autoApprovalEnabled: z.boolean().optional(),
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ export type ExtensionState = Pick<
| "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId"
| "condensingApiConfigOverride"
| "condensingApiConfigId"
| "customCondensingPrompt"
| "codebaseIndexConfig"
| "codebaseIndexModels"
Expand Down
54 changes: 54 additions & 0 deletions src/core/condense/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,60 @@ describe("summarizeConversation with custom settings", () => {
expect(createMessageCalls[0][0]).toContain("CRITICAL: This is a summarization-only request")
})

it("should use the selected condensing handler while counting the resulting context with the current handler", async () => {
const condensingApiHandler = {
createMessage: vi.fn().mockImplementation(() => {
return (async function* () {
yield { type: "text" as const, text: "Summary from selected profile" }
yield { type: "usage" as const, totalCost: 0.01, outputTokens: 40 }
})()
}),
getModel: vi.fn().mockReturnValue({
id: "condensing-model",
info: {
contextWindow: 4000,
supportsImages: false,
supportsVision: false,
maxTokens: 2000,
},
}),
} as unknown as ApiHandler

const result = await summarizeConversation({
messages: sampleMessages,
apiHandler: mockMainApiHandler,
condensingApiHandler,
systemPrompt: defaultSystemPrompt,
taskId: localTaskId,
})

expect(condensingApiHandler.createMessage).toHaveBeenCalledOnce()
expect(mockMainApiHandler.createMessage).not.toHaveBeenCalled()
expect(mockMainApiHandler.countTokens).toHaveBeenCalled()
expect(result.summary).toBe("Summary from selected profile")
})

it("should fall back to the current handler when the selected condensing handler is invalid", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
const invalidHandler = {
getModel: vi.fn(),
} as unknown as ApiHandler

try {
await summarizeConversation({
messages: sampleMessages,
apiHandler: mockMainApiHandler,
condensingApiHandler: invalidHandler,
systemPrompt: defaultSystemPrompt,
taskId: localTaskId,
})
} finally {
warnSpy.mockRestore()
}

expect(mockMainApiHandler.createMessage).toHaveBeenCalledOnce()
})

/**
* Test that telemetry is called for custom prompt usage
*/
Expand Down
18 changes: 15 additions & 3 deletions src/core/condense/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ export type SummarizeResponse = {
export type SummarizeConversationOptions = {
messages: ApiMessage[]
apiHandler: ApiHandler
/** Optional API handler used only for the summarization request. Token counting still uses apiHandler. */
condensingApiHandler?: ApiHandler
systemPrompt: string
taskId: string
isAutomaticTrigger?: boolean
Expand Down Expand Up @@ -257,6 +259,7 @@ export async function summarizeConversation(options: SummarizeConversationOption
const {
messages,
apiHandler,
condensingApiHandler,
systemPrompt,
taskId,
isAutomaticTrigger,
Expand Down Expand Up @@ -311,8 +314,17 @@ export async function summarizeConversation(options: SummarizeConversationOption
// This is necessary because some providers (like Bedrock via LiteLLM) require the `tools` parameter
// when tool blocks are present. By converting them to text, we can send the conversation for
// summarization without needing to pass the tools parameter.
const handlerToUse =
condensingApiHandler && typeof condensingApiHandler.createMessage === "function"
? condensingApiHandler
: apiHandler

if (condensingApiHandler && handlerToUse !== condensingApiHandler) {
console.warn("Selected API handler for condensing is invalid; using the current mode's API handler.")
}

const messagesWithTextToolBlocks = transformMessagesForCondensing(
maybeRemoveImageBlocks([...messagesWithToolResults, finalRequestMessage], apiHandler),
maybeRemoveImageBlocks([...messagesWithToolResults, finalRequestMessage], handlerToUse),
)

const requestMessages = messagesWithTextToolBlocks.map(({ role, content }) => ({ role, content }))
Expand All @@ -321,7 +333,7 @@ export async function summarizeConversation(options: SummarizeConversationOption
const promptToUse = SUMMARY_PROMPT

// Validate that the API handler supports message creation
if (!apiHandler || typeof apiHandler.createMessage !== "function") {
if (!handlerToUse || typeof handlerToUse.createMessage !== "function") {
console.error("API handler is invalid for condensing. Cannot proceed.")
const error = t("common:errors.condense_handler_invalid")
return { ...response, error }
Expand All @@ -332,7 +344,7 @@ export async function summarizeConversation(options: SummarizeConversationOption
let outputTokens = 0

try {
const stream = apiHandler.createMessage(promptToUse, requestMessages, metadata)
const stream = handlerToUse.createMessage(promptToUse, requestMessages, metadata)

for await (const chunk of stream) {
if (chunk.type === "text") {
Expand Down
3 changes: 3 additions & 0 deletions src/core/context-management/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export type ContextManagementOptions = {
contextWindow: number
maxTokens?: number | null
apiHandler: ApiHandler
condensingApiHandler?: ApiHandler
autoCondenseContext: boolean
autoCondenseContextPercent: number
systemPrompt: string
Expand Down Expand Up @@ -294,6 +295,7 @@ export async function manageContext({
contextWindow,
maxTokens,
apiHandler,
condensingApiHandler,
autoCondenseContext,
autoCondenseContextPercent,
systemPrompt,
Expand Down Expand Up @@ -361,6 +363,7 @@ export async function manageContext({
const result = await summarizeConversation({
messages,
apiHandler,
condensingApiHandler,
systemPrompt,
taskId,
isAutomaticTrigger: true,
Expand Down
40 changes: 40 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,37 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}

private async getCondensingApiHandler(
state:
| {
condensingApiConfigOverride?: boolean
condensingApiConfigId?: string
listApiConfigMeta?: Array<{ id: string }>
}
| undefined,
): Promise<ApiHandler | undefined> {
const configId = state?.condensingApiConfigId
if (!state?.condensingApiConfigOverride || !configId) {
return undefined
}

if (!state.listApiConfigMeta?.some((config) => config.id === configId)) {
console.warn(`[Task] Context condensing profile "${configId}" no longer exists; using the current mode.`)
return undefined
}

try {
const profile = await this.providerRef.deref()?.providerSettingsManager.getProfile({ id: configId })
return profile?.apiProvider ? buildApiHandler(profile) : undefined
} catch (error) {
console.warn(
`[Task] Failed to load context condensing profile "${configId}"; using the current mode.`,
error,
)
return undefined
}
}

public async condenseContext(): Promise<void> {
// CRITICAL: Flush any pending tool results before condensing
// to ensure tool_use/tool_result pairs are complete in history
Expand All @@ -1583,6 +1614,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const state = await this.providerRef.deref()?.getState()
const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
const { mode, apiConfiguration } = state ?? {}
const condensingApiHandler = await this.getCondensingApiHandler(state)

const { contextTokens: prevContextTokens } = this.getTokenUsage()

Expand Down Expand Up @@ -1638,6 +1670,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
} = await summarizeConversation({
messages: this.apiConversationHistory,
apiHandler: this.api,
condensingApiHandler,
systemPrompt,
taskId: this.taskId,
isAutomaticTrigger: false,
Expand Down Expand Up @@ -3802,6 +3835,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
private async handleContextWindowExceededError(): Promise<void> {
const state = await this.providerRef.deref()?.getState()
const { profileThresholds = {}, mode, apiConfiguration } = state ?? {}
const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
const condensingApiHandler = await this.getCondensingApiHandler(state)

const { contextTokens } = this.getTokenUsage()
const modelInfo = this.api.getModel().info
Expand Down Expand Up @@ -3876,10 +3911,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
maxTokens,
contextWindow,
apiHandler: this.api,
condensingApiHandler,
autoCondenseContext: true,
autoCondenseContextPercent: FORCED_CONTEXT_REDUCTION_PERCENT,
systemPrompt: await this.getSystemPrompt(),
taskId: this.taskId,
customCondensingPrompt,
profileThresholds,
currentProfileId,
metadata,
Expand Down Expand Up @@ -4042,11 +4079,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
lastMessageTokens,
useAvailableInputForContextPercent,
})
let condensingApiHandler: ApiHandler | undefined

// Send condenseTaskContextStarted BEFORE manageContext to show in-progress indicator
// This notification must be sent here (not earlier) because the early check uses stale token count
// (before user message is added to history), which could incorrectly skip showing the indicator
if (contextManagementWillRun && autoCondenseContext) {
condensingApiHandler = await this.getCondensingApiHandler(state)
await this.providerRef
.deref()
?.postMessageToWebview({ type: "condenseTaskContextStarted", text: this.taskId })
Expand Down Expand Up @@ -4111,6 +4150,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
maxTokens,
contextWindow,
apiHandler: this.api,
condensingApiHandler,
autoCondenseContext,
autoCondenseContextPercent,
systemPrompt,
Expand Down
Loading
Loading