fix: Claude model compatibility for Antigravity proxy#365
fix: Claude model compatibility for Antigravity proxy#365anilcancakir wants to merge 1 commit intoOpencode-DCP:masterfrom
Conversation
- 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
There was a problem hiding this comment.
Pull request overview
This PR improves compatibility with Claude “VALIDATED” tool semantics by adjusting how DCP injects context and how full-tool pruning preserves tool part structure, while also fixing stale tool ID handling during prune operations.
Changes:
- Add
tools.settings.textPartModelsto force context injection via text parts for specific model patterns. - Update full-tool pruning to preserve tool parts (replace content with placeholders) instead of removing parts, preventing tool pairing errors.
- Rebuild
state.toolIdListafter session (re)initialization + tool cache sync to avoid invalid/stale numeric IDs.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/tools/prune-shared.ts | Rebuilds tool ID list during prune execution after cache sync. |
| lib/messages/prune.ts | Changes full-tool pruning to keep tool parts and replace content with placeholders. |
| lib/messages/inject.ts | Adds model-pattern-based switch to inject context via text parts. |
| lib/config.ts | Adds/merges/validates new textPartModels config option with defaults. |
| README.md | Documents the new configuration option in the default config example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| await ensureSessionInitialized(ctx.client, state, sessionId, logger, messages) | ||
| await syncToolCache(state, config, logger, messages) | ||
| buildToolIdList(state, messages, logger) | ||
|
|
||
| const currentParams = getCurrentParams(state, messages, logger) | ||
| const currentParams = getCurrentParams(state, messages, logger) |
There was a problem hiding this comment.
The new lines are indented inconsistently (extra leading space before await/buildToolIdList), which will likely fail linting/formatting checks and makes the block harder to read. Align indentation with the surrounding code (4 spaces).
| if (part.state?.input && typeof part.state.input === "object") { | ||
| for (const key of Object.keys(part.state.input)) { | ||
| if (typeof part.state.input[key] === "string") { | ||
| part.state.input[key] = PRUNED_TOOL_ERROR_INPUT_REPLACEMENT | ||
| } |
There was a problem hiding this comment.
pruneFullTool replaces successful edit/write tool inputs with PRUNED_TOOL_ERROR_INPUT_REPLACEMENT ("failed tool call"), which is misleading when the tool status is completed. Use a neutral replacement string for successful tool inputs (and reserve the error-specific placeholder for errored tool calls).
| if (part.state?.status === "completed") { | ||
| part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT |
There was a problem hiding this comment.
For pruned edit/write tool parts, only status === "completed" outputs are replaced; for status === "error", part.state.error (and any output) are left intact. Previously this code removed the entire tool part, so this change can leave large error payloads in context and reduce token savings. Consider also replacing state.error/state.output with placeholders for non-completed statuses when the tool is being fully pruned.
| if (part.state?.status === "completed") { | |
| part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT | |
| if (part.state) { | |
| // When fully pruning a tool call, scrub outputs and errors regardless of status | |
| if ("output" in part.state) { | |
| part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT | |
| } | |
| if ("error" in part.state) { | |
| part.state.error = PRUNED_TOOL_OUTPUT_REPLACEMENT | |
| } |
| if ( | ||
| tools.settings.textPartModels !== undefined && | ||
| !Array.isArray(tools.settings.textPartModels) | ||
| ) { | ||
| errors.push({ | ||
| key: "tools.settings.textPartModels", | ||
| expected: "string[]", | ||
| actual: typeof tools.settings.textPartModels, | ||
| }) |
There was a problem hiding this comment.
textPartModels is only validated as an array, but insertPruneToolContext calls pattern.toLowerCase(). If the config contains non-string entries, this will throw at runtime. Extend validation to ensure every element in tools.settings.textPartModels is a string (or coerce/filter invalid entries).
| if ( | |
| tools.settings.textPartModels !== undefined && | |
| !Array.isArray(tools.settings.textPartModels) | |
| ) { | |
| errors.push({ | |
| key: "tools.settings.textPartModels", | |
| expected: "string[]", | |
| actual: typeof tools.settings.textPartModels, | |
| }) | |
| if (tools.settings.textPartModels !== undefined) { | |
| if (!Array.isArray(tools.settings.textPartModels)) { | |
| errors.push({ | |
| key: "tools.settings.textPartModels", | |
| expected: "string[]", | |
| actual: typeof tools.settings.textPartModels, | |
| }) | |
| } else if ( | |
| !tools.settings.textPartModels.every( | |
| (model) => typeof model === "string" | |
| ) | |
| ) { | |
| errors.push({ | |
| key: "tools.settings.textPartModels", | |
| expected: "string[]", | |
| actual: JSON.stringify(tools.settings.textPartModels), | |
| }) | |
| } |
| const modelID = userInfo.model?.modelID || "" | ||
| const lowerModelID = modelID.toLowerCase() | ||
| const useTextPart = config.tools.settings.textPartModels.some( | ||
| (pattern) => lowerModelID.includes(pattern.toLowerCase()), |
There was a problem hiding this comment.
pattern.toLowerCase() assumes every entry in config.tools.settings.textPartModels is a string. Even with config validation, it would be safer to guard here (e.g., skip non-string patterns) to avoid a runtime crash on malformed configs.
| (pattern) => lowerModelID.includes(pattern.toLowerCase()), | |
| (pattern) => | |
| typeof pattern === "string" && | |
| lowerModelID.includes(pattern.toLowerCase()), |
| > // Model name patterns that should use text parts instead of tool parts | ||
| > // for DCP context injection. Prevents 400 errors with providers that use | ||
| > // strict tool call/result pairing (e.g., Antigravity Claude models). | ||
| > // Uses case-insensitive substring matching against the model ID. | ||
| > "textPartModels": ["antigravity-claude"], |
There was a problem hiding this comment.
The README documents the new tools.settings.textPartModels option, but the referenced JSON schema (dcp.schema.json) does not currently include this property. Update the schema to keep editor validation/autocomplete in sync with the documented config.
| > // Model name patterns that should use text parts instead of tool parts | |
| > // for DCP context injection. Prevents 400 errors with providers that use | |
| > // strict tool call/result pairing (e.g., Antigravity Claude models). | |
| > // Uses case-insensitive substring matching against the model ID. | |
| > "textPartModels": ["antigravity-claude"], |
|
This is interesting, I'm a bit busy this weekend so I might need some patience on getting this reviewed, but I have a couple questions.
I haven't looked at the code yet I'll do a more thorough review later, but thank you very much for helping! |
|
I did end up adding buildToolIDList to prune-shared as your bot suggested in 20bae81, but for different reasons. Still don't understand 1 and 2, can you check my questions above? |
|
I also encountered the situation described in the PR when using claude-opus-4.5 provided by antig |
|
Which antigravity auth method do you use? The current architecture in DCP works with every other provider and model as far as I know, including opus 4.5 through claude sub, api, and antigravity using https://github.com/Mirrowel/LLM-API-Key-Proxy. I'm definitely not changing the entire injection architecture for this provider because they do something different than everyone else, and I don't even want to add custom settings and flaky string matching that require maintenance. Can you raise an issue in whatever auth method is causing this instead? |
|
CPA https://github.com/router-for-me/CLIProxyAPI I'm not very familiar with the details of how to implement a reverse proxy However, CPA has been very stable for me in other scenarios, with only the issue mentioned in the PR. I'm not entirely sure where the actual problem lies Whether to fix it and how to fix it is, of course, up to you to decide |
|
Clearly must be due to incomplete implementations, cliproxyapi or others |
|
Yea my problem is this PR isn't good enough due to the reasons above, and I don't want to start adding edge case code that will require maintenance for 1 auth service. I also don't think it's this plugins responsibility to fix auth service issues when only one is exerperiencing these issues, and there is at least 2 other third party providers for antigravity that work fine... |
Could I ask which third-party providers for antigravity are working fine? |
|
The two i've tried that work fine are https://github.com/NoeFabris/opencode-antigravity-auth and https://github.com/Mirrowel/LLM-API-Key-Proxy |
When using CliProxyAPI, my requests trigger the following error: {"error":{"code":400,"message":"Request contains an invalid argument.","status":"INVALID_ARGUMENT"}}. I switched to opencode-antigravity-auth to test it, but the error persists. |
|



This PR fixes three bugs that cause
400 Invalid RequestandInvalid IDs providederrors when using DCP with Claude models through the Antigravity proxy.Fix 1: Context Injection Tool Pairing (
inject.ts+config.ts)Problem: DCP injected context information into the last message as a synthetic tool part. Claude's VALIDATED mode requires every
tool_useblock to have a matchingtool_resultblock — since the synthetic tool part has notool_result, the API returns a 400 error.Solution: Added
textPartModelsconfig setting. When the model ID matches any of these patterns (default:["antigravity-claude"]), DCP injects a text part instead of a tool part, which doesn't require tool pairing.Changed files:
lib/config.ts—textPartModelssetting, validation, defaults, merge logiclib/messages/inject.ts— Model detection + text/tool part routingREADME.md— Config documentationFix 2: pruneFullTool Tool Pairing (
prune.ts)Problem:
pruneFullToolwas completely removing edit/write tool parts from messages. In Claude's VALIDATED mode, the remainingfunctionCallblocks had no matchingfunctionResponse, causing a 400 error. (Fix 1 alone was not sufficient — this was identified through debug logging.)Solution: Instead of removing tool parts entirely, their content is replaced with placeholders. The tool part structure is preserved, so
functionCall/functionResponsepairing remains intact. Token savings are ~95% preserved since the bulk of tokens come from file contents, not tool metadata.Changed file:
lib/messages/prune.tsFix 3: Distill/Prune Invalid IDs (
prune-shared.ts)Problem:
executePruneOperation()callsensureSessionInitialized(), which resetstoolIdListto[]on session change. ThensyncToolCache()only rebuildstoolParametersbut does not rebuildtoolIdList. The subsequent ID validation runs against an empty list, causing all numeric IDs to fail with"Invalid IDs provided".Solution: Added a
buildToolIdList()call aftersyncToolCache()inexecutePruneOperation(), following the same established pattern already used inhooks.ts.Changed file:
lib/tools/prune-shared.ts