diff --git a/package-lock.json b/package-lock.json index fcc4d6f..f15f2dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,28 @@ { "name": "claude-dev", - "version": "1.4.11", + "version": "1.4.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "1.4.11", + "version": "1.4.12", "license": "MIT", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/tokenizer": "^0.0.4", "@kodu-ai/cloud-api": "^1.0.1", + "@types/clone-deep": "^4.0.4", "@vscode/codicons": "^0.0.36", "axios": "^1.7.4", + "clone-deep": "^4.0.1", "default-shell": "^2.2.0", "delay": "^6.0.0", "diff": "^5.2.0", "execa": "^9.3.0", "globby": "^14.0.2", + "image-size": "^1.1.1", "openai": "^4.54.0", "os-name": "^6.0.0", "p-wait-for": "^5.0.2", @@ -4528,6 +4531,11 @@ "https://trpc.io/sponsor" ] }, + "node_modules/@types/clone-deep": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/clone-deep/-/clone-deep-4.0.4.tgz", + "integrity": "sha512-vXh6JuuaAha6sqEbJueYdh5zNBPPgG1OYumuz2UvLvriN6ABHDSW8ludREGWJb1MLIzbwZn4q4zUbUCerJTJfA==" + }, "node_modules/@types/diff": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz", @@ -5401,6 +5409,19 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6844,6 +6865,20 @@ "node": ">= 4" } }, + "node_modules/image-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", + "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -6894,7 +6929,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -7142,6 +7176,17 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -7285,6 +7330,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -7420,6 +7473,14 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -8642,6 +8703,14 @@ "node": ">=6" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9048,6 +9117,17 @@ "dev": true, "license": "MIT" }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 3c5a378..4f262b0 100644 --- a/package.json +++ b/package.json @@ -136,13 +136,16 @@ "@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/tokenizer": "^0.0.4", "@kodu-ai/cloud-api": "^1.0.1", + "@types/clone-deep": "^4.0.4", "@vscode/codicons": "^0.0.36", "axios": "^1.7.4", + "clone-deep": "^4.0.1", "default-shell": "^2.2.0", "delay": "^6.0.0", "diff": "^5.2.0", "execa": "^9.3.0", "globby": "^14.0.2", + "image-size": "^1.1.1", "openai": "^4.54.0", "os-name": "^6.0.0", "p-wait-for": "^5.0.2", diff --git a/src/utils/context-management.ts b/src/utils/context-management.ts index c2cf273..71aa5ba 100644 --- a/src/utils/context-management.ts +++ b/src/utils/context-management.ts @@ -1,5 +1,8 @@ import { Anthropic } from "@anthropic-ai/sdk" import { countTokens } from "@anthropic-ai/tokenizer" +import { Buffer } from "buffer" +import sizeOf from "image-size" +import cloneDeep from "clone-deep" export function slidingWindowContextManagement( contextWindow: number, @@ -18,28 +21,31 @@ export function slidingWindowContextManagement( } // If over limit, remove messages starting from the third message onwards (task and claude's step-by-step thought process are important to keep in context) - const newMessages = [...messages] + const newMessages = cloneDeep(messages) // since we're manipulating nested objects and arrays, need to deep clone to prevent mutating original history let index = 2 while (totalMessageTokens > availableTokens && index < newMessages.length) { const messageToEmpty = newMessages[index] const originalTokens = countMessageTokens(messageToEmpty) // Empty the content of the message (messages must be in a specific order so we can't just remove) if (typeof messageToEmpty.content === "string") { - messageToEmpty.content = "" + messageToEmpty.content = "(truncated due to context limits)" } else if (Array.isArray(messageToEmpty.content)) { messageToEmpty.content = messageToEmpty.content.map((item) => { if (typeof item === "string") { return { type: "text", - text: "(truncated due to context window)", + text: "(truncated due to context limits)", } as Anthropic.Messages.TextBlockParam } else if (item.type === "text") { return { type: "text", - text: "(truncated due to context window)", + text: "(truncated due to context limits)", } as Anthropic.Messages.TextBlockParam } else if (item.type === "image") { - return { ...item, source: { type: "base64", data: "" } } as Anthropic.Messages.ImageBlockParam + return { + type: "text", + text: "(image removed due to context limits)", + } as Anthropic.Messages.TextBlockParam } else if (item.type === "tool_use") { return { ...item, input: {} } as Anthropic.Messages.ToolUseBlockParam } else if (item.type === "tool_result") { @@ -48,9 +54,9 @@ export function slidingWindowContextManagement( content: Array.isArray(item.content) ? item.content.map((contentItem) => contentItem.type === "text" - ? { ...contentItem, text: "(truncated due to context window)" } + ? { type: "text", text: "(truncated due to context limits)" } : contentItem.type === "image" - ? { ...contentItem, source: { type: "base64", data: "" } } + ? { type: "text", text: "(image removed due to context limits)" } : contentItem ) : "", @@ -69,7 +75,50 @@ export function slidingWindowContextManagement( function countMessageTokens(message: Anthropic.Messages.MessageParam): number { if (typeof message.content === "string") { return countTokens(message.content) + } else if (Array.isArray(message.content)) { + return message.content.reduce((sum, item) => { + if (typeof item === "string") { + return sum + countTokens(item) + } else if (item.type === "text") { + return sum + countTokens(item.text) + } else if (item.type === "image") { + return sum + estimateImageTokens(item.source.data) + } else if (item.type === "tool_use") { + return sum + countTokens(JSON.stringify(item.input)) + } else if (item.type === "tool_result") { + if (Array.isArray(item.content)) { + return ( + sum + + item.content.reduce((contentSum, contentItem) => { + if (contentItem.type === "text") { + return contentSum + countTokens(contentItem.text) + } else if (contentItem.type === "image") { + return contentSum + estimateImageTokens(contentItem.source.data) + } + return contentSum + countTokens(JSON.stringify(contentItem)) + }, 0) + ) + } else { + return sum + countTokens(item.content || "") + } + } else { + return sum + countTokens(JSON.stringify(item)) + } + }, 0) } else { return countTokens(JSON.stringify(message.content)) } } + +function estimateImageTokens(base64: string): number { + const base64Data = base64.split(";base64,").pop() + if (base64Data) { + const buffer = Buffer.from(base64Data, "base64") + const dimensions = sizeOf(buffer) + if (dimensions.width && dimensions.height) { + // "you can estimate the number of tokens used through this algorithm: tokens = (width px * height px)/750" + return Math.ceil((dimensions.width * dimensions.height) / 750) + } + } + return countTokens(base64) +}