mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-11 06:18:09 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -146,15 +146,22 @@ function toChatCompletionResponse(response) {
|
|||||||
|
|
||||||
export async function chat({ apiKey, model, messages, tools }) {
|
export async function chat({ apiKey, model, messages, tools }) {
|
||||||
const request = splitSystemAndMessages(messages);
|
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 = {
|
const body = {
|
||||||
model,
|
model,
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
messages: request.messages,
|
messages: request.messages,
|
||||||
tools: toAnthropicTools(tools),
|
tools: anthropicTools,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (request.system) {
|
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", {
|
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
||||||
|
|||||||
+29
-4
@@ -418,8 +418,8 @@ function callWorker(type, payload = {}) {
|
|||||||
// ---- Tool schemas (should match ifcmcp.core openai_tools()) ----
|
// ---- Tool schemas (should match ifcmcp.core openai_tools()) ----
|
||||||
const tools = [
|
const tools = [
|
||||||
{
|
{
|
||||||
type: "function", function: { name: "ifc_new", description: "Create a new empty IFC model in memory.",
|
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" } }, required: [], additionalProperties: false } }
|
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.",
|
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)
|
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) {
|
async function runAgentTurn(userText) {
|
||||||
const apiKey = apiKeyEl.value.trim();
|
const apiKey = apiKeyEl.value.trim();
|
||||||
if (!apiKey) throw new Error("Missing API key");
|
if (!apiKey) throw new Error("Missing API key");
|
||||||
@@ -506,6 +527,7 @@ async function runAgentTurn(userText) {
|
|||||||
const baseURL = provider.baseUrlDefault ? baseUrlEl.value.trim() : undefined;
|
const baseURL = provider.baseUrlDefault ? baseUrlEl.value.trim() : undefined;
|
||||||
|
|
||||||
messages.push({ role: "user", content: userText });
|
messages.push({ role: "user", content: userText });
|
||||||
|
trimHistory();
|
||||||
|
|
||||||
for (let i = 0; i < 64; i++) {
|
for (let i = 0; i < 64; i++) {
|
||||||
const response = await chat({
|
const response = await chat({
|
||||||
@@ -535,12 +557,15 @@ async function runAgentTurn(userText) {
|
|||||||
|
|
||||||
const toolRes = await callWorker("toolCall", { name: call.function.name, args });
|
const toolRes = await callWorker("toolCall", { name: call.function.name, args });
|
||||||
|
|
||||||
|
const fullResult = JSON.stringify(toolRes.result);
|
||||||
|
|
||||||
messages.push({
|
messages.push({
|
||||||
role: "tool",
|
role: "tool",
|
||||||
tool_call_id: call.id,
|
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)}`);
|
addMessage("tool", `← ${call.function.name}: ${JSON.stringify(toolRes.result, null, 2)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -588,7 +613,7 @@ ifcFileEl.onchange = async () => {
|
|||||||
newBtn.onclick = async () => {
|
newBtn.onclick = async () => {
|
||||||
try {
|
try {
|
||||||
setBusy(true, "Creating new model…");
|
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)}`);
|
addMessage("assistant", `New model: ${JSON.stringify(r.result)}`);
|
||||||
setBusy(false, "Ready");
|
setBusy(false, "Ready");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user