fix(claude): decloak tool names when toolNameMap misses (#4342)

- Add stripCloakSuffix fallback in decloakToolNames and decloakStreamChunk
- Prevent client errors when toolNameMap is missing or lost on retry
This commit is contained in:
Gesya Gayatree Solih
2026-09-26 17:28:27 +07:00
parent 6aea3875ef
commit b65d2d0a6a

View File

@@ -104,14 +104,29 @@ export function cloakClaudeTools(body) {
};
}
// Strip a trailing CLAUDE_TOOL_SUFFIX from a cloaked name as a last-resort
// fallback when the name isn't in toolNameMap (e.g. map lost across a retry/
// reconnect). Never strips decoy names — those are meant to reach the client
// unresolved so it can see "tool unavailable" instead of silently no-oping.
function stripCloakSuffix(name) {
if (typeof name !== "string" || !name.endsWith(CLAUDE_TOOL_SUFFIX)) return null;
if (CC_DEFAULT_TOOLS.has(name)) return null;
const original = name.slice(0, -CLAUDE_TOOL_SUFFIX.length);
return original.length > 0 ? original : null;
}
// Decloak tool_use names in non-streaming Claude response body (INPUT side)
export function decloakToolNames(body, toolNameMap) {
if (!toolNameMap?.size || !Array.isArray(body?.content)) return body;
if (!Array.isArray(body?.content)) return body;
const content = body.content.map(block => {
if (block?.type === "tool_use" && toolNameMap.has(block.name)) {
if (block?.type !== "tool_use") return block;
if (toolNameMap?.has(block.name)) {
return { ...block, name: toolNameMap.get(block.name) };
}
return block;
// toolNameMap missing/stale for this name — fall back to suffix stripping
// rather than forwarding an unresolvable "<tool>_ide" name to the client.
const fallback = stripCloakSuffix(block.name);
return fallback ? { ...block, name: fallback } : block;
});
return { ...body, content };
}
@@ -126,19 +141,21 @@ export function decloakToolNames(body, toolNameMap) {
* name appears exactly once per call — on the content_block_start event of
* a tool_use block; argument deltas carry no name.
*
* Unknown names (e.g. a CC decoy tool the model called anyway) pass through
* unchanged, matching the non-streaming decloak behavior.
* Falls back to stripping the literal CLAUDE_TOOL_SUFFIX when the name isn't
* in toolNameMap (map lost across a retry/reconnect), matching the
* non-streaming decloak behavior. Decoy tool names (real CC tool names) and
* anything else pass through unchanged.
*
* @param {object|null} chunk - Parsed SSE event (may be null on stream flush)
* @param {Map|null} toolNameMap - Suffixed → original name map from cloakClaudeTools()
* @returns {object|null} The chunk, with the tool_use name restored when cloaked
*/
export function decloakStreamChunk(chunk, toolNameMap) {
if (!toolNameMap?.size || !chunk || typeof chunk !== "object") return chunk;
if (!chunk || typeof chunk !== "object") return chunk;
if (chunk.type !== "content_block_start") return chunk;
const block = chunk.content_block;
if (block?.type !== "tool_use" || typeof block.name !== "string") return chunk;
const original = toolNameMap.get(block.name);
const original = toolNameMap?.get(block.name) || stripCloakSuffix(block.name);
if (!original) return chunk;
return { ...chunk, content_block: { ...block, name: original } };
}