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:
@@ -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 } };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user