Skip to content

Commit 29c5d94

Browse files
committed
fix: Claude model compatibility for Antigravity proxy
- Add textPartModels config: configurable model patterns that use text parts instead of synthetic tool parts for context injection, preventing Claude VALIDATED mode tool_use/tool_result pairing errors (default: ['antigravity-claude']) - Fix pruneFullTool: replace edit/write tool part content with placeholders instead of removing parts entirely, preserving tool pairing integrity required by Claude's VALIDATED mode - Fix distill/prune Invalid IDs: rebuild toolIdList after syncToolCache in executePruneOperation to prevent stale/empty ID list after session reinitialization
1 parent 9aa2692 commit 29c5d94

5 files changed

Lines changed: 51 additions & 23 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ DCP uses its own config file:
110110
> "contextLimit": 100000,
111111
> // Additional tools to protect from pruning
112112
> "protectedTools": [],
113+
> // Model name patterns that should use text parts instead of tool parts
114+
> // for DCP context injection. Prevents 400 errors with providers that use
115+
> // strict tool call/result pairing (e.g., Antigravity Claude models).
116+
> // Uses case-insensitive substring matching against the model ID.
117+
> "textPartModels": ["antigravity-claude"],
113118
> },
114119
> // Distills key findings into preserved knowledge before removing raw content
115120
> "distill": {

lib/config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface ToolSettings {
2828
nudgeFrequency: number
2929
protectedTools: string[]
3030
contextLimit: number | `${number}%`
31+
textPartModels: string[]
3132
}
3233

3334
export interface Tools {
@@ -107,6 +108,7 @@ export const VALID_CONFIG_KEYS = new Set([
107108
"tools.settings.nudgeFrequency",
108109
"tools.settings.protectedTools",
109110
"tools.settings.contextLimit",
111+
"tools.settings.textPartModels",
110112
"tools.distill",
111113
"tools.distill.permission",
112114
"tools.distill.showDistillation",
@@ -303,6 +305,16 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
303305
})
304306
}
305307
}
308+
if (
309+
tools.settings.textPartModels !== undefined &&
310+
!Array.isArray(tools.settings.textPartModels)
311+
) {
312+
errors.push({
313+
key: "tools.settings.textPartModels",
314+
expected: "string[]",
315+
actual: typeof tools.settings.textPartModels,
316+
})
317+
}
306318
}
307319
if (tools.distill) {
308320
if (tools.distill.permission !== undefined) {
@@ -505,6 +517,7 @@ const defaultConfig: PluginConfig = {
505517
nudgeFrequency: 10,
506518
protectedTools: [...DEFAULT_PROTECTED_TOOLS],
507519
contextLimit: 100000,
520+
textPartModels: ["antigravity-claude"],
508521
},
509522
distill: {
510523
permission: "allow",
@@ -684,6 +697,7 @@ function mergeTools(
684697
]),
685698
],
686699
contextLimit: override.settings?.contextLimit ?? base.settings.contextLimit,
700+
textPartModels: override.settings?.textPartModels ?? base.settings.textPartModels,
687701
},
688702
distill: {
689703
permission: override.distill?.permission ?? base.distill.permission,
@@ -724,6 +738,7 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
724738
settings: {
725739
...config.tools.settings,
726740
protectedTools: [...config.tools.settings.protectedTools],
741+
textPartModels: [...config.tools.settings.textPartModels],
727742
},
728743
distill: { ...config.tools.distill },
729744
compress: { ...config.tools.compress },

lib/messages/inject.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,19 @@ export const insertPruneToolContext = (
253253

254254
// When following a user message, append a synthetic text part since models like Claude
255255
// expect assistant turns to start with reasoning parts which cannot be easily faked.
256+
// For models listed in textPartModels, always use text parts to avoid tool pairing issues.
256257
// For all other cases, append a synthetic tool part to the last message which works
257258
// across all models without disrupting their behavior.
258-
if (lastNonIgnoredMessage.info.role === "user") {
259+
const modelID = userInfo.model?.modelID || ""
260+
const lowerModelID = modelID.toLowerCase()
261+
const useTextPart = config.tools.settings.textPartModels.some(
262+
(pattern) => lowerModelID.includes(pattern.toLowerCase()),
263+
)
264+
265+
if (lastNonIgnoredMessage.info.role === "user" || useTextPart) {
259266
const textPart = createSyntheticTextPart(lastNonIgnoredMessage, combinedContent)
260267
lastNonIgnoredMessage.parts.push(textPart)
261268
} else {
262-
const modelID = userInfo.model?.modelID || ""
263269
const toolPart = createSyntheticToolPart(lastNonIgnoredMessage, combinedContent, modelID)
264270
lastNonIgnoredMessage.parts.push(toolPart)
265271
}

lib/messages/prune.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,14 @@ export const prune = (
2424
}
2525

2626
const pruneFullTool = (state: SessionState, logger: Logger, messages: WithParts[]): void => {
27-
const messagesToRemove: string[] = []
27+
let prunedCount = 0
2828

2929
for (const msg of messages) {
3030
if (isMessageCompacted(state, msg)) {
3131
continue
3232
}
3333

3434
const parts = Array.isArray(msg.parts) ? msg.parts : []
35-
const partsToRemove: string[] = []
3635

3736
for (const part of parts) {
3837
if (part.type !== "tool") {
@@ -45,26 +44,27 @@ const pruneFullTool = (state: SessionState, logger: Logger, messages: WithParts[
4544
continue
4645
}
4746

48-
partsToRemove.push(part.callID)
49-
}
50-
51-
if (partsToRemove.length === 0) {
52-
continue
53-
}
54-
55-
msg.parts = parts.filter(
56-
(part) => part.type !== "tool" || !partsToRemove.includes(part.callID),
57-
)
47+
// Instead of removing the tool part entirely (which breaks Claude's
48+
// tool_use/tool_result pairing in VALIDATED mode), replace the content
49+
// with a placeholder. This preserves the tool part structure so the
50+
// model-level conversion still generates matched functionCall/functionResponse pairs.
51+
if (part.state?.input && typeof part.state.input === "object") {
52+
for (const key of Object.keys(part.state.input)) {
53+
if (typeof part.state.input[key] === "string") {
54+
part.state.input[key] = PRUNED_TOOL_ERROR_INPUT_REPLACEMENT
55+
}
56+
}
57+
}
58+
if (part.state?.status === "completed") {
59+
part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT
60+
}
5861

59-
if (msg.parts.length === 0) {
60-
messagesToRemove.push(msg.info.id)
62+
prunedCount++
6163
}
6264
}
6365

64-
if (messagesToRemove.length > 0) {
65-
const result = messages.filter((msg) => !messagesToRemove.includes(msg.info.id))
66-
messages.length = 0
67-
messages.push(...result)
66+
if (prunedCount > 0) {
67+
logger.info(`Pruned content for ${prunedCount} edit/write tool parts`)
6868
}
6969
}
7070

lib/tools/prune-shared.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ensureSessionInitialized } from "../state"
99
import { saveSessionState } from "../state/persistence"
1010
import { calculateTokensSaved, getCurrentParams } from "../strategies/utils"
1111
import { getFilePathsFromParameters, isProtected } from "../protected-file-patterns"
12+
import { buildToolIdList } from "../messages/utils"
1213

1314
// Shared logic for executing prune operations.
1415
export async function executePruneOperation(
@@ -47,10 +48,11 @@ export async function executePruneOperation(
4748
})
4849
const messages: WithParts[] = messagesResponse.data || messagesResponse
4950

50-
await ensureSessionInitialized(ctx.client, state, sessionId, logger, messages)
51-
await syncToolCache(state, config, logger, messages)
51+
await ensureSessionInitialized(ctx.client, state, sessionId, logger, messages)
52+
await syncToolCache(state, config, logger, messages)
53+
buildToolIdList(state, messages, logger)
5254

53-
const currentParams = getCurrentParams(state, messages, logger)
55+
const currentParams = getCurrentParams(state, messages, logger)
5456

5557
const toolIdList = state.toolIdList
5658

0 commit comments

Comments
 (0)