From 495a8d33cdf2eef6580bc7c43e8feda55de62639 Mon Sep 17 00:00:00 2001 From: DesertSpringsCivil Date: Fri, 3 Apr 2026 15:47:13 -0600 Subject: [PATCH] feat: Reduce token usage in ifcchat and default to IFC4X3 - Add Anthropic prompt caching (cache_control on system prompt and tools) to reduce repeated token costs by ~90% - Truncate large tool results in conversation history (2000 char cap) to prevent context bloat from ifc_tree/ifc_select responses - Add sliding window (40 messages) on conversation history, trimming at user message boundaries to avoid breaking tool-call sequences - Default "New IFC" button to IFC4X3 schema instead of IFC4 - Constrain ifc_new schema parameter with enum to prevent invalid schema strings like "IFC4X3ADD2" Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ifcchat/api_anthropic.js | 11 +++++++++-- src/ifcchat/app.js | 33 +++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/ifcchat/api_anthropic.js b/src/ifcchat/api_anthropic.js index 0359cde936..6403317154 100644 --- a/src/ifcchat/api_anthropic.js +++ b/src/ifcchat/api_anthropic.js @@ -146,15 +146,22 @@ function toChatCompletionResponse(response) { export async function chat({ apiKey, model, messages, tools }) { const request = splitSystemAndMessages(messages); + const anthropicTools = toAnthropicTools(tools); + + // Mark the last tool with cache_control so the entire tool list is cached + if (anthropicTools.length > 0) { + anthropicTools[anthropicTools.length - 1].cache_control = { type: "ephemeral" }; + } + const body = { model, max_tokens: 4096, messages: request.messages, - tools: toAnthropicTools(tools), + tools: anthropicTools, }; if (request.system) { - body.system = request.system; + body.system = [{ type: "text", text: request.system, cache_control: { type: "ephemeral" } }]; } const res = await fetch("https://api.anthropic.com/v1/messages", { diff --git a/src/ifcchat/app.js b/src/ifcchat/app.js index b268bf8256..a3b340bc97 100644 --- a/src/ifcchat/app.js +++ b/src/ifcchat/app.js @@ -418,8 +418,8 @@ function callWorker(type, payload = {}) { // ---- Tool schemas (should match ifcmcp.core openai_tools()) ---- const tools = [ { - type: "function", function: { name: "ifc_new", description: "Create a new empty IFC model in memory.", - parameters: { type: "object", properties: { schema: { type: "string" } }, required: [], additionalProperties: false } } + type: "function", function: { name: "ifc_new", description: "Create a new empty IFC model in memory. Valid schemas: IFC4, IFC2X3, IFC4X3 (for IFC 4.3).", + parameters: { type: "object", properties: { schema: { type: "string", enum: ["IFC4", "IFC2X3", "IFC4X3"] } }, required: [], additionalProperties: false } } }, { type: "function", function: { name: "ifc_summary", description: "Get a concise overview of the loaded IFC model.", @@ -497,6 +497,27 @@ Be concise. Avoid dumping huge trees unless asked. let messages = []; // running conversation state (Chat Completions style) +const MAX_TOOL_RESULT_CHARS = 2000; +const MAX_HISTORY_MESSAGES = 40; + +function truncateToolResult(text) { + if (text.length <= MAX_TOOL_RESULT_CHARS) return text; + return text.slice(0, MAX_TOOL_RESULT_CHARS) + "\n... (truncated)"; +} + +function trimHistory() { + if (messages.length <= MAX_HISTORY_MESSAGES) return; + // Find a safe cut point — don't break mid-tool-call sequence. + // Walk forward from the trim target to find a user message boundary. + let cut = messages.length - MAX_HISTORY_MESSAGES; + while (cut < messages.length && messages[cut].role !== "user") { + cut++; + } + if (cut > 0 && cut < messages.length) { + messages.splice(0, cut); + } +} + async function runAgentTurn(userText) { const apiKey = apiKeyEl.value.trim(); if (!apiKey) throw new Error("Missing API key"); @@ -506,6 +527,7 @@ async function runAgentTurn(userText) { const baseURL = provider.baseUrlDefault ? baseUrlEl.value.trim() : undefined; messages.push({ role: "user", content: userText }); + trimHistory(); for (let i = 0; i < 64; i++) { const response = await chat({ @@ -535,12 +557,15 @@ async function runAgentTurn(userText) { const toolRes = await callWorker("toolCall", { name: call.function.name, args }); + const fullResult = JSON.stringify(toolRes.result); + messages.push({ role: "tool", tool_call_id: call.id, - content: JSON.stringify(toolRes.result), + content: truncateToolResult(fullResult), }); + // Show full result in UI, but only truncated version goes to the LLM addMessage("tool", `← ${call.function.name}: ${JSON.stringify(toolRes.result, null, 2)}`); } } @@ -588,7 +613,7 @@ ifcFileEl.onchange = async () => { newBtn.onclick = async () => { try { setBusy(true, "Creating new model…"); - const r = await callWorker("toolCall", { name: "ifc_new", args: { schema: "IFC4" } }); + const r = await callWorker("toolCall", { name: "ifc_new", args: { schema: "IFC4X3" } }); addMessage("assistant", `New model: ${JSON.stringify(r.result)}`); setBusy(false, "Ready"); } catch (e) {