mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 02:07:36 +00:00
let claude code openrouter compatibility
This commit is contained in:
committed by
Thomas Krijnen
parent
b7fc5daf82
commit
0f7f960b29
+82
-87
@@ -1,4 +1,26 @@
|
|||||||
// app.js
|
// app.js
|
||||||
|
import * as openaiApi from "./api_openai.js";
|
||||||
|
import * as openrouterApi from "./api_openrouter.js";
|
||||||
|
|
||||||
|
const PROVIDERS = {
|
||||||
|
openai: {
|
||||||
|
api: openaiApi,
|
||||||
|
models: [
|
||||||
|
{ value: "gpt-5", label: "gpt-5" },
|
||||||
|
{ value: "gpt-4.1", label: "gpt-4.1" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
openrouter: {
|
||||||
|
api: openrouterApi,
|
||||||
|
models: [
|
||||||
|
{ value: "openai/gpt-oss-20b", label: "gpt-oss-20b" },
|
||||||
|
{ value: "anthropic/claude-sonnet-4-5", label: "claude-sonnet-4-5" },
|
||||||
|
{ value: "openai/gpt-4.1", label: "gpt-4.1" },
|
||||||
|
{ value: "google/gemini-2.5-pro-preview", label: "gemini-2.5-pro" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
const statusEl = $("status");
|
const statusEl = $("status");
|
||||||
@@ -7,10 +29,19 @@ const sendBtn = $("send");
|
|||||||
const inputEl = $("input");
|
const inputEl = $("input");
|
||||||
const apiKeyEl = $("apiKey");
|
const apiKeyEl = $("apiKey");
|
||||||
const modelEl = $("model");
|
const modelEl = $("model");
|
||||||
|
const providerEl = $("provider");
|
||||||
const ifcFileEl = $("ifcFile");
|
const ifcFileEl = $("ifcFile");
|
||||||
const newBtn = $("newModel");
|
const newBtn = $("newModel");
|
||||||
const downloadBtn = $("downloadIfc");
|
const downloadBtn = $("downloadIfc");
|
||||||
|
|
||||||
|
function onProviderChange() {
|
||||||
|
const p = PROVIDERS[providerEl.value];
|
||||||
|
modelEl.innerHTML = p.models.map(m => `<option value="${m.value}">${m.label}</option>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
providerEl.addEventListener("change", onProviderChange);
|
||||||
|
onProviderChange();
|
||||||
|
|
||||||
function setBusy(isBusy, reason = "") {
|
function setBusy(isBusy, reason = "") {
|
||||||
const controls = [
|
const controls = [
|
||||||
$("send"),
|
$("send"),
|
||||||
@@ -75,74 +106,73 @@ function callWorker(type, payload = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- OpenAI Responses API tool schemas (should match ifcmcp.core openai_tools()) ----
|
// ---- Tool schemas (should match ifcmcp.core openai_tools()) ----
|
||||||
// Docs show Responses API function_call items + function_call_output loop. :contentReference[oaicite:4]{index=4}
|
|
||||||
const tools = [
|
const tools = [
|
||||||
{
|
{
|
||||||
type: "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.",
|
||||||
parameters: { type: "object", properties: { schema: { type: "string" } }, required: [], additionalProperties: false }
|
parameters: { type: "object", properties: { schema: { type: "string" } }, required: [], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "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.",
|
||||||
parameters: { type: "object", properties: {}, required: [], additionalProperties: false }
|
parameters: { type: "object", properties: {}, required: [], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_tree", description: "Get the full spatial hierarchy tree.",
|
type: "function", function: { name: "ifc_tree", description: "Get the full spatial hierarchy tree.",
|
||||||
parameters: { type: "object", properties: {}, required: [], additionalProperties: false }
|
parameters: { type: "object", properties: {}, required: [], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_select", description: "Select elements using ifcopenshell selector syntax (e.g. 'IfcWall').",
|
type: "function", function: { name: "ifc_select", description: "Select elements using ifcopenshell selector syntax (e.g. 'IfcWall').",
|
||||||
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"], additionalProperties: false }
|
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_info", description: "Inspect an entity by STEP id.",
|
type: "function", function: { name: "ifc_info", description: "Inspect an entity by STEP id.",
|
||||||
parameters: { type: "object", properties: { element_id: { type: "integer" } }, required: ["element_id"], additionalProperties: false }
|
parameters: { type: "object", properties: { element_id: { type: "integer" } }, required: ["element_id"], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_relations", description: "Get relationships for an element. traverse='up' walks to IfcProject.",
|
type: "function", function: { name: "ifc_relations", description: "Get relationships for an element. traverse='up' walks to IfcProject.",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object", properties: { element_id: { type: "integer" }, traverse: { type: "string" } },
|
type: "object", properties: { element_id: { type: "integer" }, traverse: { type: "string" } },
|
||||||
required: ["element_id"], additionalProperties: false
|
required: ["element_id"], additionalProperties: false
|
||||||
}
|
} }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_clash", description: "Run clash/clearance checks for an element.",
|
type: "function", function: { name: "ifc_clash", description: "Run clash/clearance checks for an element.",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object", properties: { element_id: { type: "integer" }, clearance: { type: "number" }, tolerance: { type: "number" }, scope: { type: "string" } },
|
type: "object", properties: { element_id: { type: "integer" }, clearance: { type: "number" }, tolerance: { type: "number" }, scope: { type: "string" } },
|
||||||
required: ["element_id"], additionalProperties: false
|
required: ["element_id"], additionalProperties: false
|
||||||
}
|
} }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_list", description: "List ifcopenshell.api modules or functions within a module.",
|
type: "function", function: { name: "ifc_list", description: "List ifcopenshell.api modules or functions within a module.",
|
||||||
parameters: { type: "object", properties: { module: { type: "string" } }, required: [], additionalProperties: false }
|
parameters: { type: "object", properties: { module: { type: "string" } }, required: [], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_docs", description: "Get documentation for an ifcopenshell.api function, 'module.function'.",
|
type: "function", function: { name: "ifc_docs", description: "Get documentation for an ifcopenshell.api function, 'module.function'.",
|
||||||
parameters: { type: "object", properties: { function_path: { type: "string" } }, required: ["function_path"], additionalProperties: false }
|
parameters: { type: "object", properties: { function_path: { type: "string" } }, required: ["function_path"], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_edit", description: "Execute an ifcopenshell.api mutation; params is a JSON string of stringly-typed kwargs.",
|
type: "function", function: { name: "ifc_edit", description: "Execute an ifcopenshell.api mutation; params is a JSON string of stringly-typed kwargs.",
|
||||||
parameters: { type: "object", properties: { function_path: { type: "string" }, params: { type: "string" } }, required: ["function_path"], additionalProperties: false }
|
parameters: { type: "object", properties: { function_path: { type: "string" }, params: { type: "string" } }, required: ["function_path"], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_validate", description: "Validate the loaded model. Returns valid bool and list of issues.",
|
type: "function", function: { name: "ifc_validate", description: "Validate the loaded model. Returns valid bool and list of issues.",
|
||||||
parameters: { type: "object", properties: { express_rules: { type: "boolean" } }, required: [], additionalProperties: false }
|
parameters: { type: "object", properties: { express_rules: { type: "boolean" } }, required: [], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_schedule", description: "List work schedules and nested tasks. Use max_depth=1 for top-level phases only on large projects.",
|
type: "function", function: { name: "ifc_schedule", description: "List work schedules and nested tasks. Use max_depth=1 for top-level phases only on large projects.",
|
||||||
parameters: { type: "object", properties: { max_depth: { type: "integer" } }, required: [], additionalProperties: false }
|
parameters: { type: "object", properties: { max_depth: { type: "integer" } }, required: [], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_cost", description: "List cost schedules and nested cost items. Use max_depth=1 for top-level sections only on large BoQs.",
|
type: "function", function: { name: "ifc_cost", description: "List cost schedules and nested cost items. Use max_depth=1 for top-level sections only on large BoQs.",
|
||||||
parameters: { type: "object", properties: { max_depth: { type: "integer" } }, required: [], additionalProperties: false }
|
parameters: { type: "object", properties: { max_depth: { type: "integer" } }, required: [], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_schema", description: "Return IFC class documentation for an entity type.",
|
type: "function", function: { name: "ifc_schema", description: "Return IFC class documentation for an entity type.",
|
||||||
parameters: { type: "object", properties: { entity_type: { type: "string" } }, required: ["entity_type"], additionalProperties: false }
|
parameters: { type: "object", properties: { entity_type: { type: "string" } }, required: ["entity_type"], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "function", name: "ifc_quantify", description: "Run quantity take-off (QTO) on the model. Modifies model in-place; call ifc_save() after.",
|
type: "function", function: { name: "ifc_quantify", description: "Run quantity take-off (QTO) on the model. Modifies model in-place; call ifc_save() after.",
|
||||||
parameters: { type: "object", properties: { rule: { type: "string" }, selector: { type: "string" } }, required: ["rule"], additionalProperties: false }
|
parameters: { type: "object", properties: { rule: { type: "string" }, selector: { type: "string" } }, required: ["rule"], additionalProperties: false } }
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -156,85 +186,50 @@ Rules:
|
|||||||
Be concise. Avoid dumping huge trees unless asked.
|
Be concise. Avoid dumping huge trees unless asked.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
let inputItems = []; // running conversation state (Responses API style)
|
let messages = []; // running conversation state (Chat Completions style)
|
||||||
|
|
||||||
async function openAIResponsesCreate({ apiKey, model, input, tools }) {
|
|
||||||
const res = await fetch("https://api.openai.com/v1/responses", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model,
|
|
||||||
instructions: SYSTEM_INSTRUCTIONS,
|
|
||||||
tools,
|
|
||||||
input,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const text = await res.text();
|
|
||||||
throw new Error(`OpenAI error ${res.status}: ${text}`);
|
|
||||||
}
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractAssistantText(response) {
|
|
||||||
const out = [];
|
|
||||||
for (const item of response.output ?? []) {
|
|
||||||
if (item.type === "message" && item.role === "assistant") {
|
|
||||||
for (const c of item.content ?? []) {
|
|
||||||
if (c.type === "output_text") out.push(c.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out.join("\n").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
||||||
|
|
||||||
// Add user message
|
const { chat } = PROVIDERS[providerEl.value].api;
|
||||||
inputItems.push({ role: "user", content: userText });
|
|
||||||
|
messages.push({ role: "user", content: userText });
|
||||||
|
|
||||||
// Tool-calling loop (Responses API): append response.output, execute function_call items, append function_call_output.
|
|
||||||
for (let i = 0; i < 64; i++) {
|
for (let i = 0; i < 64; i++) {
|
||||||
const response = await openAIResponsesCreate({
|
const response = await chat({
|
||||||
apiKey,
|
apiKey,
|
||||||
model: modelEl.value,
|
model: modelEl.value,
|
||||||
input: inputItems,
|
messages: [{ role: "system", content: SYSTEM_INSTRUCTIONS }, ...messages],
|
||||||
tools,
|
tools,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep ALL output items (incl reasoning/tool calls) in the running state.
|
const message = response.choices?.[0]?.message;
|
||||||
inputItems.push(...(response.output ?? []));
|
if (!message) throw new Error("No message in response");
|
||||||
|
|
||||||
// Show any assistant text immediately
|
messages.push(message);
|
||||||
const text = extractAssistantText(response);
|
|
||||||
if (text) addMessage("assistant", text);
|
|
||||||
|
|
||||||
const calls = (response.output ?? []).filter((x) => x.type === "function_call");
|
if (message.content) addMessage("assistant", message.content);
|
||||||
|
|
||||||
|
const calls = message.tool_calls ?? [];
|
||||||
if (calls.length === 0) return;
|
if (calls.length === 0) return;
|
||||||
|
|
||||||
for (const call of calls) {
|
for (const call of calls) {
|
||||||
let args = {};
|
let args = {};
|
||||||
try { args = call.arguments ? JSON.parse(call.arguments) : {}; }
|
try { args = call.function.arguments ? JSON.parse(call.function.arguments) : {}; }
|
||||||
catch { args = {}; }
|
catch { args = {}; }
|
||||||
|
|
||||||
addMessage("tool", `→ ${call.name}(${JSON.stringify(args)})`);
|
addMessage("tool", `→ ${call.function.name}(${JSON.stringify(args)})`);
|
||||||
|
|
||||||
const toolRes = await callWorker("toolCall", { name: call.name, args });
|
const toolRes = await callWorker("toolCall", { name: call.function.name, args });
|
||||||
|
|
||||||
// Feed tool result back to the model
|
messages.push({
|
||||||
inputItems.push({
|
role: "tool",
|
||||||
type: "function_call_output",
|
tool_call_id: call.id,
|
||||||
call_id: call.call_id,
|
content: JSON.stringify(toolRes.result),
|
||||||
output: JSON.stringify(toolRes.result),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addMessage("tool", `← ${call.name}: ${JSON.stringify(toolRes.result, null, 2)}`);
|
addMessage("tool", `← ${call.function.name}: ${JSON.stringify(toolRes.result, null, 2)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user