Skip to content

Commit 046bfd0

Browse files
feat(condense): allow provider profile override
1 parent c39535e commit 046bfd0

12 files changed

Lines changed: 416 additions & 9 deletions

File tree

packages/types/src/global-settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ export const globalSettingsSchema = z.object({
116116
openRouterImageApiKey: z.string().optional(),
117117
openRouterImageGenerationSelectedModel: z.string().optional(),
118118

119+
condensingApiConfigOverride: z.boolean().optional(),
120+
condensingApiConfigId: z.string().optional(),
119121
customCondensingPrompt: z.string().optional(),
120122

121123
autoApprovalEnabled: z.boolean().optional(),

packages/types/src/vscode-extension-host.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ export type ExtensionState = Pick<
303303
| "customModePrompts"
304304
| "customSupportPrompts"
305305
| "enhancementApiConfigId"
306+
| "condensingApiConfigOverride"
307+
| "condensingApiConfigId"
306308
| "customCondensingPrompt"
307309
| "codebaseIndexConfig"
308310
| "codebaseIndexModels"

src/core/condense/__tests__/index.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,60 @@ describe("summarizeConversation with custom settings", () => {
12501250
expect(createMessageCalls[0][0]).toContain("CRITICAL: This is a summarization-only request")
12511251
})
12521252

1253+
it("should use the selected condensing handler while counting the resulting context with the current handler", async () => {
1254+
const condensingApiHandler = {
1255+
createMessage: vi.fn().mockImplementation(() => {
1256+
return (async function* () {
1257+
yield { type: "text" as const, text: "Summary from selected profile" }
1258+
yield { type: "usage" as const, totalCost: 0.01, outputTokens: 40 }
1259+
})()
1260+
}),
1261+
getModel: vi.fn().mockReturnValue({
1262+
id: "condensing-model",
1263+
info: {
1264+
contextWindow: 4000,
1265+
supportsImages: false,
1266+
supportsVision: false,
1267+
maxTokens: 2000,
1268+
},
1269+
}),
1270+
} as unknown as ApiHandler
1271+
1272+
const result = await summarizeConversation({
1273+
messages: sampleMessages,
1274+
apiHandler: mockMainApiHandler,
1275+
condensingApiHandler,
1276+
systemPrompt: defaultSystemPrompt,
1277+
taskId: localTaskId,
1278+
})
1279+
1280+
expect(condensingApiHandler.createMessage).toHaveBeenCalledOnce()
1281+
expect(mockMainApiHandler.createMessage).not.toHaveBeenCalled()
1282+
expect(mockMainApiHandler.countTokens).toHaveBeenCalled()
1283+
expect(result.summary).toBe("Summary from selected profile")
1284+
})
1285+
1286+
it("should fall back to the current handler when the selected condensing handler is invalid", async () => {
1287+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
1288+
const invalidHandler = {
1289+
getModel: vi.fn(),
1290+
} as unknown as ApiHandler
1291+
1292+
try {
1293+
await summarizeConversation({
1294+
messages: sampleMessages,
1295+
apiHandler: mockMainApiHandler,
1296+
condensingApiHandler: invalidHandler,
1297+
systemPrompt: defaultSystemPrompt,
1298+
taskId: localTaskId,
1299+
})
1300+
} finally {
1301+
warnSpy.mockRestore()
1302+
}
1303+
1304+
expect(mockMainApiHandler.createMessage).toHaveBeenCalledOnce()
1305+
})
1306+
12531307
/**
12541308
* Test that telemetry is called for custom prompt usage
12551309
*/

src/core/condense/index.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ export type SummarizeResponse = {
224224
export type SummarizeConversationOptions = {
225225
messages: ApiMessage[]
226226
apiHandler: ApiHandler
227+
/** Optional API handler used only for the summarization request. Token counting still uses apiHandler. */
228+
condensingApiHandler?: ApiHandler
227229
systemPrompt: string
228230
taskId: string
229231
isAutomaticTrigger?: boolean
@@ -257,6 +259,7 @@ export async function summarizeConversation(options: SummarizeConversationOption
257259
const {
258260
messages,
259261
apiHandler,
262+
condensingApiHandler,
260263
systemPrompt,
261264
taskId,
262265
isAutomaticTrigger,
@@ -311,8 +314,17 @@ export async function summarizeConversation(options: SummarizeConversationOption
311314
// This is necessary because some providers (like Bedrock via LiteLLM) require the `tools` parameter
312315
// when tool blocks are present. By converting them to text, we can send the conversation for
313316
// summarization without needing to pass the tools parameter.
317+
const handlerToUse =
318+
condensingApiHandler && typeof condensingApiHandler.createMessage === "function"
319+
? condensingApiHandler
320+
: apiHandler
321+
322+
if (condensingApiHandler && handlerToUse !== condensingApiHandler) {
323+
console.warn("Selected API handler for condensing is invalid; using the current mode's API handler.")
324+
}
325+
314326
const messagesWithTextToolBlocks = transformMessagesForCondensing(
315-
maybeRemoveImageBlocks([...messagesWithToolResults, finalRequestMessage], apiHandler),
327+
maybeRemoveImageBlocks([...messagesWithToolResults, finalRequestMessage], handlerToUse),
316328
)
317329

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

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

334346
try {
335-
const stream = apiHandler.createMessage(promptToUse, requestMessages, metadata)
347+
const stream = handlerToUse.createMessage(promptToUse, requestMessages, metadata)
336348

337349
for await (const chunk of stream) {
338350
if (chunk.type === "text") {

src/core/context-management/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export type ContextManagementOptions = {
251251
contextWindow: number
252252
maxTokens?: number | null
253253
apiHandler: ApiHandler
254+
condensingApiHandler?: ApiHandler
254255
autoCondenseContext: boolean
255256
autoCondenseContextPercent: number
256257
systemPrompt: string
@@ -294,6 +295,7 @@ export async function manageContext({
294295
contextWindow,
295296
maxTokens,
296297
apiHandler,
298+
condensingApiHandler,
297299
autoCondenseContext,
298300
autoCondenseContextPercent,
299301
systemPrompt,
@@ -361,6 +363,7 @@ export async function manageContext({
361363
const result = await summarizeConversation({
362364
messages,
363365
apiHandler,
366+
condensingApiHandler,
364367
systemPrompt,
365368
taskId,
366369
isAutomaticTrigger: true,

src/core/task/Task.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,37 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15721572
}
15731573
}
15741574

1575+
private async getCondensingApiHandler(
1576+
state:
1577+
| {
1578+
condensingApiConfigOverride?: boolean
1579+
condensingApiConfigId?: string
1580+
listApiConfigMeta?: Array<{ id: string }>
1581+
}
1582+
| undefined,
1583+
): Promise<ApiHandler | undefined> {
1584+
const configId = state?.condensingApiConfigId
1585+
if (!state?.condensingApiConfigOverride || !configId) {
1586+
return undefined
1587+
}
1588+
1589+
if (!state.listApiConfigMeta?.some((config) => config.id === configId)) {
1590+
console.warn(`[Task] Context condensing profile "${configId}" no longer exists; using the current mode.`)
1591+
return undefined
1592+
}
1593+
1594+
try {
1595+
const profile = await this.providerRef.deref()?.providerSettingsManager.getProfile({ id: configId })
1596+
return profile?.apiProvider ? buildApiHandler(profile) : undefined
1597+
} catch (error) {
1598+
console.warn(
1599+
`[Task] Failed to load context condensing profile "${configId}"; using the current mode.`,
1600+
error,
1601+
)
1602+
return undefined
1603+
}
1604+
}
1605+
15751606
public async condenseContext(): Promise<void> {
15761607
// CRITICAL: Flush any pending tool results before condensing
15771608
// to ensure tool_use/tool_result pairs are complete in history
@@ -1583,6 +1614,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15831614
const state = await this.providerRef.deref()?.getState()
15841615
const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
15851616
const { mode, apiConfiguration } = state ?? {}
1617+
const condensingApiHandler = await this.getCondensingApiHandler(state)
15861618

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

@@ -1638,6 +1670,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
16381670
} = await summarizeConversation({
16391671
messages: this.apiConversationHistory,
16401672
apiHandler: this.api,
1673+
condensingApiHandler,
16411674
systemPrompt,
16421675
taskId: this.taskId,
16431676
isAutomaticTrigger: false,
@@ -3802,6 +3835,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
38023835
private async handleContextWindowExceededError(): Promise<void> {
38033836
const state = await this.providerRef.deref()?.getState()
38043837
const { profileThresholds = {}, mode, apiConfiguration } = state ?? {}
3838+
const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
3839+
const condensingApiHandler = await this.getCondensingApiHandler(state)
38053840

38063841
const { contextTokens } = this.getTokenUsage()
38073842
const modelInfo = this.api.getModel().info
@@ -3876,10 +3911,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
38763911
maxTokens,
38773912
contextWindow,
38783913
apiHandler: this.api,
3914+
condensingApiHandler,
38793915
autoCondenseContext: true,
38803916
autoCondenseContextPercent: FORCED_CONTEXT_REDUCTION_PERCENT,
38813917
systemPrompt: await this.getSystemPrompt(),
38823918
taskId: this.taskId,
3919+
customCondensingPrompt,
38833920
profileThresholds,
38843921
currentProfileId,
38853922
metadata,
@@ -4042,11 +4079,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
40424079
lastMessageTokens,
40434080
useAvailableInputForContextPercent,
40444081
})
4082+
let condensingApiHandler: ApiHandler | undefined
40454083

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

0 commit comments

Comments
 (0)