Skip to content

Commit 0f7f960

Browse files
geronimi73aothms
authored andcommitted
let claude code openrouter compatibility
1 parent b7fc5da commit 0f7f960

File tree

1 file changed

+82
-87
lines changed

1 file changed

+82
-87
lines changed

src/ifcchat/app.js

Lines changed: 82 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
11
// app.js
2+
import * as openaiApi from "./api_openai.js";
3+
import * as openrouterApi from "./api_openrouter.js";
4+
5+
const PROVIDERS = {
6+
openai: {
7+
api: openaiApi,
8+
models: [
9+
{ value: "gpt-5", label: "gpt-5" },
10+
{ value: "gpt-4.1", label: "gpt-4.1" },
11+
],
12+
},
13+
openrouter: {
14+
api: openrouterApi,
15+
models: [
16+
{ value: "openai/gpt-oss-20b", label: "gpt-oss-20b" },
17+
{ value: "anthropic/claude-sonnet-4-5", label: "claude-sonnet-4-5" },
18+
{ value: "openai/gpt-4.1", label: "gpt-4.1" },
19+
{ value: "google/gemini-2.5-pro-preview", label: "gemini-2.5-pro" },
20+
],
21+
},
22+
};
23+
224
const $ = (id) => document.getElementById(id);
325

426
const statusEl = $("status");
@@ -7,10 +29,19 @@ const sendBtn = $("send");
729
const inputEl = $("input");
830
const apiKeyEl = $("apiKey");
931
const modelEl = $("model");
32+
const providerEl = $("provider");
1033
const ifcFileEl = $("ifcFile");
1134
const newBtn = $("newModel");
1235
const downloadBtn = $("downloadIfc");
1336

37+
function onProviderChange() {
38+
const p = PROVIDERS[providerEl.value];
39+
modelEl.innerHTML = p.models.map(m => `<option value="${m.value}">${m.label}</option>`).join("");
40+
}
41+
42+
providerEl.addEventListener("change", onProviderChange);
43+
onProviderChange();
44+
1445
function setBusy(isBusy, reason = "") {
1546
const controls = [
1647
$("send"),
@@ -75,74 +106,73 @@ function callWorker(type, payload = {}) {
75106
});
76107
}
77108

78-
// ---- OpenAI Responses API tool schemas (should match ifcmcp.core openai_tools()) ----
79-
// Docs show Responses API function_call items + function_call_output loop. :contentReference[oaicite:4]{index=4}
109+
// ---- Tool schemas (should match ifcmcp.core openai_tools()) ----
80110
const tools = [
81111
{
82-
type: "function", name: "ifc_new", description: "Create a new empty IFC model in memory.",
83-
parameters: { type: "object", properties: { schema: { type: "string" } }, required: [], additionalProperties: false }
112+
type: "function", function: { name: "ifc_new", description: "Create a new empty IFC model in memory.",
113+
parameters: { type: "object", properties: { schema: { type: "string" } }, required: [], additionalProperties: false } }
84114
},
85115
{
86-
type: "function", name: "ifc_summary", description: "Get a concise overview of the loaded IFC model.",
87-
parameters: { type: "object", properties: {}, required: [], additionalProperties: false }
116+
type: "function", function: { name: "ifc_summary", description: "Get a concise overview of the loaded IFC model.",
117+
parameters: { type: "object", properties: {}, required: [], additionalProperties: false } }
88118
},
89119
{
90-
type: "function", name: "ifc_tree", description: "Get the full spatial hierarchy tree.",
91-
parameters: { type: "object", properties: {}, required: [], additionalProperties: false }
120+
type: "function", function: { name: "ifc_tree", description: "Get the full spatial hierarchy tree.",
121+
parameters: { type: "object", properties: {}, required: [], additionalProperties: false } }
92122
},
93123
{
94-
type: "function", name: "ifc_select", description: "Select elements using ifcopenshell selector syntax (e.g. 'IfcWall').",
95-
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"], additionalProperties: false }
124+
type: "function", function: { name: "ifc_select", description: "Select elements using ifcopenshell selector syntax (e.g. 'IfcWall').",
125+
parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"], additionalProperties: false } }
96126
},
97127
{
98-
type: "function", name: "ifc_info", description: "Inspect an entity by STEP id.",
99-
parameters: { type: "object", properties: { element_id: { type: "integer" } }, required: ["element_id"], additionalProperties: false }
128+
type: "function", function: { name: "ifc_info", description: "Inspect an entity by STEP id.",
129+
parameters: { type: "object", properties: { element_id: { type: "integer" } }, required: ["element_id"], additionalProperties: false } }
100130
},
101131
{
102-
type: "function", name: "ifc_relations", description: "Get relationships for an element. traverse='up' walks to IfcProject.",
132+
type: "function", function: { name: "ifc_relations", description: "Get relationships for an element. traverse='up' walks to IfcProject.",
103133
parameters: {
104134
type: "object", properties: { element_id: { type: "integer" }, traverse: { type: "string" } },
105135
required: ["element_id"], additionalProperties: false
106-
}
136+
} }
107137
},
108138
{
109-
type: "function", name: "ifc_clash", description: "Run clash/clearance checks for an element.",
139+
type: "function", function: { name: "ifc_clash", description: "Run clash/clearance checks for an element.",
110140
parameters: {
111141
type: "object", properties: { element_id: { type: "integer" }, clearance: { type: "number" }, tolerance: { type: "number" }, scope: { type: "string" } },
112142
required: ["element_id"], additionalProperties: false
113-
}
143+
} }
114144
},
115145
{
116-
type: "function", name: "ifc_list", description: "List ifcopenshell.api modules or functions within a module.",
117-
parameters: { type: "object", properties: { module: { type: "string" } }, required: [], additionalProperties: false }
146+
type: "function", function: { name: "ifc_list", description: "List ifcopenshell.api modules or functions within a module.",
147+
parameters: { type: "object", properties: { module: { type: "string" } }, required: [], additionalProperties: false } }
118148
},
119149
{
120-
type: "function", name: "ifc_docs", description: "Get documentation for an ifcopenshell.api function, 'module.function'.",
121-
parameters: { type: "object", properties: { function_path: { type: "string" } }, required: ["function_path"], additionalProperties: false }
150+
type: "function", function: { name: "ifc_docs", description: "Get documentation for an ifcopenshell.api function, 'module.function'.",
151+
parameters: { type: "object", properties: { function_path: { type: "string" } }, required: ["function_path"], additionalProperties: false } }
122152
},
123153
{
124-
type: "function", name: "ifc_edit", description: "Execute an ifcopenshell.api mutation; params is a JSON string of stringly-typed kwargs.",
125-
parameters: { type: "object", properties: { function_path: { type: "string" }, params: { type: "string" } }, required: ["function_path"], additionalProperties: false }
154+
type: "function", function: { name: "ifc_edit", description: "Execute an ifcopenshell.api mutation; params is a JSON string of stringly-typed kwargs.",
155+
parameters: { type: "object", properties: { function_path: { type: "string" }, params: { type: "string" } }, required: ["function_path"], additionalProperties: false } }
126156
},
127157
{
128-
type: "function", name: "ifc_validate", description: "Validate the loaded model. Returns valid bool and list of issues.",
129-
parameters: { type: "object", properties: { express_rules: { type: "boolean" } }, required: [], additionalProperties: false }
158+
type: "function", function: { name: "ifc_validate", description: "Validate the loaded model. Returns valid bool and list of issues.",
159+
parameters: { type: "object", properties: { express_rules: { type: "boolean" } }, required: [], additionalProperties: false } }
130160
},
131161
{
132-
type: "function", name: "ifc_schedule", description: "List work schedules and nested tasks. Use max_depth=1 for top-level phases only on large projects.",
133-
parameters: { type: "object", properties: { max_depth: { type: "integer" } }, required: [], additionalProperties: false }
162+
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.",
163+
parameters: { type: "object", properties: { max_depth: { type: "integer" } }, required: [], additionalProperties: false } }
134164
},
135165
{
136-
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.",
137-
parameters: { type: "object", properties: { max_depth: { type: "integer" } }, required: [], additionalProperties: false }
166+
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.",
167+
parameters: { type: "object", properties: { max_depth: { type: "integer" } }, required: [], additionalProperties: false } }
138168
},
139169
{
140-
type: "function", name: "ifc_schema", description: "Return IFC class documentation for an entity type.",
141-
parameters: { type: "object", properties: { entity_type: { type: "string" } }, required: ["entity_type"], additionalProperties: false }
170+
type: "function", function: { name: "ifc_schema", description: "Return IFC class documentation for an entity type.",
171+
parameters: { type: "object", properties: { entity_type: { type: "string" } }, required: ["entity_type"], additionalProperties: false } }
142172
},
143173
{
144-
type: "function", name: "ifc_quantify", description: "Run quantity take-off (QTO) on the model. Modifies model in-place; call ifc_save() after.",
145-
parameters: { type: "object", properties: { rule: { type: "string" }, selector: { type: "string" } }, required: ["rule"], additionalProperties: false }
174+
type: "function", function: { name: "ifc_quantify", description: "Run quantity take-off (QTO) on the model. Modifies model in-place; call ifc_save() after.",
175+
parameters: { type: "object", properties: { rule: { type: "string" }, selector: { type: "string" } }, required: ["rule"], additionalProperties: false } }
146176
},
147177
];
148178

@@ -156,85 +186,50 @@ Rules:
156186
Be concise. Avoid dumping huge trees unless asked.
157187
`;
158188

159-
let inputItems = []; // running conversation state (Responses API style)
160-
161-
async function openAIResponsesCreate({ apiKey, model, input, tools }) {
162-
const res = await fetch("https://api.openai.com/v1/responses", {
163-
method: "POST",
164-
headers: {
165-
"Content-Type": "application/json",
166-
"Authorization": `Bearer ${apiKey}`,
167-
},
168-
body: JSON.stringify({
169-
model,
170-
instructions: SYSTEM_INSTRUCTIONS,
171-
tools,
172-
input,
173-
}),
174-
});
175-
176-
if (!res.ok) {
177-
const text = await res.text();
178-
throw new Error(`OpenAI error ${res.status}: ${text}`);
179-
}
180-
return await res.json();
181-
}
182-
183-
function extractAssistantText(response) {
184-
const out = [];
185-
for (const item of response.output ?? []) {
186-
if (item.type === "message" && item.role === "assistant") {
187-
for (const c of item.content ?? []) {
188-
if (c.type === "output_text") out.push(c.text);
189-
}
190-
}
191-
}
192-
return out.join("\n").trim();
193-
}
189+
let messages = []; // running conversation state (Chat Completions style)
194190

195191
async function runAgentTurn(userText) {
196192
const apiKey = apiKeyEl.value.trim();
197193
if (!apiKey) throw new Error("Missing API key");
198194

199-
// Add user message
200-
inputItems.push({ role: "user", content: userText });
195+
const { chat } = PROVIDERS[providerEl.value].api;
196+
197+
messages.push({ role: "user", content: userText });
201198

202-
// Tool-calling loop (Responses API): append response.output, execute function_call items, append function_call_output.
203199
for (let i = 0; i < 64; i++) {
204-
const response = await openAIResponsesCreate({
200+
const response = await chat({
205201
apiKey,
206202
model: modelEl.value,
207-
input: inputItems,
203+
messages: [{ role: "system", content: SYSTEM_INSTRUCTIONS }, ...messages],
208204
tools,
209205
});
210206

211-
// Keep ALL output items (incl reasoning/tool calls) in the running state.
212-
inputItems.push(...(response.output ?? []));
207+
const message = response.choices?.[0]?.message;
208+
if (!message) throw new Error("No message in response");
209+
210+
messages.push(message);
213211

214-
// Show any assistant text immediately
215-
const text = extractAssistantText(response);
216-
if (text) addMessage("assistant", text);
212+
if (message.content) addMessage("assistant", message.content);
217213

218-
const calls = (response.output ?? []).filter((x) => x.type === "function_call");
214+
const calls = message.tool_calls ?? [];
219215
if (calls.length === 0) return;
220216

221217
for (const call of calls) {
222218
let args = {};
223-
try { args = call.arguments ? JSON.parse(call.arguments) : {}; }
219+
try { args = call.function.arguments ? JSON.parse(call.function.arguments) : {}; }
224220
catch { args = {}; }
225221

226-
addMessage("tool", `→ ${call.name}(${JSON.stringify(args)})`);
222+
addMessage("tool", `→ ${call.function.name}(${JSON.stringify(args)})`);
227223

228-
const toolRes = await callWorker("toolCall", { name: call.name, args });
224+
const toolRes = await callWorker("toolCall", { name: call.function.name, args });
229225

230-
// Feed tool result back to the model
231-
inputItems.push({
232-
type: "function_call_output",
233-
call_id: call.call_id,
234-
output: JSON.stringify(toolRes.result),
226+
messages.push({
227+
role: "tool",
228+
tool_call_id: call.id,
229+
content: JSON.stringify(toolRes.result),
235230
});
236231

237-
addMessage("tool", `← ${call.name}: ${JSON.stringify(toolRes.result, null, 2)}`);
232+
addMessage("tool", `← ${call.function.name}: ${JSON.stringify(toolRes.result, null, 2)}`);
238233
}
239234
}
240235

0 commit comments

Comments
 (0)