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+
224const $ = ( id ) => document . getElementById ( id ) ;
325
426const statusEl = $ ( "status" ) ;
@@ -7,10 +29,19 @@ const sendBtn = $("send");
729const inputEl = $ ( "input" ) ;
830const apiKeyEl = $ ( "apiKey" ) ;
931const modelEl = $ ( "model" ) ;
32+ const providerEl = $ ( "provider" ) ;
1033const ifcFileEl = $ ( "ifcFile" ) ;
1134const newBtn = $ ( "newModel" ) ;
1235const 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+
1445function 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()) ----
80110const 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:
156186Be 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
195191async 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